feat: purpose, folders, preparation status and idempotent sends (TPL-2543, TPL-2539) - #10
Merged
Merged
Conversation
voj-tech-j
force-pushed
the
feat/tpl-2543-2539-sdk-parity
branch
from
September 11, 2026 08:20
ea6d965 to
3ad97ff
Compare
voj-tech-j
force-pushed
the
feat/tpl-2543-2539-sdk-parity
branch
from
September 11, 2026 08:46
3ad97ff to
52e2a1d
Compare
…2543, TPL-2539) Brings this client level with lettr-php 2.7.0. Everything is additive. `client.folders.list()` is the one that unblocks the rest: nothing in the SDK ever returned a folder id, so `with_folder_id` could only be used by hardcoding an integer read out of an app URL. Read-only, because deleting a folder moves or deletes the templates inside it. `TemplatePurpose` and `TemplatePreparationStatus` follow the shape `CampaignStatus` already set here - an `Unknown(String)` variant so a value added server-side deserializes rather than failing - with a `#[serde(default = ...)]` on each field so an absent one reads as the pre-existing behaviour. `Ready` is the default rather than `Pending` because on an API that predates the field every template with HTML was simply usable, and `Pending` would look like a stalled queue. `is_settled()` rather than `is_ready()`: after an update the previous render stays live, so a pending template is still sendable while serving the old content, and a name like `is_ready()` invites wiring it into a send guard that blocks legitimate sends. The idempotency key lives on `CreateEmailOptions` rather than as a `send` argument, so `send` and `send_with_quota` both pick it up and neither signature changed - adding a parameter would have broken every caller to serve a minority of calls. It is `#[serde(skip)]`, so the request body stays byte-identical. The key is caller-supplied: the SDK does not retry, so the retry belongs to the caller, and a key minted inside `send` would differ on every attempt and protect nothing. A malformed key returns `Error::Validation` before any request goes out. The two 409s get separate predicates because one is safe to retry with the same key and the other fails forever. `Retry-After` is read in `Config::send` before `text()` consumes the response, and travels on `ApiError` so the retryable one says how long to wait. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016uJ8Gsfq5iEPsJv6GxHUgU
voj-tech-j
force-pushed
the
feat/tpl-2543-2539-sdk-parity
branch
from
September 11, 2026 09:04
52e2a1d to
72aec67
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release 1.5.0. Last of the five in TPL-2543/TPL-2539, level with lettr-php 2.7.0, node, python, go and java. Everything is additive.
client.folders.list()The one that unblocks the rest.
CreateTemplateOptions::with_folder_idhas existed for ages, but nothing in the SDK ever returned a folder id.Read-only: deleting a folder moves or deletes the templates inside it.
TemplatePurposeandTemplatePreparationStatusBoth follow the shape
CampaignStatusalready set in this crate — anUnknown(String)variant so a value added server-side deserializes rather than failing — with#[serde(default = ...)]on each field so an absent one reads as the pre-existing behaviour.Readyis the default, notPending— on an API deployment that predates the field every template with HTML was simply usable, andPendingwould look like a stalled queue and hang anything waiting for readiness.is_settled(), notis_ready()— deliberately. It answers "is what I sent what will go out", which is not "can I send this": after an update the previous render stays in place, so a pending template is still sendable while serving the old content.ListTemplatesOptions::folder_idOne
per_page(100)call reconciles a whole bulk import instead of a detail call per template, each dragging the full HTML payload against the same rate limit. A folder outside the resolved project is a 404, not an empty list.Idempotent sends
The key lives on
CreateEmailOptions, not as asendargument. That waysendandsend_with_quotaboth pick it up and neither signature changed — Rust has no optional parameters, so adding one would have broken every caller to serve a minority of calls, and a thirdsend_with_idempotency_keywould have made it combinatorial with the quota variant. It is#[serde(skip)], so the request body stays byte-identical, and there's a test asserting exactly that.You choose the key; the SDK never generates one. It only works if both attempts use the same value, and the SDK does not retry — one
sendis one HTTP request — so the retry is yours. A malformed key returnsError::Validationbefore any request goes out;is_valid_idempotency_keyis public for callers deriving keys from their own ids.Error::is_idempotency_in_progress()(retry with the same key afterError::retry_after()seconds) andError::is_idempotency_conflict()(that key was used with a different payload — retrying fails forever).Retry-Afteris read inConfig::sendbeforetext()consumes the response.Checks
cargo test— all 17 suites pass, including doctests.cargo clippy --all-targets -- -D warningsandcargo fmt --checkclean.New:
tests/folders_test.rs(shape, every filter, the pre-field default),tests/preparation_status_test.rs(all three statuses in one list call, both filters, the pre-field default,is_settledsemantics),tests/idempotency_test.rs(key format, and that setting a key leaves the serialized body byte-identical).Cargo.tomlbumped to 1.5.0 and a CHANGELOG entry added. Not tagged.🤖 Generated with Claude Code
https://claude.ai/code/session_016uJ8Gsfq5iEPsJv6GxHUgU