poli-page 0.9.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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +62 -0
  3. data/CODE_OF_CONDUCT.md +38 -0
  4. data/LICENSE +21 -0
  5. data/MIGRATION.md +68 -0
  6. data/README.md +376 -0
  7. data/SECURITY.md +100 -0
  8. data/lib/poli_page/client.rb +228 -0
  9. data/lib/poli_page/documents.rb +148 -0
  10. data/lib/poli_page/errors.rb +157 -0
  11. data/lib/poli_page/inputs/inline_mode_input.rb +31 -0
  12. data/lib/poli_page/inputs/project_mode_input.rb +38 -0
  13. data/lib/poli_page/inputs/thumbnail_options.rb +25 -0
  14. data/lib/poli_page/internal/constants.rb +38 -0
  15. data/lib/poli_page/internal/http.rb +123 -0
  16. data/lib/poli_page/internal/presigned_fetch.rb +89 -0
  17. data/lib/poli_page/internal/transport.rb +98 -0
  18. data/lib/poli_page/internal/uuid.rb +20 -0
  19. data/lib/poli_page/internal/wire.rb +49 -0
  20. data/lib/poli_page/models/document_descriptor.rb +52 -0
  21. data/lib/poli_page/models/document_preview_result.rb +10 -0
  22. data/lib/poli_page/models/orientation.rb +15 -0
  23. data/lib/poli_page/models/page_format.rb +23 -0
  24. data/lib/poli_page/models/preview_result.rb +9 -0
  25. data/lib/poli_page/models/thumbnail.rb +8 -0
  26. data/lib/poli_page/render.rb +163 -0
  27. data/lib/poli_page/render_to_file.rb +52 -0
  28. data/lib/poli_page/request_event.rb +17 -0
  29. data/lib/poli_page/response_event.rb +15 -0
  30. data/lib/poli_page/retry_event.rb +12 -0
  31. data/lib/poli_page/version.rb +5 -0
  32. data/lib/poli_page.rb +28 -0
  33. data/sig/poli_page/client.rbs +46 -0
  34. data/sig/poli_page/documents.rbs +12 -0
  35. data/sig/poli_page/errors.rbs +84 -0
  36. data/sig/poli_page/models.rbs +106 -0
  37. data/sig/poli_page/render.rbs +32 -0
  38. data/sig/poli_page/request_event.rbs +9 -0
  39. data/sig/poli_page/response_event.rbs +9 -0
  40. data/sig/poli_page/retry_event.rbs +9 -0
  41. data/sig/poli_page.rbs +3 -0
  42. metadata +87 -0
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "errors"
6
+ require_relative "retry_event"
7
+ require_relative "internal/constants"
8
+ require_relative "internal/http"
9
+ require_relative "internal/presigned_fetch"
10
+ require_relative "internal/transport"
11
+ require_relative "internal/uuid"
12
+ require_relative "internal/wire"
13
+ require_relative "render"
14
+ require_relative "documents"
15
+
16
+ module PoliPage
17
+ # Main entry point — orchestrates retries, fires hooks, and delegates HTTP
18
+ # execution to `PoliPage::Internal::Transport`. The `render` and `documents`
19
+ # resource namespaces hold a reference to this client and delegate request
20
+ # execution back through it.
21
+ #
22
+ # Thread-safety: configuration is immutable after `#initialize`; each
23
+ # request opens its own `Net::HTTP` connection. A single `Client` may be
24
+ # safely shared across threads — build one at boot and reuse it.
25
+ #
26
+ # @example Construct with defaults
27
+ # client = PoliPage::Client.new(api_key: ENV.fetch("POLI_PAGE_API_KEY"))
28
+ #
29
+ # @example Construct with full configuration
30
+ # client = PoliPage::Client.new(
31
+ # api_key: ENV.fetch("POLI_PAGE_API_KEY"),
32
+ # base_url: "https://api.example.com",
33
+ # max_retries: 3,
34
+ # retry_delay: 0.5, # seconds
35
+ # timeout: 30, # seconds
36
+ # logger: Logger.new($stdout),
37
+ # on_retry: ->(e) { metrics.increment("polipage.retry") },
38
+ # on_error: ->(err) { Sentry.capture_exception(err) },
39
+ # proxy: "http://user:pass@proxy.example.com:8080",
40
+ # ca_file: "/etc/ssl/corp-mitm-ca.pem"
41
+ # )
42
+ #
43
+ # @example Behind a corporate egress proxy (env-detected)
44
+ # # `http_proxy` / `https_proxy` / `no_proxy` env vars are honoured
45
+ # # automatically — no `proxy:` kwarg needed.
46
+ # client = PoliPage::Client.new(api_key: ENV.fetch("POLI_PAGE_API_KEY"))
47
+ class Client
48
+ include Internal::PresignedFetch
49
+
50
+ attr_reader :base_url, :max_retries, :retry_delay, :timeout, :render, :documents
51
+
52
+ def initialize(api_key:, base_url: Internal::Constants::DEFAULT_BASE_URL,
53
+ max_retries: Internal::Constants::DEFAULT_MAX_RETRIES,
54
+ retry_delay: Internal::Constants::DEFAULT_RETRY_DELAY,
55
+ timeout: Internal::Constants::DEFAULT_TIMEOUT,
56
+ logger: nil, on_request: nil, on_response: nil,
57
+ on_retry: nil, on_error: nil,
58
+ proxy: nil, ca_file: nil, ca_path: nil)
59
+ raise InvalidOptionsError, "api_key is required" if api_key.nil? || api_key.empty?
60
+
61
+ @api_key = api_key
62
+ @base_url = base_url
63
+ @max_retries = max_retries
64
+ @retry_delay = retry_delay
65
+ @timeout = timeout
66
+ @logger = logger
67
+ @on_request = on_request
68
+ @on_response = on_response
69
+ @on_retry = on_retry
70
+ @on_error = on_error
71
+ @proxy = proxy
72
+ @ca_file = ca_file
73
+ @ca_path = ca_path
74
+ init_collaborators
75
+ end
76
+
77
+ # Redacted `#inspect`. Ruby's default would dump `@api_key` into any
78
+ # `puts client` or exception backtrace — that's how secrets end up in
79
+ # logs (sdk-ruby-plan.md §10.1 redaction rule).
80
+ def inspect
81
+ "#<PoliPage::Client base_url=#{@base_url.inspect} timeout=#{@timeout} max_retries=#{@max_retries}>"
82
+ end
83
+ alias to_s inspect
84
+
85
+ # Execute a POST request against `path` with `body` (snake_case hash;
86
+ # caller's responsibility to compact nils). Returns the parsed
87
+ # snake_case-symbol-keyed Hash on 2xx, or raises a `PoliPage::Error`.
88
+ #
89
+ # @api private
90
+ def execute_post(path, body:, idempotency_key: nil)
91
+ wire_body = JSON.generate(Internal::Wire.to_wire(body))
92
+ response = run_with_retry(method: :post, path: path, body: wire_body,
93
+ idempotency_key: idempotency_key || Internal::UUID.generate)
94
+ parse_json_response(response)
95
+ end
96
+
97
+ # Execute a GET request against `path`. Returns the parsed
98
+ # snake_case-symbol-keyed Hash on 2xx, or raises a `PoliPage::Error`.
99
+ #
100
+ # @api private
101
+ def execute_get(path)
102
+ response = run_with_retry(method: :get, path: path, body: nil, idempotency_key: nil)
103
+ parse_json_response(response)
104
+ end
105
+
106
+ # Execute a DELETE request against `path`. Returns nil on 2xx; the
107
+ # response body is ignored. Raises `PoliPage::Error` on non-2xx.
108
+ #
109
+ # @api private
110
+ def execute_delete(path)
111
+ run_with_retry(method: :delete, path: path, body: nil, idempotency_key: nil)
112
+ nil
113
+ end
114
+
115
+ # Execute a GET request and return the raw `Internal::Transport::Response`
116
+ # (body + headers) without JSON parsing. Used by `documents.preview` which
117
+ # gets `text/html` directly plus the page count via the
118
+ # `X-Document-Page-Count` response header.
119
+ #
120
+ # @api private
121
+ def execute_get_raw(path)
122
+ run_with_retry(method: :get, path: path, body: nil, idempotency_key: nil)
123
+ end
124
+
125
+ private
126
+
127
+ def init_collaborators
128
+ @transport = Internal::Transport.new(base_url: @base_url, timeout: @timeout,
129
+ proxy: @proxy, ca_file: @ca_file, ca_path: @ca_path)
130
+ @render = Resources::Render.new(self)
131
+ @documents = Resources::Documents.new(self)
132
+ end
133
+
134
+ def run_with_retry(method:, path:, body:, idempotency_key:)
135
+ state = { last_error: nil, next_retry_after: nil }
136
+
137
+ (0..@max_retries).each do |attempt|
138
+ sleep_before_retry(attempt, state) if attempt.positive?
139
+ result = send_once(method: method, path: path, body: body,
140
+ idempotency_key: idempotency_key, attempt: attempt + 1)
141
+ return result[:response] if result[:ok]
142
+
143
+ record_failure(state, result)
144
+ raise_terminal(state[:last_error]) unless result[:retryable]
145
+ end
146
+
147
+ raise_terminal(state[:last_error])
148
+ end
149
+
150
+ def record_failure(state, result)
151
+ state[:last_error] = result[:error]
152
+ state[:next_retry_after] = result[:retry_after]
153
+ end
154
+
155
+ def sleep_before_retry(attempt, state)
156
+ delay = Internal::HTTP.compute_backoff(attempt: attempt, base_delay: @retry_delay,
157
+ retry_after: state[:next_retry_after])
158
+ fire_hook(@on_retry,
159
+ RetryEvent.new(attempt: attempt + 1, delay_ms: (delay * 1000).round,
160
+ reason: state[:last_error]))
161
+ sleep(delay)
162
+ end
163
+
164
+ def raise_terminal(error)
165
+ fire_hook(@on_error, error)
166
+ raise error
167
+ end
168
+
169
+ def send_once(method:, path:, body:, idempotency_key:, attempt:)
170
+ headers = build_request_headers(method, idempotency_key)
171
+ url = Internal::HTTP.build_url(@base_url, path)
172
+ fire_hook(@on_request,
173
+ RequestEvent.new(method: method.to_s.upcase, url: url, attempt: attempt))
174
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
175
+ response = @transport.execute(method: method, path: path, headers: headers, body: body)
176
+ return success_result(response, started_at) if (200..299).cover?(response.status)
177
+
178
+ build_error_result(response)
179
+ rescue PoliPage::TimeoutError, PoliPage::ConnectionError => e
180
+ { ok: false, error: e, retry_after: nil, retryable: true }
181
+ end
182
+
183
+ def build_request_headers(method, idempotency_key)
184
+ Internal::HTTP.build_headers(
185
+ method: method, api_key: @api_key,
186
+ idempotency_key: idempotency_key,
187
+ user_agent: "poli-page-sdk-ruby/#{PoliPage::VERSION}"
188
+ )
189
+ end
190
+
191
+ def success_result(response, started_at)
192
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round
193
+ fire_hook(@on_response,
194
+ ResponseEvent.new(
195
+ status: response.status,
196
+ request_id: response.headers[Internal::Constants::HEADER_REQUEST_ID],
197
+ duration_ms: duration_ms
198
+ ))
199
+ { ok: true, response: response }
200
+ end
201
+
202
+ def build_error_result(response)
203
+ status = response.status
204
+ request_id = response.headers[Internal::Constants::HEADER_REQUEST_ID]
205
+ code, message = Internal::HTTP.parse_error_body(response.body.to_s, status)
206
+ error = Internal::HTTP.classify(status: status, code: code, message: message, request_id: request_id)
207
+ retryable = status >= 500 || status == 429
208
+ retry_after_header = response.headers[Internal::Constants::HEADER_RETRY_AFTER]
209
+ retry_after = retryable ? Internal::HTTP.parse_retry_after(retry_after_header) : nil
210
+ { ok: false, error: error, retry_after: retry_after, retryable: retryable }
211
+ end
212
+
213
+ def parse_json_response(response)
214
+ parsed = JSON.parse(response.body)
215
+ Internal::Wire.from_wire(parsed)
216
+ rescue JSON::ParserError => e
217
+ raise PoliPage::InternalError.new("response body was not valid JSON: #{e.message}", status: response.status)
218
+ end
219
+
220
+ def fire_hook(hook, event)
221
+ return unless hook
222
+
223
+ hook.call(event)
224
+ rescue StandardError => e
225
+ @logger&.warn("polipage hook raised: #{e.class}: #{e.message}")
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ require_relative "models/document_descriptor"
6
+ require_relative "models/document_preview_result"
7
+ require_relative "models/thumbnail"
8
+
9
+ module PoliPage
10
+ module Resources
11
+ # `client.documents` namespace (sdk-ruby-plan.md §13 Phase 4, port of
12
+ # sdk-node/src/documents.ts).
13
+ class Documents
14
+ def initialize(client)
15
+ @client = client
16
+ end
17
+
18
+ # GET /v1/documents/:id — returns a `DocumentDescriptor` with the
19
+ # client back-reference attached so `#download_pdf` works.
20
+ #
21
+ # @param id [String] the document id (e.g. `"doc_abc123"`)
22
+ # @return [PoliPage::DocumentDescriptor]
23
+ # @raise [PoliPage::NotFoundError] on 404
24
+ # @raise [PoliPage::GoneError] on 410 (soft-deleted)
25
+ #
26
+ # @example Re-fetch a stored document and download its PDF
27
+ # doc = client.documents.get("doc_abc123")
28
+ # File.binwrite("invoice.pdf", doc.download_pdf)
29
+ def get(id)
30
+ parsed = @client.execute_get(path_for(id))
31
+ parsed[:metadata] ||= {}
32
+ PoliPage::DocumentDescriptor.new(**parsed, _client: @client)
33
+ end
34
+
35
+ # GET /v1/documents/:id/preview — returns the stored paginated HTML
36
+ # plus the page count carried by the `X-Document-Page-Count` response
37
+ # header. The body is `text/html`, NOT a JSON envelope. No engine work
38
+ # — this is a snapshot read of the stored document.
39
+ #
40
+ # @param id [String]
41
+ # @return [PoliPage::DocumentPreviewResult] `{ html:, page_count: }`
42
+ #
43
+ # @example
44
+ # stored = client.documents.preview("doc_abc123")
45
+ # File.write("preview.html", stored.html)
46
+ # puts "#{stored.page_count} pages"
47
+ def preview(id)
48
+ response = @client.execute_get_raw("#{path_for(id)}/preview")
49
+ page_count_header = response.headers[Internal::Constants::HEADER_DOCUMENT_PAGE_COUNT]
50
+ page_count = page_count_header.to_s.match?(/\A\d+\z/) ? page_count_header.to_i : 0
51
+ PoliPage::DocumentPreviewResult.new(html: response.body.to_s, page_count: page_count)
52
+ end
53
+
54
+ # POST /v1/documents/:id/thumbnails — the deployed API expects the
55
+ # options nested under a `thumbnails` key. The response envelope
56
+ # `{ thumbnails: [...] }` is unwrapped here. Returns an Array of
57
+ # `PoliPage::Thumbnail`; each carries base64-encoded image bytes in
58
+ # `data` (decode with `Base64.decode64(thumb.data)`).
59
+ #
60
+ # @param id [String]
61
+ # @param options [Hash] Forwarded to the wire under `thumbnails:`.
62
+ # - `width:` [Integer] (required)
63
+ # - `format:` ["png" | "jpeg"]
64
+ # - `quality:` [Integer] JPEG quality 1-100 (jpeg only)
65
+ # - `pages:` [Array<Integer>] 1-based page indices; default all pages
66
+ # @return [Array<PoliPage::Thumbnail>]
67
+ #
68
+ # @example
69
+ # thumbs = client.documents.thumbnails("doc_abc123", width: 320, format: "png", pages: [1])
70
+ # File.binwrite("page1.png", Base64.decode64(thumbs.first.data))
71
+ def thumbnails(id, **options)
72
+ validate_thumbnail_options!(options)
73
+ body = { thumbnails: options.compact }
74
+ parsed = @client.execute_post("#{path_for(id)}/thumbnails", body: body)
75
+ parsed[:thumbnails].map { |t| PoliPage::Thumbnail.new(**t) }
76
+ end
77
+
78
+ # DELETE /v1/documents/:id — returns nil. Re-deleting an
79
+ # already-deleted document surfaces as `PoliPage::GoneError` (HTTP
80
+ # 410) — no special handling here.
81
+ #
82
+ # @param id [String]
83
+ # @return [nil]
84
+ # @raise [PoliPage::GoneError] if already deleted
85
+ #
86
+ # @example
87
+ # client.documents.delete("doc_abc123")
88
+ def delete(id)
89
+ @client.execute_delete(path_for(id))
90
+ nil
91
+ end
92
+
93
+ private
94
+
95
+ # Ruby 3.2+ `URI.encode_uri_component` matches JS `encodeURIComponent`
96
+ # (used by sdk-node). `CGI.escape` would form-encode spaces as `+`,
97
+ # which the deployed API rejects in path segments.
98
+ def path_for(id)
99
+ "#{Internal::Constants::PATH_DOCUMENTS}/#{URI.encode_uri_component(id)}"
100
+ end
101
+
102
+ # Local arg validation — fail fast with a clear `InvalidOptionsError`
103
+ # before issuing the HTTP request. Mirrors the server's invariants for
104
+ # `documents.thumbnails` (sdk-specification.md / Node SDK).
105
+ def validate_thumbnail_options!(options)
106
+ validate_thumbnail_width!(options[:width])
107
+ validate_thumbnail_format!(options[:format])
108
+ validate_thumbnail_quality!(options[:quality], options[:format])
109
+ validate_thumbnail_pages!(options[:pages])
110
+ end
111
+
112
+ def validate_thumbnail_width!(width)
113
+ return if width.is_a?(Integer) && width.positive?
114
+
115
+ raise PoliPage::InvalidOptionsError,
116
+ "thumbnails: width is required and must be a positive Integer (got #{width.inspect})"
117
+ end
118
+
119
+ def validate_thumbnail_format!(format)
120
+ return if format.nil? || %w[png jpeg].include?(format)
121
+
122
+ raise PoliPage::InvalidOptionsError,
123
+ "thumbnails: format must be 'png' or 'jpeg' (got #{format.inspect})"
124
+ end
125
+
126
+ def validate_thumbnail_quality!(quality, format)
127
+ return if quality.nil?
128
+
129
+ unless format == "jpeg"
130
+ raise PoliPage::InvalidOptionsError,
131
+ "thumbnails: quality is only valid with format: 'jpeg'"
132
+ end
133
+ return if quality.is_a?(Integer) && (1..100).cover?(quality)
134
+
135
+ raise PoliPage::InvalidOptionsError,
136
+ "thumbnails: quality must be an Integer between 1 and 100 (got #{quality.inspect})"
137
+ end
138
+
139
+ def validate_thumbnail_pages!(pages)
140
+ return if pages.nil?
141
+ return if pages.is_a?(Array) && pages.all? { |p| p.is_a?(Integer) && p.positive? }
142
+
143
+ raise PoliPage::InvalidOptionsError,
144
+ "thumbnails: pages must be an Array of positive Integers (got #{pages.inspect})"
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PoliPage
4
+ # Base for all SDK-raised errors. Inherits from `StandardError` so that
5
+ # `rescue => e` catches it (whereas inheriting from `Exception` would
6
+ # bypass the default rescue and surprise callers).
7
+ #
8
+ # The rescue-friendly path is the class hierarchy
9
+ # (`rescue PoliPage::AuthenticationError`); the predicate methods below are
10
+ # kept for spec parity with the Node SDK and for callers who want a single
11
+ # rescue clause backed by introspection.
12
+ class Error < StandardError
13
+ attr_reader :code, :status, :request_id
14
+
15
+ def initialize(message = nil, code:, status: nil, request_id: nil)
16
+ super(message)
17
+ @code = code
18
+ @status = status
19
+ @request_id = request_id
20
+ end
21
+
22
+ def auth_error?
23
+ is_a?(AuthenticationError) || is_a?(PermissionDeniedError)
24
+ end
25
+
26
+ def rate_limit_error?
27
+ is_a?(RateLimitError)
28
+ end
29
+
30
+ def validation_error?
31
+ is_a?(ValidationError)
32
+ end
33
+
34
+ def network_error?
35
+ is_a?(ConnectionError) || is_a?(TimeoutError)
36
+ end
37
+
38
+ def retryable?
39
+ return true if network_error?
40
+ return true if status && (status >= 500 || status == 429)
41
+
42
+ false
43
+ end
44
+
45
+ # Canonical wire payload for framework integrations:
46
+ # `{code:, message:, status:, request_id:}`. `status` surfaces 503 for
47
+ # connection failures, 504 for timeouts, the API HTTP status for
48
+ # status-bearing errors. The {#status} reader itself stays nil for
49
+ # transport-error instances — only the payload surfaces 503/504.
50
+ def to_payload
51
+ {
52
+ code: @code,
53
+ message: message,
54
+ status: payload_status,
55
+ request_id: @request_id
56
+ }
57
+ end
58
+
59
+ def payload_status
60
+ @status
61
+ end
62
+ end
63
+
64
+ # API status errors. `status` carries the HTTP status; `code` is the
65
+ # machine-readable string from the response body. The subclass picked for a
66
+ # given status is decided by `PoliPage::Internal::HTTP.classify`.
67
+ #
68
+ # 400 → ValidationError
69
+ # 401 → AuthenticationError
70
+ # 403 → PermissionDeniedError
71
+ # 404 → NotFoundError
72
+ # 410 → GoneError
73
+ # 429 → RateLimitError
74
+ # other 4xx / 5xx → APIError
75
+ class ValidationError < Error; end
76
+ class AuthenticationError < Error; end
77
+ class PermissionDeniedError < Error; end
78
+ class NotFoundError < Error; end
79
+ class GoneError < Error; end
80
+ class RateLimitError < Error; end
81
+ class APIError < Error; end
82
+
83
+ # --- SDK-internal errors. `status` is nil except where the SDK observed a
84
+ # status from a non-API hop (e.g., the S3 second-hop for DownloadError). ---
85
+
86
+ class InvalidOptionsError < Error
87
+ def initialize(message, code: "invalid_options")
88
+ super(message, code: code, status: nil, request_id: nil)
89
+ end
90
+ end
91
+
92
+ class ConnectionError < Error
93
+ attr_reader :cause
94
+
95
+ def initialize(message:, cause: nil)
96
+ super(message, code: "network_error", status: nil, request_id: nil)
97
+ @cause = cause
98
+ end
99
+
100
+ def payload_status
101
+ 503
102
+ end
103
+ end
104
+
105
+ class TimeoutError < Error
106
+ attr_reader :timeout
107
+
108
+ def initialize(timeout:)
109
+ super("request timed out after #{timeout}s",
110
+ code: "timeout", status: nil, request_id: nil)
111
+ @timeout = timeout
112
+ end
113
+
114
+ def payload_status
115
+ 504
116
+ end
117
+ end
118
+
119
+ class DownloadError < Error
120
+ def initialize(message:, status: nil)
121
+ super(message, code: "DOWNLOAD_FAILED", status: status, request_id: nil)
122
+ end
123
+ end
124
+
125
+ class InternalError < Error
126
+ def initialize(message, status: nil)
127
+ super(message, code: "INTERNAL_ERROR", status: status, request_id: nil)
128
+ end
129
+ end
130
+
131
+ # Known API codes (sdk-ruby-plan.md §7.4). The SDK passes through whatever
132
+ # the API returns — callers may still see codes not in this list. Note:
133
+ # `STORAGE_REQUIRED` was retired from the deployed API and is NOT exported.
134
+ module ErrorCodes
135
+ MISSING_API_KEY = "MISSING_API_KEY"
136
+ INVALID_API_KEY = "INVALID_API_KEY"
137
+ PAYMENT_REQUIRED = "PAYMENT_REQUIRED"
138
+ FORBIDDEN = "FORBIDDEN"
139
+ ORGANIZATION_CANCELLED = "ORGANIZATION_CANCELLED"
140
+ ORGANIZATION_PURGED = "ORGANIZATION_PURGED"
141
+ NOT_FOUND = "NOT_FOUND"
142
+ VERSION_NOT_FOUND = "VERSION_NOT_FOUND"
143
+ DOCUMENT_NOT_FOUND = "DOCUMENT_NOT_FOUND"
144
+ GONE = "GONE"
145
+ VALIDATION_ERROR = "VALIDATION_ERROR"
146
+ MISSING_DATA = "MISSING_DATA"
147
+ MISSING_PROJECT_OR_TEMPLATE = "MISSING_PROJECT_OR_TEMPLATE"
148
+ MISSING_TEMPLATE_SLUG = "MISSING_TEMPLATE_SLUG"
149
+ PROJECT_REQUIRED_FOR_DOCUMENT = "PROJECT_REQUIRED_FOR_DOCUMENT"
150
+ INVALID_VERSION_FORMAT = "INVALID_VERSION_FORMAT"
151
+ VERSION_REQUIRED = "VERSION_REQUIRED"
152
+ INVALID_VERSION_FOR_KEY_ENV = "INVALID_VERSION_FOR_KEY_ENV"
153
+ QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
154
+ OVERAGE_CAP_EXCEEDED = "OVERAGE_CAP_EXCEEDED"
155
+ INTERNAL_ERROR = "INTERNAL_ERROR"
156
+ end
157
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PoliPage
4
+ # Convenience value-object form of the inline-mode render input. Inline
5
+ # mode is only accepted by `client.render.preview` (sdk-ruby-plan.md §13
6
+ # Phase 2); the render-to-document methods require project mode.
7
+ InlineModeInput = Data.define(
8
+ :template,
9
+ :data,
10
+ :format,
11
+ :orientation,
12
+ :locale,
13
+ :metadata,
14
+ :idempotency_key
15
+ ) do
16
+ def to_h
17
+ super.compact
18
+ end
19
+ end
20
+
21
+ class << InlineModeInput
22
+ alias _strict_new new
23
+
24
+ def new(template:, data:, format: nil, orientation: nil, locale: nil,
25
+ metadata: nil, idempotency_key: nil)
26
+ _strict_new(template: template, data: data, format: format,
27
+ orientation: orientation, locale: locale, metadata: metadata,
28
+ idempotency_key: idempotency_key)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PoliPage
4
+ # Convenience value-object form of the project-mode render input. Methods
5
+ # accept bare kwargs directly; this wrapper is for callers who want
6
+ # `Data.define`'s equality, frozen-by-default semantics, and a single
7
+ # marshallable type to thread through their own code.
8
+ ProjectModeInput = Data.define(
9
+ :project,
10
+ :template,
11
+ :data,
12
+ :version,
13
+ :format,
14
+ :orientation,
15
+ :locale,
16
+ :metadata,
17
+ :idempotency_key
18
+ ) do
19
+ # @return [Hash] kwargs ready to splat into the matching `client.render.*` method
20
+ def to_h
21
+ super.compact
22
+ end
23
+ end
24
+
25
+ # Allow positional-as-keyword construction with optional fields defaulted to nil
26
+ # — Data.define is strict by default. We re-open the class only to add a
27
+ # forgiving constructor that lets callers omit the optional knobs.
28
+ class << ProjectModeInput
29
+ alias _strict_new new
30
+
31
+ def new(project:, template:, data:, version: nil, format: nil,
32
+ orientation: nil, locale: nil, metadata: nil, idempotency_key: nil)
33
+ _strict_new(project: project, template: template, data: data,
34
+ version: version, format: format, orientation: orientation,
35
+ locale: locale, metadata: metadata, idempotency_key: idempotency_key)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PoliPage
4
+ # Convenience value-object form of the `client.documents.thumbnails`
5
+ # options hash. Methods accept bare kwargs directly; this wrapper is for
6
+ # callers who want `Data.define`'s equality + frozen-by-default semantics.
7
+ #
8
+ # - `width` [Integer] thumbnail width in pixels (required)
9
+ # - `format` ["png", "jpeg", nil] output format; default "png"
10
+ # - `quality` [Integer, nil] JPEG quality 1-100; only valid when format is "jpeg"
11
+ # - `pages` [Array<Integer>, nil] 1-based page indices to render; nil means all pages
12
+ ThumbnailOptions = Data.define(:width, :format, :quality, :pages) do
13
+ def to_h
14
+ super.compact
15
+ end
16
+ end
17
+
18
+ class << ThumbnailOptions
19
+ alias _strict_new new
20
+
21
+ def new(width:, format: nil, quality: nil, pages: nil)
22
+ _strict_new(width: width, format: format, quality: quality, pages: pages)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PoliPage
4
+ module Internal
5
+ # @api private
6
+ #
7
+ # Internal constants. Convention-private; do not depend on these from
8
+ # outside the gem (no semver protection).
9
+ module Constants
10
+ # API paths (relative to the configured base URL).
11
+ PATH_RENDER = "/v1/render"
12
+ PATH_RENDER_PREVIEW = "/v1/render/preview"
13
+ PATH_DOCUMENTS = "/v1/documents"
14
+
15
+ # Defaults (in seconds where applicable — Ruby's `Kernel#sleep`,
16
+ # `Net::HTTP#open_timeout`, etc. all use seconds).
17
+ DEFAULT_BASE_URL = "https://api.poli.page"
18
+ DEFAULT_MAX_RETRIES = 2
19
+ DEFAULT_RETRY_DELAY = 0.5 # Node: 500 ms.
20
+ DEFAULT_TIMEOUT = 60 # Node: 60_000 ms.
21
+ RETRY_AFTER_CAP = 30 # Node: 30_000 ms.
22
+
23
+ # Header names (lowercase form — Net::HTTP normalises to lowercase on
24
+ # the way back; matches the Node `x-request-id` convention).
25
+ HEADER_AUTHORIZATION = "Authorization"
26
+ HEADER_ACCEPT = "Accept"
27
+ HEADER_CONTENT_TYPE = "Content-Type"
28
+ HEADER_USER_AGENT = "User-Agent"
29
+ HEADER_IDEMPOTENCY_KEY = "Idempotency-Key"
30
+ HEADER_REQUEST_ID = "x-request-id"
31
+ HEADER_RETRY_AFTER = "retry-after"
32
+
33
+ # Document-preview response header carrying the page count
34
+ # (mirrors Node `documents.ts:75-77`).
35
+ HEADER_DOCUMENT_PAGE_COUNT = "x-document-page-count"
36
+ end
37
+ end
38
+ end