Skip to content

Commit

Permalink
[crystal] Fix some issues in crystal client templates (#10629)
Browse files Browse the repository at this point in the history
* Fix some issues in crystal client templates using google drive v3 oas3 api spec

* update crystal petstore sample code

* Address PR comments

Raises on unsupported feature
clean up extra new lines in partial_model_enum_class.mustache

* Use size instead of length

length is undefined for String or Array

* Fix typo

* Use default value instead of nil

* remove unused template file

* Use ::File instead of File as file type to avoid conflicts

The spec could also have a File model

* support file upload in multipart/form-data post body

* Revert breaking changes in api template

* Use double quotes to quote string values

single quotes are used for single char in crystal

* Update api_client to use global ::File and update petstore samples

* JSON Annotation Field key's value should be double quoted

* Handle nil values for form_params

* Remove default values from method definitions due to grammar error

* Fix integration tests
  • Loading branch information
cyangle committed Oct 23, 2021
1 parent 885a813 commit 27459b5
Show file tree
Hide file tree
Showing 17 changed files with 110 additions and 723 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public CrystalClientCodegen() {
languageSpecificPrimitives.add("Time");
languageSpecificPrimitives.add("Array");
languageSpecificPrimitives.add("Hash");
languageSpecificPrimitives.add("File");
languageSpecificPrimitives.add("::File");
languageSpecificPrimitives.add("Object");

typeMapping.clear();
Expand All @@ -174,7 +174,7 @@ public CrystalClientCodegen() {
typeMapping.put("set", "Set");
typeMapping.put("map", "Hash");
typeMapping.put("object", "Object");
typeMapping.put("file", "File");
typeMapping.put("file", "::File");
typeMapping.put("binary", "String");
typeMapping.put("ByteArray", "String");
typeMapping.put("UUID", "String");
Expand Down Expand Up @@ -802,7 +802,7 @@ public String toDefaultValue(Schema p) {
} else if (p.getDefault() instanceof java.time.OffsetDateTime) {
return "Time.parse(\"" + String.format(Locale.ROOT, ((java.time.OffsetDateTime) p.getDefault()).atZoneSameInstant(ZoneId.systemDefault()).toString(), "") + "\")";
} else {
return "'" + escapeText((String) p.getDefault()) + "'";
return "\"" + escapeText((String) p.getDefault()) + "\"";
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,13 @@ module {{moduleName}}
{{/required}}
{{#hasValidation}}
{{#maxLength}}
if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.to_s.length > {{{maxLength}}}
if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.to_s.size > {{{maxLength}}}
raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, the character length must be smaller than or equal to {{{maxLength}}}.")
end

{{/maxLength}}
{{#minLength}}
if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.to_s.length < {{{minLength}}}
if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.to_s.size < {{{minLength}}}
raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, the character length must be great than or equal to {{{minLength}}}.")
end

Expand All @@ -111,13 +111,13 @@ module {{moduleName}}

{{/pattern}}
{{#maxItems}}
if @api_client.config.client_side_validation && {{^required}}{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.length > {{{maxItems}}}
if @api_client.config.client_side_validation && {{^required}}{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.size > {{{maxItems}}}
raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, number of items must be less than or equal to {{{maxItems}}}.")
end

{{/maxItems}}
{{#minItems}}
if @api_client.config.client_side_validation && {{^required}}{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.length < {{{minItems}}}
if @api_client.config.client_side_validation && {{^required}}{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.size < {{{minItems}}}
raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, number of items must be greater than or equal to {{{minItems}}}.")
end

Expand All @@ -130,7 +130,7 @@ module {{moduleName}}
# query parameters
query_params = Hash(String, String).new
{{#queryParams}}
query_params["{{{baseName}}}"] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}}
query_params["{{{baseName}}}"] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}.to_s unless {{{paramName}}}.nil?{{/collectionFormat}}
{{/queryParams}}

# header parameters
Expand All @@ -148,9 +148,9 @@ module {{moduleName}}
{{/headerParams}}

# form parameters
form_params = Hash(Symbol, String).new
form_params = Hash(Symbol, (String | ::File)).new
{{#formParams}}
form_params[:"{{baseName}}"] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}}
form_params[:"{{baseName}}"] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}} unless {{{paramName}}}.nil?{{/collectionFormat}}
{{/formParams}}

# http body (model)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,118 +38,6 @@ module {{moduleName}}
(mime == "*/*") || !(mime =~ /Application\/.*json(?!p)(;.*)?/i).nil?
end

# Deserialize the response to the given return type.
#
# @param [Response] response HTTP response
# @param [String] return_type some examples: "User", "Array<User>", "Hash<String, Integer>"
def deserialize(response, return_type)
body = response.body

# handle file downloading - return the File instance processed in request callbacks
# note that response body is empty when the file is written in chunks in request on_body callback
if return_type == "File"
content_disposition = response.headers["Content-Disposition"].to_s
if content_disposition && content_disposition =~ /filename=/i
filename = content_disposition.match(/filename=[""]?([^""\s]+)[""]?/i).try &.[0]
prefix = sanitize_filename(filename)
else
prefix = "download-"
end
if !prefix.nil? && prefix.ends_with?("-")
prefix = prefix + "-"
end
encoding = response.headers["Content-Encoding"].to_s

# TODO add file support
raise ApiError.new(code: 0, message: "File response not yet supported in the client.") if return_type
return nil

#@tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding)
#@tempfile.write(@stream.join.force_encoding(encoding))
#@tempfile.close
#Log.info { "Temp file written to #{@tempfile.path}, please copy the file to a proper folder "\
# "with e.g. `FileUtils.cp(tempfile.path, \"/new/file/path\")` otherwise the temp file "\
# "will be deleted automatically with GC. It's also recommended to delete the temp file "\
# "explicitly with `tempfile.delete`" }
#return @tempfile
end

return nil if body.nil? || body.empty?

# return response body directly for String return type
return body if return_type == "String"

# ensuring a default content type
content_type = response.headers["Content-Type"] || "application/json"

raise ApiError.new(code: 0, message: "Content-Type is not supported: #{content_type}") unless json_mime?(content_type)

begin
data = JSON.parse("[#{body}]")[0]
rescue e : Exception
if %w(String Date Time).includes?(return_type)
data = body
else
raise e
end
end

convert_to_type data, return_type
end

# Convert data to the given return type.
# @param [Object] data Data to be converted
# @param [String] return_type Return type
# @return [Mixed] Data in a particular type
def convert_to_type(data, return_type)
return nil if data.nil?
case return_type
when "String"
data.to_s
when "Integer"
data.to_s.to_i
when "Float"
data.to_s.to_f
when "Boolean"
data == true
when "Time"
# parse date time (expecting ISO 8601 format)
Time.parse! data.to_s, "%Y-%m-%dT%H:%M:%S%Z"
when "Date"
# parse date (expecting ISO 8601 format)
Time.parse! data.to_s, "%Y-%m-%d"
when "Object"
# generic object (usually a Hash), return directly
data
when /\AArray<(.+)>\z/
# e.g. Array<Pet>
sub_type = $1
data.map { |item| convert_to_type(item, sub_type) }
when /\AHash\<String, (.+)\>\z/
# e.g. Hash<String, Integer>
sub_type = $1
({} of Symbol => String).tap do |hash|
data.each { |k, v| hash[k] = convert_to_type(v, sub_type) }
end
else
# models (e.g. Pet) or oneOf
klass = Petstore.const_get(return_type)
klass.respond_to?(:openapi_one_of) ? klass.build(data) : klass.build_from_hash(data)
end
end

# Sanitize filename by removing path.
# e.g. ../../sun.gif becomes sun.gif
#
# @param [String] filename the filename to be sanitized
# @return [String] the sanitized filename
def sanitize_filename(filename)
if filename.nil?
return nil
else
filename.gsub(/.*[\/\\]/, "")
end
end

def build_request_url(path : String, operation : Symbol)
# Add leading and trailing slashes to path
Expand Down Expand Up @@ -207,31 +95,6 @@ module {{moduleName}}
json_content_type || content_types.first
end

# Convert object (array, hash, object, etc) to JSON string.
# @param [Object] model object to be converted into JSON string
# @return [String] JSON string representation of the object
def object_to_http_body(model)
return model if model.nil? || model.is_a?(String)
local_body = nil
if model.is_a?(Array)
local_body = model.map { |m| object_to_hash(m) }
else
local_body = object_to_hash(model)
end
local_body.to_json
end

# Convert object(non-array) to hash.
# @param [Object] obj object to be converted into JSON string
# @return [String] JSON string representation of the object
def object_to_hash(obj)
if obj.respond_to?(:to_hash)
obj.to_hash
else
obj
end
end

# Build parameter value according to the given collection format.
# @param [String] collection_format one of :csv, :ssv, :tsv, :pipes and :multi
def build_collection_param(param, collection_format)
Expand All @@ -245,18 +108,18 @@ module {{moduleName}}
when :pipes
param.join("|")
when :multi
# return the array directly as typhoeus will handle it as expected
param
# TODO: Need to fix this
raise "multi is not supported yet"
else
fail "unknown collection format: #{collection_format.inspect}"
raise "unknown collection format: #{collection_format.inspect}"
end
end

# Call an API with given options.
#
# @return [Array<(Object, Integer, Hash)>] an array of 3 elements:
# the data deserialized from response body (could be nil), response status code and response headers.
def call_api(http_method : Symbol, path : String, operation : Symbol, return_type : String, post_body : String?, auth_names = [] of String, header_params = {} of String => String, query_params = {} of String => String, form_params = {} of Symbol => String)
def call_api(http_method : Symbol, path : String, operation : Symbol, return_type : String?, post_body : String?, auth_names = [] of String, header_params = {} of String => String, query_params = {} of String => String, form_params = {} of Symbol => (String | ::File))
#ssl_options = {
# :ca_file => @config.ssl_ca_file,
# :verify => @config.ssl_verify,
Expand All @@ -265,15 +128,6 @@ module {{moduleName}}
# :client_key => @config.ssl_client_key
#}

#connection = Faraday.new(:url => config.base_url, :ssl => ssl_options) do |conn|
# conn.basic_auth(config.username, config.password)
# if opts[:header_params]["Content-Type"] == "multipart/form-data"
# conn.request :multipart
# conn.request :url_encoded
# end
# conn.adapter(Faraday.default_adapter)
#end

update_params_for_auth! header_params, query_params, auth_names

if !post_body.nil? && !post_body.empty?
Expand Down Expand Up @@ -315,90 +169,5 @@ module {{moduleName}}

return response.body, response.status_code, response.headers
end

# Builds the HTTP request
#
# @param [String] http_method HTTP method/verb (e.g. POST)
# @param [String] path URL path (e.g. /account/new)
# @option opts [Hash] :header_params Header parameters
# @option opts [Hash] :query_params Query parameters
# @option opts [Hash] :form_params Query parameters
# @option opts [Object] :body HTTP body (JSON/XML)
# @return [Typhoeus::Request] A Typhoeus Request
def build_request(http_method, path, request, opts = {} of Symbol => String)
url = build_request_url(path, opts)
http_method = http_method.to_sym.downcase

header_params = @default_headers.merge(opts[:header_params] || {} of Symbole => String)
query_params = opts[:query_params] || {} of Symbol => String
form_params = opts[:form_params] || {} of Symbol => String

update_params_for_auth! header_params, query_params, opts[:auth_names]

req_opts = {
:method => http_method,
:headers => header_params,
:params => query_params,
:params_encoding => @config.params_encoding,
:timeout => @config.timeout,
:verbose => @config.debugging
}

if [:post, :patch, :put, :delete].includes?(http_method)
req_body = build_request_body(header_params, form_params, opts[:body])
req_opts.update body: req_body
if @config.debugging
Log.debug {"HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n"}
end
end
request.headers = header_params
request.body = req_body
request.url url
request.params = query_params
download_file(request) if opts[:return_type] == "File"
request
end

# Builds the HTTP request body
#
# @param [Hash] header_params Header parameters
# @param [Hash] form_params Query parameters
# @param [Object] body HTTP body (JSON/XML)
# @return [String] HTTP body data in the form of string
def build_request_body(header_params, form_params, body)
# http form
if header_params["Content-Type"] == "application/x-www-form-urlencoded"
data = URI.encode_www_form(form_params)
elsif header_params["Content-Type"] == "multipart/form-data"
data = {} of Symbol => String
form_params.each do |key, value|
case value
when ::File, ::Tempfile
# TODO hardcode to application/octet-stream, need better way to detect content type
data[key] = Faraday::UploadIO.new(value.path, "application/octet-stream", value.path)
when ::Array, nil
# let Faraday handle Array and nil parameters
data[key] = value
else
data[key] = value.to_s
end
end
elsif body
data = body.is_a?(String) ? body : body.to_json
else
data = nil
end
data
end

# TODO fix streaming response
#def download_file(request)
# @stream = []

# # handle streaming Responses
# request.options.on_data = Proc.new do |chunk, overall_received_bytes|
# @stream << chunk
# end
#end
end
end
Loading

0 comments on commit 27459b5

Please sign in to comment.