diff --git a/Cargo.lock b/Cargo.lock index f734114..1a47b00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1251,7 +1251,7 @@ dependencies = [ [[package]] name = "specforge-cli" -version = "0.8.0" +version = "1.6.1" dependencies = [ "anyhow", "clap", @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "specforge-core" -version = "0.8.0" +version = "1.6.1" dependencies = [ "anyhow", "indexmap", @@ -1286,7 +1286,7 @@ dependencies = [ [[package]] name = "specforge-go" -version = "0.8.0" +version = "1.6.1" dependencies = [ "rayon", "serde_json", @@ -1298,7 +1298,7 @@ dependencies = [ [[package]] name = "specforge-plugin" -version = "0.8.0" +version = "1.6.1" dependencies = [ "serde", "serde_json", @@ -1314,7 +1314,7 @@ dependencies = [ [[package]] name = "specforge-rust" -version = "0.8.0" +version = "1.6.1" dependencies = [ "rayon", "serde_json", @@ -1326,7 +1326,7 @@ dependencies = [ [[package]] name = "specforge-ts" -version = "0.8.0" +version = "1.6.1" dependencies = [ "indexmap", "rayon", @@ -1339,7 +1339,7 @@ dependencies = [ [[package]] name = "specforge-wasm" -version = "0.8.0" +version = "1.6.1" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index de3b9c3..fc9de6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ ] [workspace.package] -version = "0.8.0" +version = "1.6.1" edition = "2021" rust-version = "1.75" license = "MIT" @@ -37,11 +37,11 @@ clap = { version = "4", features = ["derive"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Internal crates -specforge-core = { path = "crates/specforge-core", version = "0.8.0" } -specforge-ts = { path = "crates/specforge-ts", version = "0.8.0" } -specforge-go = { path = "crates/specforge-go", version = "0.8.0" } -specforge-rust = { path = "crates/specforge-rust", version = "0.8.0" } -specforge-plugin = { path = "crates/specforge-plugin", version = "0.8.0" } +specforge-core = { path = "crates/specforge-core", version = "1.6.1" } +specforge-ts = { path = "crates/specforge-ts", version = "1.6.1" } +specforge-go = { path = "crates/specforge-go", version = "1.6.1" } +specforge-rust = { path = "crates/specforge-rust", version = "1.6.1" } +specforge-plugin = { path = "crates/specforge-plugin", version = "1.6.1" } tempfile = "3" # Benchmarks criterion = { version = "0.5", features = ["html_reports"] } diff --git a/README.md b/README.md index 216fda9..3f1eb8e 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Every generated SDK is a **complete, production-ready client** — not just type | **Idempotency keys** | Auto-generated `Idempotency-Key` on unsafe methods | Safe retries on POST/PUT/PATCH/DELETE without duplicate side effects | | **SSE streaming** | Server-Sent Event parser with proper error handling | Real-time data streams without manual parsing | | **Runtime validation** | Validate request/response bodies against the spec | Catch API contract violations in dev and tests, not production | -| **oneOf type guards** | `isPetCreated()`, `narrowPetEvent()` (TS); `is_pet_created()`, `discriminant()` (Rust) | Safely narrow union types at runtime | +| **oneOf type guards** | `isPetEventPetCreated()`, `narrowPetEvent()` (TS); `is_pet_created()`, `discriminant()` (Rust) | Safely narrow union types at runtime (TS guards are union-scoped to avoid collisions when two unions share an arm) | | **Response caching** | ETag-based caching with TTL expiry and 304 handling | Avoid re-fetching unchanged data | | **Rate limiting** | Token bucket and sliding window rate limiters | Prevent overwhelming APIs | | **Logging** | Pluggable `Logger` interface (ConsoleLogger, NoopLogger) | Structured request/response logging | @@ -787,10 +787,10 @@ Streaming calls **do not retry** (bodies are not replayable). ### oneOf narrowing (TypeScript) ```ts -import { isPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent"; +import { isPetEventPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent"; function handle(event: PetEvent) { - if (isPetCreated(event)) { + if (isPetEventPetCreated(event)) { console.log(event.pet.name); // narrowed return; } @@ -801,6 +801,8 @@ function handle(event: PetEvent) { } ``` +> TS guard names are union-scoped (`is{Union}{Arm}`) so two unions sharing an arm type don't collide. `narrow{Union}()` is the ergonomic entry point. + --- ## Examples diff --git a/crates/specforge-cli/tests/regression.rs b/crates/specforge-cli/tests/regression.rs index 015e14d..d9f6042 100644 --- a/crates/specforge-cli/tests/regression.rs +++ b/crates/specforge-cli/tests/regression.rs @@ -278,6 +278,16 @@ fn stripe_generates_and_compiles_rust() { generate_and_compile_rust(&FIXTURES[2], "stripe_sdk"); } +#[test] +fn github_generates_and_typechecks_ts() { + generate_and_compile_ts(&FIXTURES[1], "@test/github-ts-sdk"); +} + +#[test] +fn stripe_generates_and_typechecks_ts() { + generate_and_compile_ts(&FIXTURES[2], "@test/stripe-ts-sdk"); +} + fn generate_and_compile_go(f: &Fixture, module: &str) { let path = match spec_path(f) { Ok(p) => p, @@ -373,6 +383,55 @@ fn generate_and_compile_rust(f: &Fixture, crate_name: &str) { } } +fn generate_and_compile_ts(f: &Fixture, package_name: &str) { + let path = match spec_path(f) { + Ok(p) => p, + Err(e) => { + eprintln!("skip {} ts gate: {e}", f.name); + return; + } + }; + let spec = match specforge_core::parse_file(&path) { + Ok(s) => s, + Err(e) => panic!("{} ts: parse failed: {e}", f.name), + }; + let doc = match specforge_core::resolve(&spec) { + Ok(d) => d, + Err(e) => panic!("{} ts: resolve failed: {e}", f.name), + }; + + let out = std::env::temp_dir().join(format!("specforge-gate-{}-ts", f.name)); + let _ = std::fs::remove_dir_all(&out); + let opts = specforge_ts::GeneratorOptions { + out_dir: out.clone(), + package_name: Some(package_name.into()), + i18n: None, + }; + let written = specforge_ts::generate(&doc, &opts) + .unwrap_or_else(|e| panic!("{} ts: emit failed: {e}", f.name)); + assert!( + written.iter().any(|p| p == "package.json"), + "{} ts: missing package.json", + f.name + ); + assert!( + written.len() >= 5, + "{} ts: expected several files, got {}", + f.name, + written.len() + ); + + match assert_tsc(&out) { + Ok(()) => eprintln!( + "{} ts: generate + tsc --noEmit OK ({} files)", + f.name, + written.len() + ), + Err(e) if e.contains("tsc not available") => eprintln!("skip {} ts check: {e}", f.name), + Err(e) => panic!("{} ts typecheck failed: {e}", f.name), + } +} + /// Run `go build ./...` in `dir`. Returns Err("go not found") when the toolchain /// is missing so callers can skip; any other failure is a real compile error. fn assert_go_build(dir: &Path) -> Result<(), String> { @@ -435,6 +494,54 @@ fn assert_cargo_check(dir: &Path) -> Result<(), String> { } } +/// Run `npm install` then `tsc --noEmit` in `dir`. Returns Err("tsc not +/// available") when node/npm are missing so callers can skip; any other failure +/// is a real type error. +fn assert_tsc(dir: &Path) -> Result<(), String> { + // Resolve node/npm — skip gracefully if absent (TS gate is optional). + for tool in ["node", "npm"] { + let ok = std::process::Command::new(tool) + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + if !ok { + return Err(format!("{tool} not found — tsc not available")); + } + } + + // Install devDependencies (typescript) quietly. + let install = std::process::Command::new("npm") + .args(["install", "--no-audit", "--no-fund", "--loglevel=error"]) + .current_dir(dir) + .output() + .map_err(|e| format!("spawn npm install: {e}"))?; + if !install.status.success() { + return Err(format!( + "npm install failed (exit {:?}):\n{}", + install.status.code(), + String::from_utf8_lossy(&install.stderr) + )); + } + + // Typecheck. The generated package.json has a `typecheck` script + // (`tsc --noEmit`); invoke it directly so we exercise the shipped config. + let tsc = std::process::Command::new("npm") + .args(["run", "--silent", "typecheck"]) + .current_dir(dir) + .output() + .map_err(|e| format!("spawn tsc: {e}"))?; + if tsc.status.success() { + Ok(()) + } else { + Err(format!( + "tsc --noEmit failed (exit {:?}):\n{}", + tsc.status.code(), + String::from_utf8_lossy(&tsc.stdout), + )) + } +} + /// Helper: collect the relative file paths produced by a generate run. fn generated_files(dir: &Path) -> BTreeSet { fn walk(dir: &Path, base: &Path, out: &mut BTreeSet) { diff --git a/crates/specforge-core/src/ir.rs b/crates/specforge-core/src/ir.rs index c6b05cb..ecd632f 100644 --- a/crates/specforge-core/src/ir.rs +++ b/crates/specforge-core/src/ir.rs @@ -27,6 +27,12 @@ pub struct EnumModel { pub name: String, pub description: Option, pub va #[derive(Debug, Clone, serde::Serialize)] pub struct EnumVariant { pub value: String, pub description: Option } +// The ObjectModel variant (~336 bytes) is much larger than EnumModel (~72), +// which trips clippy::large_enum_variant. Boxing ObjectModel would force a +// deref at all 70+ match sites across the emitters and core for no real win — +// Model values are not stack-hot (they live in the resolved Document, accessed +// by reference). Suppressing rather than paying the ergonomic cost everywhere. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, serde::Serialize)] pub enum Model { Object(ObjectModel), Enum(EnumModel) } impl Model { pub fn name(&self) -> &str { match self { Model::Object(o) => &o.name, Model::Enum(e) => &e.name } } } diff --git a/crates/specforge-core/src/merge.rs b/crates/specforge-core/src/merge.rs index 028fd32..aedac54 100644 --- a/crates/specforge-core/src/merge.rs +++ b/crates/specforge-core/src/merge.rs @@ -145,7 +145,7 @@ mod tests { "info": { "title": "A", "version": "1.0" }, "paths": { "/a": {} } }); - let result = merge_specs(&[spec.clone()]).unwrap(); + let result = merge_specs(std::slice::from_ref(&spec)).unwrap(); assert_eq!(result, spec); } diff --git a/crates/specforge-go/src/lib.rs b/crates/specforge-go/src/lib.rs index 273a7a6..768f90f 100644 --- a/crates/specforge-go/src/lib.rs +++ b/crates/specforge-go/src/lib.rs @@ -385,7 +385,7 @@ fn emit_enum(e: &EnumModel) -> String { out.push_str(&go_doc(d, "")); if d.to_lowercase().contains("deprecated") { if let Some(alt) = go_schema_deprecation_alternative(d) { - out.push_str(&format!("// Deprecated: Use {alt} instead.\n")); + out.push_str(&format!("// Deprecated: Use {} instead.\n", go_inline(&alt))); } else { out.push_str(&format!("// Deprecated: {name} is deprecated.\n")); } @@ -411,7 +411,7 @@ fn emit_object(o: &ObjectModel, doc: &Document) -> String { out.push_str(&go_doc(d, "")); if d.to_lowercase().contains("deprecated") { if let Some(alt) = go_schema_deprecation_alternative(d) { - out.push_str(&format!("// Deprecated: Use {alt} instead.\n")); + out.push_str(&format!("// Deprecated: Use {} instead.\n", go_inline(&alt))); } else { out.push_str(&format!("// Deprecated: {name} is deprecated.\n")); } @@ -2661,7 +2661,7 @@ fn emit_method(op: &Operation) -> String { if !error_responses.is_empty() { for r in &error_responses { if let Some(desc) = &r.description { - body.push_str(&format!("// Returns {} for {}.\n", desc, r.status)); + body.push_str(&format!("// Returns {} for {}.\n", go_inline(desc), r.status)); } else { body.push_str(&format!("// Returns an error for {}.\n", r.status)); } @@ -2670,7 +2670,7 @@ fn emit_method(op: &Operation) -> String { // Deprecation notice. if is_operation_deprecated(op) { if let Some(alt) = go_deprecation_alternative(op) { - body.push_str(&format!("// Deprecated: Use {alt} instead.\n")); + body.push_str(&format!("// Deprecated: Use {} instead.\n", go_inline(&alt))); } else { body.push_str(&format!("// Deprecated: {name} is deprecated.\n")); } @@ -3219,6 +3219,13 @@ fn go_doc_label(text: &str, label: &str) -> String { out } +/// Sanitize a spec-derived string for safe inlining into a single Go `//` +/// comment line: escape `*/` (closes block comments) and collapse embedded +/// newlines to spaces so the comment stays on one line. +fn go_inline(text: &str) -> String { + text.replace("*/", "*/").replace(['\n', '\r'], " ") +} + fn escape_go_string(s: &str) -> String { s.replace('\\', "\\\\").replace('"', "\\\"") } diff --git a/crates/specforge-ts/src/lib.rs b/crates/specforge-ts/src/lib.rs index c54179d..692e06d 100644 --- a/crates/specforge-ts/src/lib.rs +++ b/crates/specforge-ts/src/lib.rs @@ -65,7 +65,7 @@ pub fn generate(doc: &Document, opts: &GeneratorOptions) -> std::io::Result std::io::Res body.push_str("export * from \"./auth\";\n"); body.push_str("export * from \"./retry\";\n"); body.push_str("export * from \"./paginate\";\n"); + body.push_str("export * from \"./concurrency\";\n"); + body.push_str("export * from \"./dedup\";\n"); + body.push_str("export * from \"./idempotency\";\n"); + body.push_str("export * from \"./middleware\";\n"); + body.push_str("export * from \"./streaming\";\n"); + body.push_str("export * from \"./cache\";\n"); body.push_str("export * from \"./validate\";\n"); body.push_str("export * from \"./validation-middleware\";\n"); body.push_str("export * from \"./ratelimit\";\n"); @@ -286,7 +292,7 @@ fn collect_index(doc: &Document, out_dir: &Path, has_i18n: bool) -> std::io::Res // Models. for (_, model) in doc.schemas.iter() { - let name = name::pascal(model.name()); + let name = name::safe_model_name(&name::pascal(model.name())); body.push_str(&format!("export * from \"./models/{name}\";\n")); } // Webhooks. diff --git a/crates/specforge-ts/src/models.rs b/crates/specforge-ts/src/models.rs index 0e7363b..91bd74e 100644 --- a/crates/specforge-ts/src/models.rs +++ b/crates/specforge-ts/src/models.rs @@ -48,7 +48,7 @@ pub fn emit_model_file_with_registry(model: &Model, registry: Option<&SchemaRegi /// (filename_stem, body, referenced-model-names) fn emit_object(o: &ObjectModel, registry: Option<&SchemaRegistry>) -> (String, String, BTreeSet) { - let name = pascal(&o.name); + let name = crate::name::safe_model_name(&pascal(&o.name)); let mut refs = BTreeSet::new(); let mut body = String::new(); @@ -142,7 +142,12 @@ fn emit_union_guards( for arm in &arms { let arm_ty = pascal(arm); - let guard_name = format!("is{arm_ty}"); + // Guard names are union-scoped (`is{Union}{Arm}`) because top-level + // functions named purely after the arm type collide when two unions + // share an arm (e.g. Stripe's DeletedExternalAccount and + // DeletedPaymentSource both contain DeletedBankAccount). The + // `narrow{Union}` helper below is the ergonomic entry point. + let guard_name = format!("is{union_name}{arm_ty}"); let check = arm_type_check(arm, disc_prop, registry, comp.discriminator.as_ref()); out.push_str(&format!( "export function {guard_name}(value: {union_name}): value is {arm_ty} {{\n" @@ -166,7 +171,7 @@ fn emit_union_guards( for arm in &arms { let arm_ty = pascal(arm); out.push_str(&format!( - " if (is{arm_ty}(value)) return \"{arm_ty}\";\n" + " if (is{union_name}{arm_ty}(value)) return \"{arm_ty}\";\n" )); } out.push_str(" return undefined;\n"); @@ -306,7 +311,7 @@ fn escape_str(s: &str) -> String { } fn emit_enum(e: &EnumModel) -> (String, String, BTreeSet) { - let name = pascal(&e.name); + let name = crate::name::safe_model_name(&pascal(&e.name)); let mut body = String::new(); if let Some(desc) = &e.description { body.push_str(&format_doc_comment(desc, "")); @@ -475,7 +480,7 @@ mod tests { assert!(file.contains("import { PetCreated } from \"./PetCreated\";")); assert!(file.contains("import { PetUpdated } from \"./PetUpdated\";")); // Guards are emitted even without a registry (structural fallback). - assert!(file.contains("export function isPetCreated(value: PetEvent): value is PetCreated")); + assert!(file.contains("export function isPetEventPetCreated(value: PetEvent): value is PetCreated")); assert!(file.contains("export function narrowPetEvent(value: PetEvent)")); } @@ -552,8 +557,8 @@ mod tests { let file = emit_model_file_with_registry(&Model::Object(o), Some(®istry)); assert!(file.contains("=== \"pet.created\"")); assert!(file.contains("=== \"pet.updated\"")); - assert!(file.contains("export function isPetCreated")); - assert!(file.contains("export function isPetUpdated")); + assert!(file.contains("export function isPetEventPetCreated")); + assert!(file.contains("export function isPetEventPetUpdated")); assert!(file.contains("export function narrowPetEvent")); } @@ -631,8 +636,8 @@ mod tests { let file = emit_model_file_with_registry(&Model::Object(o), Some(®istry)); assert!(file.contains("=== \"dog\""), "should use mapping value 'dog'"); assert!(file.contains("=== \"cat\""), "should use mapping value 'cat'"); - assert!(file.contains("export function isDog")); - assert!(file.contains("export function isCat")); + assert!(file.contains("export function isPetDog")); + assert!(file.contains("export function isPetCat")); assert!(file.contains("export function narrowPet")); } diff --git a/crates/specforge-ts/src/name.rs b/crates/specforge-ts/src/name.rs index 71f9c82..a283433 100644 --- a/crates/specforge-ts/src/name.rs +++ b/crates/specforge-ts/src/name.rs @@ -36,6 +36,67 @@ pub fn pascal(input: &str) -> String { out } +/// Whether `name` matches a type the TypeScript SDK emitter hardcodes into the +/// generated package (ApiClient, ResponseCache, ValidationError, etc.). User +/// models with these names are suffixed with `Model` to avoid duplicate-export +/// / redeclaration errors. Only applied to generated model/enum names, not to +/// all pascal-case identifiers. +pub fn is_ts_sdk_builtin_type(name: &str) -> bool { + matches!( + name, + "ApiClient" + | "ApiClientOptions" + | "ApiError" + | "AuthProvider" + | "AnonymousAuthProvider" + | "BearerAuthProvider" + | "ApiKeyAuthProvider" + | "CacheEntry" + | "ConfigurationError" + | "ConsoleLogger" + | "CursorPage" + | "EndpointSchema" + | "HttpError" + | "Logger" + | "Metrics" + | "MetricsCollector" + | "Middleware" + | "MiddlewareRequest" + | "MiddlewareResponse" + | "NetworkError" + | "OffsetPage" + | "RateLimiter" + | "RequestInterceptor" + | "RequestDeduper" + | "RequestOptions" + | "ResponseCache" + | "ResponseInterceptor" + | "ResponseTransformer" + | "RetryOptions" + | "RouteSchemaMap" + | "SchemaType" + | "Semaphore" + | "ServerSentEvent" + | "ServiceContainer" + | "SlidingWindow" + | "TelemetryHooks" + | "TimeoutError" + | "TokenBucket" + | "ValidationError" + | "QueryRecord" + ) +} + +/// Wrap a generated model/enum name so it can't collide with a built-in SDK +/// type. Returns `name` unchanged when safe, or `name + "Model"` when it would. +pub fn safe_model_name(name: &str) -> String { + if is_ts_sdk_builtin_type(name) { + format!("{name}Model") + } else { + name.to_string() + } +} + /// Sanitize a name into a valid TS *value* identifier (camelCase). pub fn camel(input: &str) -> String { let pascal = pascal(input); diff --git a/docs-site/docs/intro.md b/docs-site/docs/intro.md index 9ffa349..43bea80 100644 --- a/docs-site/docs/intro.md +++ b/docs-site/docs/intro.md @@ -140,7 +140,7 @@ Every generated SDK is a **complete, production-ready client** — not just type | **Idempotency keys** | Auto-generated `Idempotency-Key` on unsafe methods | Safe retries on POST/PUT/PATCH/DELETE without duplicate side effects | | **SSE streaming** | Server-Sent Event parser with proper error handling | Real-time data streams without manual parsing | | **Runtime validation** | Validate request/response bodies against the spec | Catch API contract violations in dev and tests, not production | -| **oneOf type guards** | `isPetCreated()`, `narrowPetEvent()` (TS); `is_pet_created()`, `discriminant()` (Rust) | Safely narrow union types at runtime | +| **oneOf type guards** | `isPetEventPetCreated()`, `narrowPetEvent()` (TS); `is_pet_created()`, `discriminant()` (Rust) | Safely narrow union types at runtime (TS guards are union-scoped to avoid collisions when two unions share an arm) | ### Language-specific highlights @@ -707,10 +707,10 @@ Streaming calls **do not retry** (bodies are not replayable). ### oneOf narrowing (TypeScript) ```ts -import { isPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent"; +import { isPetEventPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent"; function handle(event: PetEvent) { - if (isPetCreated(event)) { + if (isPetEventPetCreated(event)) { console.log(event.pet.name); // narrowed return; } diff --git a/docs-site/docs/reference/changelog.md b/docs-site/docs/reference/changelog.md index 963efee..d0f78f3 100644 --- a/docs-site/docs/reference/changelog.md +++ b/docs-site/docs/reference/changelog.md @@ -4,327 +4,286 @@ sidebar_position: 1 description: Version history and changes --- - # 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). +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). + ## [1.6.1] — 2026-07-30 ### Fixed +#### Codegen (all emitters) +- **Doc-comment escaping** — schema/operation descriptions from real-world specs (GitHub, Stripe) contain embedded newlines, backticks, and `/*`/`*/` sequences that broke generated code: continuation lines lost their comment prefix (→ parse errors), `*/` closed block comments (→ "unterminated block comment"), and backticks tripped token errors. The Rust (`rust_doc`/`rust_doc_indented`) and Go (`go_doc`/`go_doc_label`) emitters now escape these and prefix every line. **Result: the 4 `*_generates_and_compiles_{rust,go}` regression tests for GitHub + Stripe specs now pass** (they were failing). +- **Reserved-name collisions** — a spec model whose name matches a built-in SDK type (e.g. GitHub's `ValidationError`) would redeclare it and break compilation. The Go and Rust emitters now suffix colliding model names with `Model`. + #### Rust emitter -- **Generated SDK now compiles** — fixed six template bugs (nested interceptor/transformer loops, missing `logger` field on `ClientBuilder`, telemetry call-site signature mismatches, wrong `Result` arity, `do_once` parameter-order mismatch, `serde_json::to_vec` move-instead-of-borrow) that broke every generated Rust SDK since the 1.6.0 interceptors/validation_middleware/ServiceContainer additions. The example SDK was stale and had never been regenerated to surface this. +- **Generated SDK now compiles** — fixed six template bugs that silently broke every generated Rust SDK since the interceptors/validation_middleware/ServiceContainer modules were added in 1.6.0: a response-interceptor loop nested inside the response-transformer loop; a missing `logger` field on `ClientBuilder`; telemetry call-site signature mismatches (`&Method`/`Duration` vs `&str`/`u128`); a two-arg `Result` used where the crate alias is one-arg; a `do_once` parameter-order mismatch; and `serde_json::to_vec` moving instead of borrowing. The example SDK was stale and never regenerated to catch this. +- **Generated README expanded** — `emit_readme` now documents interceptors, runtime validation (`validation_middleware`), and the `ServiceContainer` telemetry/DI helpers. #### TypeScript emitter -- **Telemetry wired in** — `ApiClient` now stores and invokes the previously-dead `TelemetryHooks` (`onRequestStart`/`onRequestEnd`/`onRequestError`/`onRetry`/`onCacheHit`/`onCacheMiss`). -- **Per-model validators typecheck** — object validators now pass the `errors` accumulator and return it, matching the enum-validator pattern. +- **Telemetry is now wired** — the `TelemetryHooks` interface and `MetricsCollector` were generated but never stored or invoked by `ApiClient`. The client now reads `opts.telemetry`/`container.telemetry` and calls `onRequestStart`/`onRequestEnd`/`onRequestError`/`onRetry`/`onCacheHit`/`onCacheMiss` at the right points in the request lifecycle. +- **Per-model validators typecheck** — generated object validators called `validateValue` with three arguments (missing the `errors` accumulator) and returned its `void` result as `ValidationError[]`. Now mirrors the enum-validator pattern. #### Testing -- **Compile-the-generated-SDK tests** added for the Rust (`cargo check`), TypeScript (`tsc --noEmit`), and Go (`go build ./...`) emitters — regenerates into a temp crate and builds it so template breakage fails CI. All three emitters verified against both `fixtures/` specs; each test skips cleanly only when its toolchain is genuinely absent. +- **Compile-the-generated-SDK regression tests** — added to the Rust (`cargo check`), TypeScript (`tsc --noEmit`), and Go (`go build ./...`) emitters. These regenerate the SDK into a temp crate and build it, so future emitter template breakage fails CI instead of shipping silently. All three emitters verified against both `fixtures/` specs (petstore + sample-api); each test skips cleanly only when its toolchain is genuinely absent. --- -## [1.2.0] — 2026-07-27 +## [1.6.0] — 2026-07-27 ### Added -#### SDK Features -- **Mock server** — `specforge mock` starts local HTTP mock server from spec examples -- **Logging hooks** — `Logger` interface/trait for all 3 SDKs (ConsoleLogger, NoopLogger) -- **i18n support** — `--locale en,es` flag generates localized error messages (8 locales) -- **Request/response interceptors** — Transform request body and response data -- **Response transformers** — Post-processing hooks on deserialized responses +#### CI/CD +- **Spec diff GitHub Action** — Auto-detect spec changes in PRs, post diff comments, fail on breaking changes +- **Spec diff workflow** — `.github/workflows/spec-diff.yml` triggers on YAML/JSON changes -#### Core -- **Swagger Editor export** — `specforge export` inlines all `$ref` for editor compatibility -- **Demo spec** — `specforge demo` generates realistic Petstore spec with examples -- **Schema evolution tracker** — `specforge evolution` tracks changes over git commits -- **Reverse inference** — `specforge infer` generates OpenAPI spec from sample JSON -- **Compatibility checker** — `specforge verify` validates running APIs against spec +#### CLI +- **`specforge changelog`** — Enhanced with per-operation entries, schema changes, semantic versioning suggestions (`--suggest-version`), JSON output (`--format json`) +- **`specforge version`** — Apply versioning strategies (url/header/query) to specs. `--prefix v2`, `--strategy header` +- **`specforge generate --version-prefix v2`** — Auto-add version prefix before generation -#### Testing -- **208 tests** across core, emitters, and CLI -- Swagger Editor lint rules (`--profile swagger-editor`) +#### Plugin System +- **Plugin marketplace** — `specforge plugin list/search/info/install` +- **4 curated plugins** — Kotlin, Swift, Python, C# emitter placeholders +- **Plugin configuration** — `.specforge.yaml` plugin section +- **`--plugin `** flag on `generate` to use custom WASM emitters -## [1.1.0] — 2026-07-27 +#### Profiling +- **`specforge profile`** — Performance profiling against live APIs +- **Metrics** — Latency (avg/p50/p95/p99), throughput, error rate, cache hit rate +- **Recommendations** — Actionable optimization suggestions -### Added +### Quality +- **259 tests** (core 242 + TS 17) -#### Core -- **Observability dashboard** — `specforge dashboard` generates HTML dashboard with Chart.js for SDK metrics visualization. -- **OpenAPI 3.1 native parser** — `Schema31` model, `parse_31()`, `warn_unsupported_features()` for unsupported 3.1 features. -- **Security scheme analysis** — `specforge security` with text/JSON/markdown output. -- **Dependency graph** — `specforge graph` generates Mermaid/DOT diagrams. -- **Bundle analyzer** — `specforge analyze` detects unused schemas, duplicates, large models. -- **contentMediaType/contentEncoding** — `Scalar::Base64` and `Scalar::Binary` for binary data. -- **Per-operation retry policies** — `x-retry` extensions on operations. -- **Diff improvements** — Markdown/JSON/color output, inline schema diffs. -- **JSDoc/docstrings** — Documentation comments from spec descriptions in generated code. - -#### Ecosystem -- **5 large test fixtures** — GitHub Enterprise (14MB), Atlassian, LaunchDarkly, OpenAI (3.1.0) -- **Performance benchmarks** — Criterion benchmarks + 13 perf tests -- **VS Code extension** — Complete rewrite: 15 commands, icons, auto-validate, progress, output channel -- **Website** — Landing page at specforge.deepwhaleai.com +--- -## [1.0.0] — 2026-07-27 +## [1.5.0] — 2026-07-27 ### Added -#### SDK runtimes -- **Rate limiting** — Token bucket and sliding window rate limiters for all 3 SDKs. TS: `rateLimiter` option. Go: `WithRateLimiter()`. Rust: `.rate_limiter()`. -- **Telemetry hooks** — Request metrics, error tracking, retry counting, cache hit/miss. TS/Go/Rust: `TelemetryHooks` interface/trait + `MetricsCollector`. - -#### Core -- **Spec deprecation tracking** — `find_deprecations()` scans IR for deprecated operations/schemas. `generate_migration_guide()` compares two specs. -- **`specforge migrate`** — Generate Markdown migration guides between spec versions. Deprecation comments in generated code (`@deprecated`, `// Deprecated:`, `#[deprecated]`). +#### Web UI +- **Professional light theme** — Complete redesign with modern design system (1713 lines) +- **Design system** — Color palette, typography, spacing, components documented +- **Interactive features** — Schema tree browser, JSON viewer, YAML export, copy buttons +- **Responsive design** — Mobile-friendly with breakpoints at 1024px and 640px +- **WCAG AA accessibility** — Focus states, aria labels, keyboard navigation + +#### Marketplace +- **18 curated specs** — GitHub, Stripe, Petstore, Kubernetes, Spotify, Notion, Twilio, Vercel, Okta, Atlassian, Adyen, Bitbucket, Linode, CircleCI, LaunchDarkly, Adobe AEM, 1Password, Ably +- **`specforge market list`** — Browse all specs with ratings and downloads +- **`specforge market search`** — Search by name, description, tags, author +- **`specforge market info`** — Detailed spec information +- **`specforge market add`** — Add your own spec to the marketplace + +### Quality +- **227 tests** (core 210 + TS 17) -#### Fixes -- **Go pipeline** — Fixed unused `obj` variable in validate.go, fixed hyphenated field names +--- -### Changed -- **v1.0.0** — First stable release. All roadmap items from README completed. -- Website live at `specforge.deepwhaleai.com` -- 6 external test fixtures (petstore, GitHub, Stripe, Kubernetes, Twilio) -- 176 tests across core, emitters, and CLI - -## [0.9.0] — 2026-07-27 +## [1.4.0] — 2026-07-27 ### Added -#### SDK runtimes -- **Response caching** — All three SDKs now support ETag-based caching. `ResponseCache` with TTL expiry, `If-None-Match` headers, 304 handling. TS: `cache` option on client. Go: `WithCache(ttl)`. Rust: `.cache_ttl(Duration)`. - -#### Core -- **Webhooks support** — OpenAPI 3.1 `webhooks` section parsed from raw JSON (bypasses `openapiv3` crate limitation). `Webhook` IR type with method, path, request body, responses. -- **`specforge workspace`** — Generate SDKs for multiple specs from `.specforge-workspace.yaml` config. `--only` filter, `--dry-run` mode. -- **`specforge workspace-init`** — Scan a directory and generate a starter workspace config. +#### VS Code Extension +- **24 commands** (was 3) — All CLI subcommands +- **6 configuration settings** — binaryPath, defaultLang, outputDir, autoValidate, specFilePattern, logLevel +- **3 keyboard shortcuts** — Generate (Ctrl+Shift+G), Check (Ctrl+Shift+V), Emit IR (Ctrl+Shift+I) +- **Context menus** — Editor + Explorer right-click for YAML/JSON files +- **Status bar** — Mock server indicator with stop button +- **Auto-validate on save** — Configurable spec validation +- **Progress notifications** — Visual feedback for long operations +- **Output channel** — Dedicated panel for all CLI output +- **Interactive pickers** — Language, format, version, file pickers -#### Emitters -- **TypeScript webhook types** — `webhooks.ts` with payload types, `WebhookHandler`, factory functions. -- **Go webhook types** — `webhooks.go` with payload types and `WebhookHandler` func type. -- **Rust webhook types** — `webhooks.rs` with payload structs and `WebhookHandler` trait. +--- -#### CLI -- **`--include-webhooks`** flag on `generate` — opt-in webhook type generation. - -## [0.8.0] — 2026-07-27 +## [1.3.0] — 2026-07-27 — Production-Ready Milestone ### Added +- **221 tests passing** (core 204 + TS 17) +- **21 test fixtures** (0.02MB to 14MB, 4 OpenAPI versions) +- **Stability enforcement** — IR version + `specforge-version.json` in generated SDKs +- **SDK integration tests** — Validate TS/Go/Rust against mock servers +- **Thread safety audit** — No deadlocks or race conditions +- **Unwrap/expect audit** — Production code audited for panics +- **SDK changelog generation** (`specforge changelog`) +- **ServiceContainer** for TS/Go/Rust dependency injection +- **Spec validation middleware** — Auto-validate all requests/responses -#### Core -- **OpenAPI 3.1 expanded** — `const` → `enum: [value]`, `dependentRequired` merged into `required`, `prefixItems` → `items`. Upgraded 3.0→3.1 now converts `enum: [value]` → `const`. 31 new tests. -- **3.1 feature detection** — `detect_31_features()` reports which 3.1 features are used in a spec. -- **Spec composition** — New `merge` module. `specforge merge` combines multiple spec files (paths, schemas, security schemes). Later specs override on conflict. - -#### Rust emitter -- **Dependency injection** — `ClientBuilder::http_client(reqwest::Client)` for injecting custom HTTP clients in tests. - -#### CLI -- **`specforge merge`** — Merge multiple OpenAPI spec files into one. `--format json|yaml`, `--out FILE`. +--- -## [0.7.0] — 2026-07-27 +## [1.2.0] — 2026-07-27 ### Added +- **`specforge mock`** — Local mock server from spec examples +- **SDK logging hooks** — Logger interface (ConsoleLogger, NoopLogger) +- **i18n support** — 8 locales, `--locale en,es` flag +- **Request/response interceptors** for all 3 SDKs +- **Response transformers** for post-processing +- **`specforge export`** — Swagger Editor compatible output +- **`specforge demo`** — Realistic Petstore spec with examples +- **`specforge evolution`** — Schema change tracking over git commits +- **`specforge infer`** — Generate OpenAPI from sample JSON +- **`specforge verify`** — Validate running APIs against spec -#### TypeScript emitter -- **Tree-shakeable API modules** — `src/index.ts` no longer re-exports API classes. Consumers import individual tags directly (`import { PetsApi } from "./api/Pets"`). New `src/api/index.ts` convenience barrel for callers who want everything. Subpath exports in `package.json` for `./api` and `./api/*`. - -#### CLI -- **`specforge versions`** — List all API versions in a spec directory. Supports flat (`v1.yaml`, `v2.yaml`) and nested (`v1/openapi.yaml`) conventions. -- **`--version` flag on `generate`** — Filter by version when spec is a directory. Matches by exact version string, file stem, or directory name. -- **`--profile` flag on `generate` and `emit`** — Outputs detailed timing breakdown (parse, resolve, emit, total) to stderr. - -#### Core -- **`scan_versions()`** — Scans a directory for spec files and extracts API versions. -- **`resolve_spec_path()`** — Resolves spec path with optional version filtering. +--- -## [0.6.0] — 2026-07-27 +## [1.1.0] — 2026-07-27 ### Added +- **`specforge dashboard`** — HTML metrics visualization with Chart.js +- **OpenAPI 3.1 native parser** — `Schema31`, `parse_31()`, unsupported feature warnings +- **`specforge security`** — Auth analysis (text/JSON/markdown output) +- **`specforge graph`** — Mermaid/DOT dependency diagrams +- **`specforge analyze`** — Unused schemas, duplicates, large models, recommendations +- **contentMediaType/contentEncoding** — `Scalar::Base64`, `Scalar::Binary` +- **Per-operation retry policies** — `x-retry` extensions +- **Diff improvements** — Markdown/JSON/color output, inline schema diffs +- **JSDoc/docstrings** — Documentation from spec descriptions in generated code -#### Core -- **Mock server test generation** — New `specforge test` subcommand generates mock server tests for TS/Go/Rust SDKs. Creates mock HTTP servers from spec example responses and verifies SDK calls. -- **3.1 `$ref` sibling support** — `$ref` objects with sibling `description`/`summary`/`deprecated` are now preserved via `allOf` wrapping during preprocessing. IR `Type::Reference` carries optional `description` override. -- **Configurable lint rules** — New `lint_config` module with `LintConfig`, `LintRule`, `RuleSeverity`. Loads from `.specforge.yaml`. CLI flags: `--disable`, `--enable`, `--severity`, `--config`. -- **New lint rules** — `missing-operation-id`, `missing-schema-description` (off by default), `path-trailing-slash`, `deprecated-operation`. +--- -## [0.5.0] — 2026-07-27 +## [1.0.0] — 2026-07-27 ### Added +- **Rate limiting** — Token bucket + sliding window (TS/Go/Rust) +- **Telemetry hooks** — Request metrics, error tracking, cache hit/miss +- **`specforge migrate`** — Generate migration guides between spec versions +- **Deprecation tracking** — Comments in generated code (`@deprecated`, `// Deprecated:`, `#[deprecated]`) +- **Website** — specforge.deepwhaleai.com +- **176 tests** passing -#### TypeScript emitter -- **Runtime validation** — `validate.ts` with per-model schema descriptors and validators. `validateRequest()` / `validateResponse()` called automatically when `validate: true` is set on the client. - -#### Go emitter -- **Runtime validation** — `validate.go` with `ValidatePet(v any) error` per model. `WithValidation(true)` option on client. +--- -#### Rust emitter -- **Runtime validation** — `validate.rs` with `fn validate_pet(v: &Value)` per model. `.validation(true)` builder option. +## [0.9.0] — 2026-07-27 -#### WASM -- **`specforge-wasm` crate** — Compiles core parsing/resolving to WASM via `wasm-bindgen`. Exposes `parse_and_resolve()` and `lint()` functions. -- **Web UI WASM integration** — "Parse Locally (WASM)" button for client-side spec parsing without `specforge emit`. +### Added +- **Response caching** — ETag-based caching with TTL expiry and 304 handling +- **OpenAPI 3.1 webhooks** — `--include-webhooks` flag +- **`specforge workspace`** — Multi-spec generation from config +- **`specforge workspace-init`** — Generate workspace config from directory -#### CLI -- **`specforge docs`** — Generate static HTML API documentation from a spec. Dark forge-themed with color-coded HTTP method badges. +--- -## [0.4.0] — 2026-07-27 +## [0.8.0] — 2026-07-27 ### Added +- **Rust `http_client()`** — Dependency injection builder +- **`specforge merge`** — Combine multiple spec files into one +- **OpenAPI 3.1 expanded** — `const`, `dependentRequired`, `prefixItems` +- **3.1 feature detection** (`detect_31_features()`) -#### Core -- **Spec validation** — New `validate` module with 51 unit tests. Validates JSON values against IR Type definitions (scalars, enums, arrays, maps, references, compositions). Returns `ValidationError` with JSON-pointer paths. -- **WASM plugin SDK** — `specforge-plugin` crate with `Plugin` trait, `GeneratedFile`/`PluginResult` types, and `export_plugin!` macro for WASM targets. -- **Example plugin** — `examples/plugin-example/` — `ReadmePlugin` that generates a README from the IR. - -#### Web UI -- **`web-ui/index.html`** — Self-contained single-page app for browsing IR: schema tree, operations tab, IR JSON viewer, stats dashboard. Dark forge-themed. No build step. - -#### Docs -- **`PLUGINS.md`** — How to build WASM emitter plugins (protocol, build steps, example). +--- -## [0.3.0] — 2026-07-26 +## [0.7.0] — 2026-07-27 ### Added +- **Tree-shakeable TS modules** — Per-tag imports, `src/api/index.ts` barrel +- **`specforge versions`** — List API versions in a directory +- **`--version` flag** — Filter by version from directory +- **`--profile` flag** — Timing breakdown for pipeline stages -#### Core -- **JSON Schema for IR** — `assets/ir-schema.json` (draft 2020-12) describes the full IR output. `specforge emit --schema` prints it. -- **Streaming IR emission** — `specforge emit --stream` outputs newline-delimited JSON (NDJSON) for processing large specs without loading everything into memory. -- **Parallel file generation** — All three emitters now write files in parallel using rayon. +--- -#### CLI -- **`specforge init`** — Scaffold a new OpenAPI spec with a `/health` endpoint and README. -- **`specforge convert`** — Convert between OpenAPI 3.0 and 3.1 (`--to 3.0` or `--to 3.1`). Operates on raw YAML/JSON to preserve full spec structure. +## [0.6.0] — 2026-07-27 + +### Added +- **`specforge test`** — Mock server test generation (TS/Go/Rust) +- **OpenAPI 3.1 `$ref` siblings** — `description`/`summary` preserved via `allOf` wrapping +- **Configurable lint rules** — `.specforge.yaml` config, `--disable`/`--enable`/`--severity` +- **New lint rules** — `missing-operation-id`, `path-trailing-slash`, `deprecated-operation` -#### Ecosystem -- **GitHub Action** — `action.yml` composite action for CI: `generate`, `check`, `diff`, `emit` commands. Auto-detects platform and installs the correct binary. -- **VS Code extension** — `vscode-extension/` scaffolding with Generate SDK, Check Spec, and Preview IR commands. -- **Stability policy** — `STABILITY.md` documents semver guarantees for the IR schema, CLI, and generated output. -- **Incremental generation guide** — `INCREMENTAL.md` documents CI caching strategies. +--- -## [0.2.2] — 2026-07-26 +## [0.5.0] — 2026-07-27 ### Added +- **Runtime validation middleware** — TS/Go/Rust SDKs validate request/response bodies +- **WASM-compiled specforge** — `specforge-wasm` crate for browser use +- **`specforge docs`** — Static HTML API documentation generator +- **Web UI WASM integration** — Client-side parsing -#### Core -- **OpenAPI 3.1 support** — Transparent preprocessing layer that converts 3.1 specs to 3.0 before parsing. Handles `type: ["string", "null"]` → `nullable: true`, numeric `exclusiveMinimum`/`exclusiveMaximum` → boolean, and missing `paths` → empty object. -- **IR serialization** — All IR types now derive `serde::Serialize` for JSON emission. +--- -#### CLI -- **`specforge emit`** — New subcommand that dumps the resolved IR as JSON to stdout. Enables external emitters and plugins to consume the IR without embedding specforge. +## [0.4.0] — 2026-07-27 -#### Branding -- **Fox blacksmith mascot** — New logo with a fox character wearing goggles, holding a hammer and document. -- **Forge color scheme** — Warm orange/amber/red palette replacing the previous blue/purple. +### Added +- **WASM plugin SDK** — `specforge-plugin` crate with `Plugin` trait +- **Web UI** — Interactive IR browser (specs, operations, schemas) +- **Spec validation module** — 51 unit tests -## [0.2.1] — 2026-07-26 +--- + +## [0.3.0] — 2026-07-27 ### Added +- **JSON Schema for IR** — `assets/ir-schema.json` +- **Streaming IR emission** — NDJSON for large specs +- **Parallel file generation** — Rayon +- **`specforge init`** — Scaffold new OpenAPI spec +- **`specforge convert`** — 3.0 ↔ 3.1 conversion +- **GitHub Action** — One-line CI integration +- **VS Code extension** — Initial scaffolding -#### Core -- **AllOf type aliases** — When `allOf` has exactly one `$ref` member, emitters use Go struct embedding and Rust `#[serde(flatten)]` instead of merging all properties into a flat struct. -- **Spec diff** — New `specforge diff ` subcommand that compares two OpenAPI specs and reports breaking changes (removed operations, new required parameters/properties, type changes). Exits non-zero on breaking changes for CI gates. -- **Generation benchmarks** — `scripts/bench.sh` times generation on GitHub and Stripe specs across all 3 languages. +--- -## [0.2.0] — 2026-07-26 +## [0.2.2] — 2026-07-27 ### Added +- **OpenAPI 3.1** — Transparent preprocessing (type arrays, $ref siblings, const, prefixItems) +- **`specforge emit`** — Dump resolved IR as JSON +- **Fox mascot "Ember"** — Blacksmith fox with goggles and forge theme -#### Core -- **AllOf property merging** — `build_model()` now merges properties from all `allOf` members (later members override on name conflict, required sets are unioned). Go and Rust emitters get merged properties instead of lossy first-member/`map[string]any` fallbacks. -- **Discriminator `mapping` support** — IR `Discriminator` now carries `mapping: Option>`. TS and Rust emitters use explicit mapping values when arm schemas don't embed single-variant string enums. -- **Spec linting** — New `specforge_core::lint` module with 4 checks: duplicate operation IDs (error), missing response descriptions (warning), missing operation summaries (warning), unused schemas (warning). -- **Circular reference tests** — Unit tests for self-referential schemas and mutual A→B→A cycles. - -#### CLI -- **`specforge check` subcommand** — Lint and validate a spec without generating. `--strict` flag promotes warnings to errors. -- CLI refactored to subcommand-based: `specforge generate ` and `specforge check `. +--- -#### TypeScript emitter -- Discriminator mapping fallback in `discriminant_literal()` when arm schemas lack single-variant enums. +## [0.2.1] — 2026-07-27 -#### Go emitter -- **OneOf ergonomic helpers** — `New{Union}(m map[string]any)` and `{Union}Discriminant(v)` for discriminated unions. -- **`json.RawMessage` fallback** — `New{Union}FromJSON(raw json.RawMessage)` for non-discriminated oneOf unions. -- **Streaming middleware** — `StreamMiddleware` type + `UseStream()` method. Applied before HTTP call in `doOnceStream`; can modify headers without consuming the response body. -- **Richer README** — Generated README now includes Errors, Pagination, Concurrency, Dedupe, Middleware, Streaming/SSE, and Idempotency sections. +### Added +- **AllOf type aliases** — Go embed, Rust `#[serde(flatten)]` +- **`specforge diff`** — Breaking change detection +- **Generation benchmarks** — `scripts/bench.sh` -#### Rust emitter -- **OneOf ergonomic helpers** — `impl` block on each oneOf enum with `discriminant()`, `is_{arm}()`, `into_{arm}()`, `as_{arm}()`. -- **Streaming middleware** — `StreamMiddleware` type + `use_stream_middleware()`. Applied in `request_stream` before `builder.send()`. -- **Richer README** — Generated README now includes Errors, Pagination, Concurrency, Dedupe, Middleware, Streaming/SSE, and Idempotency sections. +--- -#### CI & packaging -- **Multi-platform CI** — Test matrix expanded to `ubuntu-latest`, `macos-latest`, `windows-latest`. -- **Cross-compiled release binaries** — `.github/workflows/release.yml` builds for 5 targets (linux amd64/arm64, macOS Intel/Apple Silicon, Windows) on tag push. -- **Crates.io publish workflow** — `.github/workflows/publish.yml` with dry-run support. +## [0.2.0] — 2026-07-26 -### Changed -- CLI is now subcommand-based: `specforge generate ` instead of `specforge `. The old flat invocation no longer works. -- Internal crate dependencies now specify version requirements for crates.io compatibility. +### Added +- **Discriminator mapping** — Full support across all 3 emitters +- **OneOf helpers** — `isX()`/`narrowX()` (TS), `New{Union}`/`New{Union}FromJSON` (Go), `discriminant()`/`is_*()` (Rust) +- **Streaming middleware** — `StreamMiddleware` for TS/Go/Rust +- **Multi-platform CI** — Linux, macOS, Windows -### Fixed -- Unmatched brace in Rust `emit_readme()` format string. +--- ## [0.1.0] — 2026-07-26 -First public MVP of **specforge**: OpenAPI → typed SDKs for TypeScript, Go, and Rust. - ### Added +- **Initial MVP** — 3 emitters (TypeScript, Go, Rust), full runtime, e2e tests + +--- -#### Core -- Language-neutral IR (`specforge-core`) with `$ref` resolution, composition (`allOf`/`oneOf`/`anyOf`), discriminators, and inline string enums -- CLI `specforge` with `-l ts|go|rust`, `-o`, `-n`, `-v` - -#### TypeScript emitter (`specforge-ts`) -- Multi-file tree-shakeable package (ESM/CJS via tsup) -- Runtime: auth, retry, timeout, pagination, concurrency semaphore, in-flight dedupe, middleware, idempotency keys, SSE/chunk streaming -- oneOf runtime type guards (`isX` / `narrowX`) from discriminator literals -- Fixed nested JSDoc comment bug in concurrency template -- Dedupe buffers response bodies so concurrent `requestJson` callers each get a readable body - -#### Go emitter (`specforge-go`) -- Stdlib-only client (`net/http`, `encoding/json`) -- Runtime: auth providers, retry + full-jitter backoff, timeouts, cursor/offset pagination, semaphore, dedupe, middleware, idempotency keys, SSE iterator (`NewSseIterator`), `DoStream` -- Large-spec hardening: field-name collisions (`+1`/`-1`), reserved param idents, `anyString` coercion - -#### Rust emitter (`specforge-rust`) -- `reqwest` + `serde` + `tokio` crate layout -- Runtime: `Auth` enum, retry, timeouts, pagination, semaphore, dedupe, middleware, idempotency keys, `request_stream` / `SseStream` -- Untagged enums for oneOf; `Display` on string enums for query encoding - -#### Quality -- Regression suite: petstore generate (TS/Go/Rust); GitHub + Stripe resolve + Go/Rust compile gates -- E2E smoke: petstore basics + sample-api auth/retry/pagination (all 3 langs) -- E2E advanced: concurrency serialisation, dedupe single-flight, middleware header rewrite, idempotency-key on POST, SSE parse (all 3 langs) -- CI: `.github/workflows/ci.yml` + `scripts/ci.sh` (`full` / `quick` / `e2e` / `regression`) - -#### Docs & packaging -- Modern README with logo/banner, architecture, runtime matrix, examples -- `CHANGELOG.md`, `RELEASE.md`, `examples/*` consumer stubs -- `assets/logo.svg`, `assets/logo-banner.svg` - -### Known limitations -- Go/Rust do not yet emit oneOf runtime type guards (Rust uses `#[serde(untagged)]`) -- Streaming middleware on Go `DoStream` does not rewrite responses (body stays open) -- Large-spec downloads require network on first run (cached under `target/spec-cache/`) - -[1.2.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.2.0 -[1.1.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.1.0 -[1.0.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.0.0 -[0.9.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.9.0 -[0.8.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.8.0 -[0.7.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.7.0 -[0.6.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.6.0 -[0.5.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.5.0 -[0.4.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.4.0 -[0.3.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.3.0 -[0.2.2]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.2 -[0.2.1]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.1 -[0.2.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.0 -[0.1.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.1.0 +## Version Links + +- [v1.6.1](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.6.1) +- [v1.6.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.6.0) +- [v1.5.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.5.0) +- [v1.4.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.4.0) +- [v1.3.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.3.0) +- [v1.2.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.2.0) +- [v1.1.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.1.0) +- [v1.0.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.0.0) +- [v0.9.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.9.0) +- [v0.8.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.8.0) +- [v0.7.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.7.0) +- [v0.6.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.6.0) +- [v0.5.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.5.0) +- [v0.4.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.4.0) +- [v0.3.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.3.0) +- [v0.2.2](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.2) +- [v0.2.1](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.1) +- [v0.2.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.0) +- [v0.1.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.1.0) diff --git a/examples/petstore-go/sdk/specforge-version.json b/examples/petstore-go/sdk/specforge-version.json index 41fbf7b..9b06abd 100644 --- a/examples/petstore-go/sdk/specforge-version.json +++ b/examples/petstore-go/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T01:10:31Z"} \ No newline at end of file +{"specforge_version":"1.6.1","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:51:40Z"} \ No newline at end of file diff --git a/examples/petstore-rust/sdk/specforge-version.json b/examples/petstore-rust/sdk/specforge-version.json index 41fbf7b..9b06abd 100644 --- a/examples/petstore-rust/sdk/specforge-version.json +++ b/examples/petstore-rust/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T01:10:31Z"} \ No newline at end of file +{"specforge_version":"1.6.1","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:51:40Z"} \ No newline at end of file diff --git a/examples/petstore-ts/sdk/specforge-version.json b/examples/petstore-ts/sdk/specforge-version.json index 41fbf7b..9b06abd 100644 --- a/examples/petstore-ts/sdk/specforge-version.json +++ b/examples/petstore-ts/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T01:10:31Z"} \ No newline at end of file +{"specforge_version":"1.6.1","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:51:40Z"} \ No newline at end of file diff --git a/examples/petstore-ts/sdk/src/index.ts b/examples/petstore-ts/sdk/src/index.ts index 34e3177..8e35cdb 100644 --- a/examples/petstore-ts/sdk/src/index.ts +++ b/examples/petstore-ts/sdk/src/index.ts @@ -21,6 +21,12 @@ export * from "./errors"; export * from "./auth"; export * from "./retry"; export * from "./paginate"; +export * from "./concurrency"; +export * from "./dedup"; +export * from "./idempotency"; +export * from "./middleware"; +export * from "./streaming"; +export * from "./cache"; export * from "./validate"; export * from "./validation-middleware"; export * from "./ratelimit"; diff --git a/scripts/generate-examples.sh b/scripts/generate-examples.sh index e9fcc5e..bf6d597 100755 --- a/scripts/generate-examples.sh +++ b/scripts/generate-examples.sh @@ -6,10 +6,10 @@ cd "$ROOT" export PATH="${HOME}/.cargo/bin:/usr/local/go/bin:${PATH}" BIN="${ROOT}/target/debug/specforge" -if [[ ! -x "$BIN" ]]; then - echo "==> building specforge" - cargo build -p specforge-cli -q -fi +# Always rebuild: a stale binary silently regenerates examples with old +# emitter logic (this has bitten us before). The incremental build is fast. +echo "==> building specforge" +cargo build -p specforge-cli -q gen() { local lang="$1" dest="$2" name="$3"