[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}}
include JSON::Serializable
{{#vars}}
{{#hasRequired}}
# Required properties
{{/hasRequired}}
{{#requiredVars}}
{{#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}}}
{{/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}}
class EnumAttributeValidator
getter datatype : String
@ -74,7 +88,7 @@
{{/discriminator}}
# Initializes the object
# @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
# Show invalid properties with the reasons. Usually used together with valid?
@ -82,14 +96,6 @@
def list_invalid_properties
invalid_properties = {{^parent}}Array(String).new{{/parent}}{{#parent}}super{{/parent}}
{{#vars}}
{{^isNullable}}
{{#required}}
if @{{{name}}}.nil?
invalid_properties.push("invalid value for \"{{{name}}}\", {{{name}}} cannot be nil.")
end
{{/required}}
{{/isNullable}}
{{#hasValidation}}
{{#maxLength}}
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.size > {{{maxLength}}}
@ -143,11 +149,6 @@
# @return true if the model is valid
def valid?
{{#vars}}
{{^isNullable}}
{{#required}}
return false if @{{{name}}}.nil?
{{/required}}
{{/isNullable}}
{{#isEnum}}
{{^isContainer}}
{{{name}}}_validator = EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}])
@ -217,14 +218,6 @@
# Custom attribute writer method with validation
# @param [Object] {{{name}}} Value to be assigned
def {{{name}}}=({{{name}}})
{{^isNullable}}
{{#required}}
if {{{name}}}.nil?
raise ArgumentError.new("{{{name}}} cannot be nil")
end
{{/required}}
{{/isNullable}}
{{#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}}}.")
@ -277,7 +270,7 @@
# Checks equality by comparing each attribute.
# @param [Object] Object to be compared
def ==(o)
return true if self.equal?(o)
return true if self.same?(o)
self.class == o.class{{#vars}} &&
{{name}} == o.{{name}}{{/vars}}{{#parent}} && super(o){{/parent}}
end

View File

@ -4,3 +4,11 @@
require "spec"
require "json"
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
# @return [Pet]
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
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
@ -89,20 +109,17 @@ describe "PetApi" do
api_instance = Petstore::PetApi.new(api_client)
# create a pet to start with
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)
new_pet = Petstore::Pet.new(id: nil, category: nil, name: "crystal", photo_urls: Array(String).new, tags: nil, status: nil)
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.id.should eq pet_id
result.category.id.should eq pet_id + 10
result.category.name.should eq "crystal category"
result.category.should be_nil
result.name.should eq "crystal"
result.photo_urls.should eq ["https://crystal-lang.org"]
result.status.should eq "available"
result.tags[0].id.should eq pet_id + 100
result.photo_urls.should eq Array(String).new
result.status.should be_nil
result.tags.should eq Array(Petstore::Tag).new
end
end

View File

@ -18,11 +18,49 @@ require "time"
describe Petstore::Pet do
describe "test an instance of Pet" do
it "should create an instance of Pet" do
#instance = Petstore::Pet.new
#expect(instance).to be_instance_of(Petstore::Pet)
it "should fail to compile if any required properties is missing" do
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")
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
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
it "should work" do
# 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)
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
it "should work" do
# assertion here. ref: https://crystal-lang.org/reference/guides/testing.html

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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