Skip to content

Latest commit

 

History

History
218 lines (169 loc) · 10.6 KB

MIGRATING.md

File metadata and controls

218 lines (169 loc) · 10.6 KB

Migrating

Breaking changes to the SDKs' public surface, by release, with what to change. Wire behavior is not versioned here; the conformance fixtures under conformance/tests/ hold that.

Rust, TypeScript, Kotlin and Swift: the first release after 0.34.0

A workflow stage is read as JSON

HEY now answers GET /workflows/{workflowId}/stages/{stageId}.json, so GetWorkflowStage is a JSON operation and the SDKs no longer read the stage's web page. The model's WorkflowStageDetail is the stage, and each of its topics a StagedTopic: the topic as GetTopic answers it, with staging_id, emails_count and notes_count.

The hand-written stage read keeps its signature and its answer everywhere: Workflows::stage (Rust), client.workflows.stage (Kotlin, Swift) and Go's WorkflowsService.GetStage still answer a WorkflowStageView of WorkflowStageTopics, entry_count still the thread's email count. What changes:

  • The generated get_stage / getStage / getWorkflowStage answers the stage rather than the page: WorkflowStageDetail in Rust (GetWorkflowStageResponseContent), Kotlin and Swift, and the stage object as data in TypeScript, where it was a string. GetWorkflowStageOutputPayload is gone.
  • WorkflowStageView::parse(html, stage_id) (Rust), WorkflowStageView.parse(html, stageId) (Kotlin) and WorkflowStageView.parse(_:stageId:) (Swift) are gone, with the HTML parsers behind them. Convert what get_stage answers instead:
// before
let view = WorkflowStageView::parse(&page, stage_id)?;

// after
let view = WorkflowStageView::from(client.workflows().get_stage(workflow_id, stage_id).await?);

Kotlin's is WorkflowStageView.from(stage), Swift's WorkflowStageView(stage).

  • A stage HEY does not have is its 404, not_found as before (it was also what a page without the stage read as).
  • The Rust crate no longer depends on scraper or ego-tree, nor Go's module directly on golang.org/x/net.

Go, TypeScript and Rust: the first release after 0.33.1

A client with no credentials is refused when it is built

HEY's OAuth token endpoint is now a generated operation (CreateToken, RevokeToken) that goes out unsigned. A client made only to call it holds no credentials and says so with NoCredentials. A client given nothing at all is a forgotten token, so it is now refused when it is built rather than failing at its first send:

  • Go. hey.NewClient panics when given neither a token provider nor an auth strategy. Such a client already crashed on a nil pointer at its first send. Build a credential-less client with hey.WithAuthStrategy(hey.NoCredentials{}).
  • TypeScript. new HeyClient(...) throws a usage HeyError when token is neither a string nor a TokenProvider. Before, it failed with a TypeError at the first send. Pass token: NoCredentials.

On a NoCredentials client, every signed operation fails auth_required before anything is sent.

Rust: Route gains unsigned and no_redirect

A Route written literally for Client::operation adds both fields:

// before
Route { id, service, method, /* … */ lenient_success: false, pagination, retry }

// after
Route { id, service, method, /* … */ lenient_success: false, unsigned: false, no_redirect: false, pagination, retry }

false for both keeps the route as it was: signed, and following redirects as before.

Rust: the first release after 0.32.1

CachedResponse keeps the headers that came with the body

A 304 need not repeat the Link and X-Total-Count of the 200 it confirms, so a cache entry now holds the headers it was answered with, and a 304 is answered under them, updated by whatever the 304 carried (RFC 9111 §4.3.4). CachedResponse gains a headers: HeaderMap field. A ResponseCache of your own that builds one literally adds the field, and keeps the headers it is handed in set to give them back from get:

// before
CachedResponse { etag, body }

// after
CachedResponse { etag, body, headers }

An entry whose headers a cache cannot give back can use HeaderMap::new(); a 304 is then answered under its own headers alone, as before. ResponseCache also gains revalidate, which a 304 calls to keep its validator and headers; its default reads the entry and sets it again, so a cache of your own needs no change for it. Override it to skip writing the body again, or to make the check and update a single step (the default can put back an older but still consistent entry over one stored meanwhile, which the next read refetches). FileCache writes them to responses/<key>.headers, the file the Go SDK reads, and takes an entry written without one for a miss, so each page is fetched in full once after the upgrade.

Every SDK: the first release after 0.31.1

Delivering a message answers what HEY delivered

The calls that deliver a message used to answer nothing; they now answer a SentMessage, HEY's answer for the delivery: the entry that went out (id), the thread it is on (topic_id), its subject, and whether Undo Send is holding it back (delayed, with notice, undo_action and undo_timeout while it is). A reply that breaks out into a thread of its own on a Domains account names the new thread, not the one replied to.

SDK calls was now
Go Messages().Create, Messages().Send, Messages().SendDraft, Entries().CreateReply error (*generated.SentMessage, error)
Rust messages().send, messages().send_draft, entries().reply Result<(), Error> Result<SentMessage, Error>
Kotlin messages.send, messages.sendDraft, entries.reply Unit SentMessage
Swift messages.send, messages.sendDraft, entries.reply Void SentMessage (@discardableResult)

In Go, a caller that only checked the error takes the answer too:

// before
if err := client.Messages().Create(ctx, subject, body, to, nil, nil); err != nil {

// after
sent, err := client.Messages().Create(ctx, subject, body, to, nil, nil)
if err != nil {

Kotlin and Swift callers that ignore the answer need no change; Rust callers that ? the call need none either.

Every member is optional, because a HEY that predates the ids answers {} — or only the undo members while the delivery is delayed. Check for a zero or absent id and topic_id before using one. The conveniences above read delayed from undo_action when HEY does not say. TypeScript has no delivery conveniences: its generated operations answer the SentMessage exactly as HEY served it, so there delayed is absent wherever HEY leaves it out.

The generated operations change with them: CreateMessage, UpdateMessage and CreateReply now decode their 200 answer as SentMessage (CreateMessageResponseContent, UpdateMessageResponseContent, CreateReplyResponseContent) in every SDK. They are marked @heyLenientSuccess (ADR-005): a 2xx whose body is empty, cannot be read or does not decode is an empty result rather than an error, since the message has gone out — Go's Parse*Response leaves JSON200 nil, TypeScript's data is undefined, and Rust, Kotlin and Swift answer what {} decodes to. A refused delivery is still an error. Saving a draft through them answers 204 with no body, which reads as that empty result; the draft's id is in the Location header. In Go, Rust, Kotlin and Swift, save a draft through the draft conveniences (CreateDraft, UpdateDraft, CreateReplyDraft and their equivalents), which read that header. TypeScript has no draft conveniences: read the draft's id from the response's Location header.

Route gains lenient_success in Rust (a struct literal needs it), and lenientSuccess in Kotlin and Swift (defaulted to false).

Rust: the first release after 0.30.0

Response-side types and open enums are #[non_exhaustive]

Every type the crate decodes and hands back, and every enum whose set of values is HEY's to grow, is now #[non_exhaustive]. The policy — which side a type is on and what a change to it costs — is in rust/hey-sdk/README.md; this is what it changes for code outside the crate.

Building one literally no longer compiles, ..Default::default() included. This reaches tests and fixtures more than applications. The generated types and most hand-written results keep Default, so build one and set what the test needs:

// before
let mailbox = Mailbox { name: "Imbox".into(), ..Default::default() };

// after
let mut mailbox = Mailbox::default();
mailbox.name = "Imbox".into();

A few carry no Default, and a fixture for one of those comes from where the crate itself gets it: Token, DeletedCalendar and DeletedRecording deserialize from JSON; client::Response, FormResponse, RequestInfo and RequestResult come out of a call against a mock server (the crate's own tests use wiremock for this); url::Match comes from Router::recognize.

A match over an open enum needs a wildcard arm, and a struct pattern needs ..:

match error.code() {
    ErrorCode::NotFound => …,
    ErrorCode::RateLimit => …,
    _ => …,
}

Reading fields, calling methods, Clone, PartialEq and serde are untouched.

The types affected:

  • Generated: every schema the model reads back and nothing the model sends. Request bodies (*RequestContent) and everything they mention (MessagePayload, ContactPayload, StickyPayload, …) stay literal; the generator decides by reachability from the request bodies, not by name.
  • Hand-written results: WorkflowSummary, CalendarChanges, RecordingChanges, DeletedRecording, DeletedCalendar, PostingChanges, SearchResults, ContactConflict, ListedCalendar, CalendarList, services::extenzions::Extenzion, url::Match.
  • Runtime: client::Response, FormResponse, oauth::Token, and what the hooks see — RequestInfo and RequestResult. OperationInfo stays literal: Operation::info takes one from a caller describing its own form-backed call. Route, RouteParam and Retry stay literal too: a caller may write a route for a path the model lacks and hand it to Client::operation.
  • Open enums: ErrorCode, route::Pagination, route::ParamKind, BoxKind, StickySize, BubbleUpSlot, RepeatFrequency, CountdownUnit, TimeFormat.

Not affected, on purpose: Config, the resilience configs, ExchangeRequest, RefreshRequest, ServerMetadata, Pkce, CachedResponse, OperationInfo, Route, RouteParam, Retry, the *Params structs, the *Cursor structs, and the closed enums ClearanceStatus, OccurrenceScope, RepeatUntil and ParamRole. A field added to one of those is a 0.MINOR release.