diff --git a/CHANGELOG.md b/CHANGELOG.md index 2512bd4b..06cc2505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- Bump `sigil-stitch` to 0.6.8 + ## [0.1.14] ### Added diff --git a/Cargo.lock b/Cargo.lock index 1eae98c1..0c086f0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1413,9 +1413,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "sigil-stitch" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd3a66a8b6a51ced6663b9e038205040fdf88fb6b32e41dd91760bc6ebb9c7f3" +checksum = "edd65bbca55324c017401811d7f31af0f822ce46a8209bb7c118a358df532593" dependencies = [ "pretty", "serde", @@ -1425,9 +1425,9 @@ dependencies = [ [[package]] name = "sigil-stitch-macros" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2d35a1d48a6595f4821f003163ff8f9b654c01cc72e901de083147a64f78e" +checksum = "1342ca6ef96233a0f0a61f0110a2c3f2164944f04e1ddee5ef08234a4645013a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index f9a49a40..46be41c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.148" serde_norway = "0.9.42" serde_plain = "1.0.2" -sigil-stitch = "0.6.7" +sigil-stitch = "0.6.8" similar = "2.7.0" snafu = "0.8.9" toml = "0.9.8" diff --git a/src/generators/java/okhttp/sigil_emit.rs b/src/generators/java/okhttp/sigil_emit.rs index 843a6ef2..7fb63e61 100644 --- a/src/generators/java/okhttp/sigil_emit.rs +++ b/src/generators/java/okhttp/sigil_emit.rs @@ -214,19 +214,15 @@ fn emit_object(schema: &IrSchema, obj: &IrObject, package_name: &str) -> Option< .expect("ctor param"), ); } - let assignments: Vec = obj + let assignment_fields: Vec = obj .properties .iter() - .map(|(_json_name, prop)| { - let field_name = java_field_name(&prop.name); - sigil_quote!(Java { - this.$L(field_name.as_str()) = $L(field_name.as_str()); - }) - .expect("assignment") - }) + .map(|(_json_name, prop)| java_field_name(&prop.name)) .collect(); let ctor_body = sigil_quote!(Java { - $C_each(assignments); + $for(field_name in &assignment_fields) { + this.$L(field_name.as_str()) = $L(field_name.as_str()); + } }) .expect("ctor body"); ctor = ctor.body(ctor_body); @@ -412,17 +408,14 @@ fn emit_intersection( .expect("param"), ); } - let assignments: Vec = member_bindings + let assignment_fields: Vec = member_bindings .iter() - .map(|(_member_type, field_name)| { - sigil_quote!(Java { - this.$L(field_name.as_str()) = $L(field_name.as_str()); - }) - .expect("assignment") - }) + .map(|(_member_type, field_name)| field_name.clone()) .collect(); let ctor_body = sigil_quote!(Java { - $C_each(assignments); + $for(field_name in &assignment_fields) { + this.$L(field_name.as_str()) = $L(field_name.as_str()); + } }) .expect("ctor body"); ctor = ctor.body(ctor_body); diff --git a/src/generators/java/okhttp/sigil_emit_api.rs b/src/generators/java/okhttp/sigil_emit_api.rs index a176c4d0..ec3579bb 100644 --- a/src/generators/java/okhttp/sigil_emit_api.rs +++ b/src/generators/java/okhttp/sigil_emit_api.rs @@ -228,24 +228,20 @@ fn build_response_class(plan: &OpPlan<'_>) -> TypeSpec { .expect("param"), ); } - let mut field_assignments: Vec = vec![ - sigil_quote!(Java { this.statusCode = statusCode; }).expect("assign"), - sigil_quote!(Java { this.raw = raw; }).expect("assign"), - ]; + let mut assignment_fields = Vec::new(); let mut body_seen: HashSet = HashSet::new(); for tr in &plan.typed_responses { if !body_seen.insert(tr.field_name.clone()) { continue; } - field_assignments.push( - sigil_quote!(Java { - this.$L(tr.field_name.as_str()) = $L(tr.field_name.as_str()); - }) - .expect("assign"), - ); + assignment_fields.push(tr.field_name.clone()); } let ctor_body = sigil_quote!(Java { - $C_each(field_assignments); + this.statusCode = statusCode; + this.raw = raw; + $for(field_name in &assignment_fields) { + this.$L(field_name.as_str()) = $L(field_name.as_str()); + } }) .expect("ctor body"); ctor = ctor.body(ctor_body); @@ -442,13 +438,11 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { dedup_args.push(a); } } - cb.add_statement( - &format!( - "return new {}({})", - plan.response_type, - dedup_args.join(", ") - ), - (), + cb.add_code( + sigil_quote!(Java { + return new $N(plan.response_type.as_str())($for(arg in &dedup_args; separator = ", ") { $L(arg.as_str()) }); + }) + .expect("typed response constructor return"), ); } else { cb.add_statement( diff --git a/src/generators/kotlin/okhttp/sigil_emit_api.rs b/src/generators/kotlin/okhttp/sigil_emit_api.rs index 210fe8fe..08354b16 100644 --- a/src/generators/kotlin/okhttp/sigil_emit_api.rs +++ b/src/generators/kotlin/okhttp/sigil_emit_api.rs @@ -367,9 +367,11 @@ fn emit_method_body(plan: &OpPlan<'_>) -> CodeBlock { dedup_fields.push(f); } } - cb.add( - &format!("return {}({})", plan.response_type, dedup_fields.join(", ")), - (), + cb.add_code( + sigil_quote!(Kotlin { + return $N(plan.response_type.as_str())($for(field in &dedup_fields; separator = ", ") { $L(field.as_str()) }) + }) + .expect("typed response constructor return"), ); } else { cb.add( diff --git a/src/generators/python/httpx/emit_api.rs b/src/generators/python/httpx/emit_api.rs index a823cc7b..d361dcb9 100644 --- a/src/generators/python/httpx/emit_api.rs +++ b/src/generators/python/httpx/emit_api.rs @@ -333,12 +333,11 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, error_type: &TypeName) -> C request_args.push("headers=headers".to_string()); } - cb.add_statement( - &format!( - "response = self._client.request({})", - request_args.join(", "), - ), - (), + cb.add_code( + sigil_quote!(Python { + response = self._client.request($for(arg in &request_args; separator = ", ") { $L(arg.as_str()) }) + }) + .expect("request call"), ); // Error handling diff --git a/src/generators/python/httpx/emit_models.rs b/src/generators/python/httpx/emit_models.rs index 323b1c38..0e3008d0 100644 --- a/src/generators/python/httpx/emit_models.rs +++ b/src/generators/python/httpx/emit_models.rs @@ -387,20 +387,12 @@ fn format_type_alias(name: &str, members: &[TypeName]) -> CodeBlock { }) .unwrap(); } - // Multi-member: CodeBlock::builder() for precise line-break control. - // Python sigil_quote! $for merges output inline (no leading \n), unlike - // Go mode which preserves template newlines. This is the only case where - // we deviate from sigil_quote! — see sigil-stitch issue. - let mut cb = CodeBlock::builder(); - cb.add( - "type %N = (\n %T", - (NameArg(name.to_string()), members[0].clone()), - ); - for member in &members[1..] { - cb.add("\n | %T", (member.clone(),)); - } - cb.add("\n)", ()); - cb.build_unwrap() + sigil_quote!(Python { + type $N(name) = ( + $L(" ")$for(member in members; separator = "\n | ") { $T((*member).clone()) } + ) + }) + .unwrap() } // --------------------------------------------------------------------------- diff --git a/src/generators/python/requests/emit_api.rs b/src/generators/python/requests/emit_api.rs index 5be4fef7..6ff9f9b4 100644 --- a/src/generators/python/requests/emit_api.rs +++ b/src/generators/python/requests/emit_api.rs @@ -332,12 +332,11 @@ fn build_method_body(plan: &OpPlan<'_>, ir: &IrSpec, error_type: &TypeName) -> C request_args.push("headers=headers".to_string()); } - cb.add_statement( - &format!( - "response = self._client.request({})", - request_args.join(", "), - ), - (), + cb.add_code( + sigil_quote!(Python { + response = self._client.request($for(arg in &request_args; separator = ", ") { $L(arg.as_str()) }) + }) + .expect("request call"), ); // Error handling diff --git a/src/generators/python/requests/emit_models.rs b/src/generators/python/requests/emit_models.rs index 323b1c38..0e3008d0 100644 --- a/src/generators/python/requests/emit_models.rs +++ b/src/generators/python/requests/emit_models.rs @@ -387,20 +387,12 @@ fn format_type_alias(name: &str, members: &[TypeName]) -> CodeBlock { }) .unwrap(); } - // Multi-member: CodeBlock::builder() for precise line-break control. - // Python sigil_quote! $for merges output inline (no leading \n), unlike - // Go mode which preserves template newlines. This is the only case where - // we deviate from sigil_quote! — see sigil-stitch issue. - let mut cb = CodeBlock::builder(); - cb.add( - "type %N = (\n %T", - (NameArg(name.to_string()), members[0].clone()), - ); - for member in &members[1..] { - cb.add("\n | %T", (member.clone(),)); - } - cb.add("\n)", ()); - cb.build_unwrap() + sigil_quote!(Python { + type $N(name) = ( + $L(" ")$for(member in members; separator = "\n | ") { $T((*member).clone()) } + ) + }) + .unwrap() } // --------------------------------------------------------------------------- diff --git a/src/generators/typescript/fetch/sigil_emit.rs b/src/generators/typescript/fetch/sigil_emit.rs index dc40bded..fe3aaf10 100644 --- a/src/generators/typescript/fetch/sigil_emit.rs +++ b/src/generators/typescript/fetch/sigil_emit.rs @@ -19,7 +19,7 @@ use crate::ir::types::{ IrSchemaKind, IrSpec, IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle, }; use heck::{ToLowerCamelCase, ToPascalCase}; -use sigil_stitch::code_block::{Arg, CodeBlock}; +use sigil_stitch::code_block::CodeBlock; use sigil_stitch::lang::typescript::TypeScript; use sigil_stitch::prelude::sigil_quote; use sigil_stitch::spec::field_spec::FieldSpec; @@ -875,34 +875,9 @@ fn emit_tagged_union_file( return emit_tagged_union_file_camel_case(schema, tu, &name, flags, convertible, ts); } - // Build `export type Name = | ;` where each piece - // carries `%T` slots for the variant's content type. Adjacent / External - // shapes don't fit TypeName's structural variants, so the tag wrapping - // goes into the literal format fragment and the content rides on %T. - let mut format = format!("export type {} = ", name); - let mut pieces: Vec = Vec::with_capacity(tu.variants.len()); - let mut args: Vec = Vec::with_capacity(tu.variants.len() * 2); - - for variant in &tu.variants { - let content_ty = type_expr_to_typename(&variant.content_type); - let (piece, piece_args) = render_variant_piece( - &tu.tagging, - &tu.discriminator_field, - &variant.discriminator_value, - content_ty, - ); - pieces.push(piece); - args.extend(piece_args); - } - - format.push_str(&pieces.join(" | ")); - format.push(';'); - - // sigil_quote! needs compile-time format; fall back to CodeBlock builder - // for the dynamic variant count. - let mut cb = CodeBlock::builder(); - cb.add(&format, args); - let code = cb.build().ok()?; + let code = tagged_union_type_alias_code(&name, tu, &tu.discriminator_field, |expr| { + type_expr_to_typename(expr) + })?; let filename = format!("{}.ts", name); let mut fb = FileSpec::builder_with(&filename, ts.clone()); @@ -936,46 +911,14 @@ fn emit_tagged_union_file_camel_case( let disc_field_camel = disc_field.to_lower_camel_case(); // --- $Wire type (wire discriminator field name + $Wire content refs) --- - let mut wire_fmt = format!("export type {wire_name} = "); - let mut wire_pieces: Vec = Vec::new(); - let mut wire_args: Vec = Vec::new(); - for variant in &tu.variants { - let content_ty = type_expr_to_typename_wire(&variant.content_type, convertible); - let (piece, piece_args) = render_variant_piece( - &tu.tagging, - disc_field, - &variant.discriminator_value, - content_ty, - ); - wire_pieces.push(piece); - wire_args.extend(piece_args); - } - wire_fmt.push_str(&wire_pieces.join(" | ")); - wire_fmt.push(';'); - let mut wire_cb = CodeBlock::builder(); - wire_cb.add(&wire_fmt, wire_args); - let wire_code = wire_cb.build().ok()?; + let wire_code = tagged_union_type_alias_code(&wire_name, tu, disc_field, |expr| { + type_expr_to_typename_wire(expr, convertible) + })?; // --- Ergonomic type (camelCase discriminator field name + ergonomic content refs) --- - let mut ergo_fmt = format!("export type {name} = "); - let mut ergo_pieces: Vec = Vec::new(); - let mut ergo_args: Vec = Vec::new(); - for variant in &tu.variants { - let content_ty = type_expr_to_typename(&variant.content_type); - let (piece, piece_args) = render_variant_piece( - &tu.tagging, - &disc_field_camel, - &variant.discriminator_value, - content_ty, - ); - ergo_pieces.push(piece); - ergo_args.extend(piece_args); - } - ergo_fmt.push_str(&ergo_pieces.join(" | ")); - ergo_fmt.push(';'); - let mut ergo_cb = CodeBlock::builder(); - ergo_cb.add(&ergo_fmt, ergo_args); - let ergo_code = ergo_cb.build().ok()?; + let ergo_code = tagged_union_type_alias_code(name, tu, &disc_field_camel, |expr| { + type_expr_to_typename(expr) + })?; // --- fromJSON / toJSON --- let from_json = build_tagged_union_from_json(name, &wire_name, tu, convertible)?; @@ -1049,16 +992,12 @@ fn build_tagged_union_from_json( ); case_lines.push(format!("case '{val}': {case_body}")); } - let cases: Vec = case_lines - .iter() - .map(|l| CodeBlock::of(l, ())) - .collect::>() - .ok()?; - sigil_quote!(TypeScript { export function $N(fn_name)(json: $N(wire_name)): $N(name) { switch ($L(disc_access)) { - $C_each(cases); + $for(case_line in &case_lines) { + $L(case_line.as_str()) + } } } }) @@ -1129,15 +1068,11 @@ fn build_external_tagged_from_json( body_lines.push(format!("if ('{val}' in json) return {ret_expr};")); } body_lines.push("throw new Error('Unknown variant');".to_string()); - let body: Vec = body_lines - .iter() - .map(|l| CodeBlock::of(l, ())) - .collect::>() - .ok()?; - sigil_quote!(TypeScript { export function $N(fn_name)(json: $N(wire_name)): $N(name) { - $C_each(body); + $for(body_line in &body_lines) { + $L(body_line.as_str()) + } } }) .ok() @@ -1198,16 +1133,12 @@ fn build_tagged_union_to_json( ); case_lines.push(format!("case '{val}': {case_body}")); } - let cases: Vec = case_lines - .iter() - .map(|l| CodeBlock::of(l, ())) - .collect::>() - .ok()?; - sigil_quote!(TypeScript { export function $N(fn_name)(value: $N(name)): $N(wire_name) { switch ($L(disc_access)) { - $C_each(cases); + $for(case_line in &case_lines) { + $L(case_line.as_str()) + } } } }) @@ -1286,15 +1217,11 @@ fn build_external_tagged_to_json( body_lines.push(format!("if ('{val}' in value) return {ret_expr};")); } body_lines.push("throw new Error('Unknown variant');".to_string()); - let body: Vec = body_lines - .iter() - .map(|l| CodeBlock::of(l, ())) - .collect::>() - .ok()?; - sigil_quote!(TypeScript { export function $N(fn_name)(value: $N(name)): $N(wire_name) { - $C_each(body); + $for(body_line in &body_lines) { + $L(body_line.as_str()) + } } }) .ok() @@ -1344,9 +1271,8 @@ fn emit_union_file_camel_case( if union.nullable { wire_members.push("null".to_string()); } - let wire_body = wire_members.join(" | "); let wire_type = sigil_quote!(TypeScript { - export type $N(wire_name.as_str()) = $L(wire_body); + export type $N(wire_name.as_str()) = $for(member in &wire_members; separator = " | ") { $L(member.as_str()) }; }) .ok()?; fb = fb.add_code(wire_type); @@ -1356,9 +1282,8 @@ fn emit_union_file_camel_case( if union.nullable { ergo_members.push("null".to_string()); } - let ergo_body = ergo_members.join(" | "); let ergo_type = sigil_quote!(TypeScript { - export type $N(name) = $L(ergo_body); + export type $N(name) = $for(member in &ergo_members; separator = " | ") { $L(member.as_str()) }; }) .ok()?; fb = fb.add_code(ergo_type); @@ -1429,18 +1354,16 @@ fn emit_intersection_file_camel_case( .iter() .map(|m| type_expr_str_wire(m, convertible)) .collect(); - let wire_body = wire_members.join(" & "); let wire_type = sigil_quote!(TypeScript { - export type $N(wire_name.as_str()) = $L(wire_body); + export type $N(wire_name.as_str()) = $for(member in &wire_members; separator = " & ") { $L(member.as_str()) }; }) .ok()?; fb = fb.add_code(wire_type); // Ergonomic type alias: A & B & ... let ergo_members: Vec = intersection.members.iter().map(type_expr_str).collect(); - let ergo_body = ergo_members.join(" & "); let ergo_type = sigil_quote!(TypeScript { - export type $N(name) = $L(ergo_body); + export type $N(name) = $for(member in &ergo_members; separator = " & ") { $L(member.as_str()) }; }) .ok()?; fb = fb.add_code(ergo_type); @@ -1469,10 +1392,9 @@ fn emit_intersection_file_camel_case( }) .collect(); - let from_spreads = from_spreads.join(", "); let from_json = sigil_quote!(TypeScript { export function $N(from_fn)(json: $N(wire_name.as_str())): $N(name) { - return $L("{ @{from_spreads} } as @{name};") + return $L("{ ")$for(spread in &from_spreads; separator = ", ") { $L(spread.as_str()) }$L(format!(" }} as {name};")) } }) .ok()?; @@ -1497,10 +1419,9 @@ fn emit_intersection_file_camel_case( }) .collect(); - let to_spreads = to_spreads.join(", "); let to_json = sigil_quote!(TypeScript { export function $N(to_fn)(value: $N(name)): $N(wire_name.as_str()) { - return $L("{ @{to_spreads} } as @{name}$Wire;") + return $L("{ ")$for(spread in &to_spreads; separator = ", ") { $L(spread.as_str()) }$L(format!(" }} as {name}$Wire;")) } }) .ok()?; @@ -1681,18 +1602,29 @@ fn guard_check_and_type( (check, ty) } -/// Render one variant of a tagged union as a format fragment + its args. -/// -/// Returns `(format_piece, args)` where `format_piece` contains `%T` slots -/// in the same order as `args`. Caller joins pieces with ` | `. -fn render_variant_piece( - tagging: &TaggingStyle, +fn tagged_union_type_alias_code( + name: &str, + tu: &IrTaggedUnion, discriminator_field: &str, - discriminator_value: &str, - content_ty: TypeName, -) -> (String, Vec) { - let fmt = variant_type_format(tagging, discriminator_field, discriminator_value, "%T"); - (fmt, vec![Arg::TypeName(content_ty)]) + type_name_for: F, +) -> Option +where + F: Fn(&IrTypeExpr) -> TypeName, +{ + match &tu.tagging { + TaggingStyle::Internal => sigil_quote!(TypeScript { + export type $N(name) = $for(variant in &tu.variants; separator = " | ") { ($L("{ ")$L(discriminator_field)$L(": ")$L(format!("'{}'", variant.discriminator_value))$L(" } & ")$T(type_name_for(&variant.content_type))) }; + }) + .ok(), + TaggingStyle::Adjacent { content_field } => sigil_quote!(TypeScript { + export type $N(name) = $for(variant in &tu.variants; separator = " | ") { $L(format!("{{ {discriminator_field}: '{}'; {content_field}: ", variant.discriminator_value))$T(type_name_for(&variant.content_type))$L(" }") }; + }) + .ok(), + TaggingStyle::External => sigil_quote!(TypeScript { + export type $N(name) = $for(variant in &tu.variants; separator = " | ") { $L("{ ")$L(format!("'{}'", variant.discriminator_value))$L(": ")$T(type_name_for(&variant.content_type))$L(" }") }; + }) + .ok(), + } } /// Build the pipe-joined literal body for an enum: `'a' | 1 | true | null`. diff --git a/src/generators/typescript/fetch/sigil_emit_api.rs b/src/generators/typescript/fetch/sigil_emit_api.rs index bd56338d..a2bb190b 100644 --- a/src/generators/typescript/fetch/sigil_emit_api.rs +++ b/src/generators/typescript/fetch/sigil_emit_api.rs @@ -236,19 +236,20 @@ fn build_response_aliases_block(ops: &[&IrOperation]) -> Result