diff --git a/turnkey_client/lib/turnkey_client/api_client.rb b/turnkey_client/lib/turnkey_client/api_client.rb index fa9ed48..ccab3f2 100644 --- a/turnkey_client/lib/turnkey_client/api_client.rb +++ b/turnkey_client/lib/turnkey_client/api_client.rb @@ -1,446 +1,448 @@ -=begin -#API Reference - -#Review our [API Introduction](../api-introduction) to get started. - -OpenAPI spec version: 1.0 - -Generated by: https://github.com/swagger-api/swagger-codegen.git -=end - -require 'date' -require 'json' -require 'logger' -require 'tempfile' -require 'typhoeus' -require 'uri' -require 'openssl' -require 'json' -require 'base64' - -ASN1_DER_P256_PRIVATE_KEY_PREFIX = "308141020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420" - -module TurnkeyClient - class ApiClient - # The Configuration object holding settings to be used in the API client. - attr_accessor :config - - # Defines the headers to be used in HTTP requests of all API calls by default. - # - # @return [Hash] - attr_accessor :default_headers - - # Initializes the ApiClient - # @option config [Configuration] Configuration for initializing the object - def initialize(config) - @config = config - @user_agent = "Swagger-Codegen/#{VERSION}/ruby" - @default_headers = { - 'Content-Type' => 'application/json', - 'User-Agent' => @user_agent, - 'X-Client-Version' => "turnkey-ruby/#{VERSION}" - } - - if config.api_public_key.nil? - raise "Unable to find :api_public_key in configuration. Please configure it with `TurnkeyClient.configure`" - end - - if config.api_private_key.nil? - raise "Unable to find :api_private_key in configuration. Please configure it with `TurnkeyClient.configure`" - end - - # This hex prefix represents the start of a private key object in ASN.1, encoded in DER - # This is necessary to load the private key in raw format with openssl. - pkcs8_hex = ASN1_DER_P256_PRIVATE_KEY_PREFIX + config.api_private_key - - # See https://anthonylewis.com/2011/02/09/to-hex-and-back-with-ruby/ - pkcs8_bytes = pkcs8_hex.scan(/../).map { |x| x.hex }.pack('c*') - - @turnkey_api_key = OpenSSL::PKey.read(pkcs8_bytes) - - # Check that we loaded the right key by comparing the public key to the passed in public key - if self.turnkey_api_public_key != config.api_public_key.downcase - raise "Expected public key to be #{config.api_public_key} (value configured), but computed #{self.turnkey_api_public_key} from configured private key" - end - end - - # Return the hex-encoded public key in compressed form - def turnkey_api_public_key - # This gives the public point as a [OpenSSL::BN](https://ruby-doc.org/stdlib-2.4.1/libdoc/openssl/rdoc/OpenSSL/BN.html) - public_key_bn = @turnkey_api_key.public_key.to_bn :compressed - public_key_hex = public_key_bn.to_s 16 - public_key_hex.downcase - end - - # Given a string (often, a POST request body), returns a hash with the correct auth header - # to authenticate the request. - def turnkey_stamp(data) - hash = OpenSSL::Digest::SHA256.digest(data) - signature = @turnkey_api_key.dsa_sign_asn1(hash) - - # See https://anthonylewis.com/2011/02/09/to-hex-and-back-with-ruby/ - signature_hex = signature.unpack('H*').first - - stamp = { - publicKey: self.turnkey_api_public_key, - scheme: "SIGNATURE_SCHEME_TK_API_P256", - signature: signature_hex, - } - - { "x-stamp" => Base64.urlsafe_encode64(stamp.to_json, padding: false) } - end - - def self.default - @@default ||= ApiClient.new - 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, path, opts = {}) - request = build_request(http_method, path, opts) - response = request.run - +=begin +#API Reference + +#Review our [API Introduction](../api-introduction) to get started. + +OpenAPI spec version: 1.0 + +Generated by: https://github.com/swagger-api/swagger-codegen.git +=end + +require 'date' +require 'json' +require 'logger' +require 'tempfile' +require 'typhoeus' +require 'uri' +require 'openssl' +require 'json' +require 'base64' + +ASN1_DER_P256_PRIVATE_KEY_PREFIX = "308141020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420" + +module TurnkeyClient + class ApiClient + # The Configuration object holding settings to be used in the API client. + attr_accessor :config + + # Defines the headers to be used in HTTP requests of all API calls by default. + # + # @return [Hash] + attr_accessor :default_headers + + # Initializes the ApiClient + # @option config [Configuration] Configuration for initializing the object + def initialize(config) + @config = config + @user_agent = "Swagger-Codegen/#{VERSION}/ruby" + @default_headers = { + 'Content-Type' => 'application/json', + 'User-Agent' => @user_agent, + 'X-Client-Version' => "turnkey-ruby/#{VERSION}" + } + + if config.api_public_key.nil? + raise "Unable to find :api_public_key in configuration. Please configure it with `TurnkeyClient.configure`" + end + + if config.api_private_key.nil? + raise "Unable to find :api_private_key in configuration. Please configure it with `TurnkeyClient.configure`" + end + + # This hex prefix represents the start of a private key object in ASN.1, encoded in DER + # This is necessary to load the private key in raw format with openssl. + pkcs8_hex = ASN1_DER_P256_PRIVATE_KEY_PREFIX + config.api_private_key + + # See https://anthonylewis.com/2011/02/09/to-hex-and-back-with-ruby/ + pkcs8_bytes = pkcs8_hex.scan(/../).map { |x| x.hex }.pack('c*') + + @turnkey_api_key = OpenSSL::PKey.read(pkcs8_bytes) + + # Check that we loaded the right key by comparing the public key to the passed in public key + if self.turnkey_api_public_key != config.api_public_key.downcase + raise "Expected public key to be #{config.api_public_key} (value configured), but computed #{self.turnkey_api_public_key} from configured private key" + end + end + + # Return the hex-encoded public key in compressed form + def turnkey_api_public_key + # This gives the public point as a [OpenSSL::BN](https://ruby-doc.org/stdlib-2.4.1/libdoc/openssl/rdoc/OpenSSL/BN.html) + public_key_bn = @turnkey_api_key.public_key.to_bn :compressed + public_key_hex = public_key_bn.to_s 16 + public_key_hex.downcase + end + + # Given a string (often, a POST request body), returns a hash with the correct auth header + # to authenticate the request. + def turnkey_stamp(data) + hash = OpenSSL::Digest::SHA256.digest(data) + signature = @turnkey_api_key.dsa_sign_asn1(hash) + + # See https://anthonylewis.com/2011/02/09/to-hex-and-back-with-ruby/ + signature_hex = signature.unpack('H*').first + + stamp = { + publicKey: self.turnkey_api_public_key, + scheme: "SIGNATURE_SCHEME_TK_API_P256", + signature: signature_hex, + } + + { "x-stamp" => Base64.urlsafe_encode64(stamp.to_json, padding: false) } + end + + def self.default + @@default ||= ApiClient.new + 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, path, opts = {}) + request = build_request(http_method, path, opts) + response = request.run + if @config.debugging - @config.logger.debug "HTTP response body ~BEGIN~\n#{response.body}\n~END~\n" - end - - unless response.success? - if response.timed_out? - fail ApiError.new('Connection timed out') - elsif response.code == 0 - # Errors from libcurl will be made visible here - fail ApiError.new(:code => 0, - :message => response.return_message) - else - fail ApiError.new(:code => response.code, - :response_headers => response.headers, - :response_body => response.body), - response.status_message - end - end - - if opts[:return_type] - data = deserialize(response, opts[:return_type]) - else - data = nil + # Payloads may contain private keys, signatures, or credentials; log only transport metadata. + @config.logger.debug "HTTP response: #{http_method.to_s.upcase} #{path} status=#{response.code}" end - return data, response.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, opts = {}) - url = build_request_url(path) - http_method = http_method.to_sym.downcase - - header_params = @default_headers.merge(opts[:header_params] || {}) - query_params = opts[:query_params] || {} - form_params = opts[:form_params] || {} - - update_params_for_auth! header_params, query_params, opts[:auth_names] - - # set ssl_verifyhosts option based on @config.verify_ssl_host (true/false) - _verify_ssl_host = @config.verify_ssl_host ? 2 : 0 - - req_opts = { - :method => http_method, - :headers => header_params, - :params => query_params, - :params_encoding => @config.params_encoding, - :timeout => @config.timeout, - :ssl_verifypeer => @config.verify_ssl, - :ssl_verifyhost => _verify_ssl_host, - :sslcert => @config.cert_file, - :sslkey => @config.key_file, - :verbose => @config.debugging - } - - # set custom cert, if provided - req_opts[:cainfo] = @config.ssl_ca_cert if @config.ssl_ca_cert - - if [:post, :patch, :put, :delete].include?(http_method) - req_body = build_request_body(header_params, form_params, opts[:body]) - req_opts.update :body => req_body - - # Now, stamp the request body and add the header to the list of headers - turnkey_auth_headers = turnkey_stamp(req_body) - req_opts[:headers] = header_params.merge(turnkey_auth_headers) - + + unless response.success? + if response.timed_out? + fail ApiError.new('Connection timed out') + elsif response.code == 0 + # Errors from libcurl will be made visible here + fail ApiError.new(:code => 0, + :message => response.return_message) + else + fail ApiError.new(:code => response.code, + :response_headers => response.headers, + :response_body => response.body), + response.status_message + end + end + + if opts[:return_type] + data = deserialize(response, opts[:return_type]) + else + data = nil + end + return data, response.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, opts = {}) + url = build_request_url(path) + http_method = http_method.to_sym.downcase + + header_params = @default_headers.merge(opts[:header_params] || {}) + query_params = opts[:query_params] || {} + form_params = opts[:form_params] || {} + + update_params_for_auth! header_params, query_params, opts[:auth_names] + + # set ssl_verifyhosts option based on @config.verify_ssl_host (true/false) + _verify_ssl_host = @config.verify_ssl_host ? 2 : 0 + + req_opts = { + :method => http_method, + :headers => header_params, + :params => query_params, + :params_encoding => @config.params_encoding, + :timeout => @config.timeout, + :ssl_verifypeer => @config.verify_ssl, + :ssl_verifyhost => _verify_ssl_host, + :sslcert => @config.cert_file, + :sslkey => @config.key_file, + :verbose => @config.debugging + } + + # set custom cert, if provided + req_opts[:cainfo] = @config.ssl_ca_cert if @config.ssl_ca_cert + + if [:post, :patch, :put, :delete].include?(http_method) + req_body = build_request_body(header_params, form_params, opts[:body]) + req_opts.update :body => req_body + + # Now, stamp the request body and add the header to the list of headers + turnkey_auth_headers = turnkey_stamp(req_body) + req_opts[:headers] = header_params.merge(turnkey_auth_headers) + if @config.debugging - @config.logger.debug "HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n" - end - end - - request = Typhoeus::Request.new(url, req_opts) - 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' || - header_params['Content-Type'] == 'multipart/form-data' - data = {} - form_params.each do |key, value| - case value - when ::File, ::Array, nil - # let typhoeus handle File, 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 - - # Check if the given MIME is a JSON MIME. - # JSON MIME examples: - # application/json - # application/json; charset=UTF8 - # APPLICATION/JSON - # */* - # @param [String] mime MIME - # @return [Boolean] True if the MIME is application/json - def json_mime?(mime) - (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", "Hash" - 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 - return @tempfile if return_type == 'File' - - 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' - - fail "Content-Type is not supported: #{content_type}" unless json_mime?(content_type) - - begin - data = JSON.parse("[#{body}]", :symbolize_names => true)[0] - rescue JSON::ParserError => e - if %w(String Date DateTime).include?(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_i - when 'Float' - data.to_f - when 'Boolean' - data == true - when 'DateTime' - # parse date time (expecting ISO 8601 format) - DateTime.parse data - when 'Date' - # parse date time (expecting ISO 8601 format) - Date.parse data - when 'Object' - # generic object (usually a Hash), return directly - data - when /\AArray<(.+)>\z/ - # e.g. Array - sub_type = $1 - data.map { |item| convert_to_type(item, sub_type) } - when /\AHash\\z/ - # e.g. Hash - sub_type = $1 - {}.tap do |hash| - data.each { |k, v| hash[k] = convert_to_type(v, sub_type) } + # Never log request bodies because signing and key-management calls can contain secrets. + @config.logger.debug "HTTP request prepared: #{http_method.to_s.upcase} #{path}" end - else - # models, e.g. Pet - TurnkeyClient.const_get(return_type).build_from_hash(data) - end - end - - # Save response body into a file in (the defined) temporary folder, using the filename - # from the "Content-Disposition" header if provided, otherwise a random filename. - # The response body is written to the file in chunks in order to handle files which - # size is larger than maximum Ruby String or even larger than the maximum memory a Ruby - # process can use. - # - # @see Configuration#temp_folder_path - def download_file(request) - tempfile = nil - encoding = nil - request.on_headers do |response| - content_disposition = response.headers['Content-Disposition'] - if content_disposition && content_disposition =~ /filename=/i - filename = content_disposition[/filename=['"]?([^'"\s]+)['"]?/, 1] - prefix = sanitize_filename(filename) - else - prefix = 'download-' - end - prefix = prefix + '-' unless prefix.end_with?('-') - encoding = response.body.encoding - tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding) - @tempfile = tempfile - end - request.on_body do |chunk| - chunk.force_encoding(encoding) - tempfile.write(chunk) - end - request.on_complete do |response| - if tempfile - tempfile.close - @config.logger.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`" - end - 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) - filename.gsub(/.*[\/\\]/, '') - end - - def build_request_url(path) - # Add leading and trailing slashes to path - path = "/#{path}".gsub(/\/+/, '/') - @config.base_url + path - end - - # Update hearder and query params based on authentication settings. - # - # @param [Hash] header_params Header parameters - # @param [Hash] query_params Query parameters - # @param [String] auth_names Authentication scheme name - def update_params_for_auth!(header_params, query_params, auth_names) - Array(auth_names).each do |auth_name| - auth_setting = @config.auth_settings[auth_name] - next unless auth_setting - case auth_setting[:in] - when 'header' then header_params[auth_setting[:key]] = auth_setting[:value] - when 'query' then query_params[auth_setting[:key]] = auth_setting[:value] - else fail ArgumentError, 'Authentication token must be in `query` of `header`' - end - end - end - - # Sets user agent in HTTP header - # - # @param [String] user_agent User agent (e.g. swagger-codegen/ruby/1.0.0) - def user_agent=(user_agent) - @user_agent = user_agent - @default_headers['User-Agent'] = @user_agent - end - - # Return Accept header based on an array of accepts provided. - # @param [Array] accepts array for Accept - # @return [String] the Accept header (e.g. application/json) - def select_header_accept(accepts) - return nil if accepts.nil? || accepts.empty? - # use JSON when present, otherwise use all of the provided - json_accept = accepts.find { |s| json_mime?(s) } - json_accept || accepts.join(',') - end - - # Return Content-Type header based on an array of content types provided. - # @param [Array] content_types array for Content-Type - # @return [String] the Content-Type header (e.g. application/json) - def select_header_content_type(content_types) - # use application/json by default - return 'application/json' if content_types.nil? || content_types.empty? - # use JSON when present, otherwise use the first one - json_content_type = content_types.find { |s| json_mime?(s) } - 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) - case collection_format - when :csv - param.join(',') - when :ssv - param.join(' ') - when :tsv - param.join("\t") - when :pipes - param.join('|') - when :multi - # return the array directly as typhoeus will handle it as expected - param - else - fail "unknown collection format: #{collection_format.inspect}" - end - end - end -end + end + + request = Typhoeus::Request.new(url, req_opts) + 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' || + header_params['Content-Type'] == 'multipart/form-data' + data = {} + form_params.each do |key, value| + case value + when ::File, ::Array, nil + # let typhoeus handle File, 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 + + # Check if the given MIME is a JSON MIME. + # JSON MIME examples: + # application/json + # application/json; charset=UTF8 + # APPLICATION/JSON + # */* + # @param [String] mime MIME + # @return [Boolean] True if the MIME is application/json + def json_mime?(mime) + (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", "Hash" + 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 + return @tempfile if return_type == 'File' + + 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' + + fail "Content-Type is not supported: #{content_type}" unless json_mime?(content_type) + + begin + data = JSON.parse("[#{body}]", :symbolize_names => true)[0] + rescue JSON::ParserError => e + if %w(String Date DateTime).include?(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_i + when 'Float' + data.to_f + when 'Boolean' + data == true + when 'DateTime' + # parse date time (expecting ISO 8601 format) + DateTime.parse data + when 'Date' + # parse date time (expecting ISO 8601 format) + Date.parse data + when 'Object' + # generic object (usually a Hash), return directly + data + when /\AArray<(.+)>\z/ + # e.g. Array + sub_type = $1 + data.map { |item| convert_to_type(item, sub_type) } + when /\AHash\\z/ + # e.g. Hash + sub_type = $1 + {}.tap do |hash| + data.each { |k, v| hash[k] = convert_to_type(v, sub_type) } + end + else + # models, e.g. Pet + TurnkeyClient.const_get(return_type).build_from_hash(data) + end + end + + # Save response body into a file in (the defined) temporary folder, using the filename + # from the "Content-Disposition" header if provided, otherwise a random filename. + # The response body is written to the file in chunks in order to handle files which + # size is larger than maximum Ruby String or even larger than the maximum memory a Ruby + # process can use. + # + # @see Configuration#temp_folder_path + def download_file(request) + tempfile = nil + encoding = nil + request.on_headers do |response| + content_disposition = response.headers['Content-Disposition'] + if content_disposition && content_disposition =~ /filename=/i + filename = content_disposition[/filename=['"]?([^'"\s]+)['"]?/, 1] + prefix = sanitize_filename(filename) + else + prefix = 'download-' + end + prefix = prefix + '-' unless prefix.end_with?('-') + encoding = response.body.encoding + tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding) + @tempfile = tempfile + end + request.on_body do |chunk| + chunk.force_encoding(encoding) + tempfile.write(chunk) + end + request.on_complete do |response| + if tempfile + tempfile.close + @config.logger.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`" + end + 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) + filename.gsub(/.*[\/\\]/, '') + end + + def build_request_url(path) + # Add leading and trailing slashes to path + path = "/#{path}".gsub(/\/+/, '/') + @config.base_url + path + end + + # Update hearder and query params based on authentication settings. + # + # @param [Hash] header_params Header parameters + # @param [Hash] query_params Query parameters + # @param [String] auth_names Authentication scheme name + def update_params_for_auth!(header_params, query_params, auth_names) + Array(auth_names).each do |auth_name| + auth_setting = @config.auth_settings[auth_name] + next unless auth_setting + case auth_setting[:in] + when 'header' then header_params[auth_setting[:key]] = auth_setting[:value] + when 'query' then query_params[auth_setting[:key]] = auth_setting[:value] + else fail ArgumentError, 'Authentication token must be in `query` of `header`' + end + end + end + + # Sets user agent in HTTP header + # + # @param [String] user_agent User agent (e.g. swagger-codegen/ruby/1.0.0) + def user_agent=(user_agent) + @user_agent = user_agent + @default_headers['User-Agent'] = @user_agent + end + + # Return Accept header based on an array of accepts provided. + # @param [Array] accepts array for Accept + # @return [String] the Accept header (e.g. application/json) + def select_header_accept(accepts) + return nil if accepts.nil? || accepts.empty? + # use JSON when present, otherwise use all of the provided + json_accept = accepts.find { |s| json_mime?(s) } + json_accept || accepts.join(',') + end + + # Return Content-Type header based on an array of content types provided. + # @param [Array] content_types array for Content-Type + # @return [String] the Content-Type header (e.g. application/json) + def select_header_content_type(content_types) + # use application/json by default + return 'application/json' if content_types.nil? || content_types.empty? + # use JSON when present, otherwise use the first one + json_content_type = content_types.find { |s| json_mime?(s) } + 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) + case collection_format + when :csv + param.join(',') + when :ssv + param.join(' ') + when :tsv + param.join("\t") + when :pipes + param.join('|') + when :multi + # return the array directly as typhoeus will handle it as expected + param + else + fail "unknown collection format: #{collection_format.inspect}" + end + end + end +end diff --git a/turnkey_client_inputs/templates/api.mustache b/turnkey_client_inputs/templates/api.mustache index 380e24b..64da3b9 100644 --- a/turnkey_client_inputs/templates/api.mustache +++ b/turnkey_client_inputs/templates/api.mustache @@ -1,191 +1,192 @@ -=begin -{{> api_info}} -=end - -module {{moduleName}} -{{#operations}} - class {{classname}} - attr_accessor :api_client - - def initialize(api_client = ApiClient.default) - @api_client = api_client - end -{{#operation}} -{{#contents}} - {{#summary}} - # {{{summary}}} - {{/summary}} - {{#notes}} - # {{{notes}}} - {{/notes}} -{{#parameters}}{{#required}} # @param {{paramName}} {{description}} -{{/required}}{{/parameters}} # @param [Hash] opts the optional parameters -{{#parameters}}{{^required}} # @option opts [{{{dataType}}}] :{{paramName}} {{description}}{{#defaultValue}} (default to {{{.}}}){{/defaultValue}} -{{/required}}{{/parameters}} # @return [{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}nil{{/returnType}}] - def {{operationId}}({{#parameters}}{{#required}}{{paramName}}, {{/required}}{{/parameters}}opts = {}) - {{#returnType}}data, _status_code, _headers = {{/returnType}}{{operationId}}_with_http_info({{#parameters}}{{#required}}{{paramName}}, {{/required}}{{/parameters}}opts) - {{#returnType}}data{{/returnType}}{{^returnType}}nil{{/returnType}} - end - - {{#summary}} - # {{summary}} - {{/summary}} - {{#notes}} - # {{notes}} - {{/notes}} -{{#parameters}}{{#required}} # @param {{paramName}} {{description}} -{{/required}}{{/parameters}} # @param [Hash] opts the optional parameters -{{#parameters}}{{^required}} # @option opts [{{{dataType}}}] :{{paramName}} {{description}} -{{/required}}{{/parameters}} # @return [Array<({{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}nil{{/returnType}}, Integer, Hash)>] {{#returnType}}{{{returnType}}} data{{/returnType}}{{^returnType}}nil{{/returnType}}, response status code and response headers - def {{operationId}}_with_http_info({{#parameters}}{{#required}}{{paramName}}, {{/required}}{{/parameters}}opts = {}) +=begin +{{> api_info}} +=end + +module {{moduleName}} +{{#operations}} + class {{classname}} + attr_accessor :api_client + + def initialize(api_client = ApiClient.default) + @api_client = api_client + end +{{#operation}} +{{#contents}} + {{#summary}} + # {{{summary}}} + {{/summary}} + {{#notes}} + # {{{notes}}} + {{/notes}} +{{#parameters}}{{#required}} # @param {{paramName}} {{description}} +{{/required}}{{/parameters}} # @param [Hash] opts the optional parameters +{{#parameters}}{{^required}} # @option opts [{{{dataType}}}] :{{paramName}} {{description}}{{#defaultValue}} (default to {{{.}}}){{/defaultValue}} +{{/required}}{{/parameters}} # @return [{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}nil{{/returnType}}] + def {{operationId}}({{#parameters}}{{#required}}{{paramName}}, {{/required}}{{/parameters}}opts = {}) + {{#returnType}}data, _status_code, _headers = {{/returnType}}{{operationId}}_with_http_info({{#parameters}}{{#required}}{{paramName}}, {{/required}}{{/parameters}}opts) + {{#returnType}}data{{/returnType}}{{^returnType}}nil{{/returnType}} + end + + {{#summary}} + # {{summary}} + {{/summary}} + {{#notes}} + # {{notes}} + {{/notes}} +{{#parameters}}{{#required}} # @param {{paramName}} {{description}} +{{/required}}{{/parameters}} # @param [Hash] opts the optional parameters +{{#parameters}}{{^required}} # @option opts [{{{dataType}}}] :{{paramName}} {{description}} +{{/required}}{{/parameters}} # @return [Array<({{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}nil{{/returnType}}, Integer, Hash)>] {{#returnType}}{{{returnType}}} data{{/returnType}}{{^returnType}}nil{{/returnType}}, response status code and response headers + def {{operationId}}_with_http_info({{#parameters}}{{#required}}{{paramName}}, {{/required}}{{/parameters}}opts = {}) + if @api_client.config.debugging + @api_client.config.logger.debug 'Calling API: {{classname}}.{{operationId}} ...' + end + {{#parameters}} + {{#required}} + # verify the required parameter '{{paramName}}' is set + if @api_client.config.client_side_validation && {{{paramName}}}.nil? + fail ArgumentError, "Missing the required parameter '{{paramName}}' when calling {{classname}}.{{operationId}}" + end + {{#isEnum}} + {{^isContainer}} + # verify enum value + if @api_client.config.client_side_validation && ![{{#allowableValues}}{{#values}}'{{{this}}}'{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}].include?({{{paramName}}}) + fail ArgumentError, "invalid value for '{{{paramName}}}', must be one of {{#allowableValues}}{{#values}}{{{this}}}{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}" + end + {{/isContainer}} + {{/isEnum}} + {{/required}} + {{^required}} + {{#isEnum}} + {{#collectionFormat}} + if @api_client.config.client_side_validation && opts[:'{{{paramName}}}'] && !opts[:'{{{paramName}}}'].all? { |item| [{{#allowableValues}}{{#values}}'{{{this}}}'{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}].include?(item) } + fail ArgumentError, 'invalid value for "{{{paramName}}}", must include one of {{#allowableValues}}{{#values}}{{{this}}}{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}' + end + {{/collectionFormat}} + {{^collectionFormat}} + if @api_client.config.client_side_validation && opts[:'{{{paramName}}}'] && ![{{#allowableValues}}{{#values}}'{{{this}}}'{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}].include?(opts[:'{{{paramName}}}']) + fail ArgumentError, 'invalid value for "{{{paramName}}}", must be one of {{#allowableValues}}{{#values}}{{{this}}}{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}' + end + {{/collectionFormat}} + {{/isEnum}} + {{/required}} + {{#hasValidation}} + {{#maxLength}} + if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}.to_s.length > {{{maxLength}}} + fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" 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}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}.to_s.length < {{{minLength}}} + fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" when calling {{classname}}.{{operationId}}, the character length must be great than or equal to {{{minLength}}}.' + end + + {{/minLength}} + {{#maximum}} + if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}} + fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" when calling {{classname}}.{{operationId}}, must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{{maximum}}}.' + end + + {{/maximum}} + {{#minimum}} + if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}} + fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" when calling {{classname}}.{{operationId}}, must be greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{{minimum}}}.' + end + + {{/minimum}} + {{#pattern}} + if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}} !~ Regexp.new({{{pattern}}}) + fail ArgumentError, "invalid value for '{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:\"{{{paramName}}}\"]{{/required}}' when calling {{classname}}.{{operationId}}, must conform to the pattern {{{pattern}}}." + end + + {{/pattern}} + {{#maxItems}} + if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}.length > {{{maxItems}}} + fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" 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}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}.length < {{{minItems}}} + fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" when calling {{classname}}.{{operationId}}, number of items must be greater than or equal to {{{minItems}}}.' + end + + {{/minItems}} + {{/hasValidation}} + {{/parameters}} + # resource path + local_var_path = '{{{path}}}'{{#pathParams}}.sub('{' + '{{baseName}}' + '}', {{paramName}}.to_s){{/pathParams}} + + # query parameters + query_params = opts[:query_params] || {} + {{#queryParams}} + {{#required}} + query_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} + {{/required}} + {{/queryParams}} + {{#queryParams}} + {{^required}} + query_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param(opts[:'{{{paramName}}}'], :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}opts[:'{{{paramName}}}']{{/collectionFormat}} if !opts[:'{{{paramName}}}'].nil? + {{/required}} + {{/queryParams}} + + # header parameters + header_params = opts[:header_params] || {} + {{#hasProduces}} + # HTTP header 'Accept' (if needed) + header_params['Accept'] = @api_client.select_header_accept([{{#produces}}'{{{mediaType}}}'{{#hasMore}}, {{/hasMore}}{{/produces}}]) + {{/hasProduces}} + {{#hasConsumes}} + # HTTP header 'Content-Type' + header_params['Content-Type'] = @api_client.select_header_content_type([{{#consumes}}'{{{mediaType}}}'{{#hasMore}}, {{/hasMore}}{{/consumes}}]) + {{/hasConsumes}} + {{#headerParams}} + {{#required}} + header_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} + {{/required}} + {{/headerParams}} + {{#headerParams}} + {{^required}} + header_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param(opts[:'{{{paramName}}}'], :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}opts[:'{{{paramName}}}']{{/collectionFormat}} if !opts[:'{{{paramName}}}'].nil? + {{/required}} + {{/headerParams}} + + # form parameters + form_params = opts[:form_params] || {} + {{#formParams}} + {{#required}} + form_params['{{baseName}}'] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} + {{/required}} + {{/formParams}} + {{#formParams}} + {{^required}} + form_params['{{baseName}}'] = {{#collectionFormat}}@api_client.build_collection_param(opts[:'{{{paramName}}}'], :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}opts[:'{{{paramName}}}']{{/collectionFormat}} if !opts[:'{{paramName}}'].nil? + {{/required}} + {{/formParams}} + + # http body (model) + post_body = opts[:body] {{#bodyParam}}|| @api_client.object_to_http_body({{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}) {{/bodyParam}} + + return_type = opts[:return_type] {{#returnType}}|| '{{{returnType}}}' {{/returnType}} + + auth_names = opts[:auth_names] || [{{#authMethods}}'{{name}}'{{#hasMore}}, {{/hasMore}}{{/authMethods}}] + data, status_code, headers = @api_client.call_api(:{{httpMethod}}, local_var_path, + :header_params => header_params, + :query_params => query_params, + :form_params => form_params, + :body => post_body, + :auth_names => auth_names, + :return_type => return_type) + if @api_client.config.debugging - @api_client.config.logger.debug 'Calling API: {{classname}}.{{operationId}} ...' + # Response data and headers can contain credentials or key material; log only the operation and status. + @api_client.config.logger.debug "API called: {{classname}}#{{operationId}} status=#{status_code}" end - {{#parameters}} - {{#required}} - # verify the required parameter '{{paramName}}' is set - if @api_client.config.client_side_validation && {{{paramName}}}.nil? - fail ArgumentError, "Missing the required parameter '{{paramName}}' when calling {{classname}}.{{operationId}}" - end - {{#isEnum}} - {{^isContainer}} - # verify enum value - if @api_client.config.client_side_validation && ![{{#allowableValues}}{{#values}}'{{{this}}}'{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}].include?({{{paramName}}}) - fail ArgumentError, "invalid value for '{{{paramName}}}', must be one of {{#allowableValues}}{{#values}}{{{this}}}{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}" - end - {{/isContainer}} - {{/isEnum}} - {{/required}} - {{^required}} - {{#isEnum}} - {{#collectionFormat}} - if @api_client.config.client_side_validation && opts[:'{{{paramName}}}'] && !opts[:'{{{paramName}}}'].all? { |item| [{{#allowableValues}}{{#values}}'{{{this}}}'{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}].include?(item) } - fail ArgumentError, 'invalid value for "{{{paramName}}}", must include one of {{#allowableValues}}{{#values}}{{{this}}}{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}' - end - {{/collectionFormat}} - {{^collectionFormat}} - if @api_client.config.client_side_validation && opts[:'{{{paramName}}}'] && ![{{#allowableValues}}{{#values}}'{{{this}}}'{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}].include?(opts[:'{{{paramName}}}']) - fail ArgumentError, 'invalid value for "{{{paramName}}}", must be one of {{#allowableValues}}{{#values}}{{{this}}}{{^@last}}, {{/@last}}{{/values}}{{/allowableValues}}' - end - {{/collectionFormat}} - {{/isEnum}} - {{/required}} - {{#hasValidation}} - {{#maxLength}} - if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}.to_s.length > {{{maxLength}}} - fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" 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}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}.to_s.length < {{{minLength}}} - fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" when calling {{classname}}.{{operationId}}, the character length must be great than or equal to {{{minLength}}}.' - end - - {{/minLength}} - {{#maximum}} - if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}} - fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" when calling {{classname}}.{{operationId}}, must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{{maximum}}}.' - end - - {{/maximum}} - {{#minimum}} - if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}} - fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" when calling {{classname}}.{{operationId}}, must be greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{{minimum}}}.' - end - - {{/minimum}} - {{#pattern}} - if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}} !~ Regexp.new({{{pattern}}}) - fail ArgumentError, "invalid value for '{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:\"{{{paramName}}}\"]{{/required}}' when calling {{classname}}.{{operationId}}, must conform to the pattern {{{pattern}}}." - end - - {{/pattern}} - {{#maxItems}} - if @api_client.config.client_side_validation && {{^required}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}.length > {{{maxItems}}} - fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" 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}}!opts[:'{{{paramName}}}'].nil? && {{/required}}{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}.length < {{{minItems}}} - fail ArgumentError, 'invalid value for "{{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:"{{{paramName}}}"]{{/required}}" when calling {{classname}}.{{operationId}}, number of items must be greater than or equal to {{{minItems}}}.' - end - - {{/minItems}} - {{/hasValidation}} - {{/parameters}} - # resource path - local_var_path = '{{{path}}}'{{#pathParams}}.sub('{' + '{{baseName}}' + '}', {{paramName}}.to_s){{/pathParams}} - - # query parameters - query_params = opts[:query_params] || {} - {{#queryParams}} - {{#required}} - query_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} - {{/required}} - {{/queryParams}} - {{#queryParams}} - {{^required}} - query_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param(opts[:'{{{paramName}}}'], :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}opts[:'{{{paramName}}}']{{/collectionFormat}} if !opts[:'{{{paramName}}}'].nil? - {{/required}} - {{/queryParams}} - - # header parameters - header_params = opts[:header_params] || {} - {{#hasProduces}} - # HTTP header 'Accept' (if needed) - header_params['Accept'] = @api_client.select_header_accept([{{#produces}}'{{{mediaType}}}'{{#hasMore}}, {{/hasMore}}{{/produces}}]) - {{/hasProduces}} - {{#hasConsumes}} - # HTTP header 'Content-Type' - header_params['Content-Type'] = @api_client.select_header_content_type([{{#consumes}}'{{{mediaType}}}'{{#hasMore}}, {{/hasMore}}{{/consumes}}]) - {{/hasConsumes}} - {{#headerParams}} - {{#required}} - header_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} - {{/required}} - {{/headerParams}} - {{#headerParams}} - {{^required}} - header_params[:'{{{baseName}}}'] = {{#collectionFormat}}@api_client.build_collection_param(opts[:'{{{paramName}}}'], :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}opts[:'{{{paramName}}}']{{/collectionFormat}} if !opts[:'{{{paramName}}}'].nil? - {{/required}} - {{/headerParams}} - - # form parameters - form_params = opts[:form_params] || {} - {{#formParams}} - {{#required}} - form_params['{{baseName}}'] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} - {{/required}} - {{/formParams}} - {{#formParams}} - {{^required}} - form_params['{{baseName}}'] = {{#collectionFormat}}@api_client.build_collection_param(opts[:'{{{paramName}}}'], :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}opts[:'{{{paramName}}}']{{/collectionFormat}} if !opts[:'{{paramName}}'].nil? - {{/required}} - {{/formParams}} - - # http body (model) - post_body = opts[:body] {{#bodyParam}}|| @api_client.object_to_http_body({{#required}}{{{paramName}}}{{/required}}{{^required}}opts[:'{{{paramName}}}']{{/required}}) {{/bodyParam}} - - return_type = opts[:return_type] {{#returnType}}|| '{{{returnType}}}' {{/returnType}} - - auth_names = opts[:auth_names] || [{{#authMethods}}'{{name}}'{{#hasMore}}, {{/hasMore}}{{/authMethods}}] - data, status_code, headers = @api_client.call_api(:{{httpMethod}}, local_var_path, - :header_params => header_params, - :query_params => query_params, - :form_params => form_params, - :body => post_body, - :auth_names => auth_names, - :return_type => return_type) - - if @api_client.config.debugging - @api_client.config.logger.debug "API called: {{classname}}#{{operationId}}\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" - end - return data, status_code, headers - end -{{/contents}} -{{/operation}} - end -{{/operations}} -end + return data, status_code, headers + end +{{/contents}} +{{/operation}} + end +{{/operations}} +end diff --git a/turnkey_client_inputs/templates/api_client.mustache b/turnkey_client_inputs/templates/api_client.mustache index 8ca5e6a..690e2b7 100644 --- a/turnkey_client_inputs/templates/api_client.mustache +++ b/turnkey_client_inputs/templates/api_client.mustache @@ -1,442 +1,444 @@ -=begin -{{> api_info}} -=end - -require 'date' -require 'json' -require 'logger' -require 'tempfile' -require 'typhoeus' -require 'uri' -require 'openssl' -require 'json' -require 'base64' - -ASN1_DER_P256_PRIVATE_KEY_PREFIX = "308141020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420" - -module {{moduleName}} - class ApiClient - # The Configuration object holding settings to be used in the API client. - attr_accessor :config - - # Defines the headers to be used in HTTP requests of all API calls by default. - # - # @return [Hash] - attr_accessor :default_headers - - # Initializes the ApiClient - # @option config [Configuration] Configuration for initializing the object - def initialize(config) - @config = config - @user_agent = "{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}Swagger-Codegen/#{VERSION}/ruby{{/httpUserAgent}}" - @default_headers = { - 'Content-Type' => 'application/json', - 'User-Agent' => @user_agent, - 'X-Client-Version' => "turnkey-ruby/#{VERSION}" - } - - if config.api_public_key.nil? - raise "Unable to find :api_public_key in configuration. Please configure it with `TurnkeyClient.configure`" - end - - if config.api_private_key.nil? - raise "Unable to find :api_private_key in configuration. Please configure it with `TurnkeyClient.configure`" - end - - # This hex prefix represents the start of a private key object in ASN.1, encoded in DER - # This is necessary to load the private key in raw format with openssl. - pkcs8_hex = ASN1_DER_P256_PRIVATE_KEY_PREFIX + config.api_private_key - - # See https://anthonylewis.com/2011/02/09/to-hex-and-back-with-ruby/ - pkcs8_bytes = pkcs8_hex.scan(/../).map { |x| x.hex }.pack('c*') - - @turnkey_api_key = OpenSSL::PKey.read(pkcs8_bytes) - - # Check that we loaded the right key by comparing the public key to the passed in public key - if self.turnkey_api_public_key != config.api_public_key.downcase - raise "Expected public key to be #{config.api_public_key} (value configured), but computed #{self.turnkey_api_public_key} from configured private key" - end - end - - # Return the hex-encoded public key in compressed form - def turnkey_api_public_key - # This gives the public point as a [OpenSSL::BN](https://ruby-doc.org/stdlib-2.4.1/libdoc/openssl/rdoc/OpenSSL/BN.html) - public_key_bn = @turnkey_api_key.public_key.to_bn :compressed - public_key_hex = public_key_bn.to_s 16 - public_key_hex.downcase - end - - # Given a string (often, a POST request body), returns a hash with the correct auth header - # to authenticate the request. - def turnkey_stamp(data) - hash = OpenSSL::Digest::SHA256.digest(data) - signature = @turnkey_api_key.dsa_sign_asn1(hash) - - # See https://anthonylewis.com/2011/02/09/to-hex-and-back-with-ruby/ - signature_hex = signature.unpack('H*').first - - stamp = { - publicKey: self.turnkey_api_public_key, - scheme: "SIGNATURE_SCHEME_TK_API_P256", - signature: signature_hex, - } - - { "x-stamp" => Base64.urlsafe_encode64(stamp.to_json, padding: false) } - end - - def self.default - @@default ||= ApiClient.new - 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, path, opts = {}) - request = build_request(http_method, path, opts) - response = request.run - +=begin +{{> api_info}} +=end + +require 'date' +require 'json' +require 'logger' +require 'tempfile' +require 'typhoeus' +require 'uri' +require 'openssl' +require 'json' +require 'base64' + +ASN1_DER_P256_PRIVATE_KEY_PREFIX = "308141020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420" + +module {{moduleName}} + class ApiClient + # The Configuration object holding settings to be used in the API client. + attr_accessor :config + + # Defines the headers to be used in HTTP requests of all API calls by default. + # + # @return [Hash] + attr_accessor :default_headers + + # Initializes the ApiClient + # @option config [Configuration] Configuration for initializing the object + def initialize(config) + @config = config + @user_agent = "{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}Swagger-Codegen/#{VERSION}/ruby{{/httpUserAgent}}" + @default_headers = { + 'Content-Type' => 'application/json', + 'User-Agent' => @user_agent, + 'X-Client-Version' => "turnkey-ruby/#{VERSION}" + } + + if config.api_public_key.nil? + raise "Unable to find :api_public_key in configuration. Please configure it with `TurnkeyClient.configure`" + end + + if config.api_private_key.nil? + raise "Unable to find :api_private_key in configuration. Please configure it with `TurnkeyClient.configure`" + end + + # This hex prefix represents the start of a private key object in ASN.1, encoded in DER + # This is necessary to load the private key in raw format with openssl. + pkcs8_hex = ASN1_DER_P256_PRIVATE_KEY_PREFIX + config.api_private_key + + # See https://anthonylewis.com/2011/02/09/to-hex-and-back-with-ruby/ + pkcs8_bytes = pkcs8_hex.scan(/../).map { |x| x.hex }.pack('c*') + + @turnkey_api_key = OpenSSL::PKey.read(pkcs8_bytes) + + # Check that we loaded the right key by comparing the public key to the passed in public key + if self.turnkey_api_public_key != config.api_public_key.downcase + raise "Expected public key to be #{config.api_public_key} (value configured), but computed #{self.turnkey_api_public_key} from configured private key" + end + end + + # Return the hex-encoded public key in compressed form + def turnkey_api_public_key + # This gives the public point as a [OpenSSL::BN](https://ruby-doc.org/stdlib-2.4.1/libdoc/openssl/rdoc/OpenSSL/BN.html) + public_key_bn = @turnkey_api_key.public_key.to_bn :compressed + public_key_hex = public_key_bn.to_s 16 + public_key_hex.downcase + end + + # Given a string (often, a POST request body), returns a hash with the correct auth header + # to authenticate the request. + def turnkey_stamp(data) + hash = OpenSSL::Digest::SHA256.digest(data) + signature = @turnkey_api_key.dsa_sign_asn1(hash) + + # See https://anthonylewis.com/2011/02/09/to-hex-and-back-with-ruby/ + signature_hex = signature.unpack('H*').first + + stamp = { + publicKey: self.turnkey_api_public_key, + scheme: "SIGNATURE_SCHEME_TK_API_P256", + signature: signature_hex, + } + + { "x-stamp" => Base64.urlsafe_encode64(stamp.to_json, padding: false) } + end + + def self.default + @@default ||= ApiClient.new + 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, path, opts = {}) + request = build_request(http_method, path, opts) + response = request.run + if @config.debugging - @config.logger.debug "HTTP response body ~BEGIN~\n#{response.body}\n~END~\n" - end - - unless response.success? - if response.timed_out? - fail ApiError.new('Connection timed out') - elsif response.code == 0 - # Errors from libcurl will be made visible here - fail ApiError.new(:code => 0, - :message => response.return_message) - else - fail ApiError.new(:code => response.code, - :response_headers => response.headers, - :response_body => response.body), - response.status_message - end - end - - if opts[:return_type] - data = deserialize(response, opts[:return_type]) - else - data = nil + # Payloads may contain private keys, signatures, or credentials; log only transport metadata. + @config.logger.debug "HTTP response: #{http_method.to_s.upcase} #{path} status=#{response.code}" end - return data, response.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, opts = {}) - url = build_request_url(path) - http_method = http_method.to_sym.downcase - - header_params = @default_headers.merge(opts[:header_params] || {}) - query_params = opts[:query_params] || {} - form_params = opts[:form_params] || {} - - {{#hasAuthMethods}} - update_params_for_auth! header_params, query_params, opts[:auth_names] - {{/hasAuthMethods}} - - # set ssl_verifyhosts option based on @config.verify_ssl_host (true/false) - _verify_ssl_host = @config.verify_ssl_host ? 2 : 0 - - req_opts = { - :method => http_method, - :headers => header_params, - :params => query_params, - :params_encoding => @config.params_encoding, - :timeout => @config.timeout, - :ssl_verifypeer => @config.verify_ssl, - :ssl_verifyhost => _verify_ssl_host, - :sslcert => @config.cert_file, - :sslkey => @config.key_file, - :verbose => @config.debugging - } - - # set custom cert, if provided - req_opts[:cainfo] = @config.ssl_ca_cert if @config.ssl_ca_cert - - if [:post, :patch, :put, :delete].include?(http_method) - req_body = build_request_body(header_params, form_params, opts[:body]) - req_opts.update :body => req_body - - # Now, stamp the request body and add the header to the list of headers - turnkey_auth_headers = turnkey_stamp(req_body) - req_opts[:headers] = header_params.merge(turnkey_auth_headers) - + + unless response.success? + if response.timed_out? + fail ApiError.new('Connection timed out') + elsif response.code == 0 + # Errors from libcurl will be made visible here + fail ApiError.new(:code => 0, + :message => response.return_message) + else + fail ApiError.new(:code => response.code, + :response_headers => response.headers, + :response_body => response.body), + response.status_message + end + end + + if opts[:return_type] + data = deserialize(response, opts[:return_type]) + else + data = nil + end + return data, response.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, opts = {}) + url = build_request_url(path) + http_method = http_method.to_sym.downcase + + header_params = @default_headers.merge(opts[:header_params] || {}) + query_params = opts[:query_params] || {} + form_params = opts[:form_params] || {} + + {{#hasAuthMethods}} + update_params_for_auth! header_params, query_params, opts[:auth_names] + {{/hasAuthMethods}} + + # set ssl_verifyhosts option based on @config.verify_ssl_host (true/false) + _verify_ssl_host = @config.verify_ssl_host ? 2 : 0 + + req_opts = { + :method => http_method, + :headers => header_params, + :params => query_params, + :params_encoding => @config.params_encoding, + :timeout => @config.timeout, + :ssl_verifypeer => @config.verify_ssl, + :ssl_verifyhost => _verify_ssl_host, + :sslcert => @config.cert_file, + :sslkey => @config.key_file, + :verbose => @config.debugging + } + + # set custom cert, if provided + req_opts[:cainfo] = @config.ssl_ca_cert if @config.ssl_ca_cert + + if [:post, :patch, :put, :delete].include?(http_method) + req_body = build_request_body(header_params, form_params, opts[:body]) + req_opts.update :body => req_body + + # Now, stamp the request body and add the header to the list of headers + turnkey_auth_headers = turnkey_stamp(req_body) + req_opts[:headers] = header_params.merge(turnkey_auth_headers) + if @config.debugging - @config.logger.debug "HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n" - end - end - - request = Typhoeus::Request.new(url, req_opts) - 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' || - header_params['Content-Type'] == 'multipart/form-data' - data = {} - form_params.each do |key, value| - case value - when ::File, ::Array, nil - # let typhoeus handle File, 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 - - # Check if the given MIME is a JSON MIME. - # JSON MIME examples: - # application/json - # application/json; charset=UTF8 - # APPLICATION/JSON - # */* - # @param [String] mime MIME - # @return [Boolean] True if the MIME is application/json - def json_mime?(mime) - (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", "Hash" - 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 - return @tempfile if return_type == 'File' - - 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' - - fail "Content-Type is not supported: #{content_type}" unless json_mime?(content_type) - - begin - data = JSON.parse("[#{body}]", :symbolize_names => true)[0] - rescue JSON::ParserError => e - if %w(String Date DateTime).include?(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_i - when 'Float' - data.to_f - when 'Boolean' - data == true - when 'DateTime' - # parse date time (expecting ISO 8601 format) - DateTime.parse data - when 'Date' - # parse date time (expecting ISO 8601 format) - Date.parse data - when 'Object' - # generic object (usually a Hash), return directly - data - when /\AArray<(.+)>\z/ - # e.g. Array - sub_type = $1 - data.map { |item| convert_to_type(item, sub_type) } - when /\AHash\\z/ - # e.g. Hash - sub_type = $1 - {}.tap do |hash| - data.each { |k, v| hash[k] = convert_to_type(v, sub_type) } + # Never log request bodies because signing and key-management calls can contain secrets. + @config.logger.debug "HTTP request prepared: #{http_method.to_s.upcase} #{path}" end - else - # models, e.g. Pet - {{moduleName}}.const_get(return_type).build_from_hash(data) - end - end - - # Save response body into a file in (the defined) temporary folder, using the filename - # from the "Content-Disposition" header if provided, otherwise a random filename. - # The response body is written to the file in chunks in order to handle files which - # size is larger than maximum Ruby String or even larger than the maximum memory a Ruby - # process can use. - # - # @see Configuration#temp_folder_path - def download_file(request) - tempfile = nil - encoding = nil - request.on_headers do |response| - content_disposition = response.headers['Content-Disposition'] - if content_disposition && content_disposition =~ /filename=/i - filename = content_disposition[/filename=['"]?([^'"\s]+)['"]?/, 1] - prefix = sanitize_filename(filename) - else - prefix = 'download-' - end - prefix = prefix + '-' unless prefix.end_with?('-') - encoding = response.body.encoding - tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding) - @tempfile = tempfile - end - request.on_body do |chunk| - chunk.force_encoding(encoding) - tempfile.write(chunk) - end - request.on_complete do |response| - if tempfile - tempfile.close - @config.logger.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`" - end - 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) - filename.gsub(/.*[\/\\]/, '') - end - - def build_request_url(path) - # Add leading and trailing slashes to path - path = "/#{path}".gsub(/\/+/, '/') - @config.base_url + path - end - - # Update hearder and query params based on authentication settings. - # - # @param [Hash] header_params Header parameters - # @param [Hash] query_params Query parameters - # @param [String] auth_names Authentication scheme name - def update_params_for_auth!(header_params, query_params, auth_names) - Array(auth_names).each do |auth_name| - auth_setting = @config.auth_settings[auth_name] - next unless auth_setting - case auth_setting[:in] - when 'header' then header_params[auth_setting[:key]] = auth_setting[:value] - when 'query' then query_params[auth_setting[:key]] = auth_setting[:value] - else fail ArgumentError, 'Authentication token must be in `query` of `header`' - end - end - end - - # Sets user agent in HTTP header - # - # @param [String] user_agent User agent (e.g. swagger-codegen/ruby/1.0.0) - def user_agent=(user_agent) - @user_agent = user_agent - @default_headers['User-Agent'] = @user_agent - end - - # Return Accept header based on an array of accepts provided. - # @param [Array] accepts array for Accept - # @return [String] the Accept header (e.g. application/json) - def select_header_accept(accepts) - return nil if accepts.nil? || accepts.empty? - # use JSON when present, otherwise use all of the provided - json_accept = accepts.find { |s| json_mime?(s) } - json_accept || accepts.join(',') - end - - # Return Content-Type header based on an array of content types provided. - # @param [Array] content_types array for Content-Type - # @return [String] the Content-Type header (e.g. application/json) - def select_header_content_type(content_types) - # use application/json by default - return 'application/json' if content_types.nil? || content_types.empty? - # use JSON when present, otherwise use the first one - json_content_type = content_types.find { |s| json_mime?(s) } - 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) - case collection_format - when :csv - param.join(',') - when :ssv - param.join(' ') - when :tsv - param.join("\t") - when :pipes - param.join('|') - when :multi - # return the array directly as typhoeus will handle it as expected - param - else - fail "unknown collection format: #{collection_format.inspect}" - end - end - end -end + end + + request = Typhoeus::Request.new(url, req_opts) + 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' || + header_params['Content-Type'] == 'multipart/form-data' + data = {} + form_params.each do |key, value| + case value + when ::File, ::Array, nil + # let typhoeus handle File, 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 + + # Check if the given MIME is a JSON MIME. + # JSON MIME examples: + # application/json + # application/json; charset=UTF8 + # APPLICATION/JSON + # */* + # @param [String] mime MIME + # @return [Boolean] True if the MIME is application/json + def json_mime?(mime) + (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", "Hash" + 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 + return @tempfile if return_type == 'File' + + 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' + + fail "Content-Type is not supported: #{content_type}" unless json_mime?(content_type) + + begin + data = JSON.parse("[#{body}]", :symbolize_names => true)[0] + rescue JSON::ParserError => e + if %w(String Date DateTime).include?(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_i + when 'Float' + data.to_f + when 'Boolean' + data == true + when 'DateTime' + # parse date time (expecting ISO 8601 format) + DateTime.parse data + when 'Date' + # parse date time (expecting ISO 8601 format) + Date.parse data + when 'Object' + # generic object (usually a Hash), return directly + data + when /\AArray<(.+)>\z/ + # e.g. Array + sub_type = $1 + data.map { |item| convert_to_type(item, sub_type) } + when /\AHash\\z/ + # e.g. Hash + sub_type = $1 + {}.tap do |hash| + data.each { |k, v| hash[k] = convert_to_type(v, sub_type) } + end + else + # models, e.g. Pet + {{moduleName}}.const_get(return_type).build_from_hash(data) + end + end + + # Save response body into a file in (the defined) temporary folder, using the filename + # from the "Content-Disposition" header if provided, otherwise a random filename. + # The response body is written to the file in chunks in order to handle files which + # size is larger than maximum Ruby String or even larger than the maximum memory a Ruby + # process can use. + # + # @see Configuration#temp_folder_path + def download_file(request) + tempfile = nil + encoding = nil + request.on_headers do |response| + content_disposition = response.headers['Content-Disposition'] + if content_disposition && content_disposition =~ /filename=/i + filename = content_disposition[/filename=['"]?([^'"\s]+)['"]?/, 1] + prefix = sanitize_filename(filename) + else + prefix = 'download-' + end + prefix = prefix + '-' unless prefix.end_with?('-') + encoding = response.body.encoding + tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding) + @tempfile = tempfile + end + request.on_body do |chunk| + chunk.force_encoding(encoding) + tempfile.write(chunk) + end + request.on_complete do |response| + if tempfile + tempfile.close + @config.logger.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`" + end + 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) + filename.gsub(/.*[\/\\]/, '') + end + + def build_request_url(path) + # Add leading and trailing slashes to path + path = "/#{path}".gsub(/\/+/, '/') + @config.base_url + path + end + + # Update hearder and query params based on authentication settings. + # + # @param [Hash] header_params Header parameters + # @param [Hash] query_params Query parameters + # @param [String] auth_names Authentication scheme name + def update_params_for_auth!(header_params, query_params, auth_names) + Array(auth_names).each do |auth_name| + auth_setting = @config.auth_settings[auth_name] + next unless auth_setting + case auth_setting[:in] + when 'header' then header_params[auth_setting[:key]] = auth_setting[:value] + when 'query' then query_params[auth_setting[:key]] = auth_setting[:value] + else fail ArgumentError, 'Authentication token must be in `query` of `header`' + end + end + end + + # Sets user agent in HTTP header + # + # @param [String] user_agent User agent (e.g. swagger-codegen/ruby/1.0.0) + def user_agent=(user_agent) + @user_agent = user_agent + @default_headers['User-Agent'] = @user_agent + end + + # Return Accept header based on an array of accepts provided. + # @param [Array] accepts array for Accept + # @return [String] the Accept header (e.g. application/json) + def select_header_accept(accepts) + return nil if accepts.nil? || accepts.empty? + # use JSON when present, otherwise use all of the provided + json_accept = accepts.find { |s| json_mime?(s) } + json_accept || accepts.join(',') + end + + # Return Content-Type header based on an array of content types provided. + # @param [Array] content_types array for Content-Type + # @return [String] the Content-Type header (e.g. application/json) + def select_header_content_type(content_types) + # use application/json by default + return 'application/json' if content_types.nil? || content_types.empty? + # use JSON when present, otherwise use the first one + json_content_type = content_types.find { |s| json_mime?(s) } + 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) + case collection_format + when :csv + param.join(',') + when :ssv + param.join(' ') + when :tsv + param.join("\t") + when :pipes + param.join('|') + when :multi + # return the array directly as typhoeus will handle it as expected + param + else + fail "unknown collection format: #{collection_format.inspect}" + end + end + end +end