Skip to content

Commit c91df9e

Browse files
authored
[crystal] fix Model#to_h method (#22508)
* fix(crystal): fix Model#to_h method * fix(crystal): make optional parameters truly optional * fix(crystal): update samples * fix(crystal): fixx model validation with nil values * feat(crystal): improve enum validation * fix(crystal): use litteral regex * fix(crystal): call #to_h instead of #to_hash * fix(crystal): update samples
1 parent b86213b commit c91df9e

18 files changed

Lines changed: 435 additions & 351 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CrystalClientCodegen.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import org.openapitools.codegen.model.OperationMap;
3030
import org.openapitools.codegen.model.OperationsMap;
3131
import org.openapitools.codegen.templating.mustache.PrefixWithHashLambda;
32+
import org.openapitools.codegen.templating.mustache.UppercaseLambda;
33+
import org.openapitools.codegen.templating.mustache.TitlecaseLambda;
3234
import org.openapitools.codegen.utils.ModelUtils;
3335
import org.slf4j.Logger;
3436
import org.slf4j.LoggerFactory;
@@ -284,6 +286,7 @@ public void processOpts() {
284286
supportingFiles.add(new SupportingFile("api_error.mustache", shardFolder, "api_error.cr"));
285287
supportingFiles.add(new SupportingFile("configuration.mustache", shardFolder, "configuration.cr"));
286288
supportingFiles.add(new SupportingFile("api_client.mustache", shardFolder, "api_client.cr"));
289+
supportingFiles.add(new SupportingFile("recursive_hash.mustache", shardFolder, "recursive_hash.cr"));
287290
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
288291
supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh"));
289292
supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore"));
@@ -296,6 +299,8 @@ public void processOpts() {
296299

297300
// add lambda for mustache templates
298301
additionalProperties.put("lambdaPrefixWithHash", new PrefixWithHashLambda());
302+
additionalProperties.put("lambdaUppercase", new UppercaseLambda());
303+
additionalProperties.put("lambdaTitlecase", new TitlecaseLambda());
299304

300305
}
301306

modules/openapi-generator/src/main/resources/crystal/base_object.mustache

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -76,44 +76,38 @@
7676
# Returns the string representation of the object
7777
# @return [String] String presentation of the object
7878
def to_s
79-
to_hash.to_s
79+
to_h.to_s
8080
end
8181

82-
# to_body is an alias to to_hash (backward compatibility)
82+
# to_body is an alias to to_h (backward compatibility)
8383
# @return [Hash] Returns the object in the form of hash
8484
def to_body
85-
to_hash
85+
to_h
8686
end
8787

8888
# Returns the object in the form of hash
8989
# @return [Hash] Returns the object in the form of hash
90-
def to_hash
91-
hash = {{^parent}}{} of Symbol => String{{/parent}}{{#parent}}super{{/parent}}
92-
self.class.attribute_map.each_pair do |attr, param|
93-
value = self.send(attr)
94-
if value.nil?
95-
is_nullable = self.class.openapi_nullable.includes?(attr)
96-
next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}"))
97-
end
98-
99-
hash[param] = _to_hash(value)
100-
end
101-
hash
90+
def to_h
91+
hash = NetboxClient::RecursiveHash.new
92+
{{#vars}}
93+
hash["{{{baseName}}}"] = _to_h({{{name}}})
94+
{{/vars}}
95+
hash.to_h
10296
end
10397

10498
# Outputs non-array value in the form of hash
105-
# For object, use to_hash. Otherwise, just return the value
99+
# For object, use to_h. Otherwise, just return the value
106100
# @param [Object] value Any valid value
107101
# @return [Hash] Returns the value in the form of hash
108-
def _to_hash(value)
109-
if value.is_a?(Array)
110-
value.compact.map { |v| _to_hash(v) }
111-
elsif value.is_a?(Hash)
112-
({} of Symbol => String).tap do |hash|
113-
value.each { |k, v| hash[k] = _to_hash(v) }
114-
end
115-
elsif value.respond_to? :to_hash
116-
value.to_hash
102+
private def _to_h(value)
103+
if value.is_a?(Hash)
104+
hash = NetboxClient::RecursiveHash.new
105+
value.each { |k, v| hash[k] = _to_h(v) }
106+
hash
107+
elsif value.is_a?(Array)
108+
value.compact.map { |v| _to_h(v) }
109+
elsif value.responds_to?(:to_h)
110+
value.to_h
117111
else
118112
value
119113
end

modules/openapi-generator/src/main/resources/crystal/partial_model_generic.mustache

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,48 @@
2828

2929
{{/optionalVars}}
3030
{{#hasEnums}}
31-
class EnumAttributeValidator
32-
getter datatype : String
33-
getter allowable_values : Array(String)
34-
35-
def initialize(datatype, allowable_values)
36-
@datatype = datatype
37-
@allowable_values = allowable_values.map do |value|
38-
case datatype.to_s
39-
when /Integer/i
40-
value.to_i
41-
when /Float/i
42-
value.to_f
43-
else
44-
value
45-
end
31+
abstract class EnumAttributeValidator
32+
def valid?(value)
33+
!value || @allowable_values.includes?(value)
34+
end
35+
36+
def message
37+
"invalid value for \"#{@attribute}\", must be one of #{@allowable_values}."
38+
end
39+
40+
def to(_type, value)
41+
case _type
42+
when Int32
43+
value.to_i32
44+
when Int64
45+
value.to_i64
46+
when Float32
47+
value.to_f32
48+
when Float64
49+
value.to_f64
50+
else
51+
value.to_s
4652
end
4753
end
54+
end
4855

49-
def valid?(value)
50-
!value || allowable_values.includes?(value)
56+
{{#vars}}
57+
{{#isEnum}}
58+
{{^isContainer}}
59+
class EnumAttributeValidatorFor{{#lambdaTitlecase}}{{{name}}}{{/lambdaTitlecase}} < EnumAttributeValidator
60+
@attribute : String
61+
@allowable_values : Array(Int32 | Int64 | Float32 | Float64 | String)
62+
63+
def initialize
64+
@attribute = "{{{name}}}"
65+
@allowable_values = [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}].map { |value| to({{{dataType}}}, value)}
5166
end
5267
end
5368

69+
{{/isContainer}}
70+
{{/isEnum}}
71+
{{/vars}}
72+
5473
{{/hasEnums}}
5574
{{#anyOf}}
5675
{{#-first}}
@@ -89,54 +108,64 @@
89108
{{/discriminator}}
90109
# Initializes the object
91110
# @param [Hash] attributes Model attributes in the form of hash
92-
def initialize({{#requiredVars}}@{{{name}}} : {{{dataType}}}{{^-last}}, {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}@{{{name}}} : {{{dataType}}}?{{^-last}}, {{/-last}}{{/optionalVars}})
111+
def initialize({{#requiredVars}}@{{{name}}} : {{{dataType}}}{{^-last}}, {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, {{/hasOptional}}{{/hasRequired}}{{#optionalVars}}@{{{name}}} : {{{dataType}}}? = nil{{^-last}}, {{/-last}}{{/optionalVars}})
93112
end
94113

95114
# Show invalid properties with the reasons. Usually used together with valid?
96115
# @return Array for valid properties with the reasons
97116
def list_invalid_properties
98117
invalid_properties = {{^parent}}Array(String).new{{/parent}}{{#parent}}super{{/parent}}
99118
{{#vars}}
119+
{{#isEnum}}
120+
{{^isContainer}}
121+
{{{name}}}_validator = EnumAttributeValidatorFor{{#lambdaTitlecase}}{{{name}}}{{/lambdaTitlecase}}.new
122+
if !{{{name}}}_validator.valid?(@{{{name}}})
123+
message = {{{name}}}_validator.message
124+
invalid_properties.push(message)
125+
end
126+
127+
{{/isContainer}}
128+
{{/isEnum}}
100129
{{#hasValidation}}
101130
{{#maxLength}}
102-
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.size > {{{maxLength}}}
131+
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.to_s.try &.size.try &.> {{{maxLength}}}
103132
invalid_properties.push("invalid value for \"{{{name}}}\", the character length must be smaller than or equal to {{{maxLength}}}.")
104133
end
105134

106135
{{/maxLength}}
107136
{{#minLength}}
108-
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.size < {{{minLength}}}
137+
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.to_s.try &.size.try &.< {{{minLength}}}
109138
invalid_properties.push("invalid value for \"{{{name}}}\", the character length must be greater than or equal to {{{minLength}}}.")
110139
end
111140

112141
{{/minLength}}
113142
{{#maximum}}
114-
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}}
143+
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.>{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}}
115144
invalid_properties.push("invalid value for \"{{{name}}}\", must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{{maximum}}}.")
116145
end
117146

118147
{{/maximum}}
119148
{{#minimum}}
120-
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}}
149+
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.<{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}}
121150
invalid_properties.push("invalid value for \"{{{name}}}\", must be greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{{minimum}}}.")
122151
end
123152

124153
{{/minimum}}
125154
{{#pattern}}
126-
pattern = Regexp.new({{{pattern}}})
127-
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} !~ pattern
155+
pattern = {{{pattern}}}
156+
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.!~ pattern
128157
invalid_properties.push("invalid value for \"{{{name}}}\", must conform to the pattern #{pattern}.")
129158
end
130159

131160
{{/pattern}}
132161
{{#maxItems}}
133-
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.size > {{{maxItems}}}
162+
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.size.try &.> {{{maxItems}}}
134163
invalid_properties.push("invalid value for \"{{{name}}}\", number of items must be less than or equal to {{{maxItems}}}."
135164
end
136165

137166
{{/maxItems}}
138167
{{#minItems}}
139-
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.size < {{{minItems}}}
168+
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.size.try &.< {{{minItems}}}
140169
invalid_properties.push("invalid value for \"{{{name}}}\", number of items must be greater than or equal to {{{minItems}}}."
141170
end
142171

@@ -152,39 +181,39 @@
152181
{{#vars}}
153182
{{#isEnum}}
154183
{{^isContainer}}
155-
{{{name}}}_validator = EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}])
184+
{{{name}}}_validator = EnumAttributeValidatorFor{{#lambdaTitlecase}}{{{name}}}{{/lambdaTitlecase}}.new
156185
return false unless {{{name}}}_validator.valid?(@{{{name}}})
157186
{{/isContainer}}
158187
{{/isEnum}}
159188
{{#hasValidation}}
160189
{{#maxLength}}
161-
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.size > {{{maxLength}}}
190+
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.to_s.try &.size.try &.> {{{maxLength}}}
162191
{{/maxLength}}
163192
{{#minLength}}
164-
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.size < {{{minLength}}}
193+
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.to_s.try &.size.try &.< {{{minLength}}}
165194
{{/minLength}}
166195
{{#maximum}}
167-
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}}
196+
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.>{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}}
168197
{{/maximum}}
169198
{{#minimum}}
170-
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}}
199+
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.<{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}}
171200
{{/minimum}}
172201
{{#pattern}}
173-
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} !~ Regexp.new({{{pattern}}})
202+
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.!~ {{{pattern}}}
174203
{{/pattern}}
175204
{{#maxItems}}
176-
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.size > {{{maxItems}}}
205+
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.size.try &.> {{{maxItems}}}
177206
{{/maxItems}}
178207
{{#minItems}}
179-
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.size < {{{minItems}}}
208+
return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.try &.size.try &.< {{{minItems}}}
180209
{{/minItems}}
181210
{{/hasValidation}}
182211
{{/vars}}
183212
{{#anyOf}}
184213
{{#-first}}
185214
_any_of_found = false
186215
self.class.openapi_any_of.each do |_class|
187-
_any_of = {{moduleName}}.const_get(_class).build_from_hash(self.to_hash)
216+
_any_of = {{moduleName}}.const_get(_class).build_from_hash(self.to_h)
188217
if _any_of.valid?
189218
_any_of_found = true
190219
end
@@ -205,9 +234,9 @@
205234
# Custom attribute writer method checking allowed values (enum).
206235
# @param [Object] {{{name}}} Object to be assigned
207236
def {{{name}}}=({{{name}}})
208-
validator = EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}])
237+
validator = EnumAttributeValidatorFor{{#lambdaTitlecase}}{{{name}}}{{/lambdaTitlecase}}.new
209238
unless validator.valid?({{{name}}})
210-
raise ArgumentError.new("invalid value for \"{{{name}}}\", must be one of #{validator.allowable_values}.")
239+
raise ArgumentError.new(validator.message)
211240
end
212241
@{{{name}}} = {{{name}}}
213242
end
@@ -244,7 +273,7 @@
244273

245274
{{/minimum}}
246275
{{#pattern}}
247-
pattern = Regexp.new({{{pattern}}})
276+
pattern = {{{pattern}}}
248277
if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}} !~ pattern
249278
raise ArgumentError.new("invalid value for \"{{{name}}}\", must conform to the pattern #{pattern}.")
250279
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module {{moduleName}}
2+
# Define possible value types for our own AnyHash class (RecursiveHash)
3+
alias ValuesType = Nil |
4+
Bool |
5+
String |
6+
Time |
7+
Int32 |
8+
Int64 |
9+
Float32 |
10+
Float64 |
11+
Array(ValuesType)
12+
13+
# Define our own AnyHash class (RecursiveHash)
14+
# RecursiveHash
15+
AnyHash.define_new klass: :RecursiveHash,
16+
key: String,
17+
value: ValuesType
18+
end

modules/openapi-generator/src/main/resources/crystal/shard.mustache

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ description: |
66
- {{{ shardDescription}}}
77
crystal: ">= 0.35.1"
88
dependencies:
9+
any_hash:
10+
github: Sija/any_hash.cr
911
crest:
1012
github: mamantoha/crest
1113
version: ~> 1.3.13

modules/openapi-generator/src/main/resources/crystal/shard_name.mustache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}}
22

33
# Dependencies
4+
require "any_hash"
45
require "crest"
56
require "log"
67

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ src/petstore/models/order.cr
2020
src/petstore/models/pet.cr
2121
src/petstore/models/tag.cr
2222
src/petstore/models/user.cr
23+
src/petstore/recursive_hash.cr

samples/client/petstore/crystal/shard.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ description: |
66
-
77
crystal: ">= 0.35.1"
88
dependencies:
9+
any_hash:
10+
github: Sija/any_hash.cr
911
crest:
1012
github: mamantoha/crest
1113
version: ~> 1.3.13

samples/client/petstore/crystal/src/petstore.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#
1010

1111
# Dependencies
12+
require "any_hash"
1213
require "crest"
1314
require "log"
1415

0 commit comments

Comments
 (0)