diff --git a/lib/google/api_client.rb b/lib/google/api_client.rb index 90f422bd0..eeaf25933 100644 --- a/lib/google/api_client.rb +++ b/lib/google/api_client.rb @@ -31,6 +31,7 @@ require 'google/api_client/media' require 'google/api_client/service_account' require 'google/api_client/batch' require 'google/api_client/gzip' +require 'google/api_client/charset' require 'google/api_client/client_secrets' require 'google/api_client/railtie' if defined?(Rails) @@ -75,6 +76,9 @@ module Google # @option options [String] :ca_file # Optional set of root certificates to use when validating SSL connections. # By default, a bundled set of trusted roots will be used. + # @options options[Hash] :force_encoding + # Experimental option. True if response body should be force encoded into the charset + # specified in the Content-Type header. Mostly intended for compressed content. # @options options[Hash] :faraday_options # Pass through of options to set on the Faraday connection def initialize(options={}) @@ -119,6 +123,7 @@ module Google @discovered_apis = {} ca_file = options[:ca_file] || File.expand_path('../../cacerts.pem', __FILE__) self.connection = Faraday.new do |faraday| + faraday.response :charset if options[:force_encoding] faraday.response :gzip faraday.options.params_encoder = Faraday::FlatParamsEncoder faraday.ssl.ca_file = ca_file @@ -265,10 +270,12 @@ module Google # @param [String, Symbol] api The API name. # @param [String] version The desired version of the API. # @param [Addressable::URI] uri The URI of the discovery document. + # @return [Google::APIClient::API] The service object. def register_discovery_uri(api, version, uri) api = api.to_s version = version || 'v1' @discovery_uris["#{api}:#{version}"] = uri + discovered_api(api, version) end ## @@ -297,6 +304,7 @@ module Google # @param [String] version The desired version of the API. # @param [String, StringIO] discovery_document # The contents of the discovery document. + # @return [Google::APIClient::API] The service object. def register_discovery_document(api, version, discovery_document) api = api.to_s version = version || 'v1' @@ -311,6 +319,7 @@ module Google end @discovery_documents["#{api}:#{version}"] = MultiJson.load(discovery_document) + discovered_api(api, version) end ## diff --git a/lib/google/api_client/charset.rb b/lib/google/api_client/charset.rb new file mode 100644 index 000000000..47b11ba84 --- /dev/null +++ b/lib/google/api_client/charset.rb @@ -0,0 +1,33 @@ +require 'faraday' +require 'zlib' + +module Google + class APIClient + class Charset < Faraday::Response::Middleware + include Google::APIClient::Logging + + def charset_for_content_type(type) + if type + m = type.match(/(?:charset|encoding)="?([a-z0-9-]+)"?/i) + if m + return Encoding.find(m[1]) + end + end + nil + end + + def adjust_encoding(env) + charset = charset_for_content_type(env[:response_headers]['content-type']) + if charset && env[:body].encoding != charset + env[:body].force_encoding(charset) + end + end + + def on_complete(env) + adjust_encoding(env) + end + end + end +end + +Faraday::Response.register_middleware :charset => Google::APIClient::Charset \ No newline at end of file diff --git a/spec/fixtures/files/zoo.json b/spec/fixtures/files/zoo.json new file mode 100644 index 000000000..4abd957c9 --- /dev/null +++ b/spec/fixtures/files/zoo.json @@ -0,0 +1,584 @@ +{ + "kind": "discovery#describeItem", + "name": "zoo", + "version": "v1", + "description": "Zoo API used for testing", + "basePath": "/zoo/", + "rootUrl": "https://www.googleapis.com/", + "servicePath": "zoo/v1/", + "rpcPath": "/rpc", + "parameters": { + "alt": { + "type": "string", + "description": "Data format for the response.", + "default": "json", + "enum": [ + "json" + ], + "enumDescriptions": [ + "Responses with Content-Type of application/json" + ], + "location": "query" + }, + "fields": { + "type": "string", + "description": "Selector specifying which fields to include in a partial response.", + "location": "query" + }, + "key": { + "type": "string", + "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + "location": "query" + }, + "oauth_token": { + "type": "string", + "description": "OAuth 2.0 token for the current user.", + "location": "query" + }, + "prettyPrint": { + "type": "boolean", + "description": "Returns response with indentations and line breaks.", + "default": "true", + "location": "query" + }, + "quotaUser": { + "type": "string", + "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", + "location": "query" + }, + "userIp": { + "type": "string", + "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", + "location": "query" + } + }, + "features": [ + "dataWrapper" + ], + "schemas": { + "Animal": { + "id": "Animal", + "type": "object", + "properties": { + "etag": { + "type": "string" + }, + "kind": { + "type": "string", + "default": "zoo#animal" + }, + "name": { + "type": "string" + }, + "photo": { + "type": "object", + "properties": { + "filename": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "hashAlgorithm": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + } + } + }, + "Animal2": { + "id": "Animal2", + "type": "object", + "properties": { + "kind": { + "type": "string", + "default": "zoo#animal" + }, + "name": { + "type": "string" + } + } + }, + "AnimalFeed": { + "id": "AnimalFeed", + "type": "object", + "properties": { + "etag": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "Animal" + } + }, + "kind": { + "type": "string", + "default": "zoo#animalFeed" + } + } + }, + "AnimalMap": { + "id": "AnimalMap", + "type": "object", + "properties": { + "etag": { + "type": "string" + }, + "animals": { + "type": "object", + "description": "Map of animal id to animal data", + "additionalProperties": { + "$ref": "Animal" + } + }, + "kind": { + "type": "string", + "default": "zoo#animalMap" + } + } + }, + "LoadFeed": { + "id": "LoadFeed", + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "doubleVal": { + "type": "number" + }, + "nullVal": { + "type": "null" + }, + "booleanVal": { + "type": "boolean", + "description": "True or False." + }, + "anyVal": { + "type": "any", + "description": "Anything will do." + }, + "enumVal": { + "type": "string" + }, + "kind": { + "type": "string", + "default": "zoo#loadValue" + }, + "longVal": { + "type": "integer" + }, + "stringVal": { + "type": "string" + } + } + } + }, + "kind": { + "type": "string", + "default": "zoo#loadFeed" + } + } + } + }, + "methods": { + "query": { + "path": "query", + "id": "bigquery.query", + "httpMethod": "GET", + "parameters": { + "q": { + "type": "string", + "location": "query", + "required": false, + "repeated": false + }, + "i": { + "type": "integer", + "location": "query", + "required": false, + "repeated": false, + "minimum": "0", + "maximum": "4294967295", + "default": "20" + }, + "n": { + "type": "number", + "location": "query", + "required": false, + "repeated": false + }, + "b": { + "type": "boolean", + "location": "query", + "required": false, + "repeated": false + }, + "a": { + "type": "any", + "location": "query", + "required": false, + "repeated": false + }, + "o": { + "type": "object", + "location": "query", + "required": false, + "repeated": false + }, + "e": { + "type": "string", + "location": "query", + "required": false, + "repeated": false, + "enum": [ + "foo", + "bar" + ] + }, + "er": { + "type": "string", + "location": "query", + "required": false, + "repeated": true, + "enum": [ + "one", + "two", + "three" + ] + }, + "rr": { + "type": "string", + "location": "query", + "required": false, + "repeated": true, + "pattern": "[a-z]+" + } + } + } + }, + "resources": { + "my": { + "resources": { + "favorites": { + "methods": { + "list": { + "path": "favorites/@me/mine", + "id": "zoo.animals.mine", + "httpMethod": "GET", + "parameters": { + "max-results": { + "location": "query", + "required": false + } + } + } + } + } + } + }, + "global": { + "resources": { + "print": { + "methods": { + "assert": { + "path": "global/print/assert", + "id": "zoo.animals.mine", + "httpMethod": "GET", + "parameters": { + "max-results": { + "location": "query", + "required": false + } + } + } + } + } + } + }, + "animals": { + "methods": { + "crossbreed": { + "path": "animals/crossbreed", + "id": "zoo.animals.crossbreed", + "httpMethod": "POST", + "description": "Cross-breed animals", + "response": { + "$ref": "Animal2" + }, + "mediaUpload": { + "accept": [ + "image/png" + ], + "protocols": { + "simple": { + "multipart": true, + "path": "upload/activities/{userId}/@self" + }, + "resumable": { + "multipart": true, + "path": "upload/activities/{userId}/@self" + } + } + } + }, + "delete": { + "path": "animals/{name}", + "id": "zoo.animals.delete", + "httpMethod": "DELETE", + "description": "Delete animals", + "parameters": { + "name": { + "location": "path", + "required": true, + "description": "Name of the animal to delete", + "type": "string" + } + }, + "parameterOrder": [ + "name" + ] + }, + "get": { + "path": "animals/{name}", + "id": "zoo.animals.get", + "httpMethod": "GET", + "description": "Get animals", + "supportsMediaDownload": true, + "parameters": { + "name": { + "location": "path", + "required": true, + "description": "Name of the animal to load", + "type": "string" + }, + "projection": { + "location": "query", + "type": "string", + "enum": [ + "full" + ], + "enumDescriptions": [ + "Include everything" + ] + } + }, + "parameterOrder": [ + "name" + ], + "response": { + "$ref": "Animal" + } + }, + "getmedia": { + "path": "animals/{name}", + "id": "zoo.animals.get", + "httpMethod": "GET", + "description": "Get animals", + "parameters": { + "name": { + "location": "path", + "required": true, + "description": "Name of the animal to load", + "type": "string" + }, + "projection": { + "location": "query", + "type": "string", + "enum": [ + "full" + ], + "enumDescriptions": [ + "Include everything" + ] + } + }, + "parameterOrder": [ + "name" + ] + }, + "insert": { + "path": "animals", + "id": "zoo.animals.insert", + "httpMethod": "POST", + "description": "Insert animals", + "request": { + "$ref": "Animal" + }, + "response": { + "$ref": "Animal" + }, + "mediaUpload": { + "accept": [ + "image/png" + ], + "maxSize": "1KB", + "protocols": { + "simple": { + "multipart": true, + "path": "upload/activities/{userId}/@self" + }, + "resumable": { + "multipart": true, + "path": "upload/activities/{userId}/@self" + } + } + } + }, + "list": { + "path": "animals", + "id": "zoo.animals.list", + "httpMethod": "GET", + "description": "List animals", + "parameters": { + "max-results": { + "location": "query", + "description": "Maximum number of results to return", + "type": "integer", + "minimum": "0" + }, + "name": { + "location": "query", + "description": "Restrict result to animals with this name", + "type": "string" + }, + "projection": { + "location": "query", + "type": "string", + "enum": [ + "full" + ], + "enumDescriptions": [ + "Include absolutely everything" + ] + }, + "start-token": { + "location": "query", + "description": "Pagination token", + "type": "string" + } + }, + "response": { + "$ref": "AnimalFeed" + } + }, + "patch": { + "path": "animals/{name}", + "id": "zoo.animals.patch", + "httpMethod": "PATCH", + "description": "Update animals", + "parameters": { + "name": { + "location": "path", + "required": true, + "description": "Name of the animal to update", + "type": "string" + } + }, + "parameterOrder": [ + "name" + ], + "request": { + "$ref": "Animal" + }, + "response": { + "$ref": "Animal" + } + }, + "update": { + "path": "animals/{name}", + "id": "zoo.animals.update", + "httpMethod": "PUT", + "description": "Update animals", + "parameters": { + "name": { + "location": "path", + "description": "Name of the animal to update", + "type": "string" + } + }, + "parameterOrder": [ + "name" + ], + "request": { + "$ref": "Animal" + }, + "response": { + "$ref": "Animal" + } + } + } + }, + "load": { + "methods": { + "list": { + "path": "load", + "id": "zoo.load.list", + "httpMethod": "GET", + "response": { + "$ref": "LoadFeed" + } + } + } + }, + "loadNoTemplate": { + "methods": { + "list": { + "path": "loadNoTemplate", + "id": "zoo.loadNoTemplate.list", + "httpMethod": "GET" + } + } + }, + "scopedAnimals": { + "methods": { + "list": { + "path": "scopedanimals", + "id": "zoo.scopedAnimals.list", + "httpMethod": "GET", + "description": "List animals (scoped)", + "parameters": { + "max-results": { + "location": "query", + "description": "Maximum number of results to return", + "type": "integer", + "minimum": "0" + }, + "name": { + "location": "query", + "description": "Restrict result to animals with this name", + "type": "string" + }, + "projection": { + "location": "query", + "type": "string", + "enum": [ + "full" + ], + "enumDescriptions": [ + "Include absolutely everything" + ] + }, + "start-token": { + "location": "query", + "description": "Pagination token", + "type": "string" + } + }, + "response": { + "$ref": "AnimalFeed" + } + } + } + } + } +} \ No newline at end of file diff --git a/spec/google/api_client/discovery_spec.rb b/spec/google/api_client/discovery_spec.rb index d1c23759f..a2eb4e5f8 100644 --- a/spec/google/api_client/discovery_spec.rb +++ b/spec/google/api_client/discovery_spec.rb @@ -23,6 +23,8 @@ require 'compat/multi_json' require 'signet/oauth_1/client' require 'google/api_client' +fixtures_path = File.expand_path('../../../fixtures', __FILE__) + RSpec.describe Google::APIClient do include ConnectionHelpers CLIENT = Google::APIClient.new(:application_name => 'API Client Tests') unless defined?(CLIENT) @@ -70,6 +72,15 @@ RSpec.describe Google::APIClient do expect(CLIENT.preferred_version('bogus')).to eq(nil) end + describe 'with zoo API' do + it 'should return API instance registered from file' do + zoo_json = File.join(fixtures_path, 'files', 'zoo.json') + contents = File.open(zoo_json, 'rb') { |io| io.read } + api = CLIENT.register_discovery_document('zoo', 'v1', contents) + expect(api).to be_kind_of(Google::APIClient::API) + end + end + describe 'with the prediction API' do before do CLIENT.authorization = nil diff --git a/spec/google/api_client/gzip_spec.rb b/spec/google/api_client/gzip_spec.rb index 9448b009f..0539b97d9 100644 --- a/spec/google/api_client/gzip_spec.rb +++ b/spec/google/api_client/gzip_spec.rb @@ -1,3 +1,4 @@ +# Encoding: utf-8 # Copyright 2012 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +21,7 @@ RSpec.describe Google::APIClient::Gzip do def create_connection(&block) Faraday.new do |b| + b.response :charset b.response :gzip b.adapter :test do |stub| stub.get '/', &block @@ -43,6 +45,17 @@ RSpec.describe Google::APIClient::Gzip do expect(result.body).to eq("Hello world\n") end + it 'should inflate with the correct charset encoding' do + conn = create_connection do |env| + [200, + { 'Content-Encoding' => 'deflate', 'Content-Type' => 'application/json;charset=BIG5'}, + Base64.decode64('eJxb8nLp7t2VAA8fBCI=')] + end + result = conn.get('/') + expect(result.body.encoding).to eq(Encoding::BIG5) + expect(result.body).to eq('日本語'.encode("BIG5")) + end + describe 'with API Client' do before do