diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0575fd0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,101 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2026-06-16 + +First release of the dexpace Go SDK: a transport-agnostic HTTP-client toolkit +built on `net/http`, with zero third-party runtime dependencies. Requests and +responses are standard `*http.Request` / `*http.Response` values, and the +transport seam is satisfied by `*http.Client`. + +### Added + +#### Pipeline and client + +- A composable policy pipeline (`pipeline`) that runs an ordered chain of + policies over an `*http.Request` and terminates in a transport. Policies can + inspect or mutate a request, continue the chain, or short-circuit, and can + replay the request body across attempts. +- A default `net/http`-backed transport (`transport`) that terminates a + pipeline, cloned from `http.DefaultTransport` with larger idle-connection + limits. +- An umbrella `Client` configured through functional options, wiring the default + policy stack with sensible ordering. + +#### Resilience + +- Retry policy (`retry`) with exponential backoff and full jitter, support for + the `Retry-After` header, and automatic request-body rewind so retried + requests resend their payload. +- Idempotency-key stamping (`idempotency`) that adds an `Idempotency-Key` header + to POST requests by default, and can be disabled per client. + +#### Authentication + +All credential policies require an HTTPS transport and refuse to attach +credentials over plaintext. + +- Bearer-token authentication (`auth`) driven by a pluggable `TokenCredential`, + with a token cache that can be shared across clients. +- HTTP Basic authentication. +- API-key authentication via a configurable header. +- HTTP Digest authentication (RFC 7616, MD5/SHA-256, `qop=auth`). + +#### Errors, logging, and observability + +- An opt-in typed error model (`httperr`): a `ResponseError` for non-success + responses (buffering and rewinding the response body) and a `TransportError` + for transport failures. +- Structured request/response logging (`logging`) via `log/slog`. +- Vendor-neutral tracing and metrics SPIs (`instrumentation`) with no-op + defaults, plus tracing and metrics policies. The tracing policy emits a span + per request and injects a W3C `traceparent` header; the metrics policy records + request duration and in-flight requests. +- Default-deny URL redaction (`redact`) shared by logs, traces, and errors: + userinfo is stripped and query values are redacted unless explicitly + allowlisted. + +#### Value types and helpers + +- Immutable media-type value (`mediatype`) with parsing and common constants. +- Canonical HTTP header-name constants (`header`). +- Conditional- and range-request value types (`conditions`): ETag, Range, and + Conditions. +- A serialization seam (`serde`) with a JSON default and a `Tristate` type for + distinguishing absent, null, and present fields in PATCH payloads. +- A layered settings resolver (`config`) that sources values from explicit + overrides, then `DEXPACE_*` environment variables, then defaults. + +#### Streaming and bodies + +- Server-Sent Events (`sse`): a WHATWG-compliant `text/event-stream` parser, a + reconnecting stream that replays `Last-Event-ID` after an interruption, and + `Client.EventStream` to run a stream through the pipeline. +- JSON Lines / NDJSON streaming decoder (`jsonl`) exposed as a generic + `iter.Seq2`. +- Multipart `multipart/form-data` request-body builder (`formdata`) with + replayable bodies and file uploads. +- Generic pagination (`pagination`) as `iter.Seq2` range-over-func iterators, + with cursor/token, page-number, and RFC 8288 Link-header strategies and a + page cap. + +#### Webhooks + +- Inbound webhook signature verification (`webhook`): constant-time HMAC-SHA256 + comparison with a configurable timestamp-tolerance window for replay + protection. + +### Requirements + +- Go 1.26 or newer. +- Zero third-party runtime dependencies; only the standard library is imported + by non-test code. + +[Unreleased]: https://github.com/dexpace/go-sdk/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/dexpace/go-sdk/releases/tag/v0.1.0 diff --git a/README.md b/README.md index f70469f..9c06645 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,8 @@ The SDK ships **zero third-party runtime dependencies**; only the standard library is imported by non-test code. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for conventions and [`CLAUDE.md`](./CLAUDE.md) for the enforced rules. +See [CHANGELOG.md](./CHANGELOG.md) for release notes. + ## License MIT — see [LICENSE](./LICENSE). diff --git a/jsonl/example_test.go b/jsonl/example_test.go new file mode 100644 index 0000000..022535b --- /dev/null +++ b/jsonl/example_test.go @@ -0,0 +1,30 @@ +// Copyright (c) 2026 dexpace and Omar Aljarrah. +// Licensed under the MIT License. See LICENSE in the repository root for details. + +package jsonl_test + +import ( + "fmt" + "strings" + + "github.com/dexpace/go-sdk/jsonl" +) + +func ExampleDecode() { + type point struct { + N int `json:"n"` + } + + stream := strings.NewReader("{\"n\":1}\n{\"n\":2}\n") + + for p, err := range jsonl.Decode[point](stream) { + if err != nil { + fmt.Println("error:", err) + return + } + fmt.Println(p.N) + } + // Output: + // 1 + // 2 +} diff --git a/pagination/example_test.go b/pagination/example_test.go new file mode 100644 index 0000000..0e1644f --- /dev/null +++ b/pagination/example_test.go @@ -0,0 +1,39 @@ +// Copyright (c) 2026 dexpace and Omar Aljarrah. +// Licensed under the MIT License. See LICENSE in the repository root for details. + +package pagination_test + +import ( + "context" + "fmt" + + "github.com/dexpace/go-sdk/pagination" +) + +func ExamplePager_Items() { + // fetch returns two fixed in-memory pages. The first page points to the + // second via its NextToken; the second has no NextToken, ending iteration. + fetch := func(_ context.Context, token string) (pagination.Page[string], error) { + switch token { + case "": + return pagination.Page[string]{Items: []string{"a", "b"}, NextToken: "page-2"}, nil + default: + return pagination.Page[string]{Items: []string{"c"}}, nil + } + } + + pager := pagination.New(fetch) + + var items []string + for item, err := range pager.Items(context.Background()) { + if err != nil { + fmt.Println("error:", err) + return + } + items = append(items, item) + } + + fmt.Println(items) + // Output: + // [a b c] +} diff --git a/sse/example_test.go b/sse/example_test.go new file mode 100644 index 0000000..5c45707 --- /dev/null +++ b/sse/example_test.go @@ -0,0 +1,26 @@ +// Copyright (c) 2026 dexpace and Omar Aljarrah. +// Licensed under the MIT License. See LICENSE in the repository root for details. + +package sse_test + +import ( + "fmt" + "strings" + + "github.com/dexpace/go-sdk/sse" +) + +func ExampleParse() { + stream := strings.NewReader("data: hello\n\ndata: world\n\n") + + for event, err := range sse.Parse(stream) { + if err != nil { + fmt.Println("error:", err) + return + } + fmt.Println(event.Data) + } + // Output: + // hello + // world +} diff --git a/webhook/example_test.go b/webhook/example_test.go new file mode 100644 index 0000000..2e50487 --- /dev/null +++ b/webhook/example_test.go @@ -0,0 +1,26 @@ +// Copyright (c) 2026 dexpace and Omar Aljarrah. +// Licensed under the MIT License. See LICENSE in the repository root for details. + +package webhook_test + +import ( + "fmt" + + "github.com/dexpace/go-sdk/webhook" +) + +func ExampleVerifier_Verify() { + secret := []byte("shared-secret") + payload := []byte(`{"event":"ping"}`) + + // The sender computes the signature over the payload with the shared secret. + signature := webhook.Sign(secret, payload) + + // The receiver verifies the payload against the signature. + verifier := webhook.NewVerifier(secret) + err := verifier.Verify(payload, signature) + + fmt.Println(err == nil) + // Output: + // true +}