Skip to content

Commit c4a6c4a

Browse files
authored
[crystal-lang] Various fixes for Crystal client (#21011)
* fix(crystal): fix typos in Crystal templates * fix(crystal): various fixes for partial_oneof_module.mustache Crystal template 1. `class << self` doesn't exist in Crystal, you must prefix class methods with `self.` 2. use double quotes for String litterals (simple quotes are for Char litterals) 3. global `private` keyword doesn't exist in Crystal, you must prefix private methods with `private` 4. you must specify types when using `each_with_object({})` * fix(crystal): add isKeyInCookie in Crystal configuration.mustache * fix(crystal): wrap `rescue` in a `begin` * fix(crystal): use Spectator shard to run tests (`described_class` is not available in Crystal std lib) * fix(crystal): dry tests * fix(crystal): enable some API tests, mark others as pending * fix(crystal): update samples * fix(crystal): update sample app specs * fix(crystal): install development dependencies
1 parent c761f11 commit c4a6c4a

29 files changed

+276
-288
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ module {{moduleName}}
8787
{{/maxLength}}
8888
{{#minLength}}
8989
if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.to_s.size < {{{minLength}}}
90-
raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, the character length must be great than or equal to {{{minLength}}}.")
90+
raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, the character length must be greater than or equal to {{{minLength}}}.")
9191
end
9292

9393
{{/minLength}}

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

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}}
22

33
require "../spec_helper"
4-
require "json"
5-
require "time"
64

75
# Unit tests for {{moduleName}}::{{classname}}
86
# Automatically generated by openapi-generator (https://openapi-generator.tech)
97
# Please update as you see appropriate
10-
{{#operations}}describe "{{classname}}" do
8+
{{#operations}}
9+
Spectator.describe "{{classname}}" do
1110
describe "test an instance of {{classname}}" do
1211
it "should create an instance of {{classname}}" do
1312
api_instance = {{moduleName}}::{{classname}}.new
14-
# TODO expect(api_instance).to be_instance_of({{moduleName}}::{{classname}})
13+
expect(api_instance).to be_instance_of({{moduleName}}::{{classname}})
1514
end
1615
end
1716

@@ -28,7 +27,7 @@ require "time"
2827
{{#allParams}}{{^required}} # @option opts [{{{dataType}}}] :{{paramName}} {{description}}
2928
{{/required}}{{/allParams}} # @return [{{{returnType}}}{{^returnType}}nil{{/returnType}}]
3029
describe "{{operationId}} test" do
31-
it "should work" do
30+
skip "should work" do
3231
# assertion here. ref: https://crystal-lang.org/reference/guides/testing.html
3332
end
3433
end

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ module {{moduleName}}
214214
{{#isApiKey}}
215215
"{{name}}" => {
216216
type: "api_key",
217-
in: {{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}},
217+
in: {{#isKeyInCookie}}"cookie"{{/isKeyInCookie}}{{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}},
218218
key: "{{keyParamName}}",
219219
value: api_key_with_prefix(:{{keyParamName}})
220220
},

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
require 'spec_helper'
66

7-
describe {{moduleName}}::Configuration do
7+
Spectator.describe {{moduleName}}::Configuration do
88
let(:config) { {{moduleName}}::Configuration.default }
99

1010
before(:each) do

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

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}}
22

33
require "../spec_helper"
4-
require "json"
5-
require "time"
64

75
# Unit tests for {{moduleName}}::{{classname}}
86
# Automatically generated by openapi-generator (https://openapi-generator.tech)
97
# Please update as you see appropriate
108
{{#models}}
119
{{#model}}
12-
describe {{moduleName}}::{{classname}} do
10+
Spectator.describe {{moduleName}}::{{classname}} do
1311
{{^oneOf}}
1412

1513
describe "test an instance of {{classname}}" do
16-
it "should create an instance of {{classname}}" do
14+
skip "should create an instance of {{classname}}" do
1715
#instance = {{moduleName}}::{{classname}}.new
1816
#expect(instance).to be_instance_of({{moduleName}}::{{classname}})
1917
end
2018
end
2119
{{#vars}}
2220
describe "test attribute '{{{name}}}'" do
23-
it "should work" do
21+
skip "should work" do
2422
{{#isEnum}}
2523
# assertion here. ref: https://crystal-lang.org/reference/guides/testing.html
2624
# validator = Petstore::EnumTest::EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}])
@@ -65,7 +63,7 @@ describe {{moduleName}}::{{classname}} do
6563
{{/mappedModels}}
6664
{{/discriminator}}
6765
describe ".build" do
68-
it "returns the correct model" do
66+
skip "returns the correct model" do
6967
end
7068
end
7169
{{/-first}}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
{{/maxLength}}
106106
{{#minLength}}
107107
if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.size < {{{minLength}}}
108-
invalid_properties.push("invalid value for \"{{{name}}}\", the character length must be great than or equal to {{{minLength}}}.")
108+
invalid_properties.push("invalid value for \"{{{name}}}\", the character length must be greater than or equal to {{{minLength}}}.")
109109
end
110110

111111
{{/minLength}}
@@ -226,7 +226,7 @@
226226
{{/maxLength}}
227227
{{#minLength}}
228228
if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}}.to_s.size < {{{minLength}}}
229-
raise ArgumentError.new("invalid value for \"{{{name}}}\", the character length must be great than or equal to {{{minLength}}}.")
229+
raise ArgumentError.new("invalid value for \"{{{name}}}\", the character length must be greater than or equal to {{{minLength}}}.")
230230
end
231231

232232
{{/minLength}}

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

+82-84
Original file line numberDiff line numberDiff line change
@@ -2,106 +2,104 @@
22
# {{{.}}}
33
{{/description}}
44
module {{classname}}
5-
class << self
6-
{{#oneOf}}
7-
{{#-first}}
8-
# List of class defined in oneOf (OpenAPI v3)
9-
def openapi_one_of
10-
[
11-
{{/-first}}
12-
:'{{{.}}}'{{^-last}},{{/-last}}
13-
{{#-last}}
14-
]
15-
end
5+
{{#oneOf}}
6+
{{#-first}}
7+
# List of class defined in oneOf (OpenAPI v3)
8+
def self.openapi_one_of
9+
[
10+
{{/-first}}
11+
:"{{{.}}}"{{^-last}},{{/-last}}
12+
{{#-last}}
13+
]
14+
end
1615

17-
{{/-last}}
18-
{{/oneOf}}
19-
{{#discriminator}}
20-
{{#propertyName}}
21-
# Discriminator's property name (OpenAPI v3)
22-
def openapi_discriminator_name
23-
:'{{{.}}}'
24-
end
16+
{{/-last}}
17+
{{/oneOf}}
18+
{{#discriminator}}
19+
{{#propertyName}}
20+
# Discriminator's property name (OpenAPI v3)
21+
def self.openapi_discriminator_name
22+
:"{{{.}}}"
23+
end
2524

26-
{{/propertyName}}
27-
{{#mappedModels}}
28-
{{#-first}}
29-
# Discriminator's mapping (OpenAPI v3)
30-
def openapi_discriminator_mapping
31-
{
32-
{{/-first}}
33-
:'{{{mappingName}}}' => :'{{{modelName}}}'{{^-last}},{{/-last}}
34-
{{#-last}}
35-
}
36-
end
25+
{{/propertyName}}
26+
{{#mappedModels}}
27+
{{#-first}}
28+
# Discriminator's mapping (OpenAPI v3)
29+
def self.openapi_discriminator_mapping
30+
{
31+
{{/-first}}
32+
:"{{{mappingName}}}" => :"{{{modelName}}}"{{^-last}},{{/-last}}
33+
{{#-last}}
34+
}
35+
end
3736

38-
{{/-last}}
39-
{{/mappedModels}}
40-
{{/discriminator}}
41-
# Builds the object
42-
# @param [Mixed] Data to be matched against the list of oneOf items
43-
# @return [Object] Returns the model or the data itself
44-
def build(data)
45-
{{#discriminator}}
46-
discriminator_value = data[openapi_discriminator_name]
47-
return nil unless discriminator_value
48-
{{#mappedModels}}
49-
{{#-first}}
37+
{{/-last}}
38+
{{/mappedModels}}
39+
{{/discriminator}}
40+
# Builds the object
41+
# @param [Mixed] Data to be matched against the list of oneOf items
42+
# @return [Object] Returns the model or the data itself
43+
def self.build(data)
44+
{{#discriminator}}
45+
discriminator_value = data[openapi_discriminator_name]
46+
return nil unless discriminator_value
47+
{{#mappedModels}}
48+
{{#-first}}
5049

51-
klass = openapi_discriminator_mapping[discriminator_value.to_sym]
52-
return nil unless klass
50+
klass = openapi_discriminator_mapping[discriminator_value.to_sym]
51+
return nil unless klass
5352

54-
{{moduleName}}.const_get(klass).build_from_hash(data)
55-
{{/-first}}
56-
{{/mappedModels}}
57-
{{^mappedModels}}
58-
{{moduleName}}.const_get(discriminator_value).build_from_hash(data)
59-
{{/mappedModels}}
60-
{{/discriminator}}
61-
{{^discriminator}}
62-
# Go through the list of oneOf items and attempt to identify the appropriate one.
63-
# Note:
64-
# - We do not attempt to check whether exactly one item matches.
65-
# - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 })
66-
# due to the way the deserialization is made in the base_object template (it just casts without verifying).
67-
# - TODO: scalar values are de facto behaving as if they were nullable.
68-
# - TODO: logging when debugging is set.
69-
openapi_one_of.each do |klass|
70-
begin
71-
next if klass == :AnyType # "nullable: true"
72-
typed_data = find_and_cast_into_type(klass, data)
73-
return typed_data if typed_data
74-
rescue # rescue all errors so we keep iterating even if the current item lookup raises
75-
end
53+
{{moduleName}}.const_get(klass).build_from_hash(data)
54+
{{/-first}}
55+
{{/mappedModels}}
56+
{{^mappedModels}}
57+
{{moduleName}}.const_get(discriminator_value).build_from_hash(data)
58+
{{/mappedModels}}
59+
{{/discriminator}}
60+
{{^discriminator}}
61+
# Go through the list of oneOf items and attempt to identify the appropriate one.
62+
# Note:
63+
# - We do not attempt to check whether exactly one item matches.
64+
# - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 })
65+
# due to the way the deserialization is made in the base_object template (it just casts without verifying).
66+
# - TODO: scalar values are de facto behaving as if they were nullable.
67+
# - TODO: logging when debugging is set.
68+
openapi_one_of.each do |klass|
69+
begin
70+
next if klass == :AnyType # "nullable: true"
71+
typed_data = find_and_cast_into_type(klass, data)
72+
return typed_data if typed_data
73+
rescue # rescue all errors so we keep iterating even if the current item lookup raises
7674
end
77-
78-
openapi_one_of.includes?(:AnyType) ? data : nil
79-
{{/discriminator}}
8075
end
81-
{{^discriminator}}
8276

83-
private
77+
openapi_one_of.includes?(:AnyType) ? data : nil
78+
{{/discriminator}}
79+
end
80+
{{^discriminator}}
8481

85-
SchemaMismatchError = Class.new(StandardError)
82+
SchemaMismatchError = Class.new(StandardError)
8683

87-
# Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse.
88-
def find_and_cast_into_type(klass, data)
89-
return if data.nil?
84+
# Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse.
85+
private def self.find_and_cast_into_type(klass, data)
86+
return if data.nil?
9087

88+
begin
9189
case klass.to_s
92-
when 'Boolean'
90+
when "Boolean"
9391
return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass)
94-
when 'Float'
92+
when "Float"
9593
return data if data.instance_of?(Float)
96-
when 'Integer'
94+
when "Integer"
9795
return data if data.instance_of?(Integer)
98-
when 'Time'
96+
when "Time"
9997
return Time.parse(data)
100-
when 'Date'
98+
when "Date"
10199
return Date.parse(data)
102-
when 'String'
100+
when "String"
103101
return data if data.instance_of?(String)
104-
when 'Object' # "type: object"
102+
when "Object" # "type: object"
105103
return data if data.instance_of?(Hash)
106104
when /\AArray<(?<sub_type>.+)>\z/ # "type: array"
107105
if data.instance_of?(Array)
@@ -111,7 +109,7 @@
111109
when /\AHash<String, (?<sub_type>.+)>\z/ # "type: object" with "additionalProperties: { ... }"
112110
if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) }
113111
sub_type = Regexp.last_match[:sub_type]
114-
return data.each_with_object({}) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) }
112+
return data.each_with_object({} of String | Symbol => Bool | Float | Integer | Time | Date | String | Array | Hash) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) }
115113
end
116114
else # model
117115
const = {{moduleName}}.const_get(klass)
@@ -132,6 +130,6 @@
132130
rescue
133131
raise SchemaMismatchError, "#{data} doesn't match the #{klass} type"
134132
end
135-
{{/discriminator}}
136133
end
134+
{{/discriminator}}
137135
end

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

+3
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,8 @@ development_dependencies:
1616
version: ~>1.5.0
1717
ameba:
1818
github: crystal-ameba/ameba
19+
spectator:
20+
gitlab: arctic-fox/spectator
21+
version: ~> 0.12.0
1922

2023
license: {{{shardLicense}}}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}}
22

33
# load modules
4-
require "spec"
4+
require "spectator"
55
require "json"
6+
require "time"
67
require "../src/{{{shardName}}}"
78

89
def assert_compilation_error(path : String, message : String) : Nil

samples/client/petstore/crystal/pom.xml

-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
<arguments>
3838
<argument>install</argument>
3939
<argument>--ignore-crystal-version</argument>
40-
<argument>--without-development</argument>
4140
</arguments>
4241
</configuration>
4342
</execution>

samples/client/petstore/crystal/shard.lock

+24
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
version: 2.0
22
shards:
3+
ameba:
4+
git: https://github.com/crystal-ameba/ameba.git
5+
version: 1.6.4
6+
7+
backtracer:
8+
git: https://github.com/sija/backtracer.cr.git
9+
version: 1.2.4
10+
311
crest:
412
git: https://github.com/mamantoha/crest.git
513
version: 1.3.13
614

15+
exception_page:
16+
git: https://github.com/crystal-loot/exception_page.git
17+
version: 0.4.1
18+
719
http-client-digest_auth:
820
git: https://github.com/mamantoha/http-client-digest_auth.git
921
version: 0.6.0
@@ -12,3 +24,15 @@ shards:
1224
git: https://github.com/mamantoha/http_proxy.git
1325
version: 0.10.3
1426

27+
kemal:
28+
git: https://github.com/kemalcr/kemal.git
29+
version: 1.5.0
30+
31+
radix:
32+
git: https://github.com/luislavena/radix.git
33+
version: 0.4.1
34+
35+
spectator:
36+
git: https://gitlab.com/arctic-fox/spectator.git
37+
version: 0.12.1
38+

samples/client/petstore/crystal/shard.yml

+3
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,8 @@ development_dependencies:
1616
version: ~>1.5.0
1717
ameba:
1818
github: crystal-ameba/ameba
19+
spectator:
20+
gitlab: arctic-fox/spectator
21+
version: ~> 0.12.0
1922

2023
license:

0 commit comments

Comments
 (0)