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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +359 -0
  4. data/lib/appnigma/checks.rb +90 -0
  5. data/lib/appnigma/client.rb +118 -0
  6. data/lib/appnigma/connections.rb +31 -0
  7. data/lib/appnigma/constants.rb +80 -0
  8. data/lib/appnigma/crm.rb +40 -0
  9. data/lib/appnigma/errors.rb +135 -0
  10. data/lib/appnigma/generated/api/default_api.rb +972 -0
  11. data/lib/appnigma/generated/api_client.rb +441 -0
  12. data/lib/appnigma/generated/api_error.rb +58 -0
  13. data/lib/appnigma/generated/api_model_base.rb +88 -0
  14. data/lib/appnigma/generated/configuration.rb +392 -0
  15. data/lib/appnigma/generated/models/connection_status.rb +41 -0
  16. data/lib/appnigma/generated/models/create_connection_link_input.rb +265 -0
  17. data/lib/appnigma/generated/models/create_connection_link_output.rb +205 -0
  18. data/lib/appnigma/generated/models/crm_company.rb +322 -0
  19. data/lib/appnigma/generated/models/crm_company_get_output.rb +177 -0
  20. data/lib/appnigma/generated/models/crm_company_list_output.rb +192 -0
  21. data/lib/appnigma/generated/models/crm_contact.rb +404 -0
  22. data/lib/appnigma/generated/models/crm_contact_get_output.rb +177 -0
  23. data/lib/appnigma/generated/models/crm_contact_list_output.rb +192 -0
  24. data/lib/appnigma/generated/models/crm_custom_field.rb +189 -0
  25. data/lib/appnigma/generated/models/crm_deal.rb +479 -0
  26. data/lib/appnigma/generated/models/crm_deal_get_output.rb +177 -0
  27. data/lib/appnigma/generated/models/crm_deal_list_output.rb +192 -0
  28. data/lib/appnigma/generated/models/crm_email.rb +249 -0
  29. data/lib/appnigma/generated/models/crm_get_input.rb +272 -0
  30. data/lib/appnigma/generated/models/crm_labelled.rb +190 -0
  31. data/lib/appnigma/generated/models/crm_list_input.rb +303 -0
  32. data/lib/appnigma/generated/models/crm_phone.rb +249 -0
  33. data/lib/appnigma/generated/models/function_entry.rb +346 -0
  34. data/lib/appnigma/generated/models/function_owner.rb +105 -0
  35. data/lib/appnigma/generated/models/function_owner_one_of.rb +201 -0
  36. data/lib/appnigma/generated/models/function_owner_one_of1.rb +227 -0
  37. data/lib/appnigma/generated/models/get_connection_output.rb +251 -0
  38. data/lib/appnigma/generated/models/get_guide_output.rb +178 -0
  39. data/lib/appnigma/generated/models/json_value.rb +106 -0
  40. data/lib/appnigma/generated/models/list_functions_output.rb +179 -0
  41. data/lib/appnigma/generated/models/platform_request_input.rb +314 -0
  42. data/lib/appnigma/generated/models/platform_request_input_body.rb +104 -0
  43. data/lib/appnigma/generated/models/platform_request_output.rb +221 -0
  44. data/lib/appnigma/generated/models/post_input.rb +327 -0
  45. data/lib/appnigma/generated/models/post_input_record_value.rb +104 -0
  46. data/lib/appnigma/generated/models/post_output.rb +203 -0
  47. data/lib/appnigma/generated/models/pull_input.rb +335 -0
  48. data/lib/appnigma/generated/models/pull_output.rb +191 -0
  49. data/lib/appnigma/generated/models/pull_output_records_inner.rb +205 -0
  50. data/lib/appnigma/generated/models/run_input.rb +332 -0
  51. data/lib/appnigma/generated/models/run_output.rb +164 -0
  52. data/lib/appnigma/generated/models/sdk_error.rb +177 -0
  53. data/lib/appnigma/generated/models/sdk_error_error.rb +254 -0
  54. data/lib/appnigma/generated/models/sdk_error_error_details.rb +211 -0
  55. data/lib/appnigma/generated/models/sdk_problem.rb +205 -0
  56. data/lib/appnigma/generated/version.rb +15 -0
  57. data/lib/appnigma/generated.rb +82 -0
  58. data/lib/appnigma/names.rb +62 -0
  59. data/lib/appnigma/retry.rb +80 -0
  60. data/lib/appnigma/transport.rb +138 -0
  61. data/lib/appnigma/version.rb +9 -0
  62. data/lib/appnigma.rb +16 -0
  63. 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
@@ -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