Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ described under [Webhooks](#webhooks).

Requests rejected by the Seam API raise a `Seam::Http::ApiError` subclass
carrying the HTTP `status_code`, API error `code`, and `request_id`.
A successful response that does not carry the resource the endpoint returns,
for example when a proxy rewrites the body,
raises a `Seam::Http::InvalidResponseError` exposing the request `path`
and the `response_key` the SDK expected.

#### Validation errors

Expand Down
4 changes: 2 additions & 2 deletions codegen/layouts/partials/client-method.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
{{/if}} {{#if usesRes}}res = {{/if}}@client.{{httpMethod}}("{{path}}"{{#if hasParams}}, {{bodyParams}}.compact{{/if}})
{{#if isResource}}

Seam::Resources::{{returnResource}}.load_from_response(res.body["{{returnPath}}"])
Seam::Resources::{{returnResource}}.{{readMethod}}(res, "{{returnPath}}", "{{path}}")
{{/if}}
{{#if isPoll}}

wait_for_action_attempt = wait_for_action_attempt.nil? ? @defaults.wait_for_action_attempt : wait_for_action_attempt

Seam::ActionAttemptResolver.resolve(Seam::Resources::ActionAttempt.load_from_response(res.body["{{returnPath}}"]), @client, wait_for_action_attempt)
Seam::ActionAttemptResolver.resolve(Seam::Resources::ActionAttempt.from_http_response(res, "{{returnPath}}", "{{path}}"), @client, wait_for_action_attempt)
{{/if}}
{{#if isNil}}

Expand Down
3 changes: 3 additions & 0 deletions codegen/lib/layouts/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ClientMethodLayoutContext {
httpMethod: string
returnResource: string
returnPath: string
readMethod: string
}

export interface ClientLayoutContext {
Expand All @@ -47,6 +48,7 @@ const getMethodLayoutContext = (
parameters,
returnResource,
returnPath,
returnsList,
httpMethod,
} = method

Expand Down Expand Up @@ -92,6 +94,7 @@ const getMethodLayoutContext = (
isNil,
returnResource: returnResource ?? '',
returnPath,
readMethod: returnsList ? 'list_from_http_response' : 'from_http_response',
}
}

Expand Down
16 changes: 11 additions & 5 deletions codegen/lib/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,9 @@ const getAtLeastOneParameterNames = (endpoint: Endpoint): string[] =>
: []

const createClientMethod = (endpoint: Endpoint): ClientMethod => {
const { returnPath, returnResource } = getEndpointReturn(endpoint.response)
const { returnPath, returnResource, returnsList } = getEndpointReturn(
endpoint.response,
)
const atLeastOneParameterNames = getAtLeastOneParameterNames(endpoint)

return {
Expand All @@ -322,29 +324,33 @@ const createClientMethod = (endpoint: Endpoint): ClientMethod => {
})),
returnPath,
returnResource,
returnsList,
}
}

const getEndpointReturn = (
response: Response,
): Pick<ClientMethod, 'returnPath' | 'returnResource'> => {
): Pick<ClientMethod, 'returnPath' | 'returnResource' | 'returnsList'> => {
if (response.responseType === 'void') {
return { returnPath: '', returnResource: null }
return { returnPath: '', returnResource: null, returnsList: false }
}

const returnsList = response.responseType === 'resource_list'

const { responseKey, resourceType } = response

if (resourceType === 'unknown') {
// Batch responses hold multiple resource types keyed by batch key, which
// the Batch resource models directly.
if (responseKey === 'batch') {
return { returnPath: 'batch', returnResource: 'Batch' }
return { returnPath: 'batch', returnResource: 'Batch', returnsList }
}
return { returnPath: '', returnResource: null }
return { returnPath: '', returnResource: null, returnsList: false }
}

return {
returnPath: responseKey,
returnResource: pascalCase(convertCustomResourceName(resourceType)),
returnsList,
}
}
1 change: 1 addition & 0 deletions codegen/lib/ruby-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ClientMethod {
parameters: ClientMethodParameter[]
returnResource: string | null
returnPath: string
returnsList: boolean
}

export interface ChildClientIdentifier {
Expand Down
4 changes: 3 additions & 1 deletion lib/seam/action_attempt_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ def self.validate_poll_options(timeout, polling_interval)
def self.update_action_attempt(action_attempt, client)
response = client.get("/action_attempts/get", {action_attempt_id: action_attempt.action_attempt_id})

action_attempt.update_from_response(response.body["action_attempt"])
action_attempt.update_from_response(
Seam::Resources::ActionAttempt.from_http_response(response, "action_attempt", "/action_attempts/get").data
)
action_attempt
end

Expand Down
42 changes: 42 additions & 0 deletions lib/seam/base_resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "time"
require_relative "deep_hash_accessor"
require_relative "invalid_response_error"

module Seam
module Resources
Expand All @@ -21,6 +22,47 @@ def update_from_response(data)
process_data_attributes(@data)
end

def self.from_http_response(response, response_key, path, client = nil)
value = read_http_response_key(response, response_key, path)

unless value.is_a?(Hash)
raise Http::InvalidResponseError.new(path, response_key, "got #{value.class} instead of an object")
end

load_from_response(value, client)
end

def self.list_from_http_response(response, response_key, path, client = nil)
value = read_http_response_key(response, response_key, path)

unless value.is_a?(Array)
raise Http::InvalidResponseError.new(path, response_key, "got #{value.class} instead of a list")
end

load_from_response(value, client)
end

def self.read_http_response_key(response, response_key, path)
unless response.success?
raise Http::InvalidResponseError.new(
path, response_key, "got a #{response.status} response instead of a success response"
)
end

body = response.body

unless body.is_a?(Hash)
raise Http::InvalidResponseError.new(path, response_key, "got #{body.class} instead of a response object")
end

unless body.key?(response_key)
raise Http::InvalidResponseError.new(path, response_key, "which the response does not contain")
end

body[response_key]
end
private_class_method :read_http_response_key

def self.load_from_response(data, client = nil)
return nil if data.nil?

Expand Down
17 changes: 17 additions & 0 deletions lib/seam/invalid_response_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require_relative "errors"

module Seam
module Http
class InvalidResponseError < Seam::Error
attr_reader :path, :response_key

def initialize(path, response_key, reason)
super("Seam returned an invalid response for #{path}: expected \"#{response_key}\", #{reason}")
@path = path
@response_key = response_key
end
end
end
end
29 changes: 17 additions & 12 deletions lib/seam/paginator.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# frozen_string_literal: true

require "faraday"
require_relative "invalid_response_error"

module Seam
THREAD_CONTEXT_KEY = :seam_pagination_context
PaginationContext = Struct.new(:pagination)
PaginationContext = Struct.new(:body, :path)

class Paginator
def initialize(request, params = {})
Expand Down Expand Up @@ -60,13 +61,24 @@ def walk
end
end

def read_pagination(body, path)
unless body.is_a?(Hash) && body.key?("pagination")
raise Http::InvalidResponseError.new(path, "pagination", "which the response does not contain")
end

pagination = body["pagination"]
return pagination if pagination.is_a?(Hash)

raise Http::InvalidResponseError.new(path, "pagination", "got #{pagination.class} instead of a pagination object")
end

def fetch_page(params)
context = PaginationContext.new(nil)
context = PaginationContext.new(nil, nil)
Thread.current[THREAD_CONTEXT_KEY] = context

begin
res_data = @request.call(**params)
pagination_result = Pagination.from_hash(context.pagination)
pagination_result = Pagination.from_hash(read_pagination(context.body, context.path))
[res_data, pagination_result]
ensure
Thread.current[THREAD_CONTEXT_KEY] = nil
Expand Down Expand Up @@ -95,15 +107,8 @@ def on_complete(env)
context = Thread.current[THREAD_CONTEXT_KEY]
return unless context.is_a?(PaginationContext)

pagination_hash = extract_pagination(env)
context.pagination = pagination_hash if pagination_hash
end

private

def extract_pagination(env)
body = env[:body]
body["pagination"] if body.is_a?(Hash) && body.key?("pagination")
context.body = env[:body]
context.path = env.url.path
end
end
end
12 changes: 6 additions & 6 deletions lib/seam/routes/access_codes.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/seam/routes/access_codes_simulate.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions lib/seam/routes/access_codes_unmanaged.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions lib/seam/routes/access_grants.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading