pago-sdk 1.0.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 +131 -0
- data/lib/pago/base_client.rb +116 -0
- data/lib/pago/errors.rb +83 -0
- data/lib/pago/http.rb +78 -0
- data/lib/pago/model.rb +86 -0
- data/lib/pago/paginator.rb +67 -0
- data/lib/pago/serde.rb +186 -0
- data/lib/pago/service.rb +17 -0
- data/lib/pago/v2026_04/client.rb +124 -0
- data/lib/pago/v2026_04/enums.rb +4552 -0
- data/lib/pago/v2026_04/errors.rb +1121 -0
- data/lib/pago/v2026_04/models.rb +46344 -0
- data/lib/pago/v2026_04/services/benefit_grants.rb +48 -0
- data/lib/pago/v2026_04/services/benefits.rb +179 -0
- data/lib/pago/v2026_04/services/checkout_links.rb +131 -0
- data/lib/pago/v2026_04/services/checkouts.rb +185 -0
- data/lib/pago/v2026_04/services/custom_fields.rb +132 -0
- data/lib/pago/v2026_04/services/customer_meters.rb +69 -0
- data/lib/pago/v2026_04/services/customer_portal.rb +1181 -0
- data/lib/pago/v2026_04/services/customer_seats.rb +137 -0
- data/lib/pago/v2026_04/services/customer_sessions.rb +35 -0
- data/lib/pago/v2026_04/services/customers.rb +556 -0
- data/lib/pago/v2026_04/services/discounts.rb +131 -0
- data/lib/pago/v2026_04/services/disputes.rb +93 -0
- data/lib/pago/v2026_04/services/event_types.rb +74 -0
- data/lib/pago/v2026_04/services/events.rb +126 -0
- data/lib/pago/v2026_04/services/files.rb +135 -0
- data/lib/pago/v2026_04/services/license_keys.rb +183 -0
- data/lib/pago/v2026_04/services/members.rb +47 -0
- data/lib/pago/v2026_04/services/meters.rb +142 -0
- data/lib/pago/v2026_04/services/metrics.rb +188 -0
- data/lib/pago/v2026_04/services/oauth2.rb +175 -0
- data/lib/pago/v2026_04/services/orders.rb +238 -0
- data/lib/pago/v2026_04/services/organizations.rb +113 -0
- data/lib/pago/v2026_04/services/payments.rb +72 -0
- data/lib/pago/v2026_04/services/products.rb +142 -0
- data/lib/pago/v2026_04/services/refunds.rb +73 -0
- data/lib/pago/v2026_04/services/subscriptions.rb +171 -0
- data/lib/pago/v2026_04/services/webhooks.rb +212 -0
- data/lib/pago/v2026_04/unions.rb +739 -0
- data/lib/pago/v2026_04/webhooks.rb +86 -0
- data/lib/pago/version.rb +5 -0
- data/lib/pago/webhooks.rb +159 -0
- data/lib/pago.rb +39 -0
- data/sig/pago/v2026_04/generated.rbs +12401 -0
- data/sig/pago.rbs +204 -0
- metadata +91 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e1d8d540d5ad5f23bf997c722a53baaac2fe9f157ea8f5700b8553ce9a12b854
|
|
4
|
+
data.tar.gz: 58912be52a4a7efb47c1431732aaa497b71b9faf4e50cbea47a66d6313988744
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e681002b41f4b3457ff8c23a2a9833d307f41c17dfcdce5e574a229ccaa440825e7b7f2b8270498129e9130bc54f9e82e92c63a47627f153fab2e10f4cce2989
|
|
7
|
+
data.tar.gz: 19074f7bf295a69e73cbf2bae4fe57a6088cbf3869300a4d2724ff146633fbb0409c6dc3a5008ae50a396c93d1138a216dfde305504b135e78afd8b146f1f593
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Pago
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Pago Ruby SDK
|
|
2
|
+
|
|
3
|
+
Ruby client for the [Pago](https://pago.sh) API, generated from its OpenAPI
|
|
4
|
+
specification. Requires Ruby 3.1 or newer and has **no runtime dependencies** —
|
|
5
|
+
only `net/http`, `json` and `openssl` from the standard library.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
gem "pago-sdk"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "pago"
|
|
15
|
+
|
|
16
|
+
client = Pago::Client.new(access_token: ENV.fetch("PAGO_ACCESS_TOKEN"))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`Pago::Client` always tracks the latest API version (2026-04).
|
|
20
|
+
Pin a version explicitly to opt out of that:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
client = Pago::V2026_04::Client.new(access_token: token)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The base URL defaults to `https://api.pago.sh` and reads `PAGO_BASE_URL` when
|
|
27
|
+
set; the sandbox lives at `Pago::BaseClient::SANDBOX_BASE_URL`.
|
|
28
|
+
|
|
29
|
+
## Models
|
|
30
|
+
|
|
31
|
+
Models are immutable value objects built with keyword arguments. Required
|
|
32
|
+
properties are required keyword arguments; everything else defaults to unset.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
order = Pago::Models::Order.from_json(payload)
|
|
36
|
+
order.id
|
|
37
|
+
order.to_json_hash # Hash ready for JSON.generate
|
|
38
|
+
order.to_json # String
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Only the attributes you actually set are serialized, so a partial update never
|
|
42
|
+
sends `null` for the fields it does not touch. Properties this SDK release does
|
|
43
|
+
not know about survive a `from_json` / `to_json` round trip untouched.
|
|
44
|
+
|
|
45
|
+
## Pagination
|
|
46
|
+
|
|
47
|
+
Every paginated operation gets an `_each` companion returning a
|
|
48
|
+
`Pago::Paginator`, a plain `Enumerable` over the items that fetches pages on
|
|
49
|
+
demand:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
client.products.list_each(organization_id: id).each { |product| puts product.name }
|
|
53
|
+
client.products.list_each(organization_id: id).first(50)
|
|
54
|
+
client.products.list_each(organization_id: id) { |product| puts product.name }
|
|
55
|
+
client.products.list_each(organization_id: id).pages.count # page objects
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Errors
|
|
59
|
+
|
|
60
|
+
Every declared error response has its own class, inheriting from the generic
|
|
61
|
+
error matching its status code:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
begin
|
|
65
|
+
client.products.get(product_id)
|
|
66
|
+
rescue Pago::Errors::ResourceNotFound => e
|
|
67
|
+
e.status_code # 404
|
|
68
|
+
e.data # deserialized error payload
|
|
69
|
+
rescue Pago::NotFoundError
|
|
70
|
+
# any 404 of the API
|
|
71
|
+
rescue Pago::APIError => e
|
|
72
|
+
# anything the API rejected
|
|
73
|
+
rescue Pago::NetworkError
|
|
74
|
+
# the request never reached the API
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Webhooks
|
|
79
|
+
|
|
80
|
+
Signature verification follows the [Standard Webhooks](https://www.standardwebhooks.com)
|
|
81
|
+
specification, and the payload comes back as the model matching its event type:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
event = Pago::Webhooks.validate_event(
|
|
85
|
+
body: request.body.read,
|
|
86
|
+
headers: request.headers,
|
|
87
|
+
secret: ENV.fetch("PAGO_WEBHOOK_SECRET")
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
case event
|
|
91
|
+
when Pago::Models::WebhookOrderPaidPayload then fulfill(event.data)
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The secret is the one shown when the endpoint is created — the `whsec_` prefix
|
|
96
|
+
followed by the base64 encoded signing key. Pass it verbatim: the SDK strips the
|
|
97
|
+
prefix and base64 decodes the rest to obtain the HMAC key, exactly like every
|
|
98
|
+
other Standard Webhooks implementation.
|
|
99
|
+
|
|
100
|
+
It raises `Pago::WebhookVerificationError` on a bad signature or a timestamp
|
|
101
|
+
outside the five minute window, `Pago::WebhookSecretError` (a subclass of it)
|
|
102
|
+
when the secret is not in `whsec_<base64>` form, and
|
|
103
|
+
`Pago::WebhookUnknownTypeError` for an event type this SDK release does not know.
|
|
104
|
+
|
|
105
|
+
## Custom HTTP adapter
|
|
106
|
+
|
|
107
|
+
The client talks to any object answering `call(Pago::HTTP::Request)` and
|
|
108
|
+
returning a `Pago::HTTP::Response` — useful for instrumentation, retries or
|
|
109
|
+
tests:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
class LoggingAdapter
|
|
113
|
+
def initialize(inner) = @inner = inner
|
|
114
|
+
|
|
115
|
+
def call(request)
|
|
116
|
+
warn "#{request.http_method} #{request.url}"
|
|
117
|
+
@inner.call(request)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
Pago::Client.new(access_token: token, adapter: LoggingAdapter.new(Pago::HTTP::NetHTTPAdapter.new))
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Types
|
|
125
|
+
|
|
126
|
+
The gem ships [RBS](https://github.com/ruby/rbs) signatures under `sig/`, and
|
|
127
|
+
every public method carries YARD documentation.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pago
|
|
4
|
+
# Shared behaviour of every generated, version specific client.
|
|
5
|
+
class BaseClient
|
|
6
|
+
DEFAULT_BASE_URL = "https://api.pago.sh"
|
|
7
|
+
SANDBOX_BASE_URL = "https://sandbox-api.pago.sh"
|
|
8
|
+
API_VERSION = nil
|
|
9
|
+
|
|
10
|
+
# @return [String] the base URL every request is sent to.
|
|
11
|
+
attr_reader :base_url
|
|
12
|
+
# @return [#call] the injected HTTP adapter.
|
|
13
|
+
attr_reader :adapter
|
|
14
|
+
|
|
15
|
+
# @param access_token [String, nil] bearer token; defaults to `PAGO_ACCESS_TOKEN`.
|
|
16
|
+
# @param base_url [String, nil] defaults to `PAGO_BASE_URL`, then to the production URL.
|
|
17
|
+
# @param adapter [#call, nil] anything answering `call(Pago::HTTP::Request)`.
|
|
18
|
+
# @param headers [Hash] extra headers sent with every request.
|
|
19
|
+
def initialize(access_token: nil, base_url: nil, adapter: nil, headers: {})
|
|
20
|
+
@access_token = access_token || ENV.fetch("PAGO_ACCESS_TOKEN", nil)
|
|
21
|
+
@base_url = (base_url || ENV.fetch("PAGO_BASE_URL", DEFAULT_BASE_URL)).sub(%r{/+\z}, "")
|
|
22
|
+
@adapter = adapter || HTTP::NetHTTPAdapter.new
|
|
23
|
+
@extra_headers = headers
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [String, nil] the API version this client speaks.
|
|
27
|
+
def api_version = self.class.const_get(:API_VERSION)
|
|
28
|
+
|
|
29
|
+
# Perform an HTTP request and decode its response.
|
|
30
|
+
#
|
|
31
|
+
# @param http_method [String]
|
|
32
|
+
# @param path [String] path template, `{name}` placeholders included.
|
|
33
|
+
# @param path_params [Hash{String => Object}]
|
|
34
|
+
# @param query [Hash{String => Object}]
|
|
35
|
+
# @param body [Object, nil] serialized as JSON when present.
|
|
36
|
+
# @param response_type [Symbol] `:json`, `:text` or `:none`.
|
|
37
|
+
# @param errors [Hash{Integer => Class}] status code to generated error class.
|
|
38
|
+
# @raise [Pago::APIError] when the API answers with a non 2xx status.
|
|
39
|
+
# @return [Object, String, nil]
|
|
40
|
+
def request(http_method:, path:, path_params: {}, query: {}, body: nil,
|
|
41
|
+
response_type: :json, errors: {})
|
|
42
|
+
response = @adapter.call(
|
|
43
|
+
HTTP::Request.new(
|
|
44
|
+
http_method: http_method,
|
|
45
|
+
url: build_url(path, path_params, query),
|
|
46
|
+
headers: build_headers(body),
|
|
47
|
+
body: body.nil? ? nil : JSON.generate(Serde.dump(body))
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
handle(response, response_type: response_type, errors: errors)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def build_url(path, path_params, query)
|
|
56
|
+
url = "#{@base_url}#{Serde.path(path, path_params || {})}"
|
|
57
|
+
pairs = Serde.query(query || {})
|
|
58
|
+
return url if pairs.empty?
|
|
59
|
+
|
|
60
|
+
"#{url}?#{URI.encode_www_form(pairs)}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_headers(body)
|
|
64
|
+
headers = {
|
|
65
|
+
"Accept" => "application/json",
|
|
66
|
+
"User-Agent" => "pago-sdk-ruby/#{Pago::VERSION}"
|
|
67
|
+
}
|
|
68
|
+
headers["Authorization"] = "Bearer #{@access_token}" if @access_token
|
|
69
|
+
headers["Pago-Version"] = api_version if api_version
|
|
70
|
+
headers["Content-Type"] = "application/json" unless body.nil?
|
|
71
|
+
headers.merge(@extra_headers)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def handle(response, response_type:, errors:)
|
|
75
|
+
raise build_error(response, errors) unless response.success?
|
|
76
|
+
|
|
77
|
+
case response_type
|
|
78
|
+
when :json then parse_json(response.body)
|
|
79
|
+
when :text then response.body
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_json(body)
|
|
84
|
+
return nil if body.nil? || body.empty?
|
|
85
|
+
|
|
86
|
+
JSON.parse(body)
|
|
87
|
+
rescue JSON::ParserError => e
|
|
88
|
+
raise SerializationError, "Failed to parse Pago API response: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_error(response, errors)
|
|
92
|
+
status = response.status
|
|
93
|
+
body = begin
|
|
94
|
+
parse_json(response.body)
|
|
95
|
+
rescue SerializationError
|
|
96
|
+
response.body
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
error_class = errors[status]
|
|
100
|
+
return error_class.from_response(status_code: status, body: body, response: response) if error_class
|
|
101
|
+
|
|
102
|
+
message = "Pago API returned an error: #{status} - #{response.body}"
|
|
103
|
+
if status == 429
|
|
104
|
+
RateLimitError.new(message, status_code: status, body: body, response: response,
|
|
105
|
+
retry_after: retry_after(response))
|
|
106
|
+
else
|
|
107
|
+
Pago.error_class_for(status).new(message, status_code: status, body: body, response: response)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def retry_after(response)
|
|
112
|
+
value = (response.headers || {}).find { |name, _| name.to_s.downcase == "retry-after" }&.last
|
|
113
|
+
value&.to_i
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/pago/errors.rb
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pago
|
|
4
|
+
# Base class of every error raised by this SDK.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the request never produced an HTTP response.
|
|
8
|
+
class NetworkError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when the connection or the read timed out.
|
|
11
|
+
class TimeoutError < NetworkError; end
|
|
12
|
+
|
|
13
|
+
# Raised when a response body could not be decoded.
|
|
14
|
+
class SerializationError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when the API answered with a non 2xx status code.
|
|
17
|
+
class APIError < Error
|
|
18
|
+
# @return [Integer] the HTTP status code of the response.
|
|
19
|
+
attr_reader :status_code
|
|
20
|
+
# @return [Object, nil] the deserialized error payload, when the endpoint declares one.
|
|
21
|
+
attr_reader :data
|
|
22
|
+
# @return [Object, nil] the raw parsed body of the response.
|
|
23
|
+
attr_reader :body
|
|
24
|
+
# @return [Pago::HTTP::Response, nil] the raw HTTP response.
|
|
25
|
+
attr_reader :response
|
|
26
|
+
|
|
27
|
+
def initialize(message = nil, status_code: nil, data: nil, body: nil, response: nil)
|
|
28
|
+
@status_code = status_code
|
|
29
|
+
@data = data
|
|
30
|
+
@body = body
|
|
31
|
+
@response = response
|
|
32
|
+
super(message || "Pago API returned an error: #{status_code}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Raised for 4xx responses.
|
|
37
|
+
class ClientError < APIError; end
|
|
38
|
+
|
|
39
|
+
class BadRequestError < ClientError; end
|
|
40
|
+
class AuthenticationError < ClientError; end
|
|
41
|
+
class PaymentRequiredError < ClientError; end
|
|
42
|
+
class PermissionDeniedError < ClientError; end
|
|
43
|
+
class NotFoundError < ClientError; end
|
|
44
|
+
class ConflictError < ClientError; end
|
|
45
|
+
class GoneError < ClientError; end
|
|
46
|
+
class PreconditionFailedError < ClientError; end
|
|
47
|
+
class UnprocessableEntityError < ClientError; end
|
|
48
|
+
|
|
49
|
+
# Raised for 429 responses.
|
|
50
|
+
class RateLimitError < ClientError
|
|
51
|
+
# @return [Integer, nil] the value of the Retry-After header, in seconds.
|
|
52
|
+
attr_reader :retry_after
|
|
53
|
+
|
|
54
|
+
def initialize(message = nil, retry_after: nil, **options)
|
|
55
|
+
@retry_after = retry_after
|
|
56
|
+
super(message, **options)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Raised for 5xx responses.
|
|
61
|
+
class ServerError < APIError; end
|
|
62
|
+
|
|
63
|
+
STATUS_ERROR_CLASSES = {
|
|
64
|
+
400 => BadRequestError,
|
|
65
|
+
401 => AuthenticationError,
|
|
66
|
+
402 => PaymentRequiredError,
|
|
67
|
+
403 => PermissionDeniedError,
|
|
68
|
+
404 => NotFoundError,
|
|
69
|
+
409 => ConflictError,
|
|
70
|
+
410 => GoneError,
|
|
71
|
+
412 => PreconditionFailedError,
|
|
72
|
+
422 => UnprocessableEntityError,
|
|
73
|
+
429 => RateLimitError
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
# @param status_code [Integer]
|
|
77
|
+
# @return [Class] the generic error class matching an HTTP status code.
|
|
78
|
+
def self.error_class_for(status_code)
|
|
79
|
+
return ServerError if status_code >= 500
|
|
80
|
+
|
|
81
|
+
STATUS_ERROR_CLASSES.fetch(status_code, ClientError)
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/pago/http.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pago
|
|
4
|
+
# Transport layer.
|
|
5
|
+
#
|
|
6
|
+
# The client only ever calls `adapter.call(request)`, so any object honouring
|
|
7
|
+
# that contract can replace {NetHTTPAdapter} — a Faraday wrapper, a test
|
|
8
|
+
# double, an instrumented adapter.
|
|
9
|
+
module HTTP
|
|
10
|
+
# @!attribute http_method
|
|
11
|
+
# @return [String] the HTTP verb, uppercased.
|
|
12
|
+
Request = Struct.new(:http_method, :url, :headers, :body, keyword_init: true)
|
|
13
|
+
|
|
14
|
+
Response = Struct.new(:status, :headers, :body, keyword_init: true) do
|
|
15
|
+
def success? = status >= 200 && status < 300
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
NET_HTTP_CLASSES = {
|
|
19
|
+
"GET" => Net::HTTP::Get,
|
|
20
|
+
"POST" => Net::HTTP::Post,
|
|
21
|
+
"PUT" => Net::HTTP::Put,
|
|
22
|
+
"PATCH" => Net::HTTP::Patch,
|
|
23
|
+
"DELETE" => Net::HTTP::Delete,
|
|
24
|
+
"HEAD" => Net::HTTP::Head,
|
|
25
|
+
"OPTIONS" => Net::HTTP::Options,
|
|
26
|
+
"TRACE" => Net::HTTP::Trace
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# The default adapter, backed by the `net/http` standard library.
|
|
30
|
+
class NetHTTPAdapter
|
|
31
|
+
def initialize(open_timeout: 10, read_timeout: 60)
|
|
32
|
+
@open_timeout = open_timeout
|
|
33
|
+
@read_timeout = read_timeout
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param request [Pago::HTTP::Request]
|
|
37
|
+
# @return [Pago::HTTP::Response]
|
|
38
|
+
def call(request)
|
|
39
|
+
uri = URI.parse(request.url)
|
|
40
|
+
net_request = build_request(uri, request)
|
|
41
|
+
response = start(uri) { |http| http.request(net_request) }
|
|
42
|
+
Response.new(
|
|
43
|
+
status: response.code.to_i,
|
|
44
|
+
headers: response.each_header.to_h,
|
|
45
|
+
body: response.body || ""
|
|
46
|
+
)
|
|
47
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Timeout::Error => e
|
|
48
|
+
raise ::Pago::TimeoutError, "Pago API request timed out: #{e.message}"
|
|
49
|
+
rescue SocketError, SystemCallError, OpenSSL::SSL::SSLError, IOError, EOFError => e
|
|
50
|
+
raise ::Pago::NetworkError, "Pago API network error: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def build_request(uri, request)
|
|
56
|
+
verb = request.http_method.to_s.upcase
|
|
57
|
+
klass = NET_HTTP_CLASSES.fetch(verb) do
|
|
58
|
+
raise ::Pago::Error, "Unsupported HTTP method: #{verb}"
|
|
59
|
+
end
|
|
60
|
+
net_request = klass.new(uri.request_uri)
|
|
61
|
+
(request.headers || {}).each { |name, value| net_request[name] = value }
|
|
62
|
+
net_request.body = request.body unless request.body.nil?
|
|
63
|
+
net_request
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def start(uri, &block)
|
|
67
|
+
Net::HTTP.start(
|
|
68
|
+
uri.host,
|
|
69
|
+
uri.port,
|
|
70
|
+
use_ssl: uri.scheme == "https",
|
|
71
|
+
open_timeout: @open_timeout,
|
|
72
|
+
read_timeout: @read_timeout,
|
|
73
|
+
&block
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/pago/model.rb
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pago
|
|
4
|
+
# Base class of every generated model.
|
|
5
|
+
#
|
|
6
|
+
# Models are immutable value objects. `from_json` rebuilds one from a parsed
|
|
7
|
+
# JSON object and keeps the raw payload around, so unknown properties added by
|
|
8
|
+
# a newer API version survive a `from_json` / `to_json` round trip.
|
|
9
|
+
class Model
|
|
10
|
+
JSON_KEYS = {}.freeze
|
|
11
|
+
REQUIRED_KEYS = [].freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# @return [Hash{Symbol => String}] attribute name to JSON property name.
|
|
15
|
+
def json_keys = const_get(:JSON_KEYS)
|
|
16
|
+
|
|
17
|
+
# @return [Array<String>] the JSON properties the schema declares required.
|
|
18
|
+
def required_json_keys = const_get(:REQUIRED_KEYS)
|
|
19
|
+
|
|
20
|
+
# @param data [Hash, String, nil]
|
|
21
|
+
# @return [Pago::Model, nil]
|
|
22
|
+
def from_json(data)
|
|
23
|
+
data = ::JSON.parse(data) if data.is_a?(String)
|
|
24
|
+
data = ::Pago::Serde.object(data)
|
|
25
|
+
return nil if data.nil?
|
|
26
|
+
|
|
27
|
+
wrap_raw(new, data)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def wrap_raw(instance, data)
|
|
31
|
+
instance.instance_variable_set(:@__raw, data)
|
|
32
|
+
instance
|
|
33
|
+
end
|
|
34
|
+
private :wrap_raw
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
@__present = {}
|
|
39
|
+
@__raw = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Boolean] whether an attribute was explicitly set.
|
|
43
|
+
def field_set?(name) = @__present.key?(name.to_sym)
|
|
44
|
+
|
|
45
|
+
# @return [Hash] the JSON representation of this model.
|
|
46
|
+
def to_json_hash
|
|
47
|
+
result = @__raw ? @__raw.dup : {}
|
|
48
|
+
self.class.json_keys.each do |name, key|
|
|
49
|
+
next unless @__present.key?(name)
|
|
50
|
+
|
|
51
|
+
result[key] = ::Pago::Serde.dump(instance_variable_get(:"@#{name}"))
|
|
52
|
+
end
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
alias to_h to_json_hash
|
|
56
|
+
|
|
57
|
+
def to_json(*args) = to_json_hash.to_json(*args)
|
|
58
|
+
|
|
59
|
+
def [](key) = to_json_hash[key.to_s]
|
|
60
|
+
|
|
61
|
+
def ==(other) = other.instance_of?(self.class) && other.to_json_hash == to_json_hash
|
|
62
|
+
alias eql? ==
|
|
63
|
+
|
|
64
|
+
def hash = [self.class, to_json_hash].hash
|
|
65
|
+
|
|
66
|
+
def inspect
|
|
67
|
+
attributes = self.class.json_keys.keys.filter_map do |name|
|
|
68
|
+
next unless @__present.key?(name)
|
|
69
|
+
|
|
70
|
+
"#{name}=#{instance_variable_get(:"@#{name}").inspect}"
|
|
71
|
+
end
|
|
72
|
+
"#<#{self.class.name}#{attributes.empty? ? "" : " #{attributes.join(", ")}"}>"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def assign(name, value)
|
|
78
|
+
if ::Pago::UNSET.equal?(value)
|
|
79
|
+
instance_variable_set(:"@#{name}", nil)
|
|
80
|
+
else
|
|
81
|
+
@__present[name] = true
|
|
82
|
+
instance_variable_set(:"@#{name}", value)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pago
|
|
4
|
+
# Lazily walks every page of a paginated endpoint.
|
|
5
|
+
#
|
|
6
|
+
# It is a plain `Enumerable` over the *items*, so the whole standard library
|
|
7
|
+
# applies: `each`, `map`, `select`, `first(20)`, `lazy`, `each_slice`… Pages
|
|
8
|
+
# are fetched on demand, one request at a time, and `#pages` exposes the raw
|
|
9
|
+
# page objects when the pagination metadata itself is needed.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# client.products.list_each(organization_id: id).first(50)
|
|
13
|
+
class Paginator
|
|
14
|
+
include Enumerable
|
|
15
|
+
|
|
16
|
+
# @yieldparam page [Integer] the 1-based page number to fetch.
|
|
17
|
+
# @yieldreturn [Object] the deserialized page, answering `items` and `pagination`.
|
|
18
|
+
def initialize(&fetch_page)
|
|
19
|
+
raise ArgumentError, "Pago::Paginator requires a block" unless block_given?
|
|
20
|
+
|
|
21
|
+
@fetch_page = fetch_page
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @yieldparam item [Object] every item of every page.
|
|
25
|
+
# @return [Enumerator] when no block is given.
|
|
26
|
+
def each
|
|
27
|
+
return to_enum(:each) unless block_given?
|
|
28
|
+
|
|
29
|
+
pages.each do |page|
|
|
30
|
+
items = page.respond_to?(:items) ? page.items : nil
|
|
31
|
+
break if items.nil?
|
|
32
|
+
|
|
33
|
+
items.each { |item| yield(item) }
|
|
34
|
+
end
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Enumerator] the successive page objects, metadata included.
|
|
39
|
+
def pages
|
|
40
|
+
return to_enum(:pages) unless block_given?
|
|
41
|
+
|
|
42
|
+
number = 1
|
|
43
|
+
loop do
|
|
44
|
+
page = @fetch_page.call(number)
|
|
45
|
+
break if page.nil?
|
|
46
|
+
|
|
47
|
+
yield(page)
|
|
48
|
+
max_page = max_page_of(page)
|
|
49
|
+
break if max_page.nil? || number >= max_page
|
|
50
|
+
|
|
51
|
+
number += 1
|
|
52
|
+
end
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def max_page_of(page)
|
|
59
|
+
return nil unless page.respond_to?(:pagination)
|
|
60
|
+
|
|
61
|
+
pagination = page.pagination
|
|
62
|
+
return nil unless pagination.respond_to?(:max_page)
|
|
63
|
+
|
|
64
|
+
pagination.max_page
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|