Skip to content

[BUG][Crystal] The current Crystal generator generates code with wrongly typed variables. #22563

@n-rodriguez

Description

@n-rodriguez

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

The issue: the current Crystal generator generates code with wrongly typed variables.

openapi-generator version

master

Generation Details

The story :

The current Crystal generator generates this code :

For example, to destroy bookmarks in bulk I can use this method :

# @param bookmark_request [Array(BookmarkRequest)]
def extras_bookmarks_bulk_destroy(bookmark_request : Array(BookmarkRequest))

which needs an array of BookmarkRequest :

module NetboxClient
  # Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
  class BookmarkRequest
    include JSON::Serializable
    include YAML::Serializable

    # Required properties
    @[JSON::Field(key: "object_type", type: String, nillable: false, emit_null: false)]
    property object_type : String

    @[JSON::Field(key: "object_id", type: Int64, nillable: false, emit_null: false)]
    property object_id : Int64

    @[JSON::Field(key: "user", type: BookmarkRequestUser, nillable: false, emit_null: false)]
    property user : BookmarkRequestUser

In BookmarkRequest there is a field user of type BookmarkRequestUser :

BookmarkRequestUser is actually not a type but more a factory that builds the real type (BriefUserRequest or Int32) depending on the data passed to the build(data) method :

module NetboxClient
  module BookmarkRequestUser
    class SchemaMismatchError < Exception
    end

    # List of class defined in oneOf (OpenAPI v3)
    def self.openapi_one_of
      [
        BriefUserRequest,
        Int32,
      ]
    end

    def self.build(data)
      # Go through the list of oneOf items and attempt to identify the appropriate one.
      # Note:
      # - We do not attempt to check whether exactly one item matches.
      # - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 })
      #   due to the way the deserialization is made in the base_object template (it just casts without verifying).
      # - TODO: scalar values are de facto behaving as if they were nullable.
      # - TODO: logging when debugging is set.
      openapi_one_of.each do |klass|
        begin
          # next if klass == :AnyType # "nullable: true"
          typed_data = find_and_cast_into_type(klass, data)
          return typed_data if typed_data
        rescue # rescue all errors so we keep iterating even if the current item lookup raises
        end
      end

      # openapi_one_of.includes?(:AnyType) ? data : nil
      nil
    end

One of the real type is BriefUserRequest which take a single string username :

module NetboxClient
  # Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
  class BriefUserRequest
    include JSON::Serializable
    include YAML::Serializable

    # Required properties
    # Requis. 150 caractères maximum. Uniquement des lettres, nombres et les caractères « @ », « . », « + », « - » et « _ ».
    @[JSON::Field(key: "username", type: String, nillable: false, emit_null: false)]
    property username : String

So let's try to delete some bookmarks :

def self.delete_bookmarks
  # Recall:
  # class BookmarkRequest
  #   @[JSON::Field(key: "user", type: BookmarkRequestUser, nillable: false, emit_null: false)]
  #   property user : BookmarkRequestUser

  user = NetboxClient::BookmarkRequestUser.build(NetboxClient::RecursiveHash.new({"username" => "foo"}))
  object = NetboxClient::BookmarkRequest.new(object_type: "Device", object_id: 42.to_i64, user: user)
  puts YAML.dump(object)
end

But because the final type NetboxClient::BriefUserRequest does not match with NetboxClient::BookmarkRequestUser it doesn't compile :

In src/netbox_extractor/controllers/test_api.cr:39:95

 39 | object = NetboxClient::BookmarkRequest.new(object_type: "Device", object_id: 42.to_i64, user: user)
                                                                                            ^
Error: expected argument 'user' to 'NetboxClient::BookmarkRequest.new' to be NetboxClient::BookmarkRequestUser, not (Int32 | NetboxClient::BriefUserRequest | Nil)
Suggest a fix

To solve this I'd like to change model definition from :

module NetboxClient
  # Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
  class BookmarkRequest
    include JSON::Serializable
    include YAML::Serializable

    # Required properties
    @[JSON::Field(key: "object_type", type: String, nillable: false, emit_null: false)]
    property object_type : String

    @[JSON::Field(key: "object_id", type: Int64, nillable: false, emit_null: false)]
    property object_id : Int64

    @[JSON::Field(key: "user", type: BookmarkRequestUser, nillable: false, emit_null: false)]
    property user : BookmarkRequestUser

to

module NetboxClient
  # Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
  class BookmarkRequest
    include JSON::Serializable
    include YAML::Serializable

    # Required properties
    @[JSON::Field(key: "object_type", type: String, nillable: false, emit_null: false)]
    property object_type : String

    @[JSON::Field(key: "object_id", type: Int64, nillable: false, emit_null: false)]
    property object_id : Int64

    @[JSON::Field(key: "user", type: BriefUserRequest | Int32, nillable: false, emit_null: false)] # <= here
    property user : BriefUserRequest | Int32 # <= and here

but I don't know if it's feasible.

or make BriefUserRequest a generic? I don't know. won't work

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions