Skip to content

Commit 7252d1a

Browse files
authored
[Ruby] add anyOf support (#16147)
* add anyOf support * remove valid? from oneOf template
1 parent f6fefd9 commit 7252d1a

File tree

15 files changed

+311
-36
lines changed

15 files changed

+311
-36
lines changed

modules/openapi-generator/src/main/resources/ruby-client/model.mustache

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@ module {{moduleName}}
1717
{{>partial_oneof_module}}
1818
{{/-first}}
1919
{{/oneOf}}
20+
{{#anyOf}}
21+
{{#-first}}
22+
{{>partial_anyof_module}}
23+
{{/-first}}
24+
{{/anyOf}}
2025
{{^oneOf}}
26+
{{^anyOf}}
2127
{{>partial_model_generic}}
28+
{{/anyOf}}
2229
{{/oneOf}}
2330
{{/isEnum}}
2431
{{/model}}

modules/openapi-generator/src/main/resources/ruby-client/model_test.mustache

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require 'date'
1313
{{#model}}
1414
describe {{moduleName}}::{{classname}} do
1515
{{^oneOf}}
16+
{{^anyOf}}
1617
let(:instance) { {{moduleName}}::{{classname}}.new }
1718

1819
describe 'test an instance of {{classname}}' do
@@ -37,6 +38,7 @@ describe {{moduleName}}::{{classname}} do
3738
end
3839

3940
{{/vars}}
41+
{{/anyOf}}
4042
{{/oneOf}}
4143
{{#oneOf}}
4244
{{#-first}}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{{#description}}
2+
# {{{.}}}
3+
{{/description}}
4+
module {{classname}}
5+
class << self
6+
{{#anyOf}}
7+
{{#-first}}
8+
# List of class defined in anyOf (OpenAPI v3)
9+
def openapi_any_of
10+
[
11+
{{/-first}}
12+
:'{{{.}}}'{{^-last}},{{/-last}}
13+
{{#-last}}
14+
]
15+
end
16+
17+
{{/-last}}
18+
{{/anyOf}}
19+
# Builds the object
20+
# @param [Mixed] Data to be matched against the list of anyOf items
21+
# @return [Object] Returns the model or the data itself
22+
def build(data)
23+
# Go through the list of anyOf items and attempt to identify the appropriate one.
24+
# Note:
25+
# - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 })
26+
# due to the way the deserialization is made in the base_object template (it just casts without verifying).
27+
# - TODO: scalar values are de facto behaving as if they were nullable.
28+
# - TODO: logging when debugging is set.
29+
openapi_any_of.each do |klass|
30+
begin
31+
next if klass == :AnyType # "nullable: true"
32+
typed_data = find_and_cast_into_type(klass, data)
33+
return typed_data if typed_data
34+
rescue # rescue all errors so we keep iterating even if the current item lookup raises
35+
end
36+
end
37+
38+
openapi_any_of.include?(:AnyType) ? data : nil
39+
end
40+
41+
private
42+
43+
SchemaMismatchError = Class.new(StandardError)
44+
45+
# Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse.
46+
def find_and_cast_into_type(klass, data)
47+
return if data.nil?
48+
49+
case klass.to_s
50+
when 'Boolean'
51+
return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass)
52+
when 'Float'
53+
return data if data.instance_of?(Float)
54+
when 'Integer'
55+
return data if data.instance_of?(Integer)
56+
when 'Time'
57+
return Time.parse(data)
58+
when 'Date'
59+
return Date.parse(data)
60+
when 'String'
61+
return data if data.instance_of?(String)
62+
when 'Object' # "type: object"
63+
return data if data.instance_of?(Hash)
64+
when /\AArray<(?<sub_type>.+)>\z/ # "type: array"
65+
if data.instance_of?(Array)
66+
sub_type = Regexp.last_match[:sub_type]
67+
return data.map { |item| find_and_cast_into_type(sub_type, item) }
68+
end
69+
when /\AHash<String, (?<sub_type>.+)>\z/ # "type: object" with "additionalProperties: { ... }"
70+
if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) }
71+
sub_type = Regexp.last_match[:sub_type]
72+
return data.each_with_object({}) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) }
73+
end
74+
else # model
75+
const = {{moduleName}}.const_get(klass)
76+
if const
77+
if const.respond_to?(:openapi_any_of) # nested anyOf model
78+
model = const.build(data)
79+
return model if model
80+
else
81+
# raise if data contains keys that are not known to the model
82+
raise unless (data.keys - const.acceptable_attributes).empty?
83+
model = const.build_from_hash(data)
84+
return model if model && model.valid?
85+
end
86+
end
87+
end
88+
89+
raise # if no match by now, raise
90+
rescue
91+
raise SchemaMismatchError, "#{data} doesn't match the #{klass} type"
92+
end
93+
end
94+
end

modules/openapi-generator/src/main/resources/ruby-client/partial_oneof_module.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
# raise if data contains keys that are not known to the model
124124
raise unless (data.keys - const.acceptable_attributes).empty?
125125
model = const.build_from_hash(data)
126-
return model if model && model.valid?
126+
return model if model
127127
end
128128
end
129129
end

modules/openapi-generator/src/test/resources/3_0/ruby/petstore-with-fake-endpoints-models-for-testing.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,10 @@ components:
19611961
enum:
19621962
- admin
19631963
- user
1964+
mammal_anyof:
1965+
anyOf:
1966+
- $ref: '#/components/schemas/whale'
1967+
- $ref: '#/components/schemas/zebra'
19641968
mammal_without_discriminator:
19651969
oneOf:
19661970
- $ref: '#/components/schemas/whale'

samples/client/petstore/ruby/.openapi-generator/FILES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ docs/HasOnlyReadOnly.md
3737
docs/HealthCheckResult.md
3838
docs/List.md
3939
docs/Mammal.md
40+
docs/MammalAnyof.md
4041
docs/MammalWithoutDiscriminator.md
4142
docs/MapTest.md
4243
docs/MixedPropertiesAndAdditionalPropertiesClass.md
@@ -103,6 +104,7 @@ lib/petstore/models/has_only_read_only.rb
103104
lib/petstore/models/health_check_result.rb
104105
lib/petstore/models/list.rb
105106
lib/petstore/models/mammal.rb
107+
lib/petstore/models/mammal_anyof.rb
106108
lib/petstore/models/mammal_without_discriminator.rb
107109
lib/petstore/models/map_test.rb
108110
lib/petstore/models/mixed_properties_and_additional_properties_class.rb

samples/client/petstore/ruby/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ Class | Method | HTTP request | Description
148148
- [Petstore::HealthCheckResult](docs/HealthCheckResult.md)
149149
- [Petstore::List](docs/List.md)
150150
- [Petstore::Mammal](docs/Mammal.md)
151+
- [Petstore::MammalAnyof](docs/MammalAnyof.md)
151152
- [Petstore::MammalWithoutDiscriminator](docs/MammalWithoutDiscriminator.md)
152153
- [Petstore::MapTest](docs/MapTest.md)
153154
- [Petstore::MixedPropertiesAndAdditionalPropertiesClass](docs/MixedPropertiesAndAdditionalPropertiesClass.md)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Petstore::MammalAnyof
2+
3+
## Properties
4+
5+
| Name | Type | Description | Notes |
6+
| ---- | ---- | ----------- | ----- |
7+
| **has_baleen** | **Boolean** | | [optional] |
8+
| **has_teeth** | **Boolean** | | [optional] |
9+
| **classname** | **String** | | |
10+
| **type** | **String** | | [optional] |
11+
12+
## Example
13+
14+
```ruby
15+
require 'petstore'
16+
17+
instance = Petstore::MammalAnyof.new(
18+
has_baleen: null,
19+
has_teeth: null,
20+
classname: null,
21+
type: null
22+
)
23+
```
24+

samples/client/petstore/ruby/lib/petstore.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
require 'petstore/models/health_check_result'
4343
require 'petstore/models/list'
4444
require 'petstore/models/mammal'
45+
require 'petstore/models/mammal_anyof'
4546
require 'petstore/models/mammal_without_discriminator'
4647
require 'petstore/models/map_test'
4748
require 'petstore/models/mixed_properties_and_additional_properties_class'
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
=begin
2+
#OpenAPI Petstore
3+
4+
#This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
5+
6+
The version of the OpenAPI document: 1.0.0
7+
8+
Generated by: https://openapi-generator.tech
9+
OpenAPI Generator version: 7.0.0-SNAPSHOT
10+
11+
=end
12+
13+
require 'date'
14+
require 'time'
15+
16+
module Petstore
17+
module MammalAnyof
18+
class << self
19+
# List of class defined in anyOf (OpenAPI v3)
20+
def openapi_any_of
21+
[
22+
:'Whale',
23+
:'Zebra'
24+
]
25+
end
26+
27+
# Builds the object
28+
# @param [Mixed] Data to be matched against the list of anyOf items
29+
# @return [Object] Returns the model or the data itself
30+
def build(data)
31+
# Go through the list of anyOf items and attempt to identify the appropriate one.
32+
# Note:
33+
# - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 })
34+
# due to the way the deserialization is made in the base_object template (it just casts without verifying).
35+
# - TODO: scalar values are de facto behaving as if they were nullable.
36+
# - TODO: logging when debugging is set.
37+
openapi_any_of.each do |klass|
38+
begin
39+
next if klass == :AnyType # "nullable: true"
40+
typed_data = find_and_cast_into_type(klass, data)
41+
return typed_data if typed_data
42+
rescue # rescue all errors so we keep iterating even if the current item lookup raises
43+
end
44+
end
45+
46+
openapi_any_of.include?(:AnyType) ? data : nil
47+
end
48+
49+
private
50+
51+
SchemaMismatchError = Class.new(StandardError)
52+
53+
# Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse.
54+
def find_and_cast_into_type(klass, data)
55+
return if data.nil?
56+
57+
case klass.to_s
58+
when 'Boolean'
59+
return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass)
60+
when 'Float'
61+
return data if data.instance_of?(Float)
62+
when 'Integer'
63+
return data if data.instance_of?(Integer)
64+
when 'Time'
65+
return Time.parse(data)
66+
when 'Date'
67+
return Date.parse(data)
68+
when 'String'
69+
return data if data.instance_of?(String)
70+
when 'Object' # "type: object"
71+
return data if data.instance_of?(Hash)
72+
when /\AArray<(?<sub_type>.+)>\z/ # "type: array"
73+
if data.instance_of?(Array)
74+
sub_type = Regexp.last_match[:sub_type]
75+
return data.map { |item| find_and_cast_into_type(sub_type, item) }
76+
end
77+
when /\AHash<String, (?<sub_type>.+)>\z/ # "type: object" with "additionalProperties: { ... }"
78+
if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) }
79+
sub_type = Regexp.last_match[:sub_type]
80+
return data.each_with_object({}) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) }
81+
end
82+
else # model
83+
const = Petstore.const_get(klass)
84+
if const
85+
if const.respond_to?(:openapi_any_of) # nested anyOf model
86+
model = const.build(data)
87+
return model if model
88+
else
89+
# raise if data contains keys that are not known to the model
90+
raise unless (data.keys - const.acceptable_attributes).empty?
91+
model = const.build_from_hash(data)
92+
return model if model && model.valid?
93+
end
94+
end
95+
end
96+
97+
raise # if no match by now, raise
98+
rescue
99+
raise SchemaMismatchError, "#{data} doesn't match the #{klass} type"
100+
end
101+
end
102+
end
103+
104+
end

0 commit comments

Comments
 (0)