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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +131 -0
  4. data/lib/pago/base_client.rb +116 -0
  5. data/lib/pago/errors.rb +83 -0
  6. data/lib/pago/http.rb +78 -0
  7. data/lib/pago/model.rb +86 -0
  8. data/lib/pago/paginator.rb +67 -0
  9. data/lib/pago/serde.rb +186 -0
  10. data/lib/pago/service.rb +17 -0
  11. data/lib/pago/v2026_04/client.rb +124 -0
  12. data/lib/pago/v2026_04/enums.rb +4552 -0
  13. data/lib/pago/v2026_04/errors.rb +1121 -0
  14. data/lib/pago/v2026_04/models.rb +46344 -0
  15. data/lib/pago/v2026_04/services/benefit_grants.rb +48 -0
  16. data/lib/pago/v2026_04/services/benefits.rb +179 -0
  17. data/lib/pago/v2026_04/services/checkout_links.rb +131 -0
  18. data/lib/pago/v2026_04/services/checkouts.rb +185 -0
  19. data/lib/pago/v2026_04/services/custom_fields.rb +132 -0
  20. data/lib/pago/v2026_04/services/customer_meters.rb +69 -0
  21. data/lib/pago/v2026_04/services/customer_portal.rb +1181 -0
  22. data/lib/pago/v2026_04/services/customer_seats.rb +137 -0
  23. data/lib/pago/v2026_04/services/customer_sessions.rb +35 -0
  24. data/lib/pago/v2026_04/services/customers.rb +556 -0
  25. data/lib/pago/v2026_04/services/discounts.rb +131 -0
  26. data/lib/pago/v2026_04/services/disputes.rb +93 -0
  27. data/lib/pago/v2026_04/services/event_types.rb +74 -0
  28. data/lib/pago/v2026_04/services/events.rb +126 -0
  29. data/lib/pago/v2026_04/services/files.rb +135 -0
  30. data/lib/pago/v2026_04/services/license_keys.rb +183 -0
  31. data/lib/pago/v2026_04/services/members.rb +47 -0
  32. data/lib/pago/v2026_04/services/meters.rb +142 -0
  33. data/lib/pago/v2026_04/services/metrics.rb +188 -0
  34. data/lib/pago/v2026_04/services/oauth2.rb +175 -0
  35. data/lib/pago/v2026_04/services/orders.rb +238 -0
  36. data/lib/pago/v2026_04/services/organizations.rb +113 -0
  37. data/lib/pago/v2026_04/services/payments.rb +72 -0
  38. data/lib/pago/v2026_04/services/products.rb +142 -0
  39. data/lib/pago/v2026_04/services/refunds.rb +73 -0
  40. data/lib/pago/v2026_04/services/subscriptions.rb +171 -0
  41. data/lib/pago/v2026_04/services/webhooks.rb +212 -0
  42. data/lib/pago/v2026_04/unions.rb +739 -0
  43. data/lib/pago/v2026_04/webhooks.rb +86 -0
  44. data/lib/pago/version.rb +5 -0
  45. data/lib/pago/webhooks.rb +159 -0
  46. data/lib/pago.rb +39 -0
  47. data/sig/pago/v2026_04/generated.rbs +12401 -0
  48. data/sig/pago.rbs +204 -0
  49. 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
@@ -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