poli_page-rails 0.13.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/CHANGELOG.md +139 -0
- data/LICENSE +21 -0
- data/README.md +254 -0
- data/lib/generators/poli_page/install/install_generator.rb +34 -0
- data/lib/generators/poli_page/install/templates/poli_page.rb.tt +52 -0
- data/lib/poli_page/rails/client.rb +53 -0
- data/lib/poli_page/rails/configuration.rb +50 -0
- data/lib/poli_page/rails/configuration_validator.rb +102 -0
- data/lib/poli_page/rails/engine.rb +15 -0
- data/lib/poli_page/rails/error_handler.rb +64 -0
- data/lib/poli_page/rails/errors.rb +16 -0
- data/lib/poli_page/rails/filename_encoder.rb +34 -0
- data/lib/poli_page/rails/notifications.rb +43 -0
- data/lib/poli_page/rails/railtie.rb +47 -0
- data/lib/poli_page/rails/renderable.rb +52 -0
- data/lib/poli_page/rails/version.rb +7 -0
- data/lib/poli_page-rails.rb +14 -0
- data/sig/poli_page/rails/client.rbs +11 -0
- data/sig/poli_page/rails/configuration.rbs +37 -0
- data/sig/poli_page/rails/engine.rbs +6 -0
- data/sig/poli_page/rails/error_handler.rbs +14 -0
- data/sig/poli_page/rails/errors.rbs +7 -0
- data/sig/poli_page/rails/filename_encoder.rbs +9 -0
- data/sig/poli_page/rails/notifications.rbs +11 -0
- data/sig/poli_page/rails/railtie.rbs +6 -0
- data/sig/poli_page/rails/renderable.rbs +11 -0
- data/sig/poli_page/rails/version.rbs +5 -0
- metadata +148 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: fc6c84ba2e602d0b343aab8a74fc7f1cd34b65d087891c7a74640c558ba70194
|
|
4
|
+
data.tar.gz: 7218a8e03696761feeba15cf51822bf669910997373a4a163e7a703036e1fd94
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 823d3338569c4d2dc5d89ee67d4ce1018c8368f9cc4ffcb6f123ec05f6c21793f6b2a845fb945b6650b29a4a4add6be990deb437cdcf1894e70c7f15a7d69f47
|
|
7
|
+
data.tar.gz: 1dd17a7968b5e0bcb3fea62225e61bb594613380a921c2189304398c206c74f0326dc49d8242df24ee50da6f73277970cbe1a09044c1dc27a1713ae379bbae5e
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `poli_page-rails` are documented here. The format is
|
|
4
|
+
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
|
|
5
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.13.0] - 2026-11-04
|
|
10
|
+
|
|
11
|
+
First release on RubyGems. It requires the `poli-page` SDK 0.13
|
|
12
|
+
(`~> 0.13.0`) and passes the Poli Page API contract v0.13.0. The gem shares
|
|
13
|
+
its major.minor with the Poli Page API, so its first version is 0.13.0; a
|
|
14
|
+
0.11.0 was prepared and never published, and neither was the 0.1.0
|
|
15
|
+
development line. The Changed, Removed and Fixed entries below describe
|
|
16
|
+
changes made during those lines.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Rails engine + Railtie wrapping the official `poli-page` Ruby SDK.
|
|
21
|
+
- `PoliPage.client` — lazy memoised class-level accessor (Monitor-guarded;
|
|
22
|
+
shared across threads per the SDK's thread-safety guarantee).
|
|
23
|
+
`PoliPage.reset_client!` clears the memo (test-only).
|
|
24
|
+
- `Rails.application.config.poli_page` — `Configuration` object exposing every
|
|
25
|
+
SDK kwarg as an `attr_accessor` plus a `notifications` opt-out switch.
|
|
26
|
+
`to_client_kwargs.compact` lets the SDK's own defaults apply for `nil` keys.
|
|
27
|
+
- `PoliPage::Rails::ConfigurationValidator` — validates `api_key` (see
|
|
28
|
+
Changed), `base_url` (http/https), `timeout`
|
|
29
|
+
(1..600), `max_retries` (0..10), `retry_delay` (0..30), `notifications`
|
|
30
|
+
(boolean), and `on_retry` / `on_error` (callable). Runs lazily on first
|
|
31
|
+
`PoliPage.client` call — keeps `assets:precompile` and `db:create` working
|
|
32
|
+
in containers without secrets at boot.
|
|
33
|
+
- `PoliPage::Rails::Renderable` — controller concern providing `render_pdf`,
|
|
34
|
+
`render_preview`, `redirect_to_document`. Sets `Cache-Control: private,
|
|
35
|
+
no-store`, `X-Content-Type-Options: nosniff`, and an RFC 5987 / RFC 6266
|
|
36
|
+
`Content-Disposition` that handles ASCII + non-ASCII filenames (`résumé.pdf`,
|
|
37
|
+
`発票.pdf`, `🦀.pdf`).
|
|
38
|
+
- `PoliPage::Rails::FilenameEncoder` — thin wrapper over
|
|
39
|
+
`ActionDispatch::Http::ContentDisposition.format`; single seam if Rails
|
|
40
|
+
changes the formatting API across versions.
|
|
41
|
+
- `ActiveSupport::Notifications` bridge for the SDK's retry/error hooks
|
|
42
|
+
(`poli_page.retry`, `poli_page.error`). **Default-on**, opt-out via
|
|
43
|
+
`c.notifications = false`. Zero overhead with no subscribers.
|
|
44
|
+
- `rails generate poli_page:install` — writes
|
|
45
|
+
`config/initializers/poli_page.rb` with a commented template documenting
|
|
46
|
+
every configuration key. Zero custom options to avoid Thor-reserved-name
|
|
47
|
+
shadowing (`--force`, `--skip`, `--pretend`, `--quiet`, etc.).
|
|
48
|
+
- Example Rails 8 app at [`example-app/`](./example-app) with an interactive
|
|
49
|
+
single-page demo UI covering every SDK method (`render.pdf`,
|
|
50
|
+
`render.pdf_stream`, `render.document`, `documents.get`,
|
|
51
|
+
`documents.thumbnails`, `documents.preview`, `documents.delete`, an
|
|
52
|
+
error demo, and a `bin/rake demo:render_to_file` task for
|
|
53
|
+
`render_to_file`).
|
|
54
|
+
- RBS signatures in `sig/` mirroring the public API surface.
|
|
55
|
+
- A default error handler, on by default: `PoliPage::Rails::ErrorHandler` is
|
|
56
|
+
included into `ActionController::Base` and `ActionController::API` and
|
|
57
|
+
answers every unrescued `PoliPage::Error` with the SDK's `to_payload`, as is,
|
|
58
|
+
as JSON `{ "code", "message", "status", "requestId" }` and the payload's
|
|
59
|
+
status (`503` for `network_error`, `504` for `timeout`, `502` for
|
|
60
|
+
`invalid_response`), or `500` with `"status": null` when the status is
|
|
61
|
+
unknown (`invalid_options`, `io_error`), always as `application/json`. Set
|
|
62
|
+
`config.poli_page.error_handler = false` to leave it out. An error raised
|
|
63
|
+
after a response is committed (an `ActionController::Live` stream that wrote
|
|
64
|
+
its first chunk or was closed) is raised again, for Live to log and report;
|
|
65
|
+
close a Live stream only once it has written, or an error before the first
|
|
66
|
+
byte ends as an empty `200`.
|
|
67
|
+
- The API contract's integration cases (`spec/contract`), run in CI against
|
|
68
|
+
the example app on every Ruby and Rails version of the matrix, with the
|
|
69
|
+
contract pinned to v0.13.0 by the `poli-page/api-contract` action, including
|
|
70
|
+
`impl.invalid-response`.
|
|
71
|
+
- Release workflow: the tag's major.minor must be the pinned API contract's (a
|
|
72
|
+
stable contract tag on the pinned commit, not the comment next to it) and
|
|
73
|
+
the required `poli-page` SDK's (`~> X.Y.0` in the gemspec and the example
|
|
74
|
+
app), and the example app's lockfile must lock the tag's version
|
|
75
|
+
(`scripts/check-release-versions.sh`); the error names both versions.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- Requires the `poli-page` SDK 0.13 (`~> 0.13.0`, that is `>= 0.13.0,
|
|
80
|
+
< 0.14`). With it, a 2xx from the API whose body is not JSON (usually from a
|
|
81
|
+
proxy) raises `PoliPage::InvalidResponseError`, code `invalid_response` (was
|
|
82
|
+
`INTERNAL_ERROR`), which the default error handler answers `502`; a
|
|
83
|
+
`render_to_file` that cannot write its file raises
|
|
84
|
+
`PoliPage::FileWriteError`, code `io_error` (was `invalid_options` or the
|
|
85
|
+
raw Ruby exception), answered `500` with `"status": null`; and the PDF
|
|
86
|
+
download is bounded by the `timeout` option (an idle timeout for
|
|
87
|
+
`render.pdf_stream` and `render_to_file`).
|
|
88
|
+
- The default error handler forwards the SDK's `to_payload` as is: the SDK
|
|
89
|
+
0.13 names the request id `requestId` itself, so the gem's own renaming of
|
|
90
|
+
`request_id` and its `ErrorHandler::WIRE_KEYS` are gone.
|
|
91
|
+
`ErrorHandler.body` returns the payload's Symbol-keyed Hash.
|
|
92
|
+
- Environment keys and no template versions, as in the SDK since 0.11:
|
|
93
|
+
render calls take no `version:`, a key belongs to one environment
|
|
94
|
+
(`pp_{environment}_{32 lowercase hex}`, for example `pp_sandbox_…`,
|
|
95
|
+
`pp_live_…`, `pp_staging_…`), and the key alone selects the environment and
|
|
96
|
+
so the published copy that renders.
|
|
97
|
+
- `ConfigurationValidator` accepts any environment key. It raises
|
|
98
|
+
`ConfigurationError` only for a missing or empty `api_key`; a value that does
|
|
99
|
+
not match `pp_{environment}_{32 lowercase hex}` logs one warning through
|
|
100
|
+
`config.logger` that names the expected format and shows no part of the
|
|
101
|
+
key, not even its `pp_{environment}_` prefix, and the client starts anyway,
|
|
102
|
+
leaving the verdict to the API.
|
|
103
|
+
`API_KEY_PATTERN` is replaced by `API_KEY_FORMAT`.
|
|
104
|
+
- `render_preview` now documents `DocumentPreviewResult` (`documents.preview`)
|
|
105
|
+
only.
|
|
106
|
+
- Example app: the demo routes move from `/api/*` to the demo-contract
|
|
107
|
+
paths (`GET /render/pdf`, `GET /documents/:id`, `GET /errors/not-found`, …).
|
|
108
|
+
`GET /api/errors/bad-version` (`INVALID_VERSION_FORMAT`) is replaced by
|
|
109
|
+
`GET /errors/not-found`, which renders the template `does-not-exist` and,
|
|
110
|
+
through the default error handler, answers `404 TEMPLATE_NOT_FOUND` with the
|
|
111
|
+
error's code, message, status and request id. `GET /documents/:id/preview`
|
|
112
|
+
now goes through `render_preview`. `GET /documents/:id` still answers a
|
|
113
|
+
`302` to the presigned PDF URL.
|
|
114
|
+
- Example app: `GET /render/stream` sets its PDF headers when the first chunk
|
|
115
|
+
arrives, after the render and the start of the download, so a render error
|
|
116
|
+
or a network error before that answers through the default error handler
|
|
117
|
+
(JSON, its own status) instead of a `200 application/pdf`.
|
|
118
|
+
- The integration spec runs only with a `pp_sandbox_` key.
|
|
119
|
+
- CI: the live API job runs on `main` (and on demand) against the production API with the sandbox key from the `POLI_PAGE_API_KEY` secret, and skips with a notice when the key is absent or not a `pp_sandbox_` key.
|
|
120
|
+
- The live API CI job passes the optional `POLI_PAGE_BASE_URL` secret to the integration spec (unset: the SDK default), next to the `pp_sandbox_` key in `POLI_PAGE_API_KEY`. The spec sets `config.base_url` from `POLI_PAGE_BASE_URL` when it is set.
|
|
121
|
+
|
|
122
|
+
### Removed
|
|
123
|
+
|
|
124
|
+
- Example app: `GET /api/render/preview` (`render.preview`) and its "HTML
|
|
125
|
+
preview" button; the SDK no longer has `render.preview`.
|
|
126
|
+
- Example app: `DocumentsController`'s own `rescue_from PoliPage::Error`; the
|
|
127
|
+
default error handler answers instead.
|
|
128
|
+
|
|
129
|
+
### Fixed
|
|
130
|
+
|
|
131
|
+
- `render_pdf` / `FilenameEncoder.disposition`: control characters (CR/LF, TAB,
|
|
132
|
+
DEL, C1) are now stripped from the `Content-Disposition` filename before
|
|
133
|
+
`ActionDispatch::Http::ContentDisposition` formats it, instead of surviving as
|
|
134
|
+
`%0D%0A`-style escapes that clients decode back into the saved filename.
|
|
135
|
+
- Example app: the demo page's status row no longer shows the first 8
|
|
136
|
+
characters of `POLI_PAGE_API_KEY`; no part of the key is shown.
|
|
137
|
+
|
|
138
|
+
[Unreleased]: https://github.com/poli-page/rubyonrails/compare/v0.13.0...HEAD
|
|
139
|
+
[0.13.0]: https://github.com/poli-page/rubyonrails/releases/tag/v0.13.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Poli Page
|
|
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,254 @@
|
|
|
1
|
+
# poli_page-rails
|
|
2
|
+
|
|
3
|
+
> Render Poli Page documents from Rails controllers.
|
|
4
|
+
|
|
5
|
+
## About
|
|
6
|
+
|
|
7
|
+
This gem wires the official [`poli-page`](https://rubygems.org/gems/poli-page) Ruby SDK into a Rails app as an Engine. A Railtie installs a `Configuration` object on `Rails.application.config.poli_page`, `PoliPage.client` returns a lazy-memoised SDK client, and a `Renderable` controller concern turns SDK responses into PDF, HTML, or 302 responses with RFC 5987 `Content-Disposition` headers. Retry and terminal-error hooks on the SDK bridge into `ActiveSupport::Notifications` so subscribers like `lograge`, `appsignal`, or `scout_apm` pick them up without extra wiring.
|
|
8
|
+
|
|
9
|
+
**When to use this:**
|
|
10
|
+
|
|
11
|
+
- You render PDFs or HTML previews from a Rails controller and want `send_data`-shaped helpers that already set the right cache and content-disposition headers.
|
|
12
|
+
- You want SDK retry and error events on the standard `ActiveSupport::Notifications` bus.
|
|
13
|
+
- You want a generator that writes a fully commented `config/initializers/poli_page.rb`.
|
|
14
|
+
|
|
15
|
+
**When not to:**
|
|
16
|
+
|
|
17
|
+
- You are outside Rails — use the [`poli-page`](https://rubygems.org/gems/poli-page) SDK directly.
|
|
18
|
+
- You need to reimplement HTTP transport, retry policy, idempotency, or error mapping. Those live in the SDK; this gem deliberately does not duplicate them.
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Ruby 3.2 or newer
|
|
23
|
+
- Rails 7.0, 7.1, 7.2, or 8.0
|
|
24
|
+
- `poli-page` SDK 0.13.x (`~> 0.13.0`); Bundler installs it with the gem
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
Add the gem to your `Gemfile`:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Gemfile
|
|
32
|
+
gem "poli_page-rails", "~> 0.13.0"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then run:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# shell
|
|
39
|
+
bundle install
|
|
40
|
+
bin/rails generate poli_page:install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The generator writes `config/initializers/poli_page.rb` with a commented template. Set `POLI_PAGE_API_KEY` in your environment, then restart the app.
|
|
44
|
+
|
|
45
|
+
### API keys
|
|
46
|
+
|
|
47
|
+
A key belongs to exactly one environment of your organization and reads `pp_{environment}_{32 lowercase hex}`. The key alone selects the environment, and so the published copy of each project that renders.
|
|
48
|
+
|
|
49
|
+
| Environment | Key | Watermark | Counted |
|
|
50
|
+
| ------------------------------------- | -------------- | --------- | ------- |
|
|
51
|
+
| `sandbox` (every org) | `pp_sandbox_…` | yes | no |
|
|
52
|
+
| `live` (every org, deletable on Pro+) | `pp_live_…` | no | yes |
|
|
53
|
+
| created on Pro+ (e.g. `staging`) | `pp_staging_…` | no | yes |
|
|
54
|
+
|
|
55
|
+
"Counted" means the render counts toward the organization's monthly PDF quota.
|
|
56
|
+
|
|
57
|
+
The API is the only authority on keys. The gem raises `ConfigurationError` only when `api_key` is missing or empty. A value that does not match `pp_{environment}_{32 lowercase hex}` logs one warning through `config.logger` that names the expected format and shows no part of the key, then the client starts anyway and the API answers `401` if the key is wrong (`INVALID_API_KEY`, or `MISSING_API_KEY` for a value without `pp_`).
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
Return a generated PDF from any controller action.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# app/controllers/invoices_controller.rb
|
|
65
|
+
class InvoicesController < ApplicationController
|
|
66
|
+
include PoliPage::Rails::Renderable
|
|
67
|
+
|
|
68
|
+
def show
|
|
69
|
+
invoice = Invoice.find(params[:id])
|
|
70
|
+
bytes = PoliPage.client.render.pdf(
|
|
71
|
+
project: "billing",
|
|
72
|
+
template: "invoice",
|
|
73
|
+
data: invoice.to_template_data
|
|
74
|
+
)
|
|
75
|
+
render_pdf(bytes, filename: "#{invoice.number}.pdf", inline: false)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`render_pdf` sets `Content-Type: application/pdf`, `Cache-Control: private, no-store`, `X-Content-Type-Options: nosniff`, and an RFC 5987 / RFC 6266 `Content-Disposition` that round-trips non-ASCII filenames.
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
Every key is optional except `api_key`. The SDK's own defaults apply when a value is left `nil`.
|
|
85
|
+
|
|
86
|
+
| Option | Default | Description |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `api_key` | — | Required. A key for one environment: `pp_sandbox_…`, `pp_live_…`, or `pp_{environment}_…` (see [API keys](#api-keys)). |
|
|
89
|
+
| `base_url` | `https://api.poli.page` | Override for the develop environment or a private proxy. |
|
|
90
|
+
| `timeout` | SDK default | Request timeout in seconds. Must be in `1..600`. |
|
|
91
|
+
| `max_retries` | SDK default | Retry attempts on retryable failures. Must be in `0..10`. |
|
|
92
|
+
| `retry_delay` | SDK default | Base backoff delay in seconds. Must be in `0..30`. |
|
|
93
|
+
| `logger` | `Rails.logger` | Any `Logger`-compatible instance. |
|
|
94
|
+
| `proxy` | `nil` | HTTP proxy URL. Honoured by `Net::HTTP` via the SDK. |
|
|
95
|
+
| `ca_file` | `nil` | Path to a CA bundle for corporate egress. |
|
|
96
|
+
| `ca_path` | `nil` | Path to a CA directory. |
|
|
97
|
+
| `notifications` | `true` | Set to `false` to disable the `ActiveSupport::Notifications` bridge. |
|
|
98
|
+
| `on_retry` | bridge | Custom callable. Setting it replaces the notifications bridge. |
|
|
99
|
+
| `on_error` | bridge | Custom callable. Setting it replaces the notifications bridge. |
|
|
100
|
+
| `error_handler` | `true` | Set to `false` to leave out the default error handler (see [Default error handler](#default-error-handler)). |
|
|
101
|
+
|
|
102
|
+
A minimal initializer:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
# config/initializers/poli_page.rb
|
|
106
|
+
Rails.application.config.poli_page.tap do |c|
|
|
107
|
+
c.api_key = ENV.fetch("POLI_PAGE_API_KEY")
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Configuration is validated lazily on the first `PoliPage.client` call, so `assets:precompile` and `db:create` keep working in containers that have no secrets at boot.
|
|
112
|
+
|
|
113
|
+
## API at a glance
|
|
114
|
+
|
|
115
|
+
| Symbol | Purpose |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `PoliPage.client` | Lazy-memoised, thread-safe SDK `PoliPage::Client`. |
|
|
118
|
+
| `PoliPage::Rails::Renderable` | Controller concern exposing `render_pdf`, `render_preview`, `redirect_to_document`. |
|
|
119
|
+
| `PoliPage::Rails::Configuration` | The object behind `Rails.application.config.poli_page`. |
|
|
120
|
+
| `PoliPage::Rails::Engine` | Rails engine subclass auto-loaded from your `Gemfile`. |
|
|
121
|
+
| `PoliPage::Rails::ErrorHandler` | Controller concern that answers a `PoliPage::Error` with JSON; included by default. |
|
|
122
|
+
| `PoliPage::Rails::Railtie` | Installs the configuration, the logger default, the notifications bridge, and the error handler. |
|
|
123
|
+
| `"poli_page.retry"` | `ActiveSupport::Notifications` event fired before each SDK retry. |
|
|
124
|
+
| `"poli_page.error"` | `ActiveSupport::Notifications` event fired on terminal SDK failure. |
|
|
125
|
+
| `rails generate poli_page:install` | Writes `config/initializers/poli_page.rb`. |
|
|
126
|
+
|
|
127
|
+
Full reference: [docs/api.md](docs/api.md) (forthcoming).
|
|
128
|
+
|
|
129
|
+
## Errors
|
|
130
|
+
|
|
131
|
+
The SDK raises a fixed hierarchy under `PoliPage::Error`. The four categories you typically branch on:
|
|
132
|
+
|
|
133
|
+
- **Auth** — `PoliPage::AuthenticationError`, `PoliPage::PermissionDeniedError`. Bad or revoked key, blocked organization.
|
|
134
|
+
- **Rate limit** — `PoliPage::RateLimitError`. The API returned `429`; honour `Retry-After`.
|
|
135
|
+
- **Request rejected** — `PoliPage::ValidationError`. The template, project, or `data` payload was rejected at `400`.
|
|
136
|
+
- **Network / transport** — `PoliPage::ConnectionError`, `PoliPage::TimeoutError`. Network failure, DNS, TLS, or timeout.
|
|
137
|
+
|
|
138
|
+
Two more come from outside the API: `PoliPage::InvalidResponseError` (`invalid_response`) when a 2xx answer's body is not JSON, usually from a proxy between your app and the API, and `PoliPage::FileWriteError` (`io_error`) when `render_to_file` cannot write its file.
|
|
139
|
+
|
|
140
|
+
A `PoliPage::Error` you do not rescue reaches the [default error handler](#default-error-handler).
|
|
141
|
+
|
|
142
|
+
This gem adds one Rails-specific error: `PoliPage::Rails::ConfigurationError`, raised by `ConfigurationValidator` on the first `PoliPage.client` call when the initializer is invalid. It inherits from `PoliPage::Error`, so a single `rescue PoliPage::Error` catches everything.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# app/controllers/invoices_controller.rb
|
|
146
|
+
def show
|
|
147
|
+
bytes = PoliPage.client.render.pdf(project: "billing", template: "invoice", data: invoice_data)
|
|
148
|
+
render_pdf(bytes, filename: "invoice.pdf")
|
|
149
|
+
rescue PoliPage::ValidationError => e
|
|
150
|
+
Rails.logger.warn("PoliPage validation failed: #{e.message}")
|
|
151
|
+
head :unprocessable_entity
|
|
152
|
+
rescue PoliPage::RateLimitError
|
|
153
|
+
head :too_many_requests
|
|
154
|
+
rescue PoliPage::AuthenticationError, PoliPage::PermissionDeniedError
|
|
155
|
+
head :forbidden
|
|
156
|
+
rescue PoliPage::ConnectionError, PoliPage::TimeoutError
|
|
157
|
+
head :bad_gateway
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Default error handler
|
|
162
|
+
|
|
163
|
+
The gem answers every `PoliPage::Error` a controller action raises without rescuing it. The Railtie includes `PoliPage::Rails::ErrorHandler` into `ActionController::Base` and `ActionController::API`, which `rescue_from PoliPage::Error` and render the SDK's `Error#to_payload` as JSON, as is:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{ "code": "TEMPLATE_NOT_FOUND", "message": "Template 'does-not-exist' not found in project 'getting-started'.", "status": 404, "requestId": "req_…" }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- The HTTP status is the payload's `status`: the API's status for an API error, `503` for `network_error`, `504` for `timeout`, `502` for `invalid_response` (the 2xx a proxy sent never reaches your client). When the status is unknown (for example `configuration_error` or `io_error`), the answer is `500` and the body's `status` is `null`.
|
|
170
|
+
- `requestId` is the API's `x-request-id`, or `null` when there is none.
|
|
171
|
+
- The `Content-Type` is `application/json`, even when the action had set another one (a PDF route, say) before the error.
|
|
172
|
+
- The keys are the SDK's, spelled the same by every Poli Page SDK: your app's inflections (an `ID` acronym, for example) do not change them.
|
|
173
|
+
|
|
174
|
+
The handler cannot answer once a response is committed: an `ActionController::Live` stream is committed when it writes its first chunk or when it is closed. The handler then raises the error again, so Live logs it and calls the stream's `on_error`. After PDF bytes went out, the client gets a short `200` (the PDF cut where the error happened); compare the bytes received with the descriptor's `sizeBytes` if you need to detect it.
|
|
175
|
+
|
|
176
|
+
So that an error before the first byte still gets the JSON answer, in a Live action:
|
|
177
|
+
|
|
178
|
+
- set the PDF headers and write the first chunk only after the SDK call has started;
|
|
179
|
+
- close the stream only once you have written to it (`ensure response.stream.close if started`). The usual unconditional `ensure response.stream.close` commits an empty `200` before the handler runs, and the error then ends as that empty `200`.
|
|
180
|
+
|
|
181
|
+
The example app's `StreamsController` does both.
|
|
182
|
+
|
|
183
|
+
A `rescue_from` in your own controllers (for `PoliPage::Error`, one of its subclasses, or `StandardError`) takes precedence, as usual in Rails. To leave the handler out entirely:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# config/initializers/poli_page.rb
|
|
187
|
+
Rails.application.config.poli_page.tap do |c|
|
|
188
|
+
c.error_handler = false
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The value must be `true` or `false`; the app refuses to boot with anything else.
|
|
193
|
+
|
|
194
|
+
## Example app
|
|
195
|
+
|
|
196
|
+
A runnable Rails 8 app lives in [`example-app/`](./example-app). It exercises every public SDK method behind a single-page UI and ships a `bin/rake demo:render_to_file` task for the `render_to_file` SDK path. It renders the `welcome` template of the `getting-started` project, which every organization has in its `sandbox` environment. Its routes follow the Poli Page demo contract: `GET /render/pdf` answers the PDF, `GET /documents/:id` redirects (`302`) to the document's presigned PDF URL, and `GET /errors/not-found` renders the template `does-not-exist`, so the default error handler answers `404` with the `TEMPLATE_NOT_FOUND` error's code, message, status and request id. If your organization renamed or removed `getting-started` from `sandbox`, the renders answer `404 PROJECT_NOT_FOUND` or `PROJECT_NOT_IN_ENVIRONMENT`: use a fresh organization.
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
# shell
|
|
200
|
+
cd example-app
|
|
201
|
+
bundle install
|
|
202
|
+
export POLI_PAGE_API_KEY=pp_sandbox_... # a sandbox key
|
|
203
|
+
# export POLI_PAGE_BASE_URL=https://api.poli.page # optional
|
|
204
|
+
bin/rails server
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Going further
|
|
208
|
+
|
|
209
|
+
- ActiveSupport::Notifications — subscribe to `poli_page.retry` and `poli_page.error` for logging, metrics, and alerting (forthcoming `docs/notifications.md`).
|
|
210
|
+
- Filename encoding — how `PoliPage::Rails::FilenameEncoder` produces RFC 5987 `Content-Disposition` values for non-ASCII names (forthcoming `docs/filenames.md`).
|
|
211
|
+
- Background rendering — running renders through Active Job for slow templates (forthcoming `docs/active_job.md`).
|
|
212
|
+
- Testing helpers — stubbing `PoliPage.client` in request specs and system tests (forthcoming `docs/testing.md`).
|
|
213
|
+
|
|
214
|
+
## Compatibility
|
|
215
|
+
|
|
216
|
+
| Gem | `poli-page` SDK | Rails | Ruby |
|
|
217
|
+
|---|---|---|---|
|
|
218
|
+
| 0.13.x | 0.13.x | 7.0 / 7.1 / 7.2 / 8.0 | 3.2 – 3.4 |
|
|
219
|
+
|
|
220
|
+
The gem shares its major.minor with the Poli Page API and requires the SDK of the same minor (see [RELEASING.md](RELEASING.md#versions)).
|
|
221
|
+
|
|
222
|
+
You receive fixes for the latest two Rails majors. Older majors receive security fixes for six months after their upstream EOL.
|
|
223
|
+
|
|
224
|
+
## Contributing
|
|
225
|
+
|
|
226
|
+
Run the lint and the specs for one Rails version with Appraisal:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
bundle install
|
|
230
|
+
bundle exec appraisal generate
|
|
231
|
+
BUNDLE_GEMFILE=gemfiles/rails_8.0.gemfile bundle install
|
|
232
|
+
bundle exec rubocop
|
|
233
|
+
bundle exec appraisal rails-8.0 rspec --exclude-pattern "spec/integration/**/*"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`spec/integration` renders against a real API and runs only with `POLI_PAGE_API_KEY` set to a `pp_sandbox_` key. It targets the SDK's default API, or the one in `POLI_PAGE_BASE_URL` when that is set. The live API CI job reads both from repository secrets: `POLI_PAGE_API_KEY` (a `pp_sandbox_` key) and the optional `POLI_PAGE_BASE_URL`.
|
|
237
|
+
|
|
238
|
+
### Contract tests
|
|
239
|
+
|
|
240
|
+
`spec/contract/impl_contract_spec.rb` runs the integration cases of the [Poli Page API contract](https://github.com/poli-page/api-contract) against the example app's controllers, routes and initializer, with the SDK pointed at a local mock server. CI runs them on every Ruby and Rails version of the matrix, against the contract version pinned in `.github/workflows/ci.yml`. Locally, download a `contract.json` and run:
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
POLI_CONTRACT=<path to contract.json> bundle exec rspec spec/contract
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Without `POLI_CONTRACT` the cases are pending (and fail when `CI` is set). Cases allowed to fail are listed with a reason in `spec/contract-skip.json`.
|
|
247
|
+
|
|
248
|
+
### Releasing
|
|
249
|
+
|
|
250
|
+
Releases are published by pushing a `vX.Y.Z` tag; see [RELEASING.md](RELEASING.md).
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
Released under the MIT License — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/base"
|
|
5
|
+
|
|
6
|
+
module PoliPage
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Writes config/initializers/poli_page.rb with a commented template."
|
|
12
|
+
|
|
13
|
+
# NOTE: Zero custom options on this generator. Rails::Generators::Base
|
|
14
|
+
# inherits Thor-reserved flags (--force, --pretend, --quiet, --skip,
|
|
15
|
+
# --skip-namespace, --skip-collision-check). Defining a custom option
|
|
16
|
+
# with any of those names would silently shadow them and make the
|
|
17
|
+
# generator appear broken.
|
|
18
|
+
|
|
19
|
+
argument :initializer_path, type: :string, required: false,
|
|
20
|
+
default: "config/initializers/poli_page.rb",
|
|
21
|
+
banner: "PATH"
|
|
22
|
+
|
|
23
|
+
def copy_initializer
|
|
24
|
+
template "poli_page.rb.tt", initializer_path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def show_post_install_hint
|
|
28
|
+
say ""
|
|
29
|
+
say "Created #{initializer_path}.", :green
|
|
30
|
+
say "Set POLI_PAGE_API_KEY in your environment, then restart the app.", :green
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Configuration for the poli_page-rails gem (https://poli.page).
|
|
4
|
+
#
|
|
5
|
+
# Set POLI_PAGE_API_KEY in your environment, then customise below as needed.
|
|
6
|
+
# Get a key at https://app.poli.page/settings/api-keys.
|
|
7
|
+
|
|
8
|
+
Rails.application.config.poli_page.tap do |c|
|
|
9
|
+
c.api_key = ENV.fetch("POLI_PAGE_API_KEY")
|
|
10
|
+
|
|
11
|
+
# Optional — defaults to https://api.poli.page (the SDK applies its default
|
|
12
|
+
# when this is nil).
|
|
13
|
+
# c.base_url = ENV["POLI_PAGE_BASE_URL"]
|
|
14
|
+
|
|
15
|
+
# Optional — request timeout in seconds (Float). SDK default applies when nil.
|
|
16
|
+
# c.timeout = 30.0
|
|
17
|
+
|
|
18
|
+
# Optional — retry policy. SDK defaults apply when nil.
|
|
19
|
+
# c.max_retries = 3
|
|
20
|
+
# c.retry_delay = 0.5
|
|
21
|
+
|
|
22
|
+
# Optional — pass any Logger-compatible instance. Defaults to Rails.logger.
|
|
23
|
+
# c.logger = Rails.logger
|
|
24
|
+
|
|
25
|
+
# Optional — corporate-egress controls. Honoured by Net::HTTP via the SDK.
|
|
26
|
+
# c.proxy = ENV["POLI_PAGE_HTTP_PROXY"]
|
|
27
|
+
# c.ca_file = ENV["POLI_PAGE_CA_FILE"]
|
|
28
|
+
# c.ca_path = ENV["POLI_PAGE_CA_PATH"]
|
|
29
|
+
|
|
30
|
+
# Optional — set to false to disable the ActiveSupport::Notifications bridge.
|
|
31
|
+
# The bridge is default-on; it costs nothing when no subscribers attach.
|
|
32
|
+
# c.notifications = false
|
|
33
|
+
|
|
34
|
+
# Optional — set to false to remove the default error handler. It answers
|
|
35
|
+
# every PoliPage::Error a controller raises with JSON
|
|
36
|
+
# { code, message, status, requestId } (the SDK's to_payload) and the
|
|
37
|
+
# payload's status, or 500 when the status is unknown.
|
|
38
|
+
# c.error_handler = false
|
|
39
|
+
|
|
40
|
+
# Optional — custom retry/error callables. Setting these REPLACES the
|
|
41
|
+
# ActiveSupport::Notifications bridge entirely (you opt out of the default
|
|
42
|
+
# bridge by providing your own callable). To preserve the bridge AND add
|
|
43
|
+
# custom behaviour, subscribe to AS::Notifications instead:
|
|
44
|
+
#
|
|
45
|
+
# ActiveSupport::Notifications.subscribe("poli_page.retry") do |*args|
|
|
46
|
+
# event = ActiveSupport::Notifications::Event.new(*args)
|
|
47
|
+
# Rails.logger.warn("PoliPage retry: #{event.payload}")
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
50
|
+
# c.on_retry = ->(event) { Rails.logger.warn("retry: #{event.attempt}") }
|
|
51
|
+
# c.on_error = ->(err) { Sentry.capture_exception(err) }
|
|
52
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require "poli_page"
|
|
5
|
+
|
|
6
|
+
require_relative "configuration_validator"
|
|
7
|
+
|
|
8
|
+
module PoliPage
|
|
9
|
+
# Re-opened from the SDK. Adds the Rails-friendly lazy accessor.
|
|
10
|
+
class << self
|
|
11
|
+
def client
|
|
12
|
+
Rails::Client.instance
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Test-only: clear the memoised instance so the next #client call rebuilds.
|
|
16
|
+
# Marked private API; do NOT use in application code.
|
|
17
|
+
def reset_client!
|
|
18
|
+
Rails::Client.reset!
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module Rails
|
|
23
|
+
# Holds the lazy memoised PoliPage::Client. Thread-safe via a Monitor.
|
|
24
|
+
# The SDK documents PoliPage::Client as thread-safe, so memoising a single
|
|
25
|
+
# instance and sharing it across threads is correct.
|
|
26
|
+
module Client
|
|
27
|
+
LOCK = Monitor.new
|
|
28
|
+
private_constant :LOCK
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def instance
|
|
32
|
+
LOCK.synchronize do
|
|
33
|
+
@instance ||= build!
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset!
|
|
38
|
+
LOCK.synchronize do
|
|
39
|
+
@instance = nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def build!
|
|
46
|
+
config = ::Rails.application.config.poli_page
|
|
47
|
+
ConfigurationValidator.validate!(config)
|
|
48
|
+
PoliPage::Client.new(**config.to_client_kwargs)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PoliPage
|
|
4
|
+
module Rails
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :api_key, :base_url, :timeout, :user_agent,
|
|
7
|
+
:max_retries, :retry_delay,
|
|
8
|
+
:logger, :on_retry, :on_error,
|
|
9
|
+
:proxy, :ca_file, :ca_path,
|
|
10
|
+
:notifications, :error_handler
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@api_key = nil
|
|
14
|
+
@base_url = nil
|
|
15
|
+
@timeout = nil
|
|
16
|
+
@user_agent = nil
|
|
17
|
+
@max_retries = nil
|
|
18
|
+
@retry_delay = nil
|
|
19
|
+
@logger = nil
|
|
20
|
+
@on_retry = nil
|
|
21
|
+
@on_error = nil
|
|
22
|
+
@proxy = nil
|
|
23
|
+
@ca_file = nil
|
|
24
|
+
@ca_path = nil
|
|
25
|
+
@notifications = true
|
|
26
|
+
@error_handler = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Single source of truth for SDK kwargs. Compact drops every nil entry,
|
|
30
|
+
# so the SDK's own defaults take over for every unset key. user_agent
|
|
31
|
+
# is intentionally NOT included — PoliPage::Client#initialize does not
|
|
32
|
+
# accept it (poli-page 0.11).
|
|
33
|
+
def to_client_kwargs
|
|
34
|
+
{
|
|
35
|
+
api_key: api_key,
|
|
36
|
+
base_url: base_url,
|
|
37
|
+
max_retries: max_retries,
|
|
38
|
+
retry_delay: retry_delay,
|
|
39
|
+
timeout: timeout,
|
|
40
|
+
logger: logger,
|
|
41
|
+
on_retry: on_retry,
|
|
42
|
+
on_error: on_error,
|
|
43
|
+
proxy: proxy,
|
|
44
|
+
ca_file: ca_file,
|
|
45
|
+
ca_path: ca_path
|
|
46
|
+
}.compact
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module PoliPage
|
|
8
|
+
module Rails
|
|
9
|
+
module ConfigurationValidator
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# pp_{environment}_{32 lowercase hex}. Only drives a startup warning: the
|
|
13
|
+
# API is the authority on keys, so a mismatch never blocks the client.
|
|
14
|
+
API_KEY_FORMAT = /\App_(?=[a-z][a-z0-9-]{0,19}_)[a-z][a-z0-9]*(?:-[a-z0-9]+)*_[0-9a-f]{32}\z/
|
|
15
|
+
|
|
16
|
+
def validate!(config)
|
|
17
|
+
validate_api_key!(config.api_key, config.logger)
|
|
18
|
+
validate_base_url!(config.base_url)
|
|
19
|
+
validate_timeout!(config.timeout)
|
|
20
|
+
validate_max_retries!(config.max_retries)
|
|
21
|
+
validate_retry_delay!(config.retry_delay)
|
|
22
|
+
validate_notifications!(config.notifications)
|
|
23
|
+
validate_callable!(config.on_retry, "on_retry")
|
|
24
|
+
validate_callable!(config.on_error, "on_error")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_api_key!(value, logger)
|
|
28
|
+
unless value.is_a?(String) && !value.empty?
|
|
29
|
+
raise ConfigurationError, "Poli Page api_key is required (a non-empty String). " \
|
|
30
|
+
"Get one at https://app.poli.page/settings/api-keys."
|
|
31
|
+
end
|
|
32
|
+
return if API_KEY_FORMAT.match?(value)
|
|
33
|
+
|
|
34
|
+
# No part of the key reaches the log: even a well-formed pp_{environment}_
|
|
35
|
+
# can be a pasted secret fragment.
|
|
36
|
+
logger&.warn("Poli Page api_key does not match the expected format " \
|
|
37
|
+
"pp_{environment}_{32 lowercase hex characters}; starting anyway, " \
|
|
38
|
+
"the API will reject it if it is not a valid key.")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_base_url!(value)
|
|
42
|
+
return if value.nil?
|
|
43
|
+
|
|
44
|
+
uri = URI.parse(value.to_s)
|
|
45
|
+
return if uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
46
|
+
|
|
47
|
+
raise ConfigurationError, "Poli Page base_url must be an http or https URL (got #{value.inspect})."
|
|
48
|
+
rescue URI::InvalidURIError
|
|
49
|
+
raise ConfigurationError, "Poli Page base_url must be an http or https URL (got #{value.inspect})."
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate_timeout!(value)
|
|
53
|
+
return if value.nil?
|
|
54
|
+
|
|
55
|
+
unless value.is_a?(Numeric) && value.positive?
|
|
56
|
+
raise ConfigurationError, "Poli Page timeout must be a positive number (got #{value.inspect})."
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return if value <= 600
|
|
60
|
+
|
|
61
|
+
raise ConfigurationError, "Poli Page timeout must be <= 600 seconds (got #{value.inspect})."
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_max_retries!(value)
|
|
65
|
+
return if value.nil?
|
|
66
|
+
return if value.is_a?(Integer) && (0..10).cover?(value)
|
|
67
|
+
|
|
68
|
+
raise ConfigurationError,
|
|
69
|
+
"Poli Page max_retries must be an integer between 0 and 10 (got #{value.inspect})."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate_retry_delay!(value)
|
|
73
|
+
return if value.nil?
|
|
74
|
+
return if value.is_a?(Numeric) && (0..30).cover?(value)
|
|
75
|
+
|
|
76
|
+
raise ConfigurationError, "Poli Page retry_delay must be a number between 0 and 30 seconds " \
|
|
77
|
+
"(got #{value.inspect})."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_notifications!(value)
|
|
81
|
+
return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
82
|
+
|
|
83
|
+
raise ConfigurationError, "Poli Page notifications must be true or false (got #{value.inspect})."
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Checked at boot by the Railtie, when it decides whether to install the
|
|
87
|
+
# error handler (not in validate!, which runs when the client is built).
|
|
88
|
+
def validate_error_handler!(value)
|
|
89
|
+
return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
90
|
+
|
|
91
|
+
raise ConfigurationError, "Poli Page error_handler must be true or false (got #{value.inspect})."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_callable!(value, name)
|
|
95
|
+
return if value.nil?
|
|
96
|
+
return if value.respond_to?(:call) && !value.is_a?(String) && !value.is_a?(Symbol)
|
|
97
|
+
|
|
98
|
+
raise ConfigurationError, "Poli Page #{name} must respond to #call (got #{value.class})."
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
|
|
5
|
+
module PoliPage
|
|
6
|
+
module Rails
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
isolate_namespace PoliPage::Rails
|
|
9
|
+
|
|
10
|
+
# config.poli_page is wired by the Railtie (Task 5). This Engine class
|
|
11
|
+
# exists primarily so future routes / generators / assets can ship as
|
|
12
|
+
# part of v0.2 without re-architecting.
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
|
|
6
|
+
module PoliPage
|
|
7
|
+
module Rails
|
|
8
|
+
# Turns every PoliPage::Error a controller action raises into a JSON answer:
|
|
9
|
+
#
|
|
10
|
+
# status: the payload's status, or 500 when it is nil
|
|
11
|
+
# body: { "code", "message", "status", "requestId" }
|
|
12
|
+
#
|
|
13
|
+
# The body is the SDK's Error#to_payload as is: the SDK names its keys
|
|
14
|
+
# code, message, status and requestId, the same in every Poli Page SDK.
|
|
15
|
+
# The Content-Type is application/json even when the action had set
|
|
16
|
+
# another one before the error (a PDF route, for example). Its status is
|
|
17
|
+
# the API status, 503 for network_error, 504 for timeout, 502 for
|
|
18
|
+
# invalid_response, and null when unknown (io_error, for example); the
|
|
19
|
+
# HTTP status falls back to 500 only in that last case.
|
|
20
|
+
#
|
|
21
|
+
# The Railtie includes this concern into ActionController::Base and
|
|
22
|
+
# ActionController::API unless the app sets
|
|
23
|
+
# `config.poli_page.error_handler = false`. A rescue_from declared later in
|
|
24
|
+
# a controller (for PoliPage::Error, a subclass, or StandardError) takes
|
|
25
|
+
# precedence over this one, as usual in Rails.
|
|
26
|
+
module ErrorHandler
|
|
27
|
+
extend ::ActiveSupport::Concern
|
|
28
|
+
|
|
29
|
+
included do
|
|
30
|
+
rescue_from ::PoliPage::Error, with: :render_poli_page_error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The JSON body for `error`: its to_payload, forwarded as is. JSON.generate
|
|
34
|
+
# writes the Symbol keys as they are spelled, so the host app's
|
|
35
|
+
# inflections (an "ID" acronym, say) cannot change them.
|
|
36
|
+
def self.body(error)
|
|
37
|
+
error.to_payload
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The HTTP status for `error`: the payload's status, or 500 when unknown.
|
|
41
|
+
def self.status(error)
|
|
42
|
+
error.to_payload[:status] || 500
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# A response already committed (an ActionController::Live stream that
|
|
48
|
+
# sent its first chunk) cannot change its status or body: the error is
|
|
49
|
+
# raised again so Live logs it and calls the stream's on_error, instead
|
|
50
|
+
# of being reported as an answer that was never sent.
|
|
51
|
+
#
|
|
52
|
+
# Why JSON.generate: the body holds only strings, integers and nil, and
|
|
53
|
+
# it keeps the answer independent of ActiveSupport's JSON encoder (whose
|
|
54
|
+
# Rails 7.0, 7.1 and 8.0 releases pass an option the json 3 gem refuses).
|
|
55
|
+
def render_poli_page_error(error)
|
|
56
|
+
raise error if response.committed?
|
|
57
|
+
|
|
58
|
+
render json: JSON.generate(ErrorHandler.body(error)),
|
|
59
|
+
status: ErrorHandler.status(error),
|
|
60
|
+
content_type: "application/json"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "poli_page/errors"
|
|
4
|
+
|
|
5
|
+
module PoliPage
|
|
6
|
+
module Rails
|
|
7
|
+
# Raised by ConfigurationValidator when Rails.application.config.poli_page
|
|
8
|
+
# contains an invalid value. Inherits from PoliPage::Error so existing
|
|
9
|
+
# `rescue PoliPage::Error` clauses catch it.
|
|
10
|
+
class ConfigurationError < PoliPage::Error
|
|
11
|
+
def initialize(message)
|
|
12
|
+
super(message, code: "configuration_error", status: nil, request_id: nil)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_dispatch/http/content_disposition"
|
|
4
|
+
|
|
5
|
+
module PoliPage
|
|
6
|
+
module Rails
|
|
7
|
+
# Builds a Content-Disposition header value with RFC 5987 / RFC 6266
|
|
8
|
+
# encoding for non-ASCII filenames. Thin wrapper over Rails's own
|
|
9
|
+
# ActionDispatch::Http::ContentDisposition.format — what ActiveStorage
|
|
10
|
+
# uses to set the same header for downloaded blobs.
|
|
11
|
+
#
|
|
12
|
+
# Why a wrapper at all: single seam if Rails changes the formatting API
|
|
13
|
+
# across versions (stable since Rails 5.2; one place to patch if it
|
|
14
|
+
# changes).
|
|
15
|
+
#
|
|
16
|
+
# Control characters are stripped before formatting: ContentDisposition
|
|
17
|
+
# percent-escapes them (%0D%0A), which keeps the header intact but hands
|
|
18
|
+
# CR/LF/TAB back to any client that decodes filename*.
|
|
19
|
+
module FilenameEncoder
|
|
20
|
+
# C0 controls (incl. TAB, CR, LF), DEL and C1 controls.
|
|
21
|
+
CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def disposition(filename, inline:)
|
|
26
|
+
disposition_type = inline ? "inline" : "attachment"
|
|
27
|
+
::ActionDispatch::Http::ContentDisposition.format(
|
|
28
|
+
disposition: disposition_type,
|
|
29
|
+
filename: filename.gsub(CONTROL_CHARS, "")
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module PoliPage
|
|
6
|
+
module Rails
|
|
7
|
+
# Bridges the SDK's on_retry / on_error Closure hooks into Rails-idiomatic
|
|
8
|
+
# ActiveSupport::Notifications events.
|
|
9
|
+
#
|
|
10
|
+
# Event names follow Rails conventions (sql.active_record, deliver.action_mailer):
|
|
11
|
+
# - "poli_page.retry" — fired before each retry attempt
|
|
12
|
+
# - "poli_page.error" — fired on terminal failure (post-retries)
|
|
13
|
+
#
|
|
14
|
+
# Default-on: the Railtie installs the bridges as the default on_retry /
|
|
15
|
+
# on_error config values, so retries and failures reach the usual Rails
|
|
16
|
+
# instrumentation subscribers without extra wiring. Users opt out via
|
|
17
|
+
# c.notifications = false in the initializer.
|
|
18
|
+
module Notifications
|
|
19
|
+
RETRY_EVENT_NAME = "poli_page.retry"
|
|
20
|
+
ERROR_EVENT_NAME = "poli_page.error"
|
|
21
|
+
|
|
22
|
+
def self.retry_bridge
|
|
23
|
+
@retry_bridge ||= lambda do |event|
|
|
24
|
+
::ActiveSupport::Notifications.instrument(
|
|
25
|
+
RETRY_EVENT_NAME,
|
|
26
|
+
attempt: event.attempt,
|
|
27
|
+
delay_ms: event.delay_ms,
|
|
28
|
+
reason: event.reason
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.error_bridge
|
|
34
|
+
@error_bridge ||= lambda do |error|
|
|
35
|
+
::ActiveSupport::Notifications.instrument(
|
|
36
|
+
ERROR_EVENT_NAME,
|
|
37
|
+
error: error
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
require_relative "configuration_validator"
|
|
6
|
+
require_relative "error_handler"
|
|
7
|
+
require_relative "notifications"
|
|
8
|
+
|
|
9
|
+
module PoliPage
|
|
10
|
+
module Rails
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
initializer "poli_page.configure", before: :load_config_initializers do |app|
|
|
13
|
+
# Why: Rails::Application::Configuration#method_missing raises
|
|
14
|
+
# NoMethodError on read when the key has never been set, so the
|
|
15
|
+
# idiomatic `||=` expansion (which reads first) blows up at boot.
|
|
16
|
+
# Assign unconditionally — user code in config/initializers/poli_page.rb
|
|
17
|
+
# runs AFTER this (we use before: :load_config_initializers) and
|
|
18
|
+
# mutates the existing Configuration object, it never reassigns.
|
|
19
|
+
app.config.poli_page = Configuration.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Runs after config/initializers/*.rb, so `config.poli_page.error_handler
|
|
23
|
+
# = false` there turns it off. The on_load hook includes the concern into
|
|
24
|
+
# ActionController::Base and ActionController::API when they load, before
|
|
25
|
+
# any application controller inherits from them.
|
|
26
|
+
initializer "poli_page.error_handler", after: :load_config_initializers do |app|
|
|
27
|
+
enabled = app.config.poli_page.error_handler
|
|
28
|
+
ConfigurationValidator.validate_error_handler!(enabled)
|
|
29
|
+
next unless enabled
|
|
30
|
+
|
|
31
|
+
::ActiveSupport.on_load(:action_controller) do
|
|
32
|
+
include PoliPage::Rails::ErrorHandler
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
config.after_initialize do |app|
|
|
37
|
+
c = app.config.poli_page
|
|
38
|
+
c.logger ||= ::Rails.logger
|
|
39
|
+
|
|
40
|
+
if c.notifications
|
|
41
|
+
c.on_retry ||= PoliPage::Rails::Notifications.retry_bridge
|
|
42
|
+
c.on_error ||= PoliPage::Rails::Notifications.error_bridge
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
require_relative "filename_encoder"
|
|
6
|
+
|
|
7
|
+
module PoliPage
|
|
8
|
+
module Rails
|
|
9
|
+
module Renderable
|
|
10
|
+
extend ::ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
# Sends raw PDF bytes with PDF headers + RFC 5987 Content-Disposition.
|
|
13
|
+
#
|
|
14
|
+
# @param bytes [String] raw PDF bytes (binary-encoded)
|
|
15
|
+
# @param filename [String] used in Content-Disposition; ASCII and
|
|
16
|
+
# non-ASCII both handled per RFC 5987 / RFC 6266
|
|
17
|
+
# @param inline [Boolean] inline (browser viewer) vs attachment (download)
|
|
18
|
+
def render_pdf(bytes, filename: "document.pdf", inline: false)
|
|
19
|
+
send_data(
|
|
20
|
+
bytes,
|
|
21
|
+
type: "application/pdf",
|
|
22
|
+
disposition: inline ? "inline" : "attachment",
|
|
23
|
+
filename: filename
|
|
24
|
+
)
|
|
25
|
+
# Why: send_data sets Content-Disposition using Rails's plain ASCII
|
|
26
|
+
# formatting. Overwrite with our RFC 5987 encoder so non-ASCII filenames
|
|
27
|
+
# survive. Cache-Control + nosniff are independent additions.
|
|
28
|
+
response.headers["Content-Disposition"] = FilenameEncoder.disposition(filename, inline: inline)
|
|
29
|
+
response.headers["Cache-Control"] = "private, no-store"
|
|
30
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Renders the .html attribute of a DocumentPreviewResult (documents.preview).
|
|
34
|
+
# The HTML is a complete document from the Poli Page render pipeline; use
|
|
35
|
+
# render body: to skip template processing and layouts entirely — no
|
|
36
|
+
# html_safe needed (Rails only escapes inside templates, not for body:).
|
|
37
|
+
def render_preview(result)
|
|
38
|
+
html = result.respond_to?(:html) ? result.html : result.to_s
|
|
39
|
+
response.headers["Cache-Control"] = "private, no-store"
|
|
40
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
41
|
+
render body: html, content_type: "text/html; charset=utf-8"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 302 to the descriptor's presigned URL. Adds Cache-Control so the
|
|
45
|
+
# redirect itself never gets intermediary-cached.
|
|
46
|
+
def redirect_to_document(descriptor, status: :found)
|
|
47
|
+
response.headers["Cache-Control"] = "private, no-store"
|
|
48
|
+
redirect_to descriptor.presigned_pdf_url, status: status, allow_other_host: true
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "poli_page"
|
|
4
|
+
require_relative "poli_page/rails/version"
|
|
5
|
+
require_relative "poli_page/rails/errors"
|
|
6
|
+
require_relative "poli_page/rails/configuration"
|
|
7
|
+
require_relative "poli_page/rails/configuration_validator"
|
|
8
|
+
require_relative "poli_page/rails/client"
|
|
9
|
+
require_relative "poli_page/rails/filename_encoder"
|
|
10
|
+
require_relative "poli_page/rails/renderable"
|
|
11
|
+
require_relative "poli_page/rails/error_handler"
|
|
12
|
+
require_relative "poli_page/rails/notifications"
|
|
13
|
+
require_relative "poli_page/rails/engine"
|
|
14
|
+
require_relative "poli_page/rails/railtie"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module PoliPage
|
|
2
|
+
module Rails
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor api_key: String?
|
|
5
|
+
attr_accessor base_url: String?
|
|
6
|
+
attr_accessor timeout: Numeric?
|
|
7
|
+
attr_accessor user_agent: String?
|
|
8
|
+
attr_accessor max_retries: Integer?
|
|
9
|
+
attr_accessor retry_delay: Numeric?
|
|
10
|
+
attr_accessor logger: untyped
|
|
11
|
+
attr_accessor on_retry: (^(untyped) -> void)?
|
|
12
|
+
attr_accessor on_error: (^(untyped) -> void)?
|
|
13
|
+
attr_accessor proxy: String?
|
|
14
|
+
attr_accessor ca_file: String?
|
|
15
|
+
attr_accessor ca_path: String?
|
|
16
|
+
attr_accessor notifications: bool
|
|
17
|
+
attr_accessor error_handler: bool
|
|
18
|
+
|
|
19
|
+
def initialize: () -> void
|
|
20
|
+
def to_client_kwargs: () -> Hash[Symbol, untyped]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module ConfigurationValidator
|
|
24
|
+
API_KEY_FORMAT: Regexp
|
|
25
|
+
|
|
26
|
+
def self.validate!: (Configuration) -> void
|
|
27
|
+
def self.validate_api_key!: (untyped, untyped logger) -> void
|
|
28
|
+
def self.validate_base_url!: (untyped) -> void
|
|
29
|
+
def self.validate_timeout!: (untyped) -> void
|
|
30
|
+
def self.validate_max_retries!: (untyped) -> void
|
|
31
|
+
def self.validate_retry_delay!: (untyped) -> void
|
|
32
|
+
def self.validate_notifications!: (untyped) -> void
|
|
33
|
+
def self.validate_error_handler!: (untyped) -> void
|
|
34
|
+
def self.validate_callable!: (untyped, String) -> void
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module PoliPage
|
|
2
|
+
module Rails
|
|
3
|
+
module ErrorHandler
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def self.body: (PoliPage::Error error) -> { code: String, message: String, status: Integer?, requestId: String? }
|
|
7
|
+
def self.status: (PoliPage::Error error) -> Integer
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def render_poli_page_error: (PoliPage::Error error) -> void
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module PoliPage
|
|
2
|
+
module Rails
|
|
3
|
+
module Renderable
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def render_pdf: (String bytes, ?filename: String, ?inline: bool) -> void
|
|
7
|
+
def render_preview: (untyped result) -> void
|
|
8
|
+
def redirect_to_document: (untyped descriptor, ?status: Symbol | Integer) -> void
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: poli_page-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.13.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Poli Page
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: poli-page
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.13.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.13.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: railties
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '9'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '7.0'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '9'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: actionpack
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '7.0'
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '9'
|
|
56
|
+
type: :runtime
|
|
57
|
+
prerelease: false
|
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '7.0'
|
|
63
|
+
- - "<"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '9'
|
|
66
|
+
- !ruby/object:Gem::Dependency
|
|
67
|
+
name: activesupport
|
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '7.0'
|
|
73
|
+
- - "<"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '9'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '7.0'
|
|
83
|
+
- - "<"
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '9'
|
|
86
|
+
description: Rails engine + controller concern + ActiveSupport::Notifications bridge
|
|
87
|
+
over the official poli-page Ruby SDK. Lazy-memoised PoliPage.client, RFC 5987 Content-Disposition
|
|
88
|
+
handling, opt-out notifications instrumentation, and a rails generate poli_page:install
|
|
89
|
+
generator that writes config/initializers/poli_page.rb.
|
|
90
|
+
email: support@poli.page
|
|
91
|
+
executables: []
|
|
92
|
+
extensions: []
|
|
93
|
+
extra_rdoc_files: []
|
|
94
|
+
files:
|
|
95
|
+
- CHANGELOG.md
|
|
96
|
+
- LICENSE
|
|
97
|
+
- README.md
|
|
98
|
+
- lib/generators/poli_page/install/install_generator.rb
|
|
99
|
+
- lib/generators/poli_page/install/templates/poli_page.rb.tt
|
|
100
|
+
- lib/poli_page-rails.rb
|
|
101
|
+
- lib/poli_page/rails/client.rb
|
|
102
|
+
- lib/poli_page/rails/configuration.rb
|
|
103
|
+
- lib/poli_page/rails/configuration_validator.rb
|
|
104
|
+
- lib/poli_page/rails/engine.rb
|
|
105
|
+
- lib/poli_page/rails/error_handler.rb
|
|
106
|
+
- lib/poli_page/rails/errors.rb
|
|
107
|
+
- lib/poli_page/rails/filename_encoder.rb
|
|
108
|
+
- lib/poli_page/rails/notifications.rb
|
|
109
|
+
- lib/poli_page/rails/railtie.rb
|
|
110
|
+
- lib/poli_page/rails/renderable.rb
|
|
111
|
+
- lib/poli_page/rails/version.rb
|
|
112
|
+
- sig/poli_page/rails/client.rbs
|
|
113
|
+
- sig/poli_page/rails/configuration.rbs
|
|
114
|
+
- sig/poli_page/rails/engine.rbs
|
|
115
|
+
- sig/poli_page/rails/error_handler.rbs
|
|
116
|
+
- sig/poli_page/rails/errors.rbs
|
|
117
|
+
- sig/poli_page/rails/filename_encoder.rbs
|
|
118
|
+
- sig/poli_page/rails/notifications.rbs
|
|
119
|
+
- sig/poli_page/rails/railtie.rbs
|
|
120
|
+
- sig/poli_page/rails/renderable.rbs
|
|
121
|
+
- sig/poli_page/rails/version.rbs
|
|
122
|
+
homepage: https://poli.page
|
|
123
|
+
licenses:
|
|
124
|
+
- MIT
|
|
125
|
+
metadata:
|
|
126
|
+
homepage_uri: https://poli.page
|
|
127
|
+
source_code_uri: https://github.com/poli-page/rubyonrails
|
|
128
|
+
changelog_uri: https://github.com/poli-page/rubyonrails/blob/main/CHANGELOG.md
|
|
129
|
+
bug_tracker_uri: https://github.com/poli-page/rubyonrails/issues
|
|
130
|
+
rubygems_mfa_required: 'true'
|
|
131
|
+
rdoc_options: []
|
|
132
|
+
require_paths:
|
|
133
|
+
- lib
|
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '3.2'
|
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - ">="
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0'
|
|
144
|
+
requirements: []
|
|
145
|
+
rubygems_version: 3.6.7
|
|
146
|
+
specification_version: 4
|
|
147
|
+
summary: Rails engine for the Poli Page PDF rendering API
|
|
148
|
+
test_files: []
|