File: api_command.rb

package info (click to toggle)
ruby-google-apis-core 1.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 284 kB
  • sloc: ruby: 2,096; makefile: 4
file content (237 lines) | stat: -rw-r--r-- 8,706 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'addressable/uri'
require 'addressable/template'
require 'google/apis/core/http_command'
require 'google/apis/errors'
require 'json'
require 'retriable'
require "securerandom"

module Google
  module Apis
    module Core
      # Command for executing most basic API request with JSON requests/responses
      class ApiCommand < HttpCommand
        JSON_CONTENT_TYPE = 'application/json'
        FIELDS_PARAM = 'fields'
        ERROR_REASON_MAPPING = {
          'rateLimitExceeded' => Google::Apis::RateLimitError,
          'userRateLimitExceeded' => Google::Apis::RateLimitError,
          'projectNotLinked' => Google::Apis::ProjectNotLinkedError
        }

        # JSON serializer for request objects
        # @return [Google::Apis::Core::JsonRepresentation]
        attr_accessor :request_representation

        # Request body to serialize
        # @return [Object]
        attr_accessor :request_object

        # JSON serializer for response objects
        # @return [Google::Apis::Core::JsonRepresentation]
        attr_accessor :response_representation

        # Class to instantiate when de-serializing responses
        # @return [Object]
        attr_accessor :response_class

        # Client library version.
        # @return [String]
        attr_accessor :client_version

        # @param [symbol] method
        #   HTTP method
        # @param [String,Addressable::URI, Addressable::Template] url
        #   HTTP URL or template
        # @param [String, #read] body
        #   Request body
        def initialize(method, url, body: nil, client_version: nil)
          super(method, url, body: body)
          self.client_version = client_version || Core::VERSION
        end

        # Serialize the request body
        #
        # @return [void]
        def prepare!
          set_api_client_header
          set_user_project_header
          if options&.api_format_version
            header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s
          end
          query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM)
          if request_representation && request_object
            header['Content-Type'] ||= JSON_CONTENT_TYPE
            if options && options.skip_serialization
              self.body = request_object
            else
              self.body = request_representation.new(request_object).to_json(user_options: { skip_undefined: true })
            end
          end
          super
        end

        # Deserialize the response body if present
        #
        # @param [String] content_type
        #  Content type of body
        # @param [String, #read] body
        #  Response body
        # @return [Object]
        #   Response object
        # noinspection RubyUnusedLocalVariable
        def decode_response_body(content_type, body)
          return super unless response_representation
          return super if options && options.skip_deserialization
          return super if content_type.nil?
          return nil unless content_type.start_with?(JSON_CONTENT_TYPE)
          body = "{}" if body.empty?
          instance = response_class.new
          response_representation.new(instance).from_json(body, unwrap: response_class)
          instance
        end

        # Check the response and raise error if needed
        #
        # @param [Fixnum] status
        #   HTTP status code of response
        # @param [Hash] header
        #   HTTP response headers
        # @param [String] body
        #   HTTP response body
        # @param [String] message
        #   Error message text
        # @return [void]
        # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
        # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
        # @raise [Google::Apis::AuthorizationError] Authorization is required
        def check_status(status, header = nil, body = nil, message = nil)
          case status
          when 400, 402...500
            reason, message = parse_error(body)
            if reason
              message = sprintf('%s: %s', reason, message)
              raise ERROR_REASON_MAPPING[reason].new(
                message,
                status_code: status,
                header: header,
                body: body
              ) if ERROR_REASON_MAPPING.key?(reason)
            end
            super(status, header, body, message)
          else
            super(status, header, body, message)
          end
        end

        def allow_form_encoding?
          request_representation.nil? && super
        end

        private

        def set_api_client_header
          old_xgac = header
            .find_all { |k, v| k.downcase == 'x-goog-api-client' }
            .map { |(a, b)| b }
            .join(' ')
            .split
            .find_all { |s| s !~ %r{^gl-ruby/|^gdcl/} }
            .join(' ')
          # Report 0.x.y versions that are in separate packages as 1.x.y.
          # Thus, reported gdcl/0.x.y versions are monopackage clients, while
          # reported gdcl/1.x.y versions are split clients.
          # In the unlikely event that we want to release post-1.0 versions of
          # these clients, we should start the versioning at 2.0 to avoid
          # confusion.
          munged_client_version = client_version.sub(/^0\./, "1.")
          xgac = "gl-ruby/#{RUBY_VERSION} gdcl/#{munged_client_version}"
          xgac = old_xgac.empty? ? xgac : "#{old_xgac} #{xgac}"
          header.delete_if { |k, v| k.downcase == 'x-goog-api-client' }
          xgac.concat(" ",invocation_id_header) if options.add_invocation_id_header
          header['X-Goog-Api-Client'] = xgac
        end

        def set_user_project_header
          quota_project_id = options.quota_project
          if !quota_project_id && options&.authorization.respond_to?(:quota_project_id)
            quota_project_id = options.authorization.quota_project_id
          end
          header['X-Goog-User-Project'] = quota_project_id if quota_project_id
        end

        def invocation_id_header
          "gccl-invocation-id/#{SecureRandom.uuid}"
        end

        # Attempt to parse a JSON error message
        # @param [String] body
        #  HTTP response body
        # @return [Array<(String, String)>]
        #   Error reason and message
        def parse_error(body)
          obj = JSON.load(body)
          error = obj['error']
          if error['details']
            return extract_v2_error_details(error)
          elsif error['errors']
            return extract_v1_error_details(error)
          else
            fail 'Can not parse error message. No "details" or "errors" detected'
          end
        rescue
          return [nil, nil]
        end

        # Extracts details from a v1 error message
        # @param [Hash] error
        #  Parsed JSON
        # @return [Array<(String, String)>]
        #   Error reason and message
        def extract_v1_error_details(error)
          reason = error['errors'].first['reason']
          message = error['message']
          return [reason, message]
        end

        # Extracts details from a v2error message
        # @param [Hash] error
        #  Parsed JSON
        # @return [Array<(String, String)>]
        #   Error reason and message
        def extract_v2_error_details(error)
          reason = error['status']
          message = error['message']
          return [reason, message]
        end

        # Convert field names from ruby conventions to original names in JSON
        #
        # @param [String] fields
        #   Value of 'fields' param
        # @return [String]
        #   Updated header value
        def normalize_fields_param(fields)
          # TODO: Generate map of parameter names during code gen. Small possibility that camelization fails
          fields.gsub(/:/, '').gsub(/\w+/) do |str|
            str.gsub(/(?:^|_)([a-z])/){ Regexp.last_match.begin(0) == 0 ? $1 : $1.upcase }
          end
        end
      end
    end
  end
end