[crystal][client] Make optional properties nillable in models (#10723)

* Add nillable data types to models

Only REQUIRED and NOT NULLABLE variables can NOT have type Nil
All OPTIONAL and NULLABLE-REQUIRED variables have type Nil
Only NULLABLE-REQUIRED variables should emit keys with null values when they are serialized, json example: property name : String? = nil; the json representation for this property is {"name": null}
For all OPTIONAL variables having Nil values, their variable keys would be skipped during serialization. The json representation for OPTIONAL property name : String? = nil;  would be: {}

* Fix failed tests in samples/client/petstore/crystal/spec/api/pet_api_spec.cr

* Remove isNullable from model template

* No need to check nillability of required property

For any required property, assigning nil value to it will result in compilation error
The datatype simply can not hold value nil, so there's no need to check it

* Place required vars first in initializor

* Refresh generated sample code for crystal client

* Required properties are not nillable

* Fix compilation error of undefined method equal?

Crystal lang doesn't have method equal?
We should use method same? instead of ruby's equal? method

Reference: https://crystal-lang.org/api/master/Reference.html#same?(other:Reference):Bool-instance-method

* Add tests for add_pet api endpoint with only required parameters

Setting Pet optional properties to nil values is allowed by add_pet api endpoint

* Add helper method to test compilation errors

* Add tests to Pet model

Test model initializations
Test compilation error when model is initialized without required properties

* Test required properties in json deserialization for Pet model
This commit is contained in:
Chao Yang 2022-01-28 20:55:44 -06:00 committed by GitHub
parent 7dad57c8b6
commit 3f0f92fb65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 200 additions and 113 deletions

View File

@ -4,14 +4,28 @@
class {{classname}}{{#parent}} < {{{.}}}{{/parent}} class {{classname}}{{#parent}} < {{{.}}}{{/parent}}
include JSON::Serializable include JSON::Serializable
{{#vars}} {{#hasRequired}}
# Required properties
{{/hasRequired}}
{{#requiredVars}}
{{#description}} {{#description}}
# {{{.}}} # {{{.}}}
{{/description}} {{/description}}
@[JSON::Field(key: "{{{baseName}}}", type: {{{dataType}}}{{#defaultValue}}, default: {{{defaultValue}}}{{/defaultValue}}{{#isNullable}}, nillable: true, emit_null: true{{/isNullable}})] @[JSON::Field(key: "{{{baseName}}}", type: {{{dataType}}}{{#defaultValue}}, default: {{{defaultValue}}}{{/defaultValue}}, nillable: false, emit_null: false)]
property {{{name}}} : {{{dataType}}} property {{{name}}} : {{{dataType}}}
{{/vars}} {{/requiredVars}}
{{#hasOptional}}
# Optional properties
{{/hasOptional}}
{{#optionalVars}}
{{#description}}
# {{{.}}}
{{/description}}
@[JSON::Field(key: "{{{baseName}}}", type: {{{dataType}}}?{{#defaultValue}}, default: {{{defaultValue}}}{{/defaultValue}}, nillable: true, emit_null: false)]
property {{{name}}} : {{{dataType}}}?
{{/optionalVars}}
{{#hasEnums}} {{#hasEnums}}
class EnumAttributeValidator class EnumAttributeValidator
getter datatype : String getter datatype : String
@ -74,7 +88,7 @@
{{/discriminator}} {{/discriminator}}
# Initializes the object # Initializes the object
# @param [Hash] attributes Model attributes in the form of hash # @param [Hash] attributes Model attributes in the form of hash
def initialize({{#vars}}@{{{name}}} : {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/vars}}) def initialize({{#requiredVars}}@{{{name}}} : {{{dataType}}}{{^-last}}, {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}@{{{name}}} : {{{dataType}}}?{{^-last}}, {{/-last}}{{/optionalVars}})
end end
# Show invalid properties with the reasons. Usually used together with valid? # Show invalid properties with the reasons. Usually used together with valid?
@ -82,14 +96,6 @@
def list_invalid_properties def list_invalid_properties
invalid_properties = {{^parent}}Array(String).new{{/parent}}{{#parent}}super{{/parent}} invalid_properties = {{^parent}}Array(String).new{{/parent}}{{#parent}}super{{/parent}}
{{#vars}} {{#vars}}
{{^isNullable}}
{{#required}}
if @{{{name}}}.nil?
invalid_properties.push("invalid value for \"{{{name}}}\", {{{name}}} cannot be nil.")
end
{{/required}}
{{/isNullable}}
{{#hasValidation}} {{#hasValidation}}
{{#maxLength}} {{#maxLength}}
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.size > {{{maxLength}}} if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.size > {{{maxLength}}}
@ -143,11 +149,6 @@
# @return true if the model is valid # @return true if the model is valid
def valid? def valid?
{{#vars}} {{#vars}}
{{^isNullable}}
{{#required}}
return false if @{{{name}}}.nil?
{{/required}}
{{/isNullable}}
{{#isEnum}} {{#isEnum}}
{{^isContainer}} {{^isContainer}}
{{{name}}}_validator = EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]) {{{name}}}_validator = EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}])
@ -217,14 +218,6 @@
# Custom attribute writer method with validation # Custom attribute writer method with validation
# @param [Object] {{{name}}} Value to be assigned # @param [Object] {{{name}}} Value to be assigned
def {{{name}}}=({{{name}}}) def {{{name}}}=({{{name}}})
{{^isNullable}}
{{#required}}
if {{{name}}}.nil?
raise ArgumentError.new("{{{name}}} cannot be nil")
end
{{/required}}
{{/isNullable}}
{{#maxLength}} {{#maxLength}}
if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}}.to_s.size > {{{maxLength}}} if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}}.to_s.size > {{{maxLength}}}
raise ArgumentError.new("invalid value for \"{{{name}}}\", the character length must be smaller than or equal to {{{maxLength}}}.") raise ArgumentError.new("invalid value for \"{{{name}}}\", the character length must be smaller than or equal to {{{maxLength}}}.")
@ -277,7 +270,7 @@
# Checks equality by comparing each attribute. # Checks equality by comparing each attribute.
# @param [Object] Object to be compared # @param [Object] Object to be compared
def ==(o) def ==(o)
return true if self.equal?(o) return true if self.same?(o)
self.class == o.class{{#vars}} && self.class == o.class{{#vars}} &&
{{name}} == o.{{name}}{{/vars}}{{#parent}} && super(o){{/parent}} {{name}} == o.{{name}}{{/vars}}{{#parent}} && super(o){{/parent}}
end end

View File

@ -4,3 +4,11 @@
require "spec" require "spec"
require "json" require "json"
require "../src/{{{shardName}}}" require "../src/{{{shardName}}}"
def assert_compilation_error(path : String, message : String) : Nil
buffer = IO::Memory.new
result = Process.run("crystal", ["run", "--no-color", "--no-codegen", path], error: buffer)
result.success?.should be_false
buffer.to_s.should contain message
buffer.close
end

View File

@ -0,0 +1,11 @@
require "./spec/spec_helper"
require "json"
require "time"
describe Petstore::Pet do
describe "test an instance of Pet" do
it "should fail to compile if any required properties is missing" do
pet = Petstore::Pet.new(id: nil, category: nil, name: nil, photo_urls: Array(String).new, tags: nil, status: nil)
end
end
end

View File

@ -29,8 +29,28 @@ describe "PetApi" do
# @param [Hash] opts the optional parameters # @param [Hash] opts the optional parameters
# @return [Pet] # @return [Pet]
describe "add_pet test" do describe "add_pet test" do
it "should work" do it "should work with only required attributes" do
# assertion here. ref: https://crystal-lang.org/reference/guides/testing.html # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html
config = Petstore::Configuration.new
config.access_token = "yyy"
config.api_key[:api_key] = "xxx"
config.api_key_prefix[:api_key] = "Token"
api_client = Petstore::ApiClient.new(config)
api_instance = Petstore::PetApi.new(api_client)
pet_name = "new pet"
new_pet = Petstore::Pet.new(id: nil, category: nil, name: pet_name, photo_urls: Array(String).new, tags: nil, status: nil)
pet = api_instance.add_pet(new_pet)
pet.id.should_not be_nil
pet.category.should be_nil
pet.name.should eq pet_name
pet.photo_urls.should eq Array(String).new
pet.status.should be_nil
pet.tags.should eq Array(Petstore::Tag).new
end end
end end
@ -89,20 +109,17 @@ describe "PetApi" do
api_instance = Petstore::PetApi.new(api_client) api_instance = Petstore::PetApi.new(api_client)
# create a pet to start with new_pet = Petstore::Pet.new(id: nil, category: nil, name: "crystal", photo_urls: Array(String).new, tags: nil, status: nil)
pet_id = Int64.new(91829)
pet = Petstore::Pet.new(id: pet_id, category: Petstore::Category.new(id: pet_id + 10, name: "crystal category"), name: "crystal", photo_urls: ["https://crystal-lang.org"], tags: [Petstore::Tag.new(id: pet_id + 100, name: "crystal tag")], status: "available")
api_instance.add_pet(pet)
pet = api_instance.add_pet(new_pet)
pet_id = pet.id.not_nil!
result = api_instance.get_pet_by_id(pet_id: pet_id) result = api_instance.get_pet_by_id(pet_id: pet_id)
result.id.should eq pet_id result.id.should eq pet_id
result.category.id.should eq pet_id + 10 result.category.should be_nil
result.category.name.should eq "crystal category"
result.name.should eq "crystal" result.name.should eq "crystal"
result.photo_urls.should eq ["https://crystal-lang.org"] result.photo_urls.should eq Array(String).new
result.status.should eq "available" result.status.should be_nil
result.tags[0].id.should eq pet_id + 100 result.tags.should eq Array(Petstore::Tag).new
end end
end end

View File

@ -18,11 +18,49 @@ require "time"
describe Petstore::Pet do describe Petstore::Pet do
describe "test an instance of Pet" do describe "test an instance of Pet" do
it "should create an instance of Pet" do it "should fail to compile if any required properties is missing" do
#instance = Petstore::Pet.new assert_compilation_error(path: "./pet_compilation_error_spec.cr", message: "Error: no overload matches 'Petstore::Pet.new', id: Nil, category: Nil, name: Nil, photo_urls: Array(String), tags: Nil, status: Nil")
#expect(instance).to be_instance_of(Petstore::Pet) end
it "should create an instance of Pet with only required properties" do
pet = Petstore::Pet.new(id: nil, category: nil, name: "new pet", photo_urls: Array(String).new, tags: nil, status: nil)
pet.should be_a(Petstore::Pet)
end
it "should create an instance of Pet with all properties" do
pet_id = 12345_i64
pet = Petstore::Pet.new(id: pet_id, category: Petstore::Category.new(id: pet_id + 10, name: "crystal category"), name: "crystal", photo_urls: ["https://crystal-lang.org"], tags: [Petstore::Tag.new(id: pet_id + 100, name: "crystal tag")], status: "available")
pet.should be_a(Petstore::Pet)
end end
end end
describe "#from_json" do
it "should instantiate a new instance from json string with required properties" do
pet = Petstore::Pet.from_json("{\"name\": \"json pet\", \"photoUrls\": []}")
pet.should be_a(Petstore::Pet)
pet.name.should eq "json pet"
pet.photo_urls.should eq Array(String).new
end
it "should raise error when instantiating a new instance from json string with missing required properties" do
expect_raises(JSON::SerializableError, "Missing JSON attribute: name") do
Petstore::Pet.from_json("{\"photoUrls\": []}")
end
expect_raises(JSON::SerializableError, "Missing JSON attribute: photoUrls") do
Petstore::Pet.from_json("{\"name\": \"json pet\"}")
end
end
it "should raise error when instantiating a new instance from json string with required properties set to null value" do
expect_raises(JSON::SerializableError, "Expected String but was Null") do
Petstore::Pet.from_json("{\"name\": null, \"photoUrls\": []}")
end
expect_raises(JSON::SerializableError, "Expected BeginArray but was Null") do
Petstore::Pet.from_json("{\"name\": \"json pet\", \"photoUrls\": null}")
end
end
end
describe "test attribute 'id'" do describe "test attribute 'id'" do
it "should work" do it "should work" do
# assertion here. ref: https://crystal-lang.org/reference/guides/testing.html # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html

View File

@ -23,6 +23,21 @@ describe Petstore::Tag do
#expect(instance).to be_instance_of(Petstore::Tag) #expect(instance).to be_instance_of(Petstore::Tag)
end end
end end
describe "test equality of Tag instances" do
it "should equal to itself" do
tag1 = Petstore::Tag.new(id: 0, name: "same")
tag2 = tag1
(tag1 == tag2).should be_true
end
it "should equal to another instance with same attributes" do
tag1 = Petstore::Tag.new(id: 0, name: "tag")
tag2 = Petstore::Tag.new(id: 0, name: "tag")
(tag1 == tag2).should be_true
end
end
describe "test attribute 'id'" do describe "test attribute 'id'" do
it "should work" do it "should work" do
# assertion here. ref: https://crystal-lang.org/reference/guides/testing.html # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html

View File

@ -12,3 +12,11 @@
require "spec" require "spec"
require "json" require "json"
require "../src/petstore" require "../src/petstore"
def assert_compilation_error(path : String, message : String) : Nil
buffer = IO::Memory.new
result = Process.run("crystal", ["run", "--no-color", "--no-codegen", path], error: buffer)
result.success?.should be_false
buffer.to_s.should contain message
buffer.close
end

View File

@ -16,14 +16,15 @@ module Petstore
class ApiResponse class ApiResponse
include JSON::Serializable include JSON::Serializable
@[JSON::Field(key: "code", type: Int32)] # Optional properties
property code : Int32 @[JSON::Field(key: "code", type: Int32?, nillable: true, emit_null: false)]
property code : Int32?
@[JSON::Field(key: "type", type: String)] @[JSON::Field(key: "type", type: String?, nillable: true, emit_null: false)]
property _type : String property _type : String?
@[JSON::Field(key: "message", type: String)] @[JSON::Field(key: "message", type: String?, nillable: true, emit_null: false)]
property message : String property message : String?
# Initializes the object # Initializes the object
# @param [Hash] attributes Model attributes in the form of hash # @param [Hash] attributes Model attributes in the form of hash
@ -46,7 +47,7 @@ module Petstore
# Checks equality by comparing each attribute. # Checks equality by comparing each attribute.
# @param [Object] Object to be compared # @param [Object] Object to be compared
def ==(o) def ==(o)
return true if self.equal?(o) return true if self.same?(o)
self.class == o.class && self.class == o.class &&
code == o.code && code == o.code &&
_type == o._type && _type == o._type &&

View File

@ -16,11 +16,12 @@ module Petstore
class Category class Category
include JSON::Serializable include JSON::Serializable
@[JSON::Field(key: "id", type: Int64)] # Optional properties
property id : Int64 @[JSON::Field(key: "id", type: Int64?, nillable: true, emit_null: false)]
property id : Int64?
@[JSON::Field(key: "name", type: String)] @[JSON::Field(key: "name", type: String?, nillable: true, emit_null: false)]
property name : String property name : String?
# Initializes the object # Initializes the object
# @param [Hash] attributes Model attributes in the form of hash # @param [Hash] attributes Model attributes in the form of hash
@ -60,7 +61,7 @@ module Petstore
# Checks equality by comparing each attribute. # Checks equality by comparing each attribute.
# @param [Object] Object to be compared # @param [Object] Object to be compared
def ==(o) def ==(o)
return true if self.equal?(o) return true if self.same?(o)
self.class == o.class && self.class == o.class &&
id == o.id && id == o.id &&
name == o.name name == o.name

View File

@ -16,24 +16,25 @@ module Petstore
class Order class Order
include JSON::Serializable include JSON::Serializable
@[JSON::Field(key: "id", type: Int64)] # Optional properties
property id : Int64 @[JSON::Field(key: "id", type: Int64?, nillable: true, emit_null: false)]
property id : Int64?
@[JSON::Field(key: "petId", type: Int64)] @[JSON::Field(key: "petId", type: Int64?, nillable: true, emit_null: false)]
property pet_id : Int64 property pet_id : Int64?
@[JSON::Field(key: "quantity", type: Int32)] @[JSON::Field(key: "quantity", type: Int32?, nillable: true, emit_null: false)]
property quantity : Int32 property quantity : Int32?
@[JSON::Field(key: "shipDate", type: Time)] @[JSON::Field(key: "shipDate", type: Time?, nillable: true, emit_null: false)]
property ship_date : Time property ship_date : Time?
# Order Status # Order Status
@[JSON::Field(key: "status", type: String)] @[JSON::Field(key: "status", type: String?, nillable: true, emit_null: false)]
property status : String property status : String?
@[JSON::Field(key: "complete", type: Bool, default: false)] @[JSON::Field(key: "complete", type: Bool?, default: false, nillable: true, emit_null: false)]
property complete : Bool property complete : Bool?
class EnumAttributeValidator class EnumAttributeValidator
getter datatype : String getter datatype : String
@ -91,7 +92,7 @@ module Petstore
# Checks equality by comparing each attribute. # Checks equality by comparing each attribute.
# @param [Object] Object to be compared # @param [Object] Object to be compared
def ==(o) def ==(o)
return true if self.equal?(o) return true if self.same?(o)
self.class == o.class && self.class == o.class &&
id == o.id && id == o.id &&
pet_id == o.pet_id && pet_id == o.pet_id &&

View File

@ -16,24 +16,26 @@ module Petstore
class Pet class Pet
include JSON::Serializable include JSON::Serializable
@[JSON::Field(key: "id", type: Int64)] # Required properties
property id : Int64 @[JSON::Field(key: "name", type: String, nillable: false, emit_null: false)]
@[JSON::Field(key: "category", type: Category)]
property category : Category
@[JSON::Field(key: "name", type: String)]
property name : String property name : String
@[JSON::Field(key: "photoUrls", type: Array(String))] @[JSON::Field(key: "photoUrls", type: Array(String), nillable: false, emit_null: false)]
property photo_urls : Array(String) property photo_urls : Array(String)
@[JSON::Field(key: "tags", type: Array(Tag))] # Optional properties
property tags : Array(Tag) @[JSON::Field(key: "id", type: Int64?, nillable: true, emit_null: false)]
property id : Int64?
@[JSON::Field(key: "category", type: Category?, nillable: true, emit_null: false)]
property category : Category?
@[JSON::Field(key: "tags", type: Array(Tag)?, nillable: true, emit_null: false)]
property tags : Array(Tag)?
# pet status in the store # pet status in the store
@[JSON::Field(key: "status", type: String)] @[JSON::Field(key: "status", type: String?, nillable: true, emit_null: false)]
property status : String property status : String?
class EnumAttributeValidator class EnumAttributeValidator
getter datatype : String getter datatype : String
@ -60,29 +62,19 @@ module Petstore
# Initializes the object # Initializes the object
# @param [Hash] attributes Model attributes in the form of hash # @param [Hash] attributes Model attributes in the form of hash
def initialize(@id : Int64?, @category : Category?, @name : String, @photo_urls : Array(String), @tags : Array(Tag)?, @status : String?) def initialize(@name : String, @photo_urls : Array(String), @id : Int64?, @category : Category?, @tags : Array(Tag)?, @status : String?)
end end
# Show invalid properties with the reasons. Usually used together with valid? # Show invalid properties with the reasons. Usually used together with valid?
# @return Array for valid properties with the reasons # @return Array for valid properties with the reasons
def list_invalid_properties def list_invalid_properties
invalid_properties = Array(String).new invalid_properties = Array(String).new
if @name.nil?
invalid_properties.push("invalid value for \"name\", name cannot be nil.")
end
if @photo_urls.nil?
invalid_properties.push("invalid value for \"photo_urls\", photo_urls cannot be nil.")
end
invalid_properties invalid_properties
end end
# Check to see if the all the properties in the model are valid # Check to see if the all the properties in the model are valid
# @return true if the model is valid # @return true if the model is valid
def valid? def valid?
return false if @name.nil?
return false if @photo_urls.nil?
status_validator = EnumAttributeValidator.new("String", ["available", "pending", "sold"]) status_validator = EnumAttributeValidator.new("String", ["available", "pending", "sold"])
return false unless status_validator.valid?(@status) return false unless status_validator.valid?(@status)
true true
@ -101,7 +93,7 @@ module Petstore
# Checks equality by comparing each attribute. # Checks equality by comparing each attribute.
# @param [Object] Object to be compared # @param [Object] Object to be compared
def ==(o) def ==(o)
return true if self.equal?(o) return true if self.same?(o)
self.class == o.class && self.class == o.class &&
id == o.id && id == o.id &&
category == o.category && category == o.category &&

View File

@ -16,11 +16,12 @@ module Petstore
class Tag class Tag
include JSON::Serializable include JSON::Serializable
@[JSON::Field(key: "id", type: Int64)] # Optional properties
property id : Int64 @[JSON::Field(key: "id", type: Int64?, nillable: true, emit_null: false)]
property id : Int64?
@[JSON::Field(key: "name", type: String)] @[JSON::Field(key: "name", type: String?, nillable: true, emit_null: false)]
property name : String property name : String?
# Initializes the object # Initializes the object
# @param [Hash] attributes Model attributes in the form of hash # @param [Hash] attributes Model attributes in the form of hash
@ -43,7 +44,7 @@ module Petstore
# Checks equality by comparing each attribute. # Checks equality by comparing each attribute.
# @param [Object] Object to be compared # @param [Object] Object to be compared
def ==(o) def ==(o)
return true if self.equal?(o) return true if self.same?(o)
self.class == o.class && self.class == o.class &&
id == o.id && id == o.id &&
name == o.name name == o.name

View File

@ -16,30 +16,31 @@ module Petstore
class User class User
include JSON::Serializable include JSON::Serializable
@[JSON::Field(key: "id", type: Int64)] # Optional properties
property id : Int64 @[JSON::Field(key: "id", type: Int64?, nillable: true, emit_null: false)]
property id : Int64?
@[JSON::Field(key: "username", type: String)] @[JSON::Field(key: "username", type: String?, nillable: true, emit_null: false)]
property username : String property username : String?
@[JSON::Field(key: "firstName", type: String)] @[JSON::Field(key: "firstName", type: String?, nillable: true, emit_null: false)]
property first_name : String property first_name : String?
@[JSON::Field(key: "lastName", type: String)] @[JSON::Field(key: "lastName", type: String?, nillable: true, emit_null: false)]
property last_name : String property last_name : String?
@[JSON::Field(key: "email", type: String)] @[JSON::Field(key: "email", type: String?, nillable: true, emit_null: false)]
property email : String property email : String?
@[JSON::Field(key: "password", type: String)] @[JSON::Field(key: "password", type: String?, nillable: true, emit_null: false)]
property password : String property password : String?
@[JSON::Field(key: "phone", type: String)] @[JSON::Field(key: "phone", type: String?, nillable: true, emit_null: false)]
property phone : String property phone : String?
# User Status # User Status
@[JSON::Field(key: "userStatus", type: Int32)] @[JSON::Field(key: "userStatus", type: Int32?, nillable: true, emit_null: false)]
property user_status : Int32 property user_status : Int32?
# Initializes the object # Initializes the object
# @param [Hash] attributes Model attributes in the form of hash # @param [Hash] attributes Model attributes in the form of hash
@ -62,7 +63,7 @@ module Petstore
# Checks equality by comparing each attribute. # Checks equality by comparing each attribute.
# @param [Object] Object to be compared # @param [Object] Object to be compared
def ==(o) def ==(o)
return true if self.equal?(o) return true if self.same?(o)
self.class == o.class && self.class == o.class &&
id == o.id && id == o.id &&
username == o.username && username == o.username &&