diff --git a/modules/openapi-generator/src/main/resources/ruby-client/model.mustache b/modules/openapi-generator/src/main/resources/ruby-client/model.mustache index fa8f8c5bbb17..64eea20eaee1 100644 --- a/modules/openapi-generator/src/main/resources/ruby-client/model.mustache +++ b/modules/openapi-generator/src/main/resources/ruby-client/model.mustache @@ -17,8 +17,15 @@ module {{moduleName}} {{>partial_oneof_module}} {{/-first}} {{/oneOf}} +{{#anyOf}} +{{#-first}} +{{>partial_anyof_module}} +{{/-first}} +{{/anyOf}} {{^oneOf}} +{{^anyOf}} {{>partial_model_generic}} +{{/anyOf}} {{/oneOf}} {{/isEnum}} {{/model}} diff --git a/modules/openapi-generator/src/main/resources/ruby-client/model_test.mustache b/modules/openapi-generator/src/main/resources/ruby-client/model_test.mustache index 99148c770a14..a3138fdee2a2 100644 --- a/modules/openapi-generator/src/main/resources/ruby-client/model_test.mustache +++ b/modules/openapi-generator/src/main/resources/ruby-client/model_test.mustache @@ -13,6 +13,7 @@ require 'date' {{#model}} describe {{moduleName}}::{{classname}} do {{^oneOf}} +{{^anyOf}} let(:instance) { {{moduleName}}::{{classname}}.new } describe 'test an instance of {{classname}}' do @@ -37,6 +38,7 @@ describe {{moduleName}}::{{classname}} do end {{/vars}} +{{/anyOf}} {{/oneOf}} {{#oneOf}} {{#-first}} diff --git a/modules/openapi-generator/src/main/resources/ruby-client/partial_anyof_module.mustache b/modules/openapi-generator/src/main/resources/ruby-client/partial_anyof_module.mustache new file mode 100644 index 000000000000..ccd63dc0d85f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/ruby-client/partial_anyof_module.mustache @@ -0,0 +1,94 @@ + {{#description}} + # {{{.}}} + {{/description}} + module {{classname}} + class << self + {{#anyOf}} + {{#-first}} + # List of class defined in anyOf (OpenAPI v3) + def openapi_any_of + [ + {{/-first}} + :'{{{.}}}'{{^-last}},{{/-last}} + {{#-last}} + ] + end + + {{/-last}} + {{/anyOf}} + # Builds the object + # @param [Mixed] Data to be matched against the list of anyOf items + # @return [Object] Returns the model or the data itself + def build(data) + # Go through the list of anyOf items and attempt to identify the appropriate one. + # Note: + # - 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_any_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_any_of.include?(:AnyType) ? data : nil + end + + private + + SchemaMismatchError = Class.new(StandardError) + + # Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse. + def find_and_cast_into_type(klass, data) + return if data.nil? + + case klass.to_s + when 'Boolean' + return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass) + when 'Float' + return data if data.instance_of?(Float) + when 'Integer' + return data if data.instance_of?(Integer) + when 'Time' + return Time.parse(data) + when 'Date' + return Date.parse(data) + when 'String' + return data if data.instance_of?(String) + when 'Object' # "type: object" + return data if data.instance_of?(Hash) + when /\AArray<(?.+)>\z/ # "type: array" + if data.instance_of?(Array) + sub_type = Regexp.last_match[:sub_type] + return data.map { |item| find_and_cast_into_type(sub_type, item) } + end + when /\AHash.+)>\z/ # "type: object" with "additionalProperties: { ... }" + if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) } + sub_type = Regexp.last_match[:sub_type] + return data.each_with_object({}) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) } + end + else # model + const = {{moduleName}}.const_get(klass) + if const + if const.respond_to?(:openapi_any_of) # nested anyOf model + model = const.build(data) + return model if model + else + # raise if data contains keys that are not known to the model + raise unless (data.keys - const.acceptable_attributes).empty? + model = const.build_from_hash(data) + return model if model && model.valid? + end + end + end + + raise # if no match by now, raise + rescue + raise SchemaMismatchError, "#{data} doesn't match the #{klass} type" + end + end + end diff --git a/modules/openapi-generator/src/main/resources/ruby-client/partial_oneof_module.mustache b/modules/openapi-generator/src/main/resources/ruby-client/partial_oneof_module.mustache index b2d22e222fc8..49e7b58d015e 100644 --- a/modules/openapi-generator/src/main/resources/ruby-client/partial_oneof_module.mustache +++ b/modules/openapi-generator/src/main/resources/ruby-client/partial_oneof_module.mustache @@ -123,7 +123,7 @@ # raise if data contains keys that are not known to the model raise unless (data.keys - const.acceptable_attributes).empty? model = const.build_from_hash(data) - return model if model && model.valid? + return model if model end end end diff --git a/modules/openapi-generator/src/test/resources/3_0/ruby/petstore-with-fake-endpoints-models-for-testing.yaml b/modules/openapi-generator/src/test/resources/3_0/ruby/petstore-with-fake-endpoints-models-for-testing.yaml index feb27a17b57b..0c8345a80f55 100644 --- a/modules/openapi-generator/src/test/resources/3_0/ruby/petstore-with-fake-endpoints-models-for-testing.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/ruby/petstore-with-fake-endpoints-models-for-testing.yaml @@ -1961,6 +1961,10 @@ components: enum: - admin - user + mammal_anyof: + anyOf: + - $ref: '#/components/schemas/whale' + - $ref: '#/components/schemas/zebra' mammal_without_discriminator: oneOf: - $ref: '#/components/schemas/whale' diff --git a/samples/client/petstore/ruby/.openapi-generator/FILES b/samples/client/petstore/ruby/.openapi-generator/FILES index a6e825e37fcd..f12b23610fb3 100644 --- a/samples/client/petstore/ruby/.openapi-generator/FILES +++ b/samples/client/petstore/ruby/.openapi-generator/FILES @@ -37,6 +37,7 @@ docs/HasOnlyReadOnly.md docs/HealthCheckResult.md docs/List.md docs/Mammal.md +docs/MammalAnyof.md docs/MammalWithoutDiscriminator.md docs/MapTest.md docs/MixedPropertiesAndAdditionalPropertiesClass.md @@ -103,6 +104,7 @@ lib/petstore/models/has_only_read_only.rb lib/petstore/models/health_check_result.rb lib/petstore/models/list.rb lib/petstore/models/mammal.rb +lib/petstore/models/mammal_anyof.rb lib/petstore/models/mammal_without_discriminator.rb lib/petstore/models/map_test.rb lib/petstore/models/mixed_properties_and_additional_properties_class.rb diff --git a/samples/client/petstore/ruby/README.md b/samples/client/petstore/ruby/README.md index 09b408b59b3d..bf91507cf8bd 100644 --- a/samples/client/petstore/ruby/README.md +++ b/samples/client/petstore/ruby/README.md @@ -148,6 +148,7 @@ Class | Method | HTTP request | Description - [Petstore::HealthCheckResult](docs/HealthCheckResult.md) - [Petstore::List](docs/List.md) - [Petstore::Mammal](docs/Mammal.md) + - [Petstore::MammalAnyof](docs/MammalAnyof.md) - [Petstore::MammalWithoutDiscriminator](docs/MammalWithoutDiscriminator.md) - [Petstore::MapTest](docs/MapTest.md) - [Petstore::MixedPropertiesAndAdditionalPropertiesClass](docs/MixedPropertiesAndAdditionalPropertiesClass.md) diff --git a/samples/client/petstore/ruby/docs/MammalAnyof.md b/samples/client/petstore/ruby/docs/MammalAnyof.md new file mode 100644 index 000000000000..bd21a482f1a6 --- /dev/null +++ b/samples/client/petstore/ruby/docs/MammalAnyof.md @@ -0,0 +1,24 @@ +# Petstore::MammalAnyof + +## Properties + +| Name | Type | Description | Notes | +| ---- | ---- | ----------- | ----- | +| **has_baleen** | **Boolean** | | [optional] | +| **has_teeth** | **Boolean** | | [optional] | +| **classname** | **String** | | | +| **type** | **String** | | [optional] | + +## Example + +```ruby +require 'petstore' + +instance = Petstore::MammalAnyof.new( + has_baleen: null, + has_teeth: null, + classname: null, + type: null +) +``` + diff --git a/samples/client/petstore/ruby/lib/petstore.rb b/samples/client/petstore/ruby/lib/petstore.rb index 08fdc5bf2bc2..455edf770585 100644 --- a/samples/client/petstore/ruby/lib/petstore.rb +++ b/samples/client/petstore/ruby/lib/petstore.rb @@ -42,6 +42,7 @@ require 'petstore/models/health_check_result' require 'petstore/models/list' require 'petstore/models/mammal' +require 'petstore/models/mammal_anyof' require 'petstore/models/mammal_without_discriminator' require 'petstore/models/map_test' require 'petstore/models/mixed_properties_and_additional_properties_class' diff --git a/samples/client/petstore/ruby/lib/petstore/models/mammal_anyof.rb b/samples/client/petstore/ruby/lib/petstore/models/mammal_anyof.rb new file mode 100644 index 000000000000..554b05586174 --- /dev/null +++ b/samples/client/petstore/ruby/lib/petstore/models/mammal_anyof.rb @@ -0,0 +1,104 @@ +=begin +#OpenAPI Petstore + +#This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\ + +The version of the OpenAPI document: 1.0.0 + +Generated by: https://openapi-generator.tech +OpenAPI Generator version: 7.0.0-SNAPSHOT + +=end + +require 'date' +require 'time' + +module Petstore + module MammalAnyof + class << self + # List of class defined in anyOf (OpenAPI v3) + def openapi_any_of + [ + :'Whale', + :'Zebra' + ] + end + + # Builds the object + # @param [Mixed] Data to be matched against the list of anyOf items + # @return [Object] Returns the model or the data itself + def build(data) + # Go through the list of anyOf items and attempt to identify the appropriate one. + # Note: + # - 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_any_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_any_of.include?(:AnyType) ? data : nil + end + + private + + SchemaMismatchError = Class.new(StandardError) + + # Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse. + def find_and_cast_into_type(klass, data) + return if data.nil? + + case klass.to_s + when 'Boolean' + return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass) + when 'Float' + return data if data.instance_of?(Float) + when 'Integer' + return data if data.instance_of?(Integer) + when 'Time' + return Time.parse(data) + when 'Date' + return Date.parse(data) + when 'String' + return data if data.instance_of?(String) + when 'Object' # "type: object" + return data if data.instance_of?(Hash) + when /\AArray<(?.+)>\z/ # "type: array" + if data.instance_of?(Array) + sub_type = Regexp.last_match[:sub_type] + return data.map { |item| find_and_cast_into_type(sub_type, item) } + end + when /\AHash.+)>\z/ # "type: object" with "additionalProperties: { ... }" + if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) } + sub_type = Regexp.last_match[:sub_type] + return data.each_with_object({}) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) } + end + else # model + const = Petstore.const_get(klass) + if const + if const.respond_to?(:openapi_any_of) # nested anyOf model + model = const.build(data) + return model if model + else + # raise if data contains keys that are not known to the model + raise unless (data.keys - const.acceptable_attributes).empty? + model = const.build_from_hash(data) + return model if model && model.valid? + end + end + end + + raise # if no match by now, raise + rescue + raise SchemaMismatchError, "#{data} doesn't match the #{klass} type" + end + end + end + +end diff --git a/samples/client/petstore/ruby/lib/petstore/models/mammal_without_discriminator.rb b/samples/client/petstore/ruby/lib/petstore/models/mammal_without_discriminator.rb index 601f0d01d4d7..d9136c15f666 100644 --- a/samples/client/petstore/ruby/lib/petstore/models/mammal_without_discriminator.rb +++ b/samples/client/petstore/ruby/lib/petstore/models/mammal_without_discriminator.rb @@ -90,7 +90,7 @@ def find_and_cast_into_type(klass, data) # raise if data contains keys that are not known to the model raise unless (data.keys - const.acceptable_attributes).empty? model = const.build_from_hash(data) - return model if model && model.valid? + return model if model end end end diff --git a/samples/client/petstore/ruby/lib/petstore/models/whale.rb b/samples/client/petstore/ruby/lib/petstore/models/whale.rb index 69f96d32d2f0..bdb9ee2bd2ac 100644 --- a/samples/client/petstore/ruby/lib/petstore/models/whale.rb +++ b/samples/client/petstore/ruby/lib/petstore/models/whale.rb @@ -75,12 +75,15 @@ def initialize(attributes = {}) if attributes.key?(:'classname') self.classname = attributes[:'classname'] + else + self.classname = nil end end # Show invalid properties with the reasons. Usually used together with valid? # @return Array for valid properties with the reasons def list_invalid_properties + warn '[DEPRECATED] the `list_invalid_properties` method is obsolete' invalid_properties = Array.new if @classname.nil? invalid_properties.push('invalid value for "classname", classname cannot be nil.') @@ -92,6 +95,7 @@ def list_invalid_properties # Check to see if the all the properties in the model are valid # @return true if the model is valid def valid? + warn '[DEPRECATED] the `valid?` method is obsolete' return false if @classname.nil? true end @@ -122,37 +126,30 @@ def hash # @param [Hash] attributes Model attributes in the form of hash # @return [Object] Returns the model itself def self.build_from_hash(attributes) - new.build_from_hash(attributes) - end - - # Builds the object from hash - # @param [Hash] attributes Model attributes in the form of hash - # @return [Object] Returns the model itself - def build_from_hash(attributes) return nil unless attributes.is_a?(Hash) attributes = attributes.transform_keys(&:to_sym) - self.class.openapi_types.each_pair do |key, type| - if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) - self.send("#{key}=", nil) + transformed_hash = {} + openapi_types.each_pair do |key, type| + if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil? + transformed_hash["#{key}"] = nil elsif type =~ /\AArray<(.*)>/i # check to ensure the input is an array given that the attribute # is documented as an array but the input is not - if attributes[self.class.attribute_map[key]].is_a?(Array) - self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + if attributes[attribute_map[key]].is_a?(Array) + transformed_hash["#{key}"] = attributes[attribute_map[key]].map { |v| _deserialize($1, v) } end - elsif !attributes[self.class.attribute_map[key]].nil? - self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + elsif !attributes[attribute_map[key]].nil? + transformed_hash["#{key}"] = _deserialize(type, attributes[attribute_map[key]]) end end - - self + new(transformed_hash) end # Deserializes the data based on type # @param string type Data type # @param string value Value to be deserialized # @return [Object] Deserialized data - def _deserialize(type, value) + def self._deserialize(type, value) case type.to_sym when :Time Time.parse(value) diff --git a/samples/client/petstore/ruby/lib/petstore/models/zebra.rb b/samples/client/petstore/ruby/lib/petstore/models/zebra.rb index df3904e31ff8..2e8b19214875 100644 --- a/samples/client/petstore/ruby/lib/petstore/models/zebra.rb +++ b/samples/client/petstore/ruby/lib/petstore/models/zebra.rb @@ -89,12 +89,15 @@ def initialize(attributes = {}) if attributes.key?(:'classname') self.classname = attributes[:'classname'] + else + self.classname = nil end end # Show invalid properties with the reasons. Usually used together with valid? # @return Array for valid properties with the reasons def list_invalid_properties + warn '[DEPRECATED] the `list_invalid_properties` method is obsolete' invalid_properties = Array.new if @classname.nil? invalid_properties.push('invalid value for "classname", classname cannot be nil.') @@ -106,6 +109,7 @@ def list_invalid_properties # Check to see if the all the properties in the model are valid # @return true if the model is valid def valid? + warn '[DEPRECATED] the `valid?` method is obsolete' type_validator = EnumAttributeValidator.new('String', ["plains", "mountain", "grevys"]) return false unless type_validator.valid?(@type) return false if @classname.nil? @@ -147,37 +151,30 @@ def hash # @param [Hash] attributes Model attributes in the form of hash # @return [Object] Returns the model itself def self.build_from_hash(attributes) - new.build_from_hash(attributes) - end - - # Builds the object from hash - # @param [Hash] attributes Model attributes in the form of hash - # @return [Object] Returns the model itself - def build_from_hash(attributes) return nil unless attributes.is_a?(Hash) attributes = attributes.transform_keys(&:to_sym) - self.class.openapi_types.each_pair do |key, type| - if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) - self.send("#{key}=", nil) + transformed_hash = {} + openapi_types.each_pair do |key, type| + if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil? + transformed_hash["#{key}"] = nil elsif type =~ /\AArray<(.*)>/i # check to ensure the input is an array given that the attribute # is documented as an array but the input is not - if attributes[self.class.attribute_map[key]].is_a?(Array) - self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + if attributes[attribute_map[key]].is_a?(Array) + transformed_hash["#{key}"] = attributes[attribute_map[key]].map { |v| _deserialize($1, v) } end - elsif !attributes[self.class.attribute_map[key]].nil? - self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + elsif !attributes[attribute_map[key]].nil? + transformed_hash["#{key}"] = _deserialize(type, attributes[attribute_map[key]]) end end - - self + new(transformed_hash) end # Deserializes the data based on type # @param string type Data type # @param string value Value to be deserialized # @return [Object] Deserialized data - def _deserialize(type, value) + def self._deserialize(type, value) case type.to_sym when :Time Time.parse(value) diff --git a/samples/client/petstore/ruby/spec/custom/model_spec.rb b/samples/client/petstore/ruby/spec/custom/model_spec.rb index 8eac76dbe19a..8e7af2c0db3e 100644 --- a/samples/client/petstore/ruby/spec/custom/model_spec.rb +++ b/samples/client/petstore/ruby/spec/custom/model_spec.rb @@ -9,6 +9,27 @@ after do end + describe "anyOf" do + it "should construct a new anyOf object mammal_anyof" do + whale = Petstore::Whale.new('classname' => "Whale", 'has_teeth' => true) + zebra = Petstore::Zebra.new('classname' => "Zebra", 'type' => 'plains') + + # oneOf whale test + expect(whale.to_hash[:'classname']).to eq("Whale") + result = Petstore::MammalAnyof.build(whale.to_hash) + expect(result).to be_a Petstore::Whale + + # oneOf zebra test + expect(zebra.to_hash[:'classname']).to eq("Zebra") + result2 = Petstore::MammalAnyof.build(zebra.to_hash) + expect(result2).to be_a Petstore::Zebra + + # invalid data/hash should result in nil + result3 = Petstore::MammalAnyof.build({"something": 123}) + expect(result3).to be_nil + end + end + describe "oneOf" do it "should construct a new oneOf object mammal" do whale = Petstore::Whale.new('classname' => "Whale", 'has_teeth' => true) diff --git a/samples/client/petstore/ruby/spec/models/mammal_anyof_spec.rb b/samples/client/petstore/ruby/spec/models/mammal_anyof_spec.rb new file mode 100644 index 000000000000..878c48c246ae --- /dev/null +++ b/samples/client/petstore/ruby/spec/models/mammal_anyof_spec.rb @@ -0,0 +1,21 @@ +=begin +#OpenAPI Petstore + +#This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\ + +The version of the OpenAPI document: 1.0.0 + +Generated by: https://openapi-generator.tech +OpenAPI Generator version: 7.0.0-SNAPSHOT + +=end + +require 'spec_helper' +require 'json' +require 'date' + +# Unit tests for Petstore::MammalAnyof +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe Petstore::MammalAnyof do +end