unisender-ruby 0.1.0 → 0.2.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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -0
  3. data/CODE_OF_CONDUCT.md +90 -0
  4. data/CONTRIBUTING.md +23 -2
  5. data/README.md +53 -90
  6. data/SECURITY.md +13 -0
  7. data/docs/api.md +395 -210
  8. data/docs/execution.md +89 -0
  9. data/docs/import-recovery.md +41 -0
  10. data/docs/live-contracts.md +34 -0
  11. data/docs/migration-0.2.md +129 -0
  12. data/docs/rails.md +248 -0
  13. data/docs/releases.md +22 -0
  14. data/docs/testing.md +83 -0
  15. data/docs/usage.md +144 -0
  16. data/docs/webhooks.md +41 -0
  17. data/examples/campaign.rb +5 -6
  18. data/examples/campaign_lifecycle.rb +28 -0
  19. data/examples/check_connection.rb +1 -1
  20. data/examples/events.rb +1 -1
  21. data/examples/export.rb +20 -6
  22. data/examples/import.rb +3 -2
  23. data/examples/import_recovery.rb +70 -0
  24. data/examples/quickstart.rb +2 -1
  25. data/examples/subscribe.rb +2 -2
  26. data/lib/unisender/acceptance.rb +104 -0
  27. data/lib/unisender/attachment.rb +47 -0
  28. data/lib/unisender/batch_report.rb +115 -0
  29. data/lib/unisender/batch_summary.rb +87 -0
  30. data/lib/unisender/batches.rb +41 -12
  31. data/lib/unisender/campaign_workflow.rb +72 -0
  32. data/lib/unisender/client.rb +65 -33
  33. data/lib/unisender/client_configuration.rb +50 -0
  34. data/lib/unisender/client_execution.rb +43 -0
  35. data/lib/unisender/client_state.rb +31 -0
  36. data/lib/unisender/client_validation.rb +97 -0
  37. data/lib/unisender/configuration.rb +151 -0
  38. data/lib/unisender/contact_records.rb +251 -0
  39. data/lib/unisender/diagnostic_urls.rb +68 -0
  40. data/lib/unisender/diagnostics.rb +83 -0
  41. data/lib/unisender/download_target.rb +61 -0
  42. data/lib/unisender/download_transfer.rb +43 -0
  43. data/lib/unisender/downloader.rb +99 -0
  44. data/lib/unisender/error_diagnostics.rb +83 -0
  45. data/lib/unisender/errors.rb +125 -8
  46. data/lib/unisender/event_batches.rb +40 -0
  47. data/lib/unisender/event_payload.rb +50 -22
  48. data/lib/unisender/events.rb +21 -5
  49. data/lib/unisender/execution_context.rb +114 -0
  50. data/lib/unisender/form.rb +20 -12
  51. data/lib/unisender/helpers.rb +70 -10
  52. data/lib/unisender/hooks.rb +79 -0
  53. data/lib/unisender/import_cells.rb +62 -0
  54. data/lib/unisender/input_types.rb +30 -0
  55. data/lib/unisender/inputs.rb +262 -0
  56. data/lib/unisender/iteration.rb +15 -0
  57. data/lib/unisender/iterators.rb +60 -0
  58. data/lib/unisender/mail_inputs.rb +37 -0
  59. data/lib/unisender/named_inputs.rb +52 -0
  60. data/lib/unisender/observation.rb +30 -0
  61. data/lib/unisender/operation_contracts.rb +106 -0
  62. data/lib/unisender/operations/contacts_1.rb +4 -3
  63. data/lib/unisender/operations/contacts_2.rb +23 -4
  64. data/lib/unisender/operations/messages_1.rb +7 -4
  65. data/lib/unisender/operations/messages_2.rb +8 -3
  66. data/lib/unisender/operations/messages_3.rb +1 -1
  67. data/lib/unisender/operations/notes_1.rb +1 -1
  68. data/lib/unisender/operations/statistics_1.rb +3 -3
  69. data/lib/unisender/operations/templates_1.rb +2 -2
  70. data/lib/unisender/outcomes.rb +320 -0
  71. data/lib/unisender/page_response.rb +48 -0
  72. data/lib/unisender/paginated_operations.rb +43 -2
  73. data/lib/unisender/pagination.rb +53 -20
  74. data/lib/unisender/persistent_transport.rb +104 -0
  75. data/lib/unisender/rate_limiter.rb +72 -0
  76. data/lib/unisender/record.rb +210 -0
  77. data/lib/unisender/record_rows.rb +69 -0
  78. data/lib/unisender/records.rb +614 -0
  79. data/lib/unisender/request_execution.rb +108 -0
  80. data/lib/unisender/response.rb +13 -10
  81. data/lib/unisender/result_registry.rb +72 -0
  82. data/lib/unisender/results.rb +105 -0
  83. data/lib/unisender/retry_policy.rb +10 -16
  84. data/lib/unisender/serialization.rb +54 -0
  85. data/lib/unisender/statistic_records.rb +129 -0
  86. data/lib/unisender/task_response.rb +85 -0
  87. data/lib/unisender/task_waiter.rb +25 -11
  88. data/lib/unisender/testing/assertions.rb +48 -0
  89. data/lib/unisender/testing/fixtures.rb +24 -0
  90. data/lib/unisender/testing.rb +91 -0
  91. data/lib/unisender/transport.rb +17 -9
  92. data/lib/unisender/validation.rb +11 -9
  93. data/lib/unisender/version.rb +1 -1
  94. data/lib/unisender/webhooks/tokens.rb +38 -0
  95. data/lib/unisender/webhooks.rb +230 -0
  96. data/lib/unisender.rb +42 -0
  97. data/sig/acceptance.rbs +30 -0
  98. data/sig/attachment.rbs +15 -0
  99. data/sig/batch_summary.rbs +38 -0
  100. data/sig/campaign_workflow.rbs +20 -0
  101. data/sig/configuration.rbs +62 -0
  102. data/sig/connection_controls.rbs +55 -0
  103. data/sig/diagnostics.rbs +37 -0
  104. data/sig/domain_records.rbs +96 -0
  105. data/sig/downloads.rbs +53 -0
  106. data/sig/execution.rbs +52 -0
  107. data/sig/execution_context.rbs +46 -0
  108. data/sig/hooks.rbs +18 -0
  109. data/sig/inputs.rbs +84 -0
  110. data/sig/iterators.rbs +11 -0
  111. data/sig/mail_inputs.rbs +8 -0
  112. data/sig/manifest.yaml +11 -0
  113. data/sig/operation_contracts.rbs +28 -0
  114. data/sig/operations.rbs +73 -69
  115. data/sig/outcomes.rbs +90 -0
  116. data/sig/record.rbs +77 -0
  117. data/sig/records.rbs +182 -0
  118. data/sig/serialization.rbs +13 -0
  119. data/sig/tasks.rbs +26 -0
  120. data/sig/testing.rbs +54 -0
  121. data/sig/unisender.rbs +128 -39
  122. data/sig/webhooks.rbs +55 -0
  123. metadata +88 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5816a7b213b4919d38f81de3568187d4dd1a2f345413b529f3961c2fad33f187
4
- data.tar.gz: 0c0dcea4ad0cb5f1e4a2f5d2a502562ff68d3edebe1fdc04c3c7e95675fc4a9f
3
+ metadata.gz: f3678167b25f05feee7a6cbb24ead0d09fea765de2cbd815f092644c1ee62598
4
+ data.tar.gz: c0d66eec6a7c5ea4b93e162a7c4ebc62c9548585c6e4cf3cc5f49a50d345f0b5
5
5
  SHA512:
6
- metadata.gz: 2d6e970aa069fb618f59335fc935ec897f4513af963ab60728c8379072a88bdf06eb08103e7b67a942d283e3e0b308c3a546aa91e26d0a26ee8c1089a158789b
7
- data.tar.gz: '09e933af3af43cd61bb7c03a9a8c852813b24f22c766e4d1446008c1bddadb4c2d24d964a919272f489ab78c78ea73e40f5e598bb24570164b3f57b342f1b474'
6
+ metadata.gz: 8c15b5e69db9bb9584e75c52a1666ed99cf5cae211be9acf9b154943068e1f168803d332b2e907ec8ab4dccbec1ad7faceb3685a7006eaad4e97bb70b3ca5c8e
7
+ data.tar.gz: a634d99895b18c0df41f1e5b01f1943ca15f56e9921624be600c0be5b0eaf9b6e5e8745b7cb2d5e20c521127b679f8e0190a1fcf56dee239a788d96157e06a49
data/CHANGELOG.md CHANGED
@@ -1,7 +1,104 @@
1
1
  # Changelog
2
2
 
3
+ Unreleased collects changes for the next version. Dated sections list user-visible changes, newest first. GitHub release notes reproduce the corresponding section.
4
+
3
5
  ## Unreleased
4
6
 
7
+ ## 0.2.0 - 2026-10-26
8
+
9
+ - Parse diagnostic bracket fields without regex substitution while preserving index paths and redaction.
10
+
11
+ - Add campaign lifecycle and durable import examples, a complete Rails integration, verified Cursor setup, and shorter task-oriented documentation.
12
+
13
+ - Keep offline pagination validation aligned with execution and report indexed event and response-shape errors.
14
+ - Mark acceptance as uncertain when responses omit inputs or return inconsistent counts; enforce false JSON expectations.
15
+
16
+ - Add optional fixture builders and redacted subset assertions for queued form and JSON requests.
17
+
18
+ - Add optional acceptance instrumentation with separate accepted, rejected, and unknown counts for sends, imports, and events.
19
+
20
+ - Identify API requests with the gem and Ruby versions and document client ownership across variants, threads, and forks.
21
+
22
+ - Add redacted error diagnostics, structural field paths, expected and actual types, and request context on malformed responses.
23
+
24
+ - Add an independent optional Event Tracking rate limiter and validate message-default containers locally.
25
+
26
+ - Add named mail headers, image-mode symbols, and explicit accepted, rejected, and unknown recipient views.
27
+
28
+ - Accept SMS phone arrays and always return a typed receipt collection.
29
+
30
+ - Give records class-sensitive value equality, forward object lookup, and avoid polling failed task handles.
31
+
32
+ - Honor top-level `only` and `except` JSON filters and reject unsupported serialization options.
33
+
34
+ - Validate named argument types locally and expose offline `Client#validate!` with shared normalization and encoding.
35
+
36
+ - Bound direct imports to 501 inspected records and add optional byte limits to attachment reads.
37
+
38
+ - Schedule Time inputs in explicit UTC with minute precision; reject conflicting timezones.
39
+
40
+ - Document the accepted MD5 exception required by UniSender's webhook protocol.
41
+
42
+ - Scan webhook JSON tokens without regular-expression backtracking and reject unterminated strings.
43
+ - Escape backslashes as well as table separators in generated API type descriptions.
44
+
45
+ - Preserve existing injected retry policies, rate limiters, and downloaders when execution scopes are enabled.
46
+ - Add cooperative execution scopes and cancellation across retries, pacing, polling, and batch boundaries, with separate execution timing events and request IDs on request events.
47
+ - Add opt-in summary retention and progress checkpoints for import and Event Tracking batches, preserving known source offsets for row diagnostics.
48
+ - Add hook management and bounded JSON/gzip webhook authentication with typed notification readers.
49
+ - Add fake-transport queue verification and JSON request access, durable export recovery examples, and opt-in staging contract probes.
50
+
51
+ - Validate malformed contact fields and count filters locally, and preserve operation names and import column paths in validation errors.
52
+ - Correct reference descriptions of legacy empty-result normalization.
53
+
54
+ - Standardize input diagnostics, validate named flags and choices, and preserve wrapper failure context.
55
+ - Normalize structured import cells and expose typed row warnings with known global indices.
56
+ - Add scoped email/phone removal inputs and explicit contact-count filters.
57
+ - Request detailed send acceptance by default; retain explicit legacy and omission options.
58
+
59
+ - Accept blocks on page iterators and expose page checkpoint offsets.
60
+ - Add Array-style collection access, Hash-style record lookup, and payload JSON serialization.
61
+ - Add stable contact address readers, optional contact details, note metadata, and mutation result readers.
62
+ - Return completed task handles without polling; expose task predicates and block-scoped client lifetime.
63
+
64
+ - Separate development installation from published usage and link gem documentation to its version tag.
65
+ - Generate API argument and result tables from RBS, with agreement checks for the Ruby interface.
66
+
67
+ - Expose all documented import counters directly and normalize string-keyed default overrides.
68
+ - Validate campaign encoding before creating a message.
69
+
70
+ - Preserve accepted batches on source exceptions and task context on polling request failures.
71
+ - Validate direct pagination controls and include named-result failures in instrumentation.
72
+ - Reject special-use download addresses and complete Time signatures for iterators.
73
+
74
+ - Add opt-in persistent HTTPS connections, lifecycle controls, and shared process-local request pacing.
75
+ - Add binary attachment snapshots and a preflighted create-message/send-campaign workflow.
76
+ - Add explicit, bounded export downloads without forwarding API credentials.
77
+
78
+ - Add direct timeout and retry options, environment construction, immutable variants, and message defaults.
79
+ - Add safe request instrumentation, structured error categories, recovery hints, and retry-safety checks.
80
+ - Ship optional test transport support and application integration guides.
81
+ - Expand typed contact, sender-domain, SMS, and campaign-statistics results.
82
+
83
+ - Expose typed acceptance reports and global batch failure indices without replaying requests.
84
+ - Split event batches by encoded bytes and count; preserve completed work when input enumeration fails.
85
+
86
+ - Accept contact records, named subscription modes, and Ruby time inputs.
87
+ - Add task handles with explicit polling, progress callbacks, and contextual timeout errors.
88
+
89
+ - Execute batch commands immediately; provide explicit lazy batch and typed item iterators.
90
+ - Add page navigation with resume offsets and immediate argument validation.
91
+
92
+ - Return typed immutable records and enumerable collections from named operations.
93
+ - Add direct field access, a boolean membership predicate, and singular message results.
94
+ - Rename response HTTP metadata to `http_status`; preserve upstream JSON through `data`.
95
+
96
+ ## 0.1.1 - 2026-10-25
97
+
98
+ - Clarify installation, example destinations, contribution guidance, and maintainer policies.
99
+ - Include security, conduct, and release verification documents in the gem.
100
+ - Publish signed releases through an approval-gated workflow with an exact-file SPDX inventory and verifiable release assets.
101
+
5
102
  ## 0.1.0 - 2026-10-25
6
103
 
7
104
  - Publish as `unisender-ruby`; load with `require "unisender"` and use the `Unisender` namespace.
@@ -0,0 +1,90 @@
1
+
2
+ # Contributor Covenant 3.0 Code of Conduct
3
+
4
+ ## Our Pledge
5
+
6
+ We pledge to make our community welcoming, safe, and equitable for all.
7
+
8
+ We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant.
9
+
10
+
11
+ ## Encouraged Behaviors
12
+
13
+ While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language.
14
+
15
+ With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
16
+
17
+ 1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
18
+ 2. Engaging **kindly and honestly** with others.
19
+ 3. Respecting **different viewpoints** and experiences.
20
+ 4. **Taking responsibility** for our actions and contributions.
21
+ 5. Gracefully giving and accepting **constructive feedback**.
22
+ 6. Committing to **repairing harm** when it occurs.
23
+ 7. Behaving in other ways that promote and sustain the **well-being of our community**.
24
+
25
+
26
+ ## Restricted Behaviors
27
+
28
+ We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
29
+
30
+ 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
31
+ 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
32
+ 3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits.
33
+ 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
34
+ 5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission.
35
+ 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
36
+ 7. Behaving in other ways that **threaten the well-being** of our community.
37
+
38
+ ### Other Restrictions
39
+
40
+ 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
41
+ 2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
42
+ 3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community.
43
+ 4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
44
+
45
+
46
+ ## Reporting an Issue
47
+
48
+ Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.
49
+
50
+ When an incident does occur, it is important to report it promptly. To report a possible violation, email [Leonid Svyatov](mailto:leonid@svyatov.com). Leonid moderates this community and will respond within seven days.
51
+
52
+ Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution.
53
+
54
+
55
+ ## Addressing and Repairing Harm
56
+
57
+ If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped.
58
+
59
+ 1) Warning
60
+ 1) Event: A violation involving a single incident or series of incidents.
61
+ 2) Consequence: A private, written warning from the Community Moderators.
62
+ 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations.
63
+ 2) Temporarily Limited Activities
64
+ 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation.
65
+ 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members.
66
+ 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over.
67
+ 3) Temporary Suspension
68
+ 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation.
69
+ 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions.
70
+ 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted.
71
+ 4) Permanent Ban
72
+ 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member.
73
+ 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior.
74
+ 3) Repair: There is no possible repair in cases of this severity.
75
+
76
+ This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community.
77
+
78
+
79
+ ## Scope
80
+
81
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
82
+
83
+
84
+ ## Attribution
85
+
86
+ This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
87
+
88
+ Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
89
+
90
+ For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion).
data/CONTRIBUTING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Contributing
2
2
 
3
- This repository is private. Collaborators can use the [issue tracker](https://github.com/svyatov/unisender-ruby/issues). Include the Ruby and gem versions, a minimal example with fake values, expected behavior, and a redacted exception message. Do not post API keys, contact data, export links, or raw request/response bodies.
3
+ Contributions and questions are welcome through the [issue tracker](https://github.com/svyatov/unisender-ruby/issues). Include the Ruby and gem versions, a minimal example with fake values, expected behavior, and a redacted exception message. Do not post API keys, contact data, export links, or raw request/response bodies.
4
4
 
5
5
  ## Set up
6
6
 
@@ -13,10 +13,16 @@ bundle exec rake check
13
13
 
14
14
  `bin/setup` installs development dependencies and Lefthook hooks. The complete check runs offline tests, RuboCop, RBS validation, Steep for the library and consumer examples, the dependency vulnerability audit, and the gem build. The audit updates its advisory database and needs network access.
15
15
 
16
+ CI runs `bundle exec rake ci` for lint, tests, types, and the build. Dependency review blocks newly introduced vulnerabilities. A separate advisory job reports vulnerabilities across the lockfile without blocking unrelated changes.
17
+
16
18
  ## Make a change
17
19
 
18
20
  Use a branch such as `fix/error-context` or `feat/new-operation`. Read the official UniSender method documentation before changing a wire contract. Add a regression test with a deterministic transport, keep signatures in `sig/` synchronized, update the relevant docs, and add an entry under Unreleased in [CHANGELOG.md](CHANGELOG.md).
19
21
 
22
+ Follow the repository requirements in [AGENTS.md](AGENTS.md) and the [code of conduct](CODE_OF_CONDUCT.md). New functionality must include tests.
23
+
24
+ Fork the repository and create your branch from `main`. Push the branch to your fork, then open a pull request against `svyatov/unisender-ruby:main`. Describe the affected public interface and include the evidence needed to assess the change. Keep unrelated changes in separate pull requests. Maintainers use squash merges.
25
+
20
26
  | Command | Purpose |
21
27
  | --- | --- |
22
28
  | `bundle exec rake test` | Minitest with 100% line and branch coverage across `lib/` and `scripts/` |
@@ -33,11 +39,26 @@ Pre-commit hooks scan staged changes for secrets, apply and stage safe Ruby form
33
39
 
34
40
  ## Build and test the package
35
41
 
42
+ API reference tables come from shipped RBS signatures and `docs/api_annotations.yml`.
43
+ After changing either, regenerate the tables:
44
+
45
+ ```sh
46
+ bundle exec ruby -r ./scripts/api_reference -e 'File.write("docs/api.md", APIReference.new(Dir.pwd).render)'
47
+ ```
48
+
49
+ The regular test suite checks that generated tables match the public Ruby keywords.
50
+
36
51
  ```sh
37
52
  bundle exec rake build
38
- gem install --local ./pkg/unisender-ruby-0.1.0.gem
53
+ ruby -e 'spec = Gem::Specification.load("unisender.gemspec"); system("gem", "install", "--local", "pkg/#{spec.file_name}") || abort("Install failed")'
39
54
  ```
40
55
 
41
56
  From outside the checkout, run `ruby -runisender -e 'puts Unisender::VERSION'`. For an isolated check, use a temporary `GEM_HOME` and `GEM_PATH`, and run the examples with a fake transport. The gem includes examples and RBS signatures; development dependencies are not runtime dependencies.
42
57
 
43
58
  Routine verification must not call a live account, send messages, or change contacts. Live checks require explicit authorization and a designated test account. Publishing and releases are separate maintainer actions.
59
+
60
+ ## Maintainer decisions
61
+
62
+ Leonid Svyatov is the sole maintainer and decides which changes merge and which versions are released. No successor is arranged if he stops maintaining the project.
63
+
64
+ Report vulnerabilities privately using [SECURITY.md](SECURITY.md).
data/README.md CHANGED
@@ -1,59 +1,51 @@
1
1
  # Unisender
2
2
 
3
- A Ruby client for the [UniSender marketing API](https://www.unisender.com/ru/support/category/api/). Ruby 3.3 or newer, no runtime gem dependencies. This client targets `api.unisender.com`, not UniSender Go.
3
+ A Ruby client for applications using the [UniSender marketing API](https://www.unisender.com/ru/support/category/api/).
4
4
 
5
- Explicit keyword methods cover contacts, lists, messages, campaigns, statistics, templates, fields, notes, and Event Tracking. Responses preserve upstream JSON, warnings, and HTTP metadata. See the [API reference](docs/api.md) for the operation index, argument types, and result shapes.
5
+ - Ruby 3.3 or newer, with no runtime gem dependencies.
6
+ - Contacts, lists, messages, campaigns, statistics, templates, fields, notes, and Event Tracking.
7
+ - Typed immutable results, offline validation, and explicit pagination and batch recovery.
8
+ - Targets `api.unisender.com` and Event Tracking, not UniSender Go.
6
9
 
7
10
  ## Install
8
11
 
9
- Add the gem to your application's Gemfile:
12
+ This guide covers version 0.2. See the [migration guide](docs/migration-0.2.md) when upgrading from 0.1.
10
13
 
11
- ```ruby
12
- gem "unisender-ruby", "~> 0.1.0", require: "unisender"
13
- ```
14
-
15
- Then run `bundle install`. To install it directly, run `gem install unisender-ruby`. The package is named `unisender-ruby`; load it with `require "unisender"`.
16
-
17
- ### Install from a checkout
18
-
19
- In your application's Gemfile, point to this repository:
14
+ Add the gem to your application's `Gemfile`:
20
15
 
21
16
  ```ruby
22
- gem "unisender-ruby", path: "../unisender-ruby", require: "unisender"
17
+ gem "unisender-ruby", "~> 0.2.0", require: "unisender"
23
18
  ```
24
19
 
25
- Adjust the path, then run `bundle install`. Alternatively, build and install the local package as described in [CONTRIBUTING.md](CONTRIBUTING.md#build-and-test-the-package).
26
-
27
- ## Make your first request
20
+ Run `bundle install`.
21
+ To build from a checkout, follow [build and installation](CONTRIBUTING.md#build-and-test-the-package).
22
+ The package name is `unisender-ruby`; the Ruby load path is `unisender`.
28
23
 
29
- Get your key from [UniSender's API settings](https://www.unisender.com/ru/support/api/common/api-key/) and put it in the `UNISENDER_API_KEY` environment variable through your shell or application's secret store. Keep it out of source control and logs.
24
+ ## Read your lists
30
25
 
31
- For a local checkout, you can store `UNISENDER_API_KEY=your-key` in `.env`, which Git ignores. Ruby does not load this file automatically. Load your own `.env` into the current shell, then run the read-only connection check:
26
+ Get your key from [UniSender's API settings](https://www.unisender.com/ru/support/api/common/api-key/).
27
+ Set `UNISENDER_API_KEY` through your shell or application's secret store.
28
+ The library does not load `.env` files. Keep credentials out of source and logs.
32
29
 
33
- ```sh
34
- set -a
35
- . ./.env
36
- set +a
37
- bundle exec ruby examples/check_connection.rb
38
- ```
39
-
40
- The check prints only the HTTP status and list count, including zero for an empty account. Failures exit with status 1. It does not print list names or IDs. The gem itself reads neither environment variables nor `.env`; application code passes the key explicitly.
41
-
42
- This example only reads your lists. It does not create contacts or send messages:
30
+ Save this as `quickstart.rb`:
43
31
 
44
32
  <!-- example: examples/quickstart.rb -->
45
33
  ```ruby
46
34
  require "unisender"
47
35
 
48
36
  client = Unisender::Client.new(api_key: ENV.fetch("UNISENDER_API_KEY"))
49
- client.get_lists.result.each { |list| puts list.fetch("title") }
37
+ client.get_lists.each { |list| puts list.title }
38
+ # Example output for a list named News: News
50
39
  ```
51
40
 
52
- Save it as `quickstart.rb` and run `bundle exec ruby quickstart.rb`. It prints one title per list, or nothing if the account has no lists. A missing environment variable raises `KeyError` before any request. To try the client without an account or network access, run `bundle exec ruby examples/offline.rb` from this checkout.
41
+ Run `bundle exec ruby quickstart.rb`. An empty account prints nothing.
42
+ To try the client without credentials, run `bundle exec ruby examples/offline.rb` from the checkout.
53
43
 
54
44
  ## Subscribe a contact
55
45
 
56
- This operation changes your account and normally **sends a confirmation email**. Use an existing list ID from `get_lists`, and an address you control for a manual trial. Set `UNISENDER_LIST_ID` and `UNISENDER_CONTACT_EMAIL` before running it.
46
+ This changes your account and normally **sends a confirmation email**.
47
+ Set `UNISENDER_LIST_ID` to an existing list and `UNISENDER_CONTACT_EMAIL` to an address you control.
48
+ Save this as `subscribe.rb`:
57
49
 
58
50
  <!-- example: examples/subscribe.rb -->
59
51
  ```ruby
@@ -62,79 +54,50 @@ require "unisender"
62
54
  client = Unisender::Client.new(api_key: ENV.fetch("UNISENDER_API_KEY"))
63
55
  response = client.subscribe(
64
56
  list_ids: [ENV.fetch("UNISENDER_LIST_ID")],
65
- fields: { email: ENV.fetch("UNISENDER_CONTACT_EMAIL") }
57
+ email: ENV.fetch("UNISENDER_CONTACT_EMAIL")
66
58
  )
67
- puts response.result.fetch("person_id")
59
+ puts response.person_id
68
60
  ```
69
61
 
70
- The contact must confirm before receiving campaigns. Use `double_optin: 3` only when you already have consent; it adds the contact without sending the confirmation email. `overwrite: 0` preserves existing fields and tags, `1` replaces them and list membership, and `2` updates supplied fields. See the [upstream subscription rules](https://www.unisender.com/ru/support/api/contacts/subscribe/).
71
-
72
- ## Complete workflows
73
-
74
- Each example is a standalone Ruby file. Its header lists required environment variables and account effects.
75
-
76
- | Task | Example | Effect |
77
- | --- | --- | --- |
78
- | Read lists | [quickstart.rb](examples/quickstart.rb) | Read only |
79
- | Check credentials without printing account data | [check_connection.rb](examples/check_connection.rb) | Read only |
80
- | Test without credentials | [offline.rb](examples/offline.rb) | No network |
81
- | Subscribe a contact | [subscribe.rb](examples/subscribe.rb) | Changes contacts; sends confirmation |
82
- | Create and send a campaign | [campaign.rb](examples/campaign.rb) | Sends to the selected list |
83
- | Export contacts | [export.rb](examples/export.rb) | Starts and polls an export job |
84
- | Import batches with partial-failure handling | [import.rb](examples/import.rb) | Changes contacts |
85
- | Track events with partial-failure handling | [events.rb](examples/events.rb) | Records events; can trigger automation |
62
+ Run `bundle exec ruby subscribe.rb`. The contact must confirm before receiving campaigns.
63
+ Use `double_optin: :already_confirmed` only with consent already established.
64
+ To check inputs without networking, call `client.validate!(:subscribe, list_ids: [123], email: "person@example.test")`.
86
65
 
87
- ## Responses and failures
66
+ ## Handle a failure
88
67
 
89
- Every operation returns `Unisender::Response`: `result`, `warnings`, `data` (the full JSON object), `status`, and `headers`. Values are deeply immutable. JSON keys are strings; numbers are not coerced. Read or copy the data explicitly when your application needs to change it.
90
-
91
- Named methods use snake_case and reject unknown keywords. Optional `nil` values are omitted, preserving server defaults. API messages default to English; use `Client.new(api_key: ..., language: :ru)` for Russian. Message-level `lang:` is a separate setting.
68
+ Put error handling around calls in your application:
92
69
 
93
70
  ```ruby
94
71
  begin
95
72
  client.get_lists
96
- rescue Unisender::APIError => error
97
- warn error.message
98
- # Fix credentials, permissions, or parameters using error.code.
99
- rescue Unisender::HTTPError => error
100
- warn error.message
101
- # Check error.response.status and error.response.headers.
102
- rescue Unisender::TransportError, Unisender::InvalidResponseError => error
103
- warn error.message
104
- # Check service availability before deciding whether to repeat the operation.
73
+ rescue Unisender::Error => error
74
+ warn error.diagnostics.inspect
105
75
  end
106
76
  ```
107
77
 
108
- `APIError` also covers API failures returned with HTTP 200; an accompanying result is discarded. `HTTPError` covers non-2xx statuses, including redirects. `InvalidResponseError` covers malformed successful responses. `TimeoutError` is a `TransportError`. All derive from `Unisender::Error`.
109
-
110
- Error messages identify known operations and HTTP statuses or documented API codes. Unknown wire method names and unrecognized API codes are not copied into messages. `error.operation` is available separately. Credentials, request bodies, and contact data are omitted. Explicit accessors such as `error.details`, `error.response.data`, and `response.data` can contain private information. No request logging is enabled.
111
-
112
- A successful envelope does not mean every recipient succeeded. Inspect `send_email(error_checking: 1, ...)` recipient errors and import counters. A `BatchError` provides `completed_responses`, the zero-based, end-exclusive `failed_range`, and the original `error`. Accepted changes remain applied; do not repeat the whole input automatically. The [batch example](examples/import.rb) shows how to inspect partial results.
113
-
114
- ## Timeouts, retries, and pagination
115
-
116
- ```ruby
117
- client = Unisender::Client.new(
118
- api_key: ENV.fetch("UNISENDER_API_KEY"),
119
- transport: Unisender::Transport.new(open_timeout: 5, read_timeout: 60, write_timeout: 30),
120
- retry_policy: Unisender::RetryPolicy.new(max_retries: 2, max_elapsed: 60)
121
- )
122
- ```
123
-
124
- Those are the default transport timeouts in seconds. TLS certificates are verified, redirects are rejected, and calls do not share connections. **Retries default to zero.** The opt-in policy retries only explicitly identified read-only operations, at most twice, with backoff, jitter, and `Retry-After`. Sends, mutations, imports, exports, events, and unknown methods are never retried automatically. The elapsed retry budget is not a hard request deadline; in-flight requests use transport timeouts.
125
-
126
- `each_page` and batch helpers return lazy Enumerators: **consume them to make requests**. Stopping enumeration prevents subsequent requests. `each_page` yields responses, accepts a supported Ruby method name, and raises `PaginationError` at `max_pages`. `wait_for_task` polls an existing job; its deadline is checked between requests. See [helper limits and recovery behavior](docs/api.md#convenience-operations) and [upstream rate limits](https://www.unisender.com/ru/support/api/common/api-limits/).
127
-
128
- ## Low-level access and types
78
+ `diagnostics` omits credentials and payloads. Local input errors raise `Unisender::ValidationError`, an `ArgumentError` subclass.
79
+ A successful request can still reject recipients. Inspect `complete?`, `partial?`, and `uncertain?` on acceptance reports.
80
+ Retries default to zero. Sends and mutations are never automatically retried.
129
81
 
130
- `client.request("getLists", params: {})` accepts exact upstream names. It handles authentication, encoding, and failures, but does not perform operation-specific conversions. Comma-separated fields must already be strings; nested arrays and hashes become bracketed form fields. Credentials and JSON format cannot be overridden.
82
+ ## Find your task
131
83
 
132
- RBS signatures ship with the gem. `Response[Result]` describes a named operation's result; the low-level method keeps the general JSON type. Signatures describe documented shapes, not runtime schema validation. Unknown fields remain accessible through `response.data`. Consumer examples are checked with Steep alongside library code.
84
+ | Task | Guide or example |
85
+ | --- | --- |
86
+ | Choose an operation or find a keyword | [API reference](docs/api.md) |
87
+ | Read results, configure clients, handle errors | [Usage guide](docs/usage.md) |
88
+ | Create, inspect, test, and schedule a campaign | [Campaign lifecycle](examples/campaign_lifecycle.rb) |
89
+ | Recover an import from durable checkpoints | [Import recovery](docs/import-recovery.md) |
90
+ | Export and download contacts | [Export example](examples/export.rb) |
91
+ | Record events | [Event example](examples/events.rb) |
92
+ | Test an application or configure Cursor | [Testing and editor setup](docs/testing.md) |
93
+ | Integrate Rails jobs and delivery webhooks | [Rails guide](docs/rails.md) |
94
+ | Control pacing, cancellation, and resource ownership | [Execution guide](docs/execution.md) |
95
+ | Upgrade from 0.1 | [Migration guide](docs/migration-0.2.md) |
133
96
 
134
- ## Project information
97
+ ## Help and project status
135
98
 
136
- - [Contributing, local checks, and package verification](CONTRIBUTING.md)
137
- - [Changes and compatibility policy](CHANGELOG.md)
138
- - [Source repository](https://github.com/svyatov/unisender-ruby) and [issue reports](https://github.com/svyatov/unisender-ruby/issues)
99
+ Leonid Svyatov maintains this project. Use the public [issue tracker](https://github.com/svyatov/unisender-ruby/issues) for questions and defects.
100
+ Report vulnerabilities privately through [SECURITY.md](SECURITY.md).
139
101
 
140
- The repository is currently private; its GitHub links require access. Tests use fake credentials and deterministic responses. They do not establish compatibility with a live account. Released under the [MIT License](LICENSE.txt).
102
+ See [contributing](CONTRIBUTING.md), [changes](CHANGELOG.md), [release verification](docs/releases.md), and the [MIT license](LICENSE.txt).
103
+ Tests use fake credentials and deterministic responses. Live account compatibility requires separate [staging checks](docs/live-contracts.md).
data/SECURITY.md ADDED
@@ -0,0 +1,13 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes target the latest published release. Upgrade to that release before reporting a problem when possible.
6
+
7
+ ## Report a vulnerability
8
+
9
+ Email [Leonid Svyatov](mailto:leonid@svyatov.com) privately. Expect an initial response within seven days.
10
+
11
+ Include the affected gem and Ruby versions, the security impact, and a minimal reproduction using fake credentials and contact data. Do not include live API keys, recipient data, or account exports. Do not disclose the vulnerability in a public issue before a fix or coordinated disclosure date is agreed.
12
+
13
+ The maintainer will assess the report and coordinate a fix and disclosure with you.