appnigma-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +359 -0
- data/lib/appnigma/checks.rb +90 -0
- data/lib/appnigma/client.rb +118 -0
- data/lib/appnigma/connections.rb +31 -0
- data/lib/appnigma/constants.rb +80 -0
- data/lib/appnigma/crm.rb +40 -0
- data/lib/appnigma/errors.rb +135 -0
- data/lib/appnigma/generated/api/default_api.rb +972 -0
- data/lib/appnigma/generated/api_client.rb +441 -0
- data/lib/appnigma/generated/api_error.rb +58 -0
- data/lib/appnigma/generated/api_model_base.rb +88 -0
- data/lib/appnigma/generated/configuration.rb +392 -0
- data/lib/appnigma/generated/models/connection_status.rb +41 -0
- data/lib/appnigma/generated/models/create_connection_link_input.rb +265 -0
- data/lib/appnigma/generated/models/create_connection_link_output.rb +205 -0
- data/lib/appnigma/generated/models/crm_company.rb +322 -0
- data/lib/appnigma/generated/models/crm_company_get_output.rb +177 -0
- data/lib/appnigma/generated/models/crm_company_list_output.rb +192 -0
- data/lib/appnigma/generated/models/crm_contact.rb +404 -0
- data/lib/appnigma/generated/models/crm_contact_get_output.rb +177 -0
- data/lib/appnigma/generated/models/crm_contact_list_output.rb +192 -0
- data/lib/appnigma/generated/models/crm_custom_field.rb +189 -0
- data/lib/appnigma/generated/models/crm_deal.rb +479 -0
- data/lib/appnigma/generated/models/crm_deal_get_output.rb +177 -0
- data/lib/appnigma/generated/models/crm_deal_list_output.rb +192 -0
- data/lib/appnigma/generated/models/crm_email.rb +249 -0
- data/lib/appnigma/generated/models/crm_get_input.rb +272 -0
- data/lib/appnigma/generated/models/crm_labelled.rb +190 -0
- data/lib/appnigma/generated/models/crm_list_input.rb +303 -0
- data/lib/appnigma/generated/models/crm_phone.rb +249 -0
- data/lib/appnigma/generated/models/function_entry.rb +346 -0
- data/lib/appnigma/generated/models/function_owner.rb +105 -0
- data/lib/appnigma/generated/models/function_owner_one_of.rb +201 -0
- data/lib/appnigma/generated/models/function_owner_one_of1.rb +227 -0
- data/lib/appnigma/generated/models/get_connection_output.rb +251 -0
- data/lib/appnigma/generated/models/get_guide_output.rb +178 -0
- data/lib/appnigma/generated/models/json_value.rb +106 -0
- data/lib/appnigma/generated/models/list_functions_output.rb +179 -0
- data/lib/appnigma/generated/models/platform_request_input.rb +314 -0
- data/lib/appnigma/generated/models/platform_request_input_body.rb +104 -0
- data/lib/appnigma/generated/models/platform_request_output.rb +221 -0
- data/lib/appnigma/generated/models/post_input.rb +327 -0
- data/lib/appnigma/generated/models/post_input_record_value.rb +104 -0
- data/lib/appnigma/generated/models/post_output.rb +203 -0
- data/lib/appnigma/generated/models/pull_input.rb +335 -0
- data/lib/appnigma/generated/models/pull_output.rb +191 -0
- data/lib/appnigma/generated/models/pull_output_records_inner.rb +205 -0
- data/lib/appnigma/generated/models/run_input.rb +332 -0
- data/lib/appnigma/generated/models/run_output.rb +164 -0
- data/lib/appnigma/generated/models/sdk_error.rb +177 -0
- data/lib/appnigma/generated/models/sdk_error_error.rb +254 -0
- data/lib/appnigma/generated/models/sdk_error_error_details.rb +211 -0
- data/lib/appnigma/generated/models/sdk_problem.rb +205 -0
- data/lib/appnigma/generated/version.rb +15 -0
- data/lib/appnigma/generated.rb +82 -0
- data/lib/appnigma/names.rb +62 -0
- data/lib/appnigma/retry.rb +80 -0
- data/lib/appnigma/transport.rb +138 -0
- data/lib/appnigma/version.rb +9 -0
- data/lib/appnigma.rb +16 -0
- metadata +155 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appnigma
|
|
4
|
+
KEY_PREFIX = "apn_live_"
|
|
5
|
+
|
|
6
|
+
# Where `Appnigma::Client.new` finds the key when none is passed.
|
|
7
|
+
KEY_ENV_NAME = "APPNIGMA_SDK_KEY"
|
|
8
|
+
|
|
9
|
+
# Where the client calls: production.
|
|
10
|
+
DEFAULT_BASE_URL = "https://engine.appnigma.ai"
|
|
11
|
+
|
|
12
|
+
# Points the client at a local or staging server for our own testing. Read
|
|
13
|
+
# from the environment, never a client setting, and never in the README: a
|
|
14
|
+
# customer always calls production.
|
|
15
|
+
API_URL_ENV_NAME = "APPNIGMA_API_URL"
|
|
16
|
+
|
|
17
|
+
# The only hosts the address may reach over plain http://, so a key never
|
|
18
|
+
# crosses a network unencrypted.
|
|
19
|
+
LOCAL_HOSTS = ["localhost", "127.0.0.1", "[::1]"].freeze
|
|
20
|
+
|
|
21
|
+
ROUTE_BASE = "/sdk/v1"
|
|
22
|
+
|
|
23
|
+
REQUEST_ID_HEADER = "x-request-id"
|
|
24
|
+
RETRY_AFTER_HEADER = "retry-after"
|
|
25
|
+
|
|
26
|
+
# How long one attempt may take before it counts as a network failure.
|
|
27
|
+
DEFAULT_TIMEOUT_MS = 30_000
|
|
28
|
+
|
|
29
|
+
# The same ceiling the TypeScript package applies, so the two READMEs show
|
|
30
|
+
# the same numbers.
|
|
31
|
+
MAX_TIMER_MS = 2_147_483_647
|
|
32
|
+
|
|
33
|
+
# Tries again after the first attempt: three attempts in all.
|
|
34
|
+
DEFAULT_MAX_RETRIES = 2
|
|
35
|
+
|
|
36
|
+
# A Retry-After longer than this is reported, not waited for.
|
|
37
|
+
DEFAULT_MAX_RETRY_WAIT_MS = 30_000
|
|
38
|
+
|
|
39
|
+
# This gem's own code for "no answer arrived". Not in the server's table,
|
|
40
|
+
# which only lists codes the server can send.
|
|
41
|
+
NETWORK_ERROR_CODE = "NETWORK_ERROR"
|
|
42
|
+
|
|
43
|
+
# The one 409 that sends an end customer through the connect flow again.
|
|
44
|
+
RECONNECT_ERROR_CODE = "CONNECTION_NEEDS_RECONNECT"
|
|
45
|
+
|
|
46
|
+
# An end customer id is 1 to 100 of letters, digits, dot, dash or underscore.
|
|
47
|
+
END_CUSTOMER_ID_PATTERN = /\A[A-Za-z0-9._-]{1,100}\z/
|
|
48
|
+
END_CUSTOMER_ID_RULE = "must be 1 to 100 characters: letters, digits, dot, dash or underscore"
|
|
49
|
+
|
|
50
|
+
# `.` and `..` fit the pattern but cannot travel in a URL path.
|
|
51
|
+
END_CUSTOMER_ID_PATH_UNSAFE = [".", ".."].freeze
|
|
52
|
+
END_CUSTOMER_ID_PATH_RULE = "cannot be only dots, it travels in the URL path"
|
|
53
|
+
|
|
54
|
+
# The server's error codes (the API description, 18.2). This gem keeps its
|
|
55
|
+
# own copy: it is published on its own. A test compares it with the
|
|
56
|
+
# TypeScript package's list.
|
|
57
|
+
ERROR_CODES = %w[
|
|
58
|
+
INVALID_INPUT
|
|
59
|
+
SDK_KEY_MISSING
|
|
60
|
+
SDK_KEY_INVALID
|
|
61
|
+
SDK_KEY_CANCELLED
|
|
62
|
+
MICROSERVER_NOT_ENABLED
|
|
63
|
+
PLATFORM_NOT_ENABLED
|
|
64
|
+
END_CUSTOMER_NOT_FOUND
|
|
65
|
+
FUNCTION_NOT_FOUND
|
|
66
|
+
CONNECTION_NEEDS_RECONNECT
|
|
67
|
+
RATE_LIMITED
|
|
68
|
+
PLATFORM_RATE_LIMITED
|
|
69
|
+
PLATFORM_ERROR
|
|
70
|
+
TIMED_OUT
|
|
71
|
+
LOGIC_FAILED
|
|
72
|
+
INTERNAL_ERROR
|
|
73
|
+
RECORD_NOT_FOUND
|
|
74
|
+
DATA_TYPE_NOT_AVAILABLE
|
|
75
|
+
].freeze
|
|
76
|
+
|
|
77
|
+
OBJECTS = %w[contact company deal].freeze
|
|
78
|
+
CONNECTION_STATUSES = %w[connected needs_reconnect disconnected].freeze
|
|
79
|
+
PLATFORM_REQUEST_METHODS = %w[GET POST PUT PATCH DELETE].freeze
|
|
80
|
+
end
|
data/lib/appnigma/crm.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "names"
|
|
4
|
+
|
|
5
|
+
module Appnigma
|
|
6
|
+
# An end customer's CRM data in one shape, whichever CRM they use:
|
|
7
|
+
# `client.crm.contacts`, `client.crm.companies` and `client.crm.deals`,
|
|
8
|
+
# each with `list` and `get`. Reads, so they are retried.
|
|
9
|
+
class Crm
|
|
10
|
+
attr_reader :contacts, :companies, :deals
|
|
11
|
+
|
|
12
|
+
def initialize(client)
|
|
13
|
+
@contacts = Objects.new(client, list: :list_crm_contacts, get: :get_crm_contact)
|
|
14
|
+
@companies = Objects.new(client, list: :list_crm_companies, get: :get_crm_company)
|
|
15
|
+
@deals = Objects.new(client, list: :list_crm_deals, get: :get_crm_deal)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The two reads of one CRM object.
|
|
19
|
+
class Objects
|
|
20
|
+
def initialize(client, list:, get:)
|
|
21
|
+
@client = client
|
|
22
|
+
@list = list
|
|
23
|
+
@get = get
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# One page of records and a marker for the next; pass `next` as
|
|
27
|
+
# `cursor` for the following page. `limit` is 1 to 100.
|
|
28
|
+
def list(end_customer_id:, platform:, limit: nil, cursor: nil, timeout_ms: nil)
|
|
29
|
+
body = Names.camel_keys(end_customer_id: end_customer_id, platform: platform, limit: limit, cursor: cursor)
|
|
30
|
+
@client.call(@list, body, timeout_ms: timeout_ms)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# One record by its id in the CRM.
|
|
34
|
+
def get(end_customer_id:, platform:, id:, timeout_ms: nil)
|
|
35
|
+
body = Names.camel_keys(end_customer_id: end_customer_id, platform: platform, id: id)
|
|
36
|
+
@client.call(@get, body, timeout_ms: timeout_ms)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
|
|
5
|
+
module Appnigma
|
|
6
|
+
# Every failure this gem raises, whether the server answered with an error
|
|
7
|
+
# or could not be reached at all. Rescue this for "anything from Appnigma",
|
|
8
|
+
# or one of the subclasses for one kind of failure. `code` is the string
|
|
9
|
+
# the server sent, known to this gem or not; `status` and `request_id` are
|
|
10
|
+
# nil when nothing came back.
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
attr_reader :code, :status, :request_id, :details
|
|
13
|
+
|
|
14
|
+
def initialize(code:, message:, status: nil, request_id: nil, details: {})
|
|
15
|
+
super(message)
|
|
16
|
+
@code = code
|
|
17
|
+
@status = status
|
|
18
|
+
@request_id = request_id
|
|
19
|
+
@details = details
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 400. The server names each field it could not accept, as `problems`:
|
|
24
|
+
# a list of `{ field:, problem: }`. Also raised locally for a bad setting
|
|
25
|
+
# or a bad end customer id, before any call.
|
|
26
|
+
class InputError < Error
|
|
27
|
+
attr_reader :problems
|
|
28
|
+
|
|
29
|
+
def initialize(**args)
|
|
30
|
+
super
|
|
31
|
+
@problems = Errors.read_problems(details)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# 401 and 403, and a client started without a key.
|
|
36
|
+
class AuthError < Error; end
|
|
37
|
+
|
|
38
|
+
# 404.
|
|
39
|
+
class NotFoundError < Error; end
|
|
40
|
+
|
|
41
|
+
# 409 CONNECTION_NEEDS_RECONNECT: the end customer has to approve again.
|
|
42
|
+
class ReconnectError < Error; end
|
|
43
|
+
|
|
44
|
+
# Any other 409: the platform cannot give this yet.
|
|
45
|
+
class NotAvailableError < Error; end
|
|
46
|
+
|
|
47
|
+
# 429. `retry_after_seconds` is what the server's Retry-After header said,
|
|
48
|
+
# or nil when it sent none.
|
|
49
|
+
class RateLimitError < Error
|
|
50
|
+
attr_reader :retry_after_seconds
|
|
51
|
+
|
|
52
|
+
def initialize(retry_after_seconds: nil, **args)
|
|
53
|
+
super(**args)
|
|
54
|
+
@retry_after_seconds = retry_after_seconds
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Any 5xx, any status this gem does not know, and any answer that is not
|
|
59
|
+
# JSON. Our side is at fault, not the caller's input.
|
|
60
|
+
class ServerError < Error; end
|
|
61
|
+
|
|
62
|
+
# No answer arrived: the call failed, or the attempt ran out of time.
|
|
63
|
+
# `cause` is the underlying exception when there is one.
|
|
64
|
+
class NetworkError < Error
|
|
65
|
+
attr_reader :cause
|
|
66
|
+
|
|
67
|
+
def initialize(message:, cause: nil)
|
|
68
|
+
super(code: NETWORK_ERROR_CODE, message: message)
|
|
69
|
+
@cause = cause
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Turns one answer from the server into the error for its status. The type
|
|
74
|
+
# is chosen by status, not by code, so a code this gem has never seen still
|
|
75
|
+
# lands in the right class with its string kept.
|
|
76
|
+
module Errors
|
|
77
|
+
PROBLEM_KEYS = %w[field problem].freeze
|
|
78
|
+
|
|
79
|
+
# Every status the server's table names; any other is a ServerError. 409
|
|
80
|
+
# is chosen by code as well, in error_class.
|
|
81
|
+
CLASS_BY_STATUS = {
|
|
82
|
+
400 => InputError,
|
|
83
|
+
401 => AuthError,
|
|
84
|
+
403 => AuthError,
|
|
85
|
+
404 => NotFoundError,
|
|
86
|
+
429 => RateLimitError
|
|
87
|
+
}.freeze
|
|
88
|
+
|
|
89
|
+
module_function
|
|
90
|
+
|
|
91
|
+
def from_answer(status:, body:, request_id:, retry_after_seconds: nil)
|
|
92
|
+
fallback = "The Appnigma API answered #{status}"
|
|
93
|
+
shape = read_error_shape(body) || { code: "INTERNAL_ERROR", message: "", details: {} }
|
|
94
|
+
args = { code: shape[:code], message: shape[:message].empty? ? fallback : shape[:message],
|
|
95
|
+
status: status, request_id: request_id, details: shape[:details] }
|
|
96
|
+
klass = error_class(status, shape[:code])
|
|
97
|
+
klass == RateLimitError ? klass.new(retry_after_seconds: retry_after_seconds, **args) : klass.new(**args)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Only CONNECTION_NEEDS_RECONNECT asks for a reconnect; any other 409 is
|
|
101
|
+
# something the platform cannot give yet.
|
|
102
|
+
def error_class(status, code)
|
|
103
|
+
return code == RECONNECT_ERROR_CODE ? ReconnectError : NotAvailableError if status == 409
|
|
104
|
+
|
|
105
|
+
CLASS_BY_STATUS.fetch(status, ServerError)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Our shape is `{ "error" => { "code", "message", "details" } }`, as JSON
|
|
109
|
+
# parses it. Anything else reads as "not our shape".
|
|
110
|
+
def read_error_shape(body)
|
|
111
|
+
return nil unless body.is_a?(Hash) && body["error"].is_a?(Hash)
|
|
112
|
+
|
|
113
|
+
error = body["error"]
|
|
114
|
+
code = error["code"]
|
|
115
|
+
return nil unless code.is_a?(String) && !code.empty?
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
code: code,
|
|
119
|
+
message: error["message"].is_a?(String) ? error["message"] : "",
|
|
120
|
+
details: error["details"].is_a?(Hash) ? error["details"] : {}
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def read_problems(details)
|
|
125
|
+
list = details["problems"]
|
|
126
|
+
return [] unless list.is_a?(Array)
|
|
127
|
+
|
|
128
|
+
list.filter_map do |entry|
|
|
129
|
+
next unless entry.is_a?(Hash) && PROBLEM_KEYS.all? { |key| entry[key].is_a?(String) }
|
|
130
|
+
|
|
131
|
+
{ field: entry["field"], problem: entry["problem"] }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|