diff --git a/src/generators/typescript/fetch/codegen.rs b/src/generators/typescript/fetch/codegen.rs index a8c120604..ba4d161fd 100644 --- a/src/generators/typescript/fetch/codegen.rs +++ b/src/generators/typescript/fetch/codegen.rs @@ -87,7 +87,9 @@ impl TypeScriptFetchCodeGenerator { let value_names = ir .schemas .get(name) - .map(|s| super::sigil_emit::value_exports_for_schema(s, flags, &convertible)) + .map(|s| { + super::sigil_emit::value_exports_for_schema(s, flags, &convertible, ir) + }) .unwrap_or_default(); let extra_types = ir .schemas diff --git a/src/generators/typescript/fetch/sigil_emit.rs b/src/generators/typescript/fetch/sigil_emit.rs index fe3aaf106..d06fda8a4 100644 --- a/src/generators/typescript/fetch/sigil_emit.rs +++ b/src/generators/typescript/fetch/sigil_emit.rs @@ -54,6 +54,7 @@ pub fn value_exports_for_schema( schema: &IrSchema, flags: EmitFlags, convertible: &HashSet, + ir: &IrSpec, ) -> Vec { match &schema.kind { IrSchemaKind::Object(_) if flags.property_naming_camel_case => { @@ -72,7 +73,12 @@ pub fn value_exports_for_schema( tu.variants .iter() .filter(|v| { - !is_unspecified_variant( + !is_tag_only_variant( + ir, + &tu.discriminator_field, + &v.discriminator_value, + &v.content_type, + ) && !is_unspecified_variant( &v.discriminator_value, &tu.discriminator_field, &v.content_type, @@ -100,7 +106,12 @@ pub fn value_exports_for_schema( .variants .iter() .filter(|v| { - !is_unspecified_variant( + !is_tag_only_variant( + ir, + &tu.discriminator_field, + &v.discriminator_value, + &v.content_type, + ) && !is_unspecified_variant( &v.discriminator_value, &tu.discriminator_field, &v.content_type, @@ -142,17 +153,23 @@ pub fn extra_type_exports_for_schema( /// Emit a TypeScript model file for an IR schema. Dispatches on `schema.kind`. pub fn emit_model_file( schema: &IrSchema, + ir: &IrSpec, flags: EmitFlags, convertible: &HashSet, + unknown_aliases: &HashSet, ts: &TypeScript, ) -> Option { match &schema.kind { - IrSchemaKind::Object(obj) => emit_object_file(schema, obj, flags, convertible, ts), + IrSchemaKind::Object(obj) => { + emit_object_file(schema, obj, flags, convertible, unknown_aliases, ts) + } IrSchemaKind::Enum(en) => emit_enum_file_from(schema, en, flags, ts), IrSchemaKind::Alias(expr) => emit_alias_file(schema, expr, ts), IrSchemaKind::Union(u) => emit_union_file(schema, u, flags, convertible, ts), IrSchemaKind::Intersection(i) => emit_intersection_file(schema, i, flags, convertible, ts), - IrSchemaKind::TaggedUnion(tu) => emit_tagged_union_file(schema, tu, flags, convertible, ts), + IrSchemaKind::TaggedUnion(tu) => { + emit_tagged_union_file(schema, ir, tu, flags, convertible, ts) + } } } @@ -169,12 +186,13 @@ fn emit_object_file( obj: &IrObject, flags: EmitFlags, convertible: &HashSet, + unknown_aliases: &HashSet, ts: &TypeScript, ) -> Option { let name = schema.name.to_pascal_case(); if flags.property_naming_camel_case { - return emit_object_file_camel_case(schema, obj, &name, convertible, ts); + return emit_object_file_camel_case(schema, obj, &name, convertible, unknown_aliases, ts); } let mut tb = TypeSpec::builder(&name, TypeKind::Interface).visibility(Visibility::Public); @@ -183,7 +201,7 @@ fn emit_object_file( } for (_json_name, prop) in &obj.properties { - tb = tb.add_field(build_field(prop)?); + tb = tb.add_field(build_field(prop, unknown_aliases)?); } let filename = format!("{}.ts", name); @@ -200,6 +218,7 @@ fn emit_object_file_camel_case( obj: &IrObject, name: &str, convertible: &HashSet, + unknown_aliases: &HashSet, ts: &TypeScript, ) -> Option { let wire_name = format!("{}$Wire", name); @@ -212,7 +231,7 @@ fn emit_object_file_camel_case( wire_tb = wire_tb.doc(doc); } for (_json_name, prop) in &obj.properties { - wire_tb = wire_tb.add_field(build_field_wire(prop, convertible)?); + wire_tb = wire_tb.add_field(build_field_wire(prop, convertible, unknown_aliases)?); } // --- Ergonomic interface (camelCase property names) --- @@ -221,7 +240,7 @@ fn emit_object_file_camel_case( ergo_tb = ergo_tb.doc(doc); } for (_json_name, prop) in &obj.properties { - ergo_tb = ergo_tb.add_field(build_field_camel(prop)?); + ergo_tb = ergo_tb.add_field(build_field_camel(prop, unknown_aliases)?); } // Collect Named refs that need converter imports (only convertible ones) @@ -252,15 +271,24 @@ fn emit_object_file_camel_case( } /// Build a field for the $Wire interface (preserves original property name). -fn build_field_wire(prop: &IrProperty, convertible: &HashSet) -> Option { +fn build_field_wire( + prop: &IrProperty, + convertible: &HashSet, + unknown_aliases: &HashSet, +) -> Option { let field_name = if is_valid_ts_identifier(&prop.name) { prop.name.clone() } else { format!("'{}'", prop.name) }; let inner_ty = type_expr_to_typename_wire(&prop.type_expr, convertible); - let field_ty = if prop.nullable && prop.required { - TypeName::optional(inner_ty) + let field_ty = if prop.nullable { + nullable_field_type_name( + &prop.type_expr, + inner_ty, + type_expr_str_wire(&prop.type_expr, convertible), + unknown_aliases, + ) } else { inner_ty }; @@ -276,7 +304,7 @@ fn build_field_wire(prop: &IrProperty, convertible: &HashSet) -> Option< } /// Build a field for the ergonomic interface (camelCase property name). -fn build_field_camel(prop: &IrProperty) -> Option { +fn build_field_camel(prop: &IrProperty, unknown_aliases: &HashSet) -> Option { let camel = prop.name.to_lower_camel_case(); let field_name = if is_valid_ts_identifier(&camel) { camel @@ -284,8 +312,13 @@ fn build_field_camel(prop: &IrProperty) -> Option { format!("'{}'", camel) }; let inner_ty = type_expr_to_typename(&prop.type_expr); - let field_ty = if prop.nullable && prop.required { - TypeName::optional(inner_ty) + let field_ty = if prop.nullable { + nullable_field_type_name( + &prop.type_expr, + inner_ty, + type_expr_str(&prop.type_expr), + unknown_aliases, + ) } else { inner_ty }; @@ -316,9 +349,10 @@ fn type_expr_to_typename_wire(expr: &IrTypeExpr, convertible: &HashSet) IrTypeExpr::Array(inner) => { TypeName::readonly_array(type_expr_to_typename_wire_nested(inner, convertible)) } - IrTypeExpr::Nullable(inner) => { - TypeName::optional(type_expr_to_typename_wire(inner, convertible)) - } + IrTypeExpr::Nullable(inner) => union_typename(vec![ + type_expr_to_typename_wire(inner, convertible), + TypeName::primitive("null"), + ]), IrTypeExpr::Map(inner) => TypeName::generic( TypeName::primitive("Record"), vec![ @@ -326,7 +360,21 @@ fn type_expr_to_typename_wire(expr: &IrTypeExpr, convertible: &HashSet) type_expr_to_typename_wire(inner, convertible), ], ), - other => type_expr_to_typename(other), + IrTypeExpr::StringLiteral(s) => TypeName::raw(&format!("'{s}'")), + IrTypeExpr::StringEnum(values) => union_typename( + values + .iter() + .map(|v| TypeName::raw(&format!("'{v}'"))) + .collect(), + ), + IrTypeExpr::Union(members) => union_typename( + members + .iter() + .map(|member| type_expr_to_typename_wire(member, convertible)) + .collect(), + ), + IrTypeExpr::Primitive(p) => TypeName::primitive(primitive_to_ts(p)), + IrTypeExpr::Any => TypeName::primitive("unknown"), } } @@ -345,11 +393,9 @@ fn type_expr_str(expr: &IrTypeExpr) -> String { IrTypeExpr::Named(name) => name.to_pascal_case(), IrTypeExpr::Primitive(p) => primitive_to_ts(p).to_string(), IrTypeExpr::StringLiteral(s) => format!("'{s}'"), - IrTypeExpr::StringEnum(values) => values - .iter() - .map(|v| format!("'{v}'")) - .collect::>() - .join(" | "), + IrTypeExpr::StringEnum(values) => { + simplify_union_strings(values.iter().map(|v| format!("'{v}'")).collect()) + } IrTypeExpr::Array(inner) => { let inner_str = type_expr_str(inner); if is_compound_type(inner) { @@ -358,13 +404,13 @@ fn type_expr_str(expr: &IrTypeExpr) -> String { format!("readonly {inner_str}[]") } } - IrTypeExpr::Nullable(inner) => format!("{} | null", type_expr_str(inner)), + IrTypeExpr::Nullable(inner) => { + simplify_union_strings(vec![type_expr_str(inner), "null".to_string()]) + } IrTypeExpr::Map(inner) => format!("Record", type_expr_str(inner)), - IrTypeExpr::Union(members) => members - .iter() - .map(type_expr_str) - .collect::>() - .join(" | "), + IrTypeExpr::Union(members) => { + simplify_union_strings(members.iter().map(type_expr_str).collect()) + } IrTypeExpr::Any => "unknown".to_string(), } } @@ -382,11 +428,9 @@ fn type_expr_str_wire(expr: &IrTypeExpr, convertible: &HashSet) -> Strin } IrTypeExpr::Primitive(p) => primitive_to_ts(p).to_string(), IrTypeExpr::StringLiteral(s) => format!("'{s}'"), - IrTypeExpr::StringEnum(values) => values - .iter() - .map(|v| format!("'{v}'")) - .collect::>() - .join(" | "), + IrTypeExpr::StringEnum(values) => { + simplify_union_strings(values.iter().map(|v| format!("'{v}'")).collect()) + } IrTypeExpr::Array(inner) => { let inner_str = type_expr_str_wire(inner, convertible); if is_compound_type(inner) { @@ -395,19 +439,82 @@ fn type_expr_str_wire(expr: &IrTypeExpr, convertible: &HashSet) -> Strin format!("readonly {inner_str}[]") } } - IrTypeExpr::Nullable(inner) => format!("{} | null", type_expr_str_wire(inner, convertible)), + IrTypeExpr::Nullable(inner) => simplify_union_strings(vec![ + type_expr_str_wire(inner, convertible), + "null".to_string(), + ]), IrTypeExpr::Map(inner) => { format!("Record", type_expr_str_wire(inner, convertible)) } - IrTypeExpr::Union(members) => members - .iter() - .map(|m| type_expr_str_wire(m, convertible)) - .collect::>() - .join(" | "), + IrTypeExpr::Union(members) => simplify_union_strings( + members + .iter() + .map(|m| type_expr_str_wire(m, convertible)) + .collect(), + ), IrTypeExpr::Any => "unknown".to_string(), } } +fn simplify_union_strings(members: Vec) -> String { + let mut out = Vec::new(); + let mut seen = HashSet::new(); + for member in members { + if member == "unknown" { + return "unknown".to_string(); + } + if seen.insert(member.clone()) { + out.push(member); + } + } + out.join(" | ") +} + +fn union_typename(members: Vec) -> TypeName { + let mut out = Vec::new(); + let mut seen = HashSet::new(); + for member in members { + if type_name_is_unknown(&member) { + return TypeName::primitive("unknown"); + } + let key = format!("{member:?}"); + if seen.insert(key) { + out.push(member); + } + } + if out.len() == 1 { + out.pop().unwrap() + } else { + TypeName::union(out) + } +} + +fn type_name_is_unknown(type_name: &TypeName) -> bool { + match type_name { + TypeName::Primitive(value) | TypeName::Raw(value) => value == "unknown", + TypeName::Union(members) => members.iter().any(type_name_is_unknown), + _ => false, + } +} + +fn nullable_field_type_name( + expr: &IrTypeExpr, + inner_ty: TypeName, + rendered_inner: String, + unknown_aliases: &HashSet, +) -> TypeName { + if type_expr_collapses_to_unknown(expr, unknown_aliases) { + inner_ty + } else if contains_any_named_ref(expr) { + union_typename(vec![inner_ty, TypeName::primitive("null")]) + } else { + TypeName::raw(&simplify_union_strings(vec![ + rendered_inner, + "null".to_string(), + ])) + } +} + fn is_compound_type(expr: &IrTypeExpr) -> bool { matches!( expr, @@ -430,7 +537,13 @@ fn build_from_json_fn( let camel = prop.name.to_lower_camel_case(); let ergo_key = obj_literal_key(&camel); let wire_access = wire_field_access(&prop.name); - let conv = from_json_expr(&prop.type_expr, &wire_access, !prop.required, convertible); + let conv = from_json_expr( + &prop.type_expr, + &wire_access, + !prop.required, + prop.nullable, + convertible, + ); CodeBlock::of(&format!("{ergo_key}: {conv},"), ()) }) .collect::>() @@ -470,7 +583,13 @@ fn build_to_json_fn( let camel = prop.name.to_lower_camel_case(); let wire_key = obj_literal_key(&prop.name); let ergo_access = ergo_field_access(&camel); - let conv = to_json_expr(&prop.type_expr, &ergo_access, !prop.required, convertible); + let conv = to_json_expr( + &prop.type_expr, + &ergo_access, + !prop.required, + prop.nullable, + convertible, + ); CodeBlock::of(&format!("{wire_key}: {conv},"), ()) }) .collect::>() @@ -527,17 +646,23 @@ fn from_json_expr( expr: &IrTypeExpr, access: &str, optional: bool, + nullable: bool, convertible: &HashSet, ) -> String { + let inner = from_json_expr_inner(expr, access, convertible); + let with_null = if nullable && needs_conversion(expr, convertible) { + format!("{access} === null ? null : {inner}") + } else { + inner + }; if optional { - let inner = from_json_expr_inner(expr, access, convertible); if needs_conversion(expr, convertible) { - format!("{access} !== undefined ? {inner} : undefined") + format!("{access} !== undefined ? {with_null} : undefined") } else { - inner + with_null } } else { - from_json_expr_inner(expr, access, convertible) + with_null } } @@ -571,17 +696,23 @@ fn to_json_expr( expr: &IrTypeExpr, access: &str, optional: bool, + nullable: bool, convertible: &HashSet, ) -> String { + let inner = to_json_expr_inner(expr, access, convertible); + let with_null = if nullable && needs_conversion(expr, convertible) { + format!("{access} === null ? null : {inner}") + } else { + inner + }; if optional { - let inner = to_json_expr_inner(expr, access, convertible); if needs_conversion(expr, convertible) { - format!("{access} !== undefined ? {inner} : undefined") + format!("{access} !== undefined ? {with_null} : undefined") } else { - inner + with_null } } else { - to_json_expr_inner(expr, access, convertible) + with_null } } @@ -632,6 +763,57 @@ fn has_named_ref(expr: &IrTypeExpr, convertible: &HashSet) -> bool { } } +fn contains_any_named_ref(expr: &IrTypeExpr) -> bool { + match expr { + IrTypeExpr::Named(_) => true, + IrTypeExpr::Array(inner) | IrTypeExpr::Nullable(inner) | IrTypeExpr::Map(inner) => { + contains_any_named_ref(inner) + } + IrTypeExpr::Union(members) => members.iter().any(contains_any_named_ref), + IrTypeExpr::Primitive(_) + | IrTypeExpr::StringLiteral(_) + | IrTypeExpr::StringEnum(_) + | IrTypeExpr::Any => false, + } +} + +fn type_expr_collapses_to_unknown(expr: &IrTypeExpr, unknown_aliases: &HashSet) -> bool { + match expr { + IrTypeExpr::Any => true, + IrTypeExpr::Named(name) => unknown_aliases.contains(name), + IrTypeExpr::Nullable(inner) => type_expr_collapses_to_unknown(inner, unknown_aliases), + IrTypeExpr::Union(members) => members + .iter() + .any(|member| type_expr_collapses_to_unknown(member, unknown_aliases)), + IrTypeExpr::Array(_) + | IrTypeExpr::Map(_) + | IrTypeExpr::Primitive(_) + | IrTypeExpr::StringLiteral(_) + | IrTypeExpr::StringEnum(_) => false, + } +} + +fn build_unknown_alias_set(ir: &IrSpec) -> HashSet { + ir.schemas + .iter() + .filter_map(|(name, schema)| { + let renders_unknown = match &schema.kind { + IrSchemaKind::Alias(expr) => type_expr_str(expr) == "unknown", + IrSchemaKind::Union(union) => { + let mut members: Vec = + union.members.iter().map(type_expr_str).collect(); + if union.nullable { + members.push("null".to_string()); + } + simplify_union_strings(members) == "unknown" + } + _ => false, + }; + renders_unknown.then(|| name.clone()) + }) + .collect() +} + /// Collect unique Named type references from an object's properties (only convertible ones). fn collect_named_refs(obj: &IrObject, convertible: &HashSet) -> Vec { let mut refs = HashSet::new(); @@ -794,7 +976,7 @@ fn emit_union_file( if union.nullable { members.push(TypeName::primitive("null")); } - let union_ty = TypeName::union(members); + let union_ty = union_typename(members); let type_alias = sigil_quote!(TypeScript { export type $N(name.as_str()) = $T(union_ty); @@ -860,6 +1042,7 @@ fn emit_intersection_file( /// as unsupported (degenerate — skip rather than emit `export type X = ;`). fn emit_tagged_union_file( schema: &IrSchema, + ir: &IrSpec, tu: &IrTaggedUnion, flags: EmitFlags, convertible: &HashSet, @@ -872,12 +1055,13 @@ fn emit_tagged_union_file( let name = schema.name.to_pascal_case(); if flags.property_naming_camel_case { - return emit_tagged_union_file_camel_case(schema, tu, &name, flags, convertible, ts); + return emit_tagged_union_file_camel_case(schema, ir, tu, &name, flags, convertible, ts); } - let code = tagged_union_type_alias_code(&name, tu, &tu.discriminator_field, |expr| { - type_expr_to_typename(expr) - })?; + let code = + tagged_union_type_alias_code(&name, ir, tu, &tu.discriminator_field, None, |expr| { + type_expr_str(expr) + })?; let filename = format!("{}.ts", name); let mut fb = FileSpec::builder_with(&filename, ts.clone()); @@ -885,9 +1069,12 @@ fn emit_tagged_union_file( fb = fb.add_raw(&format!("/** {doc} */\n")); } fb = fb.add_code(code); + for import in tagged_union_type_imports(ir, tu, convertible, false) { + fb = fb.add_import(import); + } if flags.emit_type_guards { - let guards = build_tagged_union_type_guards(&name, tu); + let guards = build_tagged_union_type_guards(&name, ir, tu, false); for guard in guards { fb = fb.add_code(guard); } @@ -899,6 +1086,7 @@ fn emit_tagged_union_file( /// Emit a tagged union file with dual types + fromJSON/toJSON when camelCase. fn emit_tagged_union_file_camel_case( schema: &IrSchema, + ir: &IrSpec, tu: &IrTaggedUnion, name: &str, flags: EmitFlags, @@ -911,18 +1099,23 @@ 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 wire_code = tagged_union_type_alias_code(&wire_name, tu, disc_field, |expr| { - type_expr_to_typename_wire(expr, convertible) + let wire_code = tagged_union_type_alias_code(&wire_name, ir, tu, disc_field, None, |expr| { + type_expr_str_wire(expr, convertible) })?; // --- Ergonomic type (camelCase discriminator field name + ergonomic content refs) --- - let ergo_code = tagged_union_type_alias_code(name, tu, &disc_field_camel, |expr| { - type_expr_to_typename(expr) - })?; + let ergo_code = tagged_union_type_alias_code( + name, + ir, + tu, + &disc_field_camel, + Some(PropertyNamingMode::CamelCase), + type_expr_str, + )?; // --- fromJSON / toJSON --- - let from_json = build_tagged_union_from_json(name, &wire_name, tu, convertible)?; - let to_json = build_tagged_union_to_json(name, &wire_name, tu, convertible)?; + let from_json = build_tagged_union_from_json(name, &wire_name, ir, tu, convertible)?; + let to_json = build_tagged_union_to_json(name, &wire_name, ir, tu, convertible)?; let mut fb = FileSpec::builder_with(&filename, ts.clone()); if let Some(doc) = &schema.description { @@ -930,27 +1123,40 @@ fn emit_tagged_union_file_camel_case( } fb = fb.add_code(wire_code); fb = fb.add_code(ergo_code); + for import in tagged_union_type_imports(ir, tu, convertible, true) { + fb = fb.add_import(import); + } // Add converter imports for Named content types (only if convertible) + let mut converter_refs = HashSet::new(); for variant in &tu.variants { - if let IrTypeExpr::Named(ref_name) = &variant.content_type - && convertible.contains(ref_name) - { - let pascal = ref_name.to_pascal_case(); - let module = format!("./{pascal}"); - let base = fn_base_name(&pascal); - let from_fn = format!("{base}FromJSON"); - let to_fn = format!("{base}ToJSON"); - fb = fb.add_import(ImportSpec::named(&module, &from_fn)); - fb = fb.add_import(ImportSpec::named(&module, &to_fn)); + if is_tag_only_variant( + ir, + &tu.discriminator_field, + &variant.discriminator_value, + &variant.content_type, + ) { + continue; } + collect_named_refs_from_expr(&variant.content_type, &mut converter_refs, convertible); + } + let mut converter_refs: Vec = converter_refs.into_iter().collect(); + converter_refs.sort(); + for ref_name in converter_refs { + let pascal = ref_name.to_pascal_case(); + let module = format!("./{pascal}"); + let base = fn_base_name(&pascal); + let from_fn = format!("{base}FromJSON"); + let to_fn = format!("{base}ToJSON"); + fb = fb.add_import(ImportSpec::named(&module, &from_fn)); + fb = fb.add_import(ImportSpec::named(&module, &to_fn)); } fb = fb.add_code(from_json); fb = fb.add_code(to_json); if flags.emit_type_guards { - let guards = build_tagged_union_type_guards(name, tu); + let guards = build_tagged_union_type_guards(name, ir, tu, true); for guard in guards { fb = fb.add_code(guard); } @@ -963,6 +1169,7 @@ fn emit_tagged_union_file_camel_case( fn build_tagged_union_from_json( name: &str, wire_name: &str, + ir: &IrSpec, tu: &IrTaggedUnion, convertible: &HashSet, ) -> Option { @@ -988,6 +1195,8 @@ fn build_tagged_union_from_json( val, &disc_field_camel, &variant.content_type, + ir, + &tu.discriminator_field, convertible, ); case_lines.push(format!("case '{val}': {case_body}")); @@ -1009,10 +1218,15 @@ fn internal_or_adjacent_from_json_case( val: &str, disc_field_camel: &str, content_type: &IrTypeExpr, + ir: &IrSpec, + disc_field: &str, convertible: &HashSet, ) -> String { match tagging { TaggingStyle::Internal => { + if is_tag_only_variant(ir, disc_field, val, content_type) { + return format!("return {{ {disc_field_camel}: '{val}' }};"); + } if let IrTypeExpr::Named(ref_name) = content_type { if convertible.contains(ref_name) { let pascal = ref_name.to_pascal_case(); @@ -1026,29 +1240,17 @@ fn internal_or_adjacent_from_json_case( } } TaggingStyle::Adjacent { content_field } => { + if is_tag_only_variant(ir, disc_field, val, content_type) { + return format!("return {{ {disc_field_camel}: '{val}' }};"); + } let content_camel = content_field.to_lower_camel_case(); let content_access = if is_valid_ts_identifier(content_field) { format!("json.{content_field}") } else { format!("json['{content_field}']") }; - if let IrTypeExpr::Named(ref_name) = content_type { - if convertible.contains(ref_name) { - let pascal = ref_name.to_pascal_case(); - let converter = format!("{}FromJSON", fn_base_name(&pascal)); - format!( - "return {{ {disc_field_camel}: '{val}', {content_camel}: {converter}({content_access}) }};" - ) - } else { - format!( - "return {{ {disc_field_camel}: '{val}', {content_camel}: {content_access} }};" - ) - } - } else { - format!( - "return {{ {disc_field_camel}: '{val}', {content_camel}: {content_access} }};" - ) - } + let content_expr = from_json_expr_inner(content_type, &content_access, convertible); + format!("return {{ {disc_field_camel}: '{val}', {content_camel}: {content_expr} }};") } TaggingStyle::External => unreachable!(), } @@ -1084,20 +1286,15 @@ fn external_variant_from_json_expr( convertible: &HashSet, ) -> String { let val_access = format!("json['{val}']"); - if let IrTypeExpr::Named(ref_name) = content_type - && convertible.contains(ref_name) - { - let pascal = ref_name.to_pascal_case(); - let converter = format!("{}FromJSON", fn_base_name(&pascal)); - return format!("{{ '{val}': {converter}({val_access}) }}"); - } - format!("{{ '{val}': {val_access} }}") + let content_expr = from_json_expr_inner(content_type, &val_access, convertible); + format!("{{ '{val}': {content_expr} }}") } /// Build toJSON for a tagged union: switch on the camelCase discriminator field. fn build_tagged_union_to_json( name: &str, wire_name: &str, + ir: &IrSpec, tu: &IrTaggedUnion, convertible: &HashSet, ) -> Option { @@ -1123,12 +1320,14 @@ fn build_tagged_union_to_json( let mut case_lines: Vec = Vec::new(); for variant in &tu.variants { let val = &variant.discriminator_value; + let tag_only = is_tag_only_variant(ir, &tu.discriminator_field, val, &variant.content_type); let case_body = internal_or_adjacent_to_json_case( &tu.tagging, val, &disc_wire_key, wire_name, &variant.content_type, + tag_only, convertible, ); case_lines.push(format!("case '{val}': {case_body}")); @@ -1151,10 +1350,14 @@ fn internal_or_adjacent_to_json_case( disc_wire_key: &str, wire_name: &str, content_type: &IrTypeExpr, + tag_only: bool, convertible: &HashSet, ) -> String { match tagging { TaggingStyle::Internal => { + if tag_only { + return format!("return {{ {disc_wire_key}: '{val}' }} as {wire_name};"); + } if let IrTypeExpr::Named(ref_name) = content_type { if convertible.contains(ref_name) { let pascal = ref_name.to_pascal_case(); @@ -1170,6 +1373,9 @@ fn internal_or_adjacent_to_json_case( } } TaggingStyle::Adjacent { content_field } => { + if tag_only { + return format!("return {{ {disc_wire_key}: '{val}' }};"); + } let content_camel = content_field.to_lower_camel_case(); let content_wire_key = if is_valid_ts_identifier(content_field) { content_field.clone() @@ -1181,23 +1387,8 @@ fn internal_or_adjacent_to_json_case( } else { format!("value['{content_camel}']") }; - if let IrTypeExpr::Named(ref_name) = content_type { - if convertible.contains(ref_name) { - let pascal = ref_name.to_pascal_case(); - let converter = format!("{}ToJSON", fn_base_name(&pascal)); - format!( - "return {{ {disc_wire_key}: '{val}', {content_wire_key}: {converter}({content_access}) }};" - ) - } else { - format!( - "return {{ {disc_wire_key}: '{val}', {content_wire_key}: {content_access} }};" - ) - } - } else { - format!( - "return {{ {disc_wire_key}: '{val}', {content_wire_key}: {content_access} }};" - ) - } + let content_expr = to_json_expr_inner(content_type, &content_access, convertible); + format!("return {{ {disc_wire_key}: '{val}', {content_wire_key}: {content_expr} }};") } TaggingStyle::External => unreachable!(), } @@ -1233,14 +1424,8 @@ fn external_variant_to_json_expr( convertible: &HashSet, ) -> String { let val_access = format!("value['{val}']"); - if let IrTypeExpr::Named(ref_name) = content_type - && convertible.contains(ref_name) - { - let pascal = ref_name.to_pascal_case(); - let converter = format!("{}ToJSON", fn_base_name(&pascal)); - return format!("{{ '{val}': {converter}({val_access}) }}"); - } - format!("{{ '{val}': {val_access} }}") + let content_expr = to_json_expr_inner(content_type, &val_access, convertible); + format!("{{ '{val}': {content_expr} }}") } // --------------------------------------------------------------------------- @@ -1271,8 +1456,9 @@ fn emit_union_file_camel_case( if union.nullable { wire_members.push("null".to_string()); } + let wire_rhs = simplify_union_strings(wire_members); let wire_type = sigil_quote!(TypeScript { - export type $N(wire_name.as_str()) = $for(member in &wire_members; separator = " | ") { $L(member.as_str()) }; + export type $N(wire_name.as_str()) = $L(wire_rhs.as_str()); }) .ok()?; fb = fb.add_code(wire_type); @@ -1282,8 +1468,9 @@ fn emit_union_file_camel_case( if union.nullable { ergo_members.push("null".to_string()); } + let ergo_rhs = simplify_union_strings(ergo_members); let ergo_type = sigil_quote!(TypeScript { - export type $N(name) = $for(member in &ergo_members; separator = " | ") { $L(member.as_str()) }; + export type $N(name) = $L(ergo_rhs.as_str()); }) .ok()?; fb = fb.add_code(ergo_type); @@ -1310,19 +1497,14 @@ fn emit_union_file_camel_case( fb = fb.add_code(to_json); // Add imports for Named members (wire + ergonomic types only if convertible) + let mut seen_imports = HashSet::new(); + let mut imports = Vec::new(); for member in &union.members { - if let IrTypeExpr::Named(ref_name) = member { - let member_pascal = ref_name.to_pascal_case(); - fb = fb.add_import(ImportSpec::named_type( - &format!("./{member_pascal}"), - &member_pascal, - )); - if convertible.contains(ref_name) { - fb = fb.add_import(ImportSpec::named_type( - &format!("./{member_pascal}"), - &format!("{member_pascal}$Wire"), - )); - } + collect_type_imports_from_expr(member, &mut seen_imports, &mut imports, convertible, true); + } + if ergo_rhs != "unknown" || wire_rhs != "unknown" { + for import in imports { + fb = fb.add_import(import); } } @@ -1456,14 +1638,87 @@ fn emit_intersection_file_camel_case( fb.build().ok() } +fn tagged_union_type_imports( + ir: &IrSpec, + tu: &IrTaggedUnion, + convertible: &HashSet, + include_wire: bool, +) -> Vec { + let mut seen = HashSet::new(); + let mut imports = Vec::new(); + for variant in &tu.variants { + if is_tag_only_variant( + ir, + &tu.discriminator_field, + &variant.discriminator_value, + &variant.content_type, + ) { + continue; + } + collect_type_imports_from_expr( + &variant.content_type, + &mut seen, + &mut imports, + convertible, + include_wire, + ); + } + imports +} + +fn collect_type_imports_from_expr( + expr: &IrTypeExpr, + seen: &mut HashSet, + imports: &mut Vec, + convertible: &HashSet, + include_wire: bool, +) { + match expr { + IrTypeExpr::Named(ref_name) => { + let pascal = ref_name.to_pascal_case(); + if seen.insert(pascal.clone()) { + imports.push(ImportSpec::named_type(&format!("./{pascal}"), &pascal)); + } + if include_wire && convertible.contains(ref_name) { + let wire = format!("{pascal}$Wire"); + if seen.insert(wire.clone()) { + imports.push(ImportSpec::named_type(&format!("./{pascal}"), &wire)); + } + } + } + IrTypeExpr::Array(inner) | IrTypeExpr::Nullable(inner) | IrTypeExpr::Map(inner) => { + collect_type_imports_from_expr(inner, seen, imports, convertible, include_wire); + } + IrTypeExpr::Union(members) => { + for member in members { + collect_type_imports_from_expr(member, seen, imports, convertible, include_wire); + } + } + IrTypeExpr::Primitive(_) + | IrTypeExpr::StringLiteral(_) + | IrTypeExpr::StringEnum(_) + | IrTypeExpr::Any => {} + } +} + /// Build `is*` type guard functions for a tagged union. /// /// Returns one CodeBlock per contentful (non-empty) variant. -fn build_tagged_union_type_guards(name: &str, tu: &IrTaggedUnion) -> Vec { +fn build_tagged_union_type_guards( + name: &str, + ir: &IrSpec, + tu: &IrTaggedUnion, + camel_case: bool, +) -> Vec { tu.variants .iter() .filter(|variant| { - !is_unspecified_variant( + !is_tag_only_variant( + ir, + &tu.discriminator_field, + &variant.discriminator_value, + &variant.content_type, + ) && !is_unspecified_variant( &variant.discriminator_value, &tu.discriminator_field, &variant.content_type, @@ -1476,6 +1731,7 @@ fn build_tagged_union_type_guards(name: &str, tu: &IrTaggedUnion) -> Vec bool { + let IrTypeExpr::Named(name) = content_type else { + return false; + }; + let Some(schema) = ir.schemas.get(name) else { + return false; + }; + let IrSchemaKind::Object(obj) = &schema.kind else { + return false; + }; + if obj.additional_properties.is_some() { + return false; + } + if obj.properties.len() != 1 { + return false; + } + let Some(prop) = obj.properties.get(disc_field) else { + return false; + }; + matches!(&prop.type_expr, IrTypeExpr::StringLiteral(value) if value == disc_value) +} + // --------------------------------------------------------------------------- // Variant helpers — shared between tagged-union type emission and guard emission // --------------------------------------------------------------------------- +#[derive(Debug, Clone, Copy)] +enum PropertyNamingMode { + CamelCase, +} + /// Build the TS type expression for one variant, with `content` slotted in. fn variant_type_format( tagging: &TaggingStyle, disc_field: &str, disc_value: &str, + content_field_mode: Option, content: &str, ) -> String { match tagging { @@ -1528,7 +1817,11 @@ fn variant_type_format( format!("({{ {disc_field}: '{disc_value}' }} & {content})") } TaggingStyle::Adjacent { content_field } => { - format!("{{ {disc_field}: '{disc_value}'; {content_field}: {content} }}") + let emitted_content_field = match content_field_mode { + Some(PropertyNamingMode::CamelCase) => content_field.to_lower_camel_case(), + None => content_field.clone(), + }; + format!("{{ {disc_field}: '{disc_value}'; {emitted_content_field}: {content} }}") } TaggingStyle::External => { format!("{{ '{disc_value}': {content} }}") @@ -1536,6 +1829,25 @@ fn variant_type_format( } } +fn tagged_variant_type_string( + tagging: &TaggingStyle, + disc_field: &str, + disc_value: &str, + content_field_mode: Option, + content: &str, + tag_only: bool, +) -> String { + if tag_only + && matches!( + tagging, + TaggingStyle::Internal | TaggingStyle::Adjacent { .. } + ) + { + return format!("{{ {disc_field}: '{disc_value}' }}"); + } + variant_type_format(tagging, disc_field, disc_value, content_field_mode, content) +} + /// Build the runtime narrowing expression for one variant. fn variant_check_body(tagging: &TaggingStyle, disc_field: &str, disc_value: &str) -> String { match tagging { @@ -1559,22 +1871,27 @@ impl std::fmt::Display for TsTypeDisplay<'_> { match self.0 { IrTypeExpr::Named(name) => write!(f, "{}", name.to_pascal_case()), IrTypeExpr::Primitive(p) => write!(f, "{}", primitive_to_ts(p)), - IrTypeExpr::Nullable(inner) => write!(f, "{} | null", TsTypeDisplay(inner)), + IrTypeExpr::Nullable(inner) => write!( + f, + "{}", + simplify_union_strings(vec![TsTypeDisplay(inner).to_string(), "null".to_string()]) + ), IrTypeExpr::Array(inner) => write!(f, "readonly {}[]", TsTypeDisplay(inner)), IrTypeExpr::Map(inner) => { write!(f, "Record", TsTypeDisplay(inner)) } IrTypeExpr::StringLiteral(s) => write!(f, "'{s}'"), - IrTypeExpr::StringEnum(values) => { - let parts: Vec = values.iter().map(|v| format!("'{v}'")).collect(); - write!(f, "{}", parts.join(" | ")) - } + IrTypeExpr::StringEnum(values) => write!( + f, + "{}", + simplify_union_strings(values.iter().map(|v| format!("'{v}'")).collect()) + ), IrTypeExpr::Union(members) => { let parts: Vec = members .iter() .map(|m| TsTypeDisplay(m).to_string()) .collect(); - write!(f, "{}", parts.join(" | ")) + write!(f, "{}", simplify_union_strings(parts)) } IrTypeExpr::Any => write!(f, "unknown"), } @@ -1591,12 +1908,24 @@ fn guard_check_and_type( disc_field: &str, disc_value: &str, content_type: &IrTypeExpr, + camel_case: bool, ) -> (String, String) { - let check = variant_check_body(tagging, disc_field, disc_value); + let emitted_disc_field = if camel_case { + disc_field.to_lower_camel_case() + } else { + disc_field.to_string() + }; + let content_field_mode = if camel_case { + Some(PropertyNamingMode::CamelCase) + } else { + None + }; + let check = variant_check_body(tagging, &emitted_disc_field, disc_value); let ty = variant_type_format( tagging, - disc_field, + &emitted_disc_field, disc_value, + content_field_mode, &TsTypeDisplay(content_type).to_string(), ); (check, ty) @@ -1604,27 +1933,40 @@ fn guard_check_and_type( fn tagged_union_type_alias_code( name: &str, + ir: &IrSpec, tu: &IrTaggedUnion, discriminator_field: &str, + content_field_mode: Option, type_name_for: F, ) -> Option where - F: Fn(&IrTypeExpr) -> TypeName, + F: Fn(&IrTypeExpr) -> String, { - 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(" }") }; + let members: Vec = tu + .variants + .iter() + .map(|variant| { + let tag_only = is_tag_only_variant( + ir, + &tu.discriminator_field, + &variant.discriminator_value, + &variant.content_type, + ); + tagged_variant_type_string( + &tu.tagging, + discriminator_field, + &variant.discriminator_value, + content_field_mode, + &type_name_for(&variant.content_type), + tag_only, + ) }) - .ok(), - } + .collect(); + + sigil_quote!(TypeScript { + export type $N(name) = $for(member in &members; separator = " | ") { $L(member.as_str()) }; + }) + .ok() } /// Build the pipe-joined literal body for an enum: `'a' | 1 | true | null`. @@ -1642,7 +1984,7 @@ fn enum_union_body(en: &IrEnum) -> Option { }) .collect(); let parts = parts?; - Some(parts.join(" | ")) + Some(simplify_union_strings(parts)) } /// Render a JSON value as a TS literal. Used for mixed-type enums. @@ -1657,15 +1999,20 @@ fn json_value_to_ts_literal(v: &serde_json::Value) -> Option { } } -fn build_field(prop: &IrProperty) -> Option { +fn build_field(prop: &IrProperty, unknown_aliases: &HashSet) -> Option { let field_name = if is_valid_ts_identifier(&prop.name) { prop.name.clone() } else { format!("'{}'", prop.name) }; let inner_ty = type_expr_to_typename(&prop.type_expr); - let field_ty = if prop.nullable && prop.required { - TypeName::optional(inner_ty) + let field_ty = if prop.nullable { + nullable_field_type_name( + &prop.type_expr, + inner_ty, + type_expr_str(&prop.type_expr), + unknown_aliases, + ) } else { inner_ty }; @@ -1693,9 +2040,15 @@ fn type_expr_to_typename(expr: &IrTypeExpr) -> TypeName { // are readonly. Field-level readonly on the property is still applied // by `FieldSpec::is_readonly()` elsewhere. IrTypeExpr::Array(inner) => TypeName::readonly_array(type_expr_to_typename_nested(inner)), - IrTypeExpr::Nullable(inner) => TypeName::optional(type_expr_to_typename(inner)), + IrTypeExpr::Nullable(inner) if !contains_any_named_ref(inner) => { + TypeName::raw(&type_expr_str(expr)) + } + IrTypeExpr::Nullable(inner) => union_typename(vec![ + type_expr_to_typename(inner), + TypeName::primitive("null"), + ]), IrTypeExpr::StringLiteral(s) => TypeName::raw(&format!("'{s}'")), - IrTypeExpr::StringEnum(values) => TypeName::union( + IrTypeExpr::StringEnum(values) => union_typename( values .iter() .map(|v| TypeName::raw(&format!("'{v}'"))) @@ -1705,8 +2058,11 @@ fn type_expr_to_typename(expr: &IrTypeExpr) -> TypeName { TypeName::primitive("Record"), vec![TypeName::primitive("string"), type_expr_to_typename(inner)], ), + IrTypeExpr::Union(_) if !contains_any_named_ref(expr) => { + TypeName::raw(&type_expr_str(expr)) + } IrTypeExpr::Union(members) => { - TypeName::union(members.iter().map(type_expr_to_typename).collect()) + union_typename(members.iter().map(type_expr_to_typename).collect()) } IrTypeExpr::Any => TypeName::primitive("unknown"), } @@ -1762,15 +2118,17 @@ pub fn generate_model_files( ) -> Result, String> { let header = super::project_files::render_file_header(&ir.info); let convertible = build_convertible_set(ir, flags); + let unknown_aliases = build_unknown_alias_set(ir); let mut files = Vec::with_capacity(ir.schemas.len()); for (name, schema) in &ir.schemas { - let file_spec = emit_model_file(schema, flags, &convertible, ts).ok_or_else(|| { - format!( - "sigil_emit: unsupported schema kind for {name}: {:?}", - schema.kind - ) - })?; + let file_spec = emit_model_file(schema, ir, flags, &convertible, &unknown_aliases, ts) + .ok_or_else(|| { + format!( + "sigil_emit: unsupported schema kind for {name}: {:?}", + schema.kind + ) + })?; let body = file_spec .render(100) .map_err(|e| format!("sigil_emit: render {name}: {e}"))?; diff --git a/src/ir/lower/v30.rs b/src/ir/lower/v30.rs index 4c9ab74ea..bede55c54 100644 --- a/src/ir/lower/v30.rs +++ b/src/ir/lower/v30.rs @@ -359,6 +359,8 @@ impl<'a> LowerCtx<'a> { return Ok(None); } + let tagging = classify_tagging_style(&patterns); + // Build variants // For variants detected as ExternallyTagged in a mixed union that has a // common tag field, re-interpret them as InternallyTagged unit variants. @@ -366,16 +368,24 @@ impl<'a> LowerCtx<'a> { for (member, pattern) in obj.one_of.iter().zip(patterns.iter()) { let pattern = pattern.as_ref().unwrap(); let effective_pattern = match pattern { - TaggedEnumPattern::ExternallyTagged { .. } => { - &TaggedEnumPattern::InternallyTagged { - variant_name: pattern.variant_name().to_string(), - tag_field: first_tag.to_string(), + TaggedEnumPattern::ExternallyTagged { .. } => TaggedEnumPattern::InternallyTagged { + variant_name: pattern.variant_name().to_string(), + tag_field: first_tag.to_string(), + }, + TaggedEnumPattern::AdjacentlyTagged { + variant_name, + tag_field, + .. + } if matches!(tagging, TaggingStyle::Internal) => { + TaggedEnumPattern::InternallyTagged { + variant_name: variant_name.clone(), + tag_field: tag_field.clone(), } } - other => other, + other => other.clone(), }; let (disc_value, content_type) = - self.extract_tagged_variant(parent_name, member, effective_pattern)?; + self.extract_tagged_variant(parent_name, member, &effective_pattern)?; let description = match member { ObjectOrReference::Object(obj) => obj.description.clone(), _ => None, @@ -389,7 +399,7 @@ impl<'a> LowerCtx<'a> { Ok(Some(IrTaggedUnion { discriminator_field: first_tag.to_string(), - tagging: classify_tagging_style(&patterns), + tagging, variants, })) } @@ -518,7 +528,10 @@ impl<'a> LowerCtx<'a> { TaggedEnumPattern::ExternallyTagged { .. } => { if let ObjectOrReference::Object(obj) = member { if let Some((prop_name, prop_schema)) = obj.properties.iter().next() { - let content_type = self.lower_schema_ref(prop_schema)?; + let content_type = self.lower_schema_ref_with_promotion( + &format!("{parent_name}{}", prop_name.to_pascal_case()), + prop_schema, + )?; Ok((prop_name.clone(), content_type)) } else { Ok((pattern.variant_name().to_string(), IrTypeExpr::Any)) @@ -549,7 +562,17 @@ impl<'a> LowerCtx<'a> { let content_type = obj .properties .get(content_field.as_str()) - .map(|prop| self.lower_schema_ref(prop)) + .map(|prop| { + self.lower_schema_ref_with_promotion( + &format!( + "{}{}{}", + parent_name, + disc_value.to_pascal_case(), + content_field.to_pascal_case() + ), + prop, + ) + }) .transpose()? .unwrap_or(IrTypeExpr::Any); @@ -1185,10 +1208,15 @@ fn classify_tagging_style( use crate::ir::tagged_enum_pattern::TaggedEnumPattern; let mut saw_internal = false; let mut saw_adjacent: Option = None; + let mut adjacent_consistent = true; for p in patterns.iter().flatten() { match p { TaggedEnumPattern::AdjacentlyTagged { content_field, .. } => { - saw_adjacent = Some(content_field.clone()); + if let Some(existing) = &saw_adjacent { + adjacent_consistent &= existing == content_field; + } else { + saw_adjacent = Some(content_field.clone()); + } } TaggedEnumPattern::InternallyTagged { .. } => { saw_internal = true; @@ -1196,7 +1224,10 @@ fn classify_tagging_style( TaggedEnumPattern::ExternallyTagged { .. } | TaggedEnumPattern::Untagged { .. } => {} } } - if let Some(content_field) = saw_adjacent { + if let Some(content_field) = saw_adjacent + && adjacent_consistent + && !saw_internal + { return TaggingStyle::Adjacent { content_field }; } if saw_internal { diff --git a/src/ir/lower/v31.rs b/src/ir/lower/v31.rs index 25279e7cf..009f9ae27 100644 --- a/src/ir/lower/v31.rs +++ b/src/ir/lower/v31.rs @@ -372,6 +372,8 @@ impl<'a> LowerCtx<'a> { return Ok(None); } + let tagging = classify_tagging_style(&patterns); + // Build variants // For variants detected as ExternallyTagged in a mixed union that has a // common tag field, re-interpret them as InternallyTagged unit variants. @@ -379,16 +381,24 @@ impl<'a> LowerCtx<'a> { for (member, pattern) in obj.one_of.iter().zip(patterns.iter()) { let pattern = pattern.as_ref().unwrap(); let effective_pattern = match pattern { - TaggedEnumPattern::ExternallyTagged { .. } => { - &TaggedEnumPattern::InternallyTagged { - variant_name: pattern.variant_name().to_string(), - tag_field: first_tag.to_string(), + TaggedEnumPattern::ExternallyTagged { .. } => TaggedEnumPattern::InternallyTagged { + variant_name: pattern.variant_name().to_string(), + tag_field: first_tag.to_string(), + }, + TaggedEnumPattern::AdjacentlyTagged { + variant_name, + tag_field, + .. + } if matches!(tagging, TaggingStyle::Internal) => { + TaggedEnumPattern::InternallyTagged { + variant_name: variant_name.clone(), + tag_field: tag_field.clone(), } } - other => other, + other => other.clone(), }; let (disc_value, content_type) = - self.extract_tagged_variant(parent_name, member, effective_pattern)?; + self.extract_tagged_variant(parent_name, member, &effective_pattern)?; let description = match member { ObjectOrReference::Object(obj) => obj.description.clone(), _ => None, @@ -402,7 +412,7 @@ impl<'a> LowerCtx<'a> { Ok(Some(IrTaggedUnion { discriminator_field: first_tag.to_string(), - tagging: classify_tagging_style(&patterns), + tagging, variants, })) } @@ -542,7 +552,10 @@ impl<'a> LowerCtx<'a> { // property value is the content type if let ObjectOrReference::Object(obj) = member { if let Some((prop_name, prop_schema)) = obj.properties.iter().next() { - let content_type = self.lower_schema_ref(prop_schema)?; + let content_type = self.lower_schema_ref_with_promotion( + &format!("{parent_name}{}", prop_name.to_pascal_case()), + prop_schema, + )?; Ok((prop_name.clone(), content_type)) } else { Ok((pattern.variant_name().to_string(), IrTypeExpr::Any)) @@ -575,7 +588,17 @@ impl<'a> LowerCtx<'a> { let content_type = obj .properties .get(content_field.as_str()) - .map(|prop| self.lower_schema_ref(prop)) + .map(|prop| { + self.lower_schema_ref_with_promotion( + &format!( + "{}{}{}", + parent_name, + disc_value.to_pascal_case(), + content_field.to_pascal_case() + ), + prop, + ) + }) .transpose()? .unwrap_or(IrTypeExpr::Any); @@ -1315,10 +1338,15 @@ fn classify_tagging_style( use crate::ir::tagged_enum_pattern::TaggedEnumPattern; let mut saw_internal = false; let mut saw_adjacent: Option = None; + let mut adjacent_consistent = true; for p in patterns.iter().flatten() { match p { TaggedEnumPattern::AdjacentlyTagged { content_field, .. } => { - saw_adjacent = Some(content_field.clone()); + if let Some(existing) = &saw_adjacent { + adjacent_consistent &= existing == content_field; + } else { + saw_adjacent = Some(content_field.clone()); + } } TaggedEnumPattern::InternallyTagged { .. } => { saw_internal = true; @@ -1326,7 +1354,10 @@ fn classify_tagging_style( TaggedEnumPattern::ExternallyTagged { .. } | TaggedEnumPattern::Untagged { .. } => {} } } - if let Some(content_field) = saw_adjacent { + if let Some(content_field) = saw_adjacent + && adjacent_consistent + && !saw_internal + { return TaggingStyle::Adjacent { content_field }; } if saw_internal { diff --git a/src/ir/lower/v32.rs b/src/ir/lower/v32.rs index e6eb0154c..f02c8af6c 100644 --- a/src/ir/lower/v32.rs +++ b/src/ir/lower/v32.rs @@ -369,6 +369,8 @@ impl<'a> LowerCtx<'a> { return Ok(None); } + let tagging = classify_tagging_style(&patterns); + // Build variants // For variants detected as ExternallyTagged in a mixed union that has a // common tag field, re-interpret them as InternallyTagged unit variants. @@ -376,16 +378,24 @@ impl<'a> LowerCtx<'a> { for (member, pattern) in obj.one_of.iter().zip(patterns.iter()) { let pattern = pattern.as_ref().unwrap(); let effective_pattern = match pattern { - TaggedEnumPattern::ExternallyTagged { .. } => { - &TaggedEnumPattern::InternallyTagged { - variant_name: pattern.variant_name().to_string(), - tag_field: first_tag.to_string(), + TaggedEnumPattern::ExternallyTagged { .. } => TaggedEnumPattern::InternallyTagged { + variant_name: pattern.variant_name().to_string(), + tag_field: first_tag.to_string(), + }, + TaggedEnumPattern::AdjacentlyTagged { + variant_name, + tag_field, + .. + } if matches!(tagging, TaggingStyle::Internal) => { + TaggedEnumPattern::InternallyTagged { + variant_name: variant_name.clone(), + tag_field: tag_field.clone(), } } - other => other, + other => other.clone(), }; let (disc_value, content_type) = - self.extract_tagged_variant(parent_name, member, effective_pattern)?; + self.extract_tagged_variant(parent_name, member, &effective_pattern)?; let description = match member { ObjectOrReference::Object(obj) => obj.description.clone(), _ => None, @@ -399,7 +409,7 @@ impl<'a> LowerCtx<'a> { Ok(Some(IrTaggedUnion { discriminator_field: first_tag.to_string(), - tagging: classify_tagging_style(&patterns), + tagging, variants, })) } @@ -539,7 +549,10 @@ impl<'a> LowerCtx<'a> { // property value is the content type if let ObjectOrReference::Object(obj) = member { if let Some((prop_name, prop_schema)) = obj.properties.iter().next() { - let content_type = self.lower_schema_ref(prop_schema)?; + let content_type = self.lower_schema_ref_with_promotion( + &format!("{parent_name}{}", prop_name.to_pascal_case()), + prop_schema, + )?; Ok((prop_name.clone(), content_type)) } else { Ok((pattern.variant_name().to_string(), IrTypeExpr::Any)) @@ -572,7 +585,17 @@ impl<'a> LowerCtx<'a> { let content_type = obj .properties .get(content_field.as_str()) - .map(|prop| self.lower_schema_ref(prop)) + .map(|prop| { + self.lower_schema_ref_with_promotion( + &format!( + "{}{}{}", + parent_name, + disc_value.to_pascal_case(), + content_field.to_pascal_case() + ), + prop, + ) + }) .transpose()? .unwrap_or(IrTypeExpr::Any); @@ -1322,10 +1345,15 @@ fn classify_tagging_style( use crate::ir::tagged_enum_pattern::TaggedEnumPattern; let mut saw_internal = false; let mut saw_adjacent: Option = None; + let mut adjacent_consistent = true; for p in patterns.iter().flatten() { match p { TaggedEnumPattern::AdjacentlyTagged { content_field, .. } => { - saw_adjacent = Some(content_field.clone()); + if let Some(existing) = &saw_adjacent { + adjacent_consistent &= existing == content_field; + } else { + saw_adjacent = Some(content_field.clone()); + } } TaggedEnumPattern::InternallyTagged { .. } => { saw_internal = true; @@ -1333,7 +1361,10 @@ fn classify_tagging_style( TaggedEnumPattern::ExternallyTagged { .. } | TaggedEnumPattern::Untagged { .. } => {} } } - if let Some(content_field) = saw_adjacent { + if let Some(content_field) = saw_adjacent + && adjacent_consistent + && !saw_internal + { return TaggingStyle::Adjacent { content_field }; } if saw_internal { diff --git a/src/ir/tagged_enum_pattern.rs b/src/ir/tagged_enum_pattern.rs index 98e91bbb9..d5bcd72a2 100644 --- a/src/ir/tagged_enum_pattern.rs +++ b/src/ir/tagged_enum_pattern.rs @@ -130,9 +130,7 @@ impl TaggedEnumPattern { } match prop_schema { ObjectOrReference::Object(content_obj) => { - if content_obj.enum_values.is_empty() - && !content_obj.properties.is_empty() - { + if content_obj.enum_values.is_empty() { content_field = Some(prop_name.clone()); } } @@ -245,9 +243,7 @@ impl TaggedEnumPattern { } match prop_schema { ObjectOrReference32::Object(content_obj) => { - if content_obj.enum_values.is_empty() - && !content_obj.properties.is_empty() - { + if content_obj.enum_values.is_empty() { content_field = Some(prop_name.clone()); } } @@ -341,9 +337,7 @@ impl TaggedEnumPattern { } match prop_schema { ObjectOrReference30::Object(content_obj) => { - if content_obj.enum_values.is_empty() - && !content_obj.properties.is_empty() - { + if content_obj.enum_values.is_empty() { content_field = Some(prop_name.clone()); } } diff --git a/tests/fixtures/valid/type-aliases/typescript-fetch-vp-camel-case-tagged-nullable-unions.yaml b/tests/fixtures/valid/type-aliases/typescript-fetch-vp-camel-case-tagged-nullable-unions.yaml new file mode 100644 index 000000000..72060ebe7 --- /dev/null +++ b/tests/fixtures/valid/type-aliases/typescript-fetch-vp-camel-case-tagged-nullable-unions.yaml @@ -0,0 +1,250 @@ +openapi: 3.1.0 +info: + title: TypeScript Fetch VP CamelCase Masked Regression Test + description: Masked fixture for TypeScript Fetch camelCase regression coverage. + version: 1.0.0 + +paths: + /foo: + post: + summary: Create foo envelope + operationId: create_foo_envelope + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FooEnvelope' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/FooEnvelope' + +components: + schemas: + FooPayloadBody: + type: object + required: + - bar_value + properties: + bar_value: + type: string + + FooTaggedRef: + oneOf: + - type: object + required: + - type + - bar_payload + properties: + type: + type: string + enum: + - foo_ref_payload + bar_payload: + $ref: '#/components/schemas/FooPayloadBody' + + FooObjectPayload: + type: object + required: + - object_name + properties: + object_name: + type: string + + FooTagOnlyUnion: + oneOf: + - type: object + required: + - type + properties: + type: + type: string + enum: + - foo_unit_alpha + - type: object + required: + - type + properties: + type: + type: string + enum: + - foo_unit_beta + - type: object + required: + - type + - object_payload + properties: + type: + type: string + enum: + - foo_object_payload + object_payload: + $ref: '#/components/schemas/FooObjectPayload' + + FooAdjacentPayload: + oneOf: + - type: object + required: + - type + properties: + type: + type: string + enum: + - foo_inline_none + - type: object + required: + - type + - payload_value + properties: + type: + type: string + enum: + - foo_inline_text + payload_value: + type: string + - type: object + required: + - type + - payload_value + properties: + type: + type: string + enum: + - foo_inline_object + payload_value: + type: object + required: + - bar_value + properties: + bar_value: + type: string + - type: object + required: + - type + - payload_value + properties: + type: + type: string + enum: + - foo_inline_list + payload_value: + type: array + items: + $ref: '#/components/schemas/FooArrayPart' + + FooArrayPart: + type: object + required: + - bar_text + properties: + bar_text: + type: string + + FooUnionContent: + oneOf: + - type: string + - type: array + items: + $ref: '#/components/schemas/FooArrayPart' + + FooClosedVariant: + type: object + required: + - type + properties: + type: + type: string + enum: + - foo_closed_variant + + FooOpenVariant: + type: object + required: + - type + properties: + type: + type: string + enum: + - foo_open_variant + additionalProperties: + type: string + + FooMappedTagUnion: + oneOf: + - $ref: '#/components/schemas/FooClosedVariant' + - $ref: '#/components/schemas/FooOpenVariant' + discriminator: + propertyName: type + mapping: + foo_closed_variant: '#/components/schemas/FooClosedVariant' + foo_open_variant: '#/components/schemas/FooOpenVariant' + + FooNullableItem: + type: object + required: + - item_name + properties: + item_name: + type: string + + FooNullableItems: + type: object + required: + - content + properties: + content: + type: + - array + - 'null' + items: + $ref: '#/components/schemas/FooNullableItem' + + FooNullableRefHolder: + type: object + required: + - maybe_items + properties: + maybe_items: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/FooNullableItems' + + FooAnyValue: + type: + - object + - array + - string + - number + - integer + - boolean + - 'null' + + FooEnvelope: + type: object + required: + - tagged_ref + - tag_only_union + - adjacent_payload + - union_content + - mapped_tag_union + - nullable_ref_holder + - any_value + properties: + tagged_ref: + $ref: '#/components/schemas/FooTaggedRef' + tag_only_union: + $ref: '#/components/schemas/FooTagOnlyUnion' + adjacent_payload: + $ref: '#/components/schemas/FooAdjacentPayload' + union_content: + $ref: '#/components/schemas/FooUnionContent' + mapped_tag_union: + $ref: '#/components/schemas/FooMappedTagUnion' + nullable_ref_holder: + $ref: '#/components/schemas/FooNullableRefHolder' + any_value: + $ref: '#/components/schemas/FooAnyValue' diff --git a/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event.go.golden b/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event.go.golden index f063fd159..17f3af19f 100644 --- a/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event.go.golden +++ b/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event.go.golden @@ -14,5 +14,6 @@ package models +// Discriminator: kind (internal). type Event = any diff --git a/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member2.go.golden b/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_created.go.golden similarity index 96% rename from tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member2.go.golden rename to tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_created.go.golden index 491a94ca2..f23902aaf 100644 --- a/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member2.go.golden +++ b/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_created.go.golden @@ -15,7 +15,7 @@ package models // Created event with timestamp -type EventMember2 struct { +type EventEventCreated struct { Kind string `json:"kind"` // When the event was created Timestamp string `json:"timestamp"` diff --git a/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member1.go.golden b/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_unspecified.go.golden similarity index 95% rename from tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member1.go.golden rename to tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_unspecified.go.golden index af2242164..b3b34127b 100644 --- a/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member1.go.golden +++ b/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_unspecified.go.golden @@ -15,6 +15,6 @@ package models // Unspecified event variant (empty) -type EventMember1 struct { +type EventEventUnspecified struct { Kind string `json:"kind"` } diff --git a/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member3.go.golden b/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_updated.go.golden similarity index 96% rename from tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member3.go.golden rename to tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_updated.go.golden index 3cd7f3c72..2c2ecd02e 100644 --- a/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_member3.go.golden +++ b/tests/golden/go/go-http/type-aliases-discriminated-union-inline-discriminator-only/models/event_event_updated.go.golden @@ -15,7 +15,7 @@ package models // Updated event with new value -type EventMember3 struct { +type EventEventUpdated struct { Kind string `json:"kind"` // The new value after update NewValue string `json:"newValue"` diff --git a/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/Event.java.golden b/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/Event.java.golden index 69aa02233..cfa5274ba 100644 --- a/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/Event.java.golden +++ b/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/Event.java.golden @@ -14,6 +14,9 @@ package com.example.sdk.models; +/** + * Discriminator: kind (internal). + */ public class Event { private Object value; diff --git a/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.java.golden b/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.java.golden similarity index 91% rename from tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.java.golden rename to tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.java.golden index cfd38d676..1c6dd916d 100644 --- a/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.java.golden +++ b/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.java.golden @@ -17,11 +17,11 @@ package com.example.sdk.models; /** * Created event with timestamp */ -public class EventMember2 { +public class EventEventCreated { private String kind; private String timestamp; - public EventMember2(String kind, String timestamp) { + public EventEventCreated(String kind, String timestamp) { this.kind = kind; this.timestamp = timestamp; } diff --git a/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.java.golden b/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.java.golden similarity index 90% rename from tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.java.golden rename to tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.java.golden index 7927579c3..dbbe8e1d8 100644 --- a/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.java.golden +++ b/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.java.golden @@ -17,10 +17,10 @@ package com.example.sdk.models; /** * Unspecified event variant (empty) */ -public class EventMember1 { +public class EventEventUnspecified { private String kind; - public EventMember1(String kind) { + public EventEventUnspecified(String kind) { this.kind = kind; } diff --git a/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.java.golden b/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.java.golden similarity index 91% rename from tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.java.golden rename to tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.java.golden index e209b2998..71da52b6c 100644 --- a/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.java.golden +++ b/tests/golden/java/java-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.java.golden @@ -17,11 +17,11 @@ package com.example.sdk.models; /** * Updated event with new value */ -public class EventMember3 { +public class EventEventUpdated { private String kind; private String newValue; - public EventMember3(String kind, String newValue) { + public EventEventUpdated(String kind, String newValue) { this.kind = kind; this.newValue = newValue; } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/Event.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/Event.kt.golden index 41f5eebdf..aa53f7eff 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/Event.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/Event.kt.golden @@ -14,4 +14,7 @@ package com.example.sdk.models +/** + * Discriminator: kind (internal). + */ typealias Event = Any diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.kt.golden similarity index 90% rename from tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.kt.golden rename to tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.kt.golden index 4681e48f6..1bac157d4 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.kt.golden @@ -17,5 +17,5 @@ package com.example.sdk.models /** * Created event with timestamp */ -data class EventMember2(val kind: String, val timestamp: String) { +data class EventEventCreated(val kind: String, val timestamp: String) { } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.kt.golden similarity index 93% rename from tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.kt.golden rename to tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.kt.golden index 449abe4a2..55380481a 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.kt.golden @@ -17,5 +17,5 @@ package com.example.sdk.models /** * Unspecified event variant (empty) */ -data class EventMember1(val kind: String) { +data class EventEventUnspecified(val kind: String) { } diff --git a/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.kt.golden b/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.kt.golden similarity index 90% rename from tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.kt.golden rename to tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.kt.golden index 0e851578d..cf55887f0 100644 --- a/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.kt.golden +++ b/tests/golden/kotlin/kotlin-okhttp/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.kt.golden @@ -17,5 +17,5 @@ package com.example.sdk.models /** * Updated event with new value */ -data class EventMember3(val kind: String, val newValue: String) { +data class EventEventUpdated(val kind: String, val newValue: String) { } diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/__init__.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/__init__.py.golden index e26d452de..bb641a151 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/__init__.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/__init__.py.golden @@ -13,6 +13,6 @@ # "EventKindUnspecified" instead of generic "Kind". from .event import Event as Event -from .event_member1 import EventMember1 as EventMember1 -from .event_member2 import EventMember2 as EventMember2 -from .event_member3 import EventMember3 as EventMember3 +from .event_event_created import EventEventCreated as EventEventCreated +from .event_event_unspecified import EventEventUnspecified as EventEventUnspecified +from .event_event_updated import EventEventUpdated as EventEventUpdated diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden index 1d86835e8..07feba3ea 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden @@ -14,12 +14,42 @@ from __future__ import annotations -from .event_member1 import EventMember1 -from .event_member2 import EventMember2 -from .event_member3 import EventMember3 +from .event_event_created import EventEventCreated +from .event_event_unspecified import EventEventUnspecified +from .event_event_updated import EventEventUpdated + +# Discriminator: kind (internal). type Event = ( - EventMember1 - | EventMember2 - | EventMember3 + EventEventUnspecified + | EventEventCreated + | EventEventUpdated ) + + +def event_from_dict(data: dict[str, object]) -> Event: + _tag = data["kind"] + if _tag == "EVENT_UNSPECIFIED": + return EventEventUnspecified.from_dict(data) + elif _tag == "EVENT_CREATED": + return EventEventCreated.from_dict(data) + elif _tag == "EVENT_UPDATED": + return EventEventUpdated.from_dict(data) + raise ValueError(f"Unknown discriminator value for Event: {data}") + + +def event_to_dict(obj: Event) -> dict[str, object]: + if isinstance(obj, EventEventUnspecified): + result = obj.to_dict() + result["kind"] = "EVENT_UNSPECIFIED" + return result + elif isinstance(obj, EventEventCreated): + result = obj.to_dict() + result["kind"] = "EVENT_CREATED" + return result + elif isinstance(obj, EventEventUpdated): + result = obj.to_dict() + result["kind"] = "EVENT_UPDATED" + return result + raise ValueError(f"Unknown variant for Event: {type(obj)}") + diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member2.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_created.py.golden similarity index 92% rename from tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member2.py.golden rename to tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_created.py.golden index 38482300b..643682bc2 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member2.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_created.py.golden @@ -19,7 +19,7 @@ from datetime import datetime from typing import Literal @dataclass -class EventMember2: +class EventEventCreated: """Created event with timestamp.""" kind: Literal["EVENT_CREATED"] timestamp: datetime @@ -31,7 +31,7 @@ class EventMember2: return result @classmethod - def from_dict(cls, data: dict[str, object]) -> EventMember2: + def from_dict(cls, data: dict[str, object]) -> EventEventCreated: return cls( kind=data["kind"], # type: ignore[assignment] timestamp=data["timestamp"], # type: ignore[assignment] diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member1.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_unspecified.py.golden similarity index 91% rename from tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member1.py.golden rename to tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_unspecified.py.golden index 94532b27e..5887e8bd1 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member1.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_unspecified.py.golden @@ -18,7 +18,7 @@ from dataclasses import dataclass from typing import Literal @dataclass -class EventMember1: +class EventEventUnspecified: """Unspecified event variant (empty).""" kind: Literal["EVENT_UNSPECIFIED"] @@ -28,7 +28,7 @@ class EventMember1: return result @classmethod - def from_dict(cls, data: dict[str, object]) -> EventMember1: + def from_dict(cls, data: dict[str, object]) -> EventEventUnspecified: return cls( kind=data["kind"], # type: ignore[assignment] ) diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member3.py.golden b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_updated.py.golden similarity index 92% rename from tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member3.py.golden rename to tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_updated.py.golden index 249aa7026..cfd64d5bb 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member3.py.golden +++ b/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_updated.py.golden @@ -18,7 +18,7 @@ from dataclasses import dataclass from typing import Literal @dataclass -class EventMember3: +class EventEventUpdated: """Updated event with new value.""" kind: Literal["EVENT_UPDATED"] new_value: str @@ -30,7 +30,7 @@ class EventMember3: return result @classmethod - def from_dict(cls, data: dict[str, object]) -> EventMember3: + def from_dict(cls, data: dict[str, object]) -> EventEventUpdated: return cls( kind=data["kind"], # type: ignore[assignment] new_value=data["newValue"], # type: ignore[assignment] diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/__init__.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/__init__.py.golden index e26d452de..bb641a151 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/__init__.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/__init__.py.golden @@ -13,6 +13,6 @@ # "EventKindUnspecified" instead of generic "Kind". from .event import Event as Event -from .event_member1 import EventMember1 as EventMember1 -from .event_member2 import EventMember2 as EventMember2 -from .event_member3 import EventMember3 as EventMember3 +from .event_event_created import EventEventCreated as EventEventCreated +from .event_event_unspecified import EventEventUnspecified as EventEventUnspecified +from .event_event_updated import EventEventUpdated as EventEventUpdated diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden index 1d86835e8..07feba3ea 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event.py.golden @@ -14,12 +14,42 @@ from __future__ import annotations -from .event_member1 import EventMember1 -from .event_member2 import EventMember2 -from .event_member3 import EventMember3 +from .event_event_created import EventEventCreated +from .event_event_unspecified import EventEventUnspecified +from .event_event_updated import EventEventUpdated + +# Discriminator: kind (internal). type Event = ( - EventMember1 - | EventMember2 - | EventMember3 + EventEventUnspecified + | EventEventCreated + | EventEventUpdated ) + + +def event_from_dict(data: dict[str, object]) -> Event: + _tag = data["kind"] + if _tag == "EVENT_UNSPECIFIED": + return EventEventUnspecified.from_dict(data) + elif _tag == "EVENT_CREATED": + return EventEventCreated.from_dict(data) + elif _tag == "EVENT_UPDATED": + return EventEventUpdated.from_dict(data) + raise ValueError(f"Unknown discriminator value for Event: {data}") + + +def event_to_dict(obj: Event) -> dict[str, object]: + if isinstance(obj, EventEventUnspecified): + result = obj.to_dict() + result["kind"] = "EVENT_UNSPECIFIED" + return result + elif isinstance(obj, EventEventCreated): + result = obj.to_dict() + result["kind"] = "EVENT_CREATED" + return result + elif isinstance(obj, EventEventUpdated): + result = obj.to_dict() + result["kind"] = "EVENT_UPDATED" + return result + raise ValueError(f"Unknown variant for Event: {type(obj)}") + diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member2.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_created.py.golden similarity index 92% rename from tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member2.py.golden rename to tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_created.py.golden index 38482300b..643682bc2 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member2.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_created.py.golden @@ -19,7 +19,7 @@ from datetime import datetime from typing import Literal @dataclass -class EventMember2: +class EventEventCreated: """Created event with timestamp.""" kind: Literal["EVENT_CREATED"] timestamp: datetime @@ -31,7 +31,7 @@ class EventMember2: return result @classmethod - def from_dict(cls, data: dict[str, object]) -> EventMember2: + def from_dict(cls, data: dict[str, object]) -> EventEventCreated: return cls( kind=data["kind"], # type: ignore[assignment] timestamp=data["timestamp"], # type: ignore[assignment] diff --git a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member1.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_unspecified.py.golden similarity index 91% rename from tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member1.py.golden rename to tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_unspecified.py.golden index 94532b27e..5887e8bd1 100644 --- a/tests/golden/python/python-httpx/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member1.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_unspecified.py.golden @@ -18,7 +18,7 @@ from dataclasses import dataclass from typing import Literal @dataclass -class EventMember1: +class EventEventUnspecified: """Unspecified event variant (empty).""" kind: Literal["EVENT_UNSPECIFIED"] @@ -28,7 +28,7 @@ class EventMember1: return result @classmethod - def from_dict(cls, data: dict[str, object]) -> EventMember1: + def from_dict(cls, data: dict[str, object]) -> EventEventUnspecified: return cls( kind=data["kind"], # type: ignore[assignment] ) diff --git a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member3.py.golden b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_updated.py.golden similarity index 92% rename from tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member3.py.golden rename to tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_updated.py.golden index 249aa7026..cfd64d5bb 100644 --- a/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_member3.py.golden +++ b/tests/golden/python/python-requests/type-aliases-discriminated-union-inline-discriminator-only/discriminated_union_with_inline_discriminator_only_test/models/event_event_updated.py.golden @@ -18,7 +18,7 @@ from dataclasses import dataclass from typing import Literal @dataclass -class EventMember3: +class EventEventUpdated: """Updated event with new value.""" kind: Literal["EVENT_UPDATED"] new_value: str @@ -30,7 +30,7 @@ class EventMember3: return result @classmethod - def from_dict(cls, data: dict[str, object]) -> EventMember3: + def from_dict(cls, data: dict[str, object]) -> EventEventUpdated: return cls( kind=data["kind"], # type: ignore[assignment] new_value=data["newValue"], # type: ignore[assignment] diff --git a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden b/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden index 6b8e84ab9..c4333c02b 100644 --- a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden +++ b/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden @@ -16,9 +16,17 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] +#[serde(tag = "kind")] pub enum Event { - EventMember1(super::EventMember1), - EventMember2(super::EventMember2), - EventMember3(super::EventMember3), + #[serde(rename = "EVENT_UNSPECIFIED")] + EventUnspecified, + #[serde(rename = "EVENT_CREATED")] + EventCreated { + timestamp: String, + }, + #[serde(rename = "EVENT_UPDATED")] + EventUpdated { + #[serde(rename = "newValue")] + new_value: String, + }, } diff --git a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden b/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden deleted file mode 100644 index b5d24599e..000000000 --- a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden +++ /dev/null @@ -1,22 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Unspecified event variant (empty) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember1 { - pub kind: String, -} diff --git a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden b/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden deleted file mode 100644 index 322f274fc..000000000 --- a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden +++ /dev/null @@ -1,24 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Created event with timestamp -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember2 { - pub kind: String, - /// When the event was created - pub timestamp: String, -} diff --git a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden b/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden deleted file mode 100644 index 517253c32..000000000 --- a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden +++ /dev/null @@ -1,25 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Updated event with new value -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember3 { - pub kind: String, - /// The new value after update - #[serde(rename = "newValue")] - pub new_value: String, -} diff --git a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden b/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden index 70b0f6ee7..a8ac7f56a 100644 --- a/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-aioduct/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden @@ -13,11 +13,5 @@ // After the fix, these should generate properly prefixed names like // "EventKindUnspecified" instead of generic "Kind". -mod event_member1; -pub use event_member1::*; -mod event_member2; -pub use event_member2::*; -mod event_member3; -pub use event_member3::*; mod event; pub use event::*; diff --git a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden b/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden index 6b8e84ab9..c4333c02b 100644 --- a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden +++ b/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden @@ -16,9 +16,17 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] +#[serde(tag = "kind")] pub enum Event { - EventMember1(super::EventMember1), - EventMember2(super::EventMember2), - EventMember3(super::EventMember3), + #[serde(rename = "EVENT_UNSPECIFIED")] + EventUnspecified, + #[serde(rename = "EVENT_CREATED")] + EventCreated { + timestamp: String, + }, + #[serde(rename = "EVENT_UPDATED")] + EventUpdated { + #[serde(rename = "newValue")] + new_value: String, + }, } diff --git a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden b/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden deleted file mode 100644 index b5d24599e..000000000 --- a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden +++ /dev/null @@ -1,22 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Unspecified event variant (empty) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember1 { - pub kind: String, -} diff --git a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden b/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden deleted file mode 100644 index 322f274fc..000000000 --- a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden +++ /dev/null @@ -1,24 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Created event with timestamp -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember2 { - pub kind: String, - /// When the event was created - pub timestamp: String, -} diff --git a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden b/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden deleted file mode 100644 index 517253c32..000000000 --- a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden +++ /dev/null @@ -1,25 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Updated event with new value -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember3 { - pub kind: String, - /// The new value after update - #[serde(rename = "newValue")] - pub new_value: String, -} diff --git a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden b/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden index 70b0f6ee7..a8ac7f56a 100644 --- a/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-reqwest/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden @@ -13,11 +13,5 @@ // After the fix, these should generate properly prefixed names like // "EventKindUnspecified" instead of generic "Kind". -mod event_member1; -pub use event_member1::*; -mod event_member2; -pub use event_member2::*; -mod event_member3; -pub use event_member3::*; mod event; pub use event::*; diff --git a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden b/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden index 6b8e84ab9..c4333c02b 100644 --- a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden +++ b/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event.rs.golden @@ -16,9 +16,17 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] +#[serde(tag = "kind")] pub enum Event { - EventMember1(super::EventMember1), - EventMember2(super::EventMember2), - EventMember3(super::EventMember3), + #[serde(rename = "EVENT_UNSPECIFIED")] + EventUnspecified, + #[serde(rename = "EVENT_CREATED")] + EventCreated { + timestamp: String, + }, + #[serde(rename = "EVENT_UPDATED")] + EventUpdated { + #[serde(rename = "newValue")] + new_value: String, + }, } diff --git a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden b/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden deleted file mode 100644 index b5d24599e..000000000 --- a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member1.rs.golden +++ /dev/null @@ -1,22 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Unspecified event variant (empty) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember1 { - pub kind: String, -} diff --git a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden b/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden deleted file mode 100644 index 322f274fc..000000000 --- a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member2.rs.golden +++ /dev/null @@ -1,24 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Created event with timestamp -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember2 { - pub kind: String, - /// When the event was created - pub timestamp: String, -} diff --git a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden b/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden deleted file mode 100644 index 517253c32..000000000 --- a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/event_member3.rs.golden +++ /dev/null @@ -1,25 +0,0 @@ -// @generated -// Code generated by openapi-nexus. DO NOT EDIT. -// -// Discriminated Union with Inline Discriminator Only Test — 1.0.0 -// Test fixture for discriminated union where variants are inline objects -// with ONLY a discriminator property (no additional fields). -// -// This specifically tests the fix for the bug where inline objects with -// only a discriminator property (like { kind: "UNSPECIFIED" }) were -// generating generic "Kind.ts" files that conflicted across different -// discriminated unions. -// -// After the fix, these should generate properly prefixed names like -// "EventKindUnspecified" instead of generic "Kind". - -use serde::{Deserialize, Serialize}; - -/// Updated event with new value -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EventMember3 { - pub kind: String, - /// The new value after update - #[serde(rename = "newValue")] - pub new_value: String, -} diff --git a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden index 70b0f6ee7..a8ac7f56a 100644 --- a/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden +++ b/tests/golden/rust/rust-ureq/type-aliases-discriminated-union-inline-discriminator-only/src/models/mod.rs.golden @@ -13,11 +13,5 @@ // After the fix, these should generate properly prefixed names like // "EventKindUnspecified" instead of generic "Kind". -mod event_member1; -pub use event_member1::*; -mod event_member2; -pub use event_member2::*; -mod event_member3; -pub use event_member3::*; mod event; pub use event::*; diff --git a/tests/golden/typescript/typescript-fetch/additional-properties/models/LeafValue.ts.golden b/tests/golden/typescript/typescript-fetch/additional-properties/models/LeafValue.ts.golden index 610b01335..b4b2f9cfd 100644 --- a/tests/golden/typescript/typescript-fetch/additional-properties/models/LeafValue.ts.golden +++ b/tests/golden/typescript/typescript-fetch/additional-properties/models/LeafValue.ts.golden @@ -19,7 +19,7 @@ export interface LeafValue { /** * Optional label. */ - readonly label?: string; + readonly label?: string | null; /** * Scores by name (additionalProperties: integer). */ diff --git a/tests/golden/typescript/typescript-fetch/petstore/models/Category.ts.golden b/tests/golden/typescript/typescript-fetch/petstore/models/Category.ts.golden index 717c89114..b92b10ef6 100644 --- a/tests/golden/typescript/typescript-fetch/petstore/models/Category.ts.golden +++ b/tests/golden/typescript/typescript-fetch/petstore/models/Category.ts.golden @@ -11,9 +11,9 @@ export interface Category { /** * Category ID */ - readonly id?: number; + readonly id?: number | null; /** * Category name */ - readonly name?: string; + readonly name?: string | null; } diff --git a/tests/golden/typescript/typescript-fetch/petstore/models/Order.ts.golden b/tests/golden/typescript/typescript-fetch/petstore/models/Order.ts.golden index ccde5c901..23e145b3d 100644 --- a/tests/golden/typescript/typescript-fetch/petstore/models/Order.ts.golden +++ b/tests/golden/typescript/typescript-fetch/petstore/models/Order.ts.golden @@ -13,22 +13,22 @@ export interface Order { /** * Complete flag */ - readonly complete?: boolean; + readonly complete?: boolean | null; /** * Order ID */ - readonly id?: number; + readonly id?: number | null; /** * Pet ID */ - readonly pet_id?: number; + readonly pet_id?: number | null; /** * Quantity */ - readonly quantity?: number; + readonly quantity?: number | null; /** * Ship date */ - readonly ship_date?: string; - readonly status?: OrderStatus; + readonly ship_date?: string | null; + readonly status?: OrderStatus | null; } diff --git a/tests/golden/typescript/typescript-fetch/petstore/models/Pet.ts.golden b/tests/golden/typescript/typescript-fetch/petstore/models/Pet.ts.golden index 9c4388d0c..2f0234634 100644 --- a/tests/golden/typescript/typescript-fetch/petstore/models/Pet.ts.golden +++ b/tests/golden/typescript/typescript-fetch/petstore/models/Pet.ts.golden @@ -12,11 +12,11 @@ import type { Tag } from './Tag'; * Pet model */ export interface Pet { - readonly category?: Category; + readonly category?: Category | null; /** * Pet ID */ - readonly id?: number; + readonly id?: number | null; /** * Pet name */ @@ -25,9 +25,9 @@ export interface Pet { * Photo URLs */ readonly photo_urls: readonly string[]; - readonly status?: PetStatus; + readonly status?: PetStatus | null; /** * Pet tags */ - readonly tags?: readonly Tag[]; + readonly tags?: readonly Tag[] | null; } diff --git a/tests/golden/typescript/typescript-fetch/petstore/models/Tag.ts.golden b/tests/golden/typescript/typescript-fetch/petstore/models/Tag.ts.golden index 3a51fd671..8ebf61160 100644 --- a/tests/golden/typescript/typescript-fetch/petstore/models/Tag.ts.golden +++ b/tests/golden/typescript/typescript-fetch/petstore/models/Tag.ts.golden @@ -11,9 +11,9 @@ export interface Tag { /** * Tag ID */ - readonly id?: number; + readonly id?: number | null; /** * Tag name */ - readonly name?: string; + readonly name?: string | null; } diff --git a/tests/golden/typescript/typescript-fetch/petstore/models/UploadResponse.ts.golden b/tests/golden/typescript/typescript-fetch/petstore/models/UploadResponse.ts.golden index 2795d1385..212c4c7a5 100644 --- a/tests/golden/typescript/typescript-fetch/petstore/models/UploadResponse.ts.golden +++ b/tests/golden/typescript/typescript-fetch/petstore/models/UploadResponse.ts.golden @@ -11,13 +11,13 @@ export interface UploadResponse { /** * Response code */ - readonly code?: number; + readonly code?: number | null; /** * Response message */ - readonly message?: string; + readonly message?: string | null; /** * Response type */ - readonly type?: string; + readonly type?: string | null; } diff --git a/tests/golden/typescript/typescript-fetch/petstore/models/User.ts.golden b/tests/golden/typescript/typescript-fetch/petstore/models/User.ts.golden index 46b0ee297..e106f9774 100644 --- a/tests/golden/typescript/typescript-fetch/petstore/models/User.ts.golden +++ b/tests/golden/typescript/typescript-fetch/petstore/models/User.ts.golden @@ -11,33 +11,33 @@ export interface User { /** * Email */ - readonly email?: string; + readonly email?: string | null; /** * First name */ - readonly first_name?: string; + readonly first_name?: string | null; /** * User ID */ - readonly id?: number; + readonly id?: number | null; /** * Last name */ - readonly last_name?: string; + readonly last_name?: string | null; /** * Password */ - readonly password?: string; + readonly password?: string | null; /** * Phone */ - readonly phone?: string; + readonly phone?: string | null; /** * User status */ - readonly user_status?: number; + readonly user_status?: number | null; /** * Username */ - readonly username?: string; + readonly username?: string | null; } diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/README.md.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/README.md.golden new file mode 100644 index 000000000..a10af0b37 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/README.md.golden @@ -0,0 +1,248 @@ +# type-script-fetch-vp-camel-case-masked-regression-test + +Masked fixture for TypeScript Fetch camelCase regression coverage. + +**Version:** 1.0.0 + +## Overview + +This package provides a TypeScript/JavaScript client for the TypeScript Fetch VP CamelCase Masked Regression Test API. It uses the native [Fetch API](https://fetch.spec.whatwg.org/) for HTTP requests and works in both Node.js and browser environments. + +## Features + +- ✨ **Type-safe** - Full TypeScript support with generated types +- 🚀 **Modern** - Uses native Fetch API, no external HTTP dependencies +- 🔧 **Configurable** - Flexible configuration options +- 🎯 **Middleware** - Support for request/response interceptors +- 📦 **Tree-shakeable** - Import only what you need +- 🌐 **Universal** - Works in Node.js and browsers + +## Installation + +### From npm (published package) + +```bash +npm install type-script-fetch-vp-camel-case-masked-regression-test +``` + +### From local path (development) + +Add the package to your `package.json` using the `file:` protocol: + +```json +{ + "dependencies": { + "type-script-fetch-vp-camel-case-masked-regression-test": "file:../../path/to/generated/package" + } +} +``` + +Then run: + +```bash +npm install +``` + +## Quick Start + +```typescript +import { Configuration, DefaultApi } from 'type-script-fetch-vp-camel-case-masked-regression-test'; + +// Create a configuration +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'Authorization': 'Bearer YOUR_TOKEN' + } +}); + +// Initialize the API client +const api = new DefaultApi(config); + +// Make API calls +try { + const result = await api.someMethod(); + console.log(result); +} catch (error) { + console.error('API Error:', error); +} +``` + +## Configuration + +The `Configuration` class accepts the following options: + +```typescript +interface ConfigurationParameters { + /** Base URL for API requests */ + basePath?: string; + + /** Custom fetch implementation */ + fetchApi?: typeof fetch; + + /** Request/response middleware */ + middleware?: Middleware[]; + + /** Custom query string serializer */ + queryParamsStringify?: (params: HTTPQuery) => string; + + /** Default headers for all requests */ + headers?: Record; + + /** Credentials mode for requests */ + credentials?: RequestCredentials; +} +``` + +### Example with custom configuration + +```typescript +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'X-API-Key': 'your-api-key', + 'Content-Type': 'application/json' + }, + credentials: 'include' +}); +``` + +## Middleware + +Add custom middleware to intercept requests and responses: + +```typescript +import { Configuration, Middleware } from 'type-script-fetch-vp-camel-case-masked-regression-test'; + +const loggingMiddleware: Middleware = { + pre: async (context) => { + console.log('Request:', context.url); + return context; + }, + post: async (context) => { + console.log('Response:', context.response.status); + return context.response; + }, + onError: async (context) => { + console.error('Error:', context.error); + return undefined; + } +}; + +const config = new Configuration({ + basePath: 'https://api.example.com', + middleware: [loggingMiddleware] +}); +``` + +## Error Handling + +The client throws typed errors for different failure scenarios: + +```typescript +import { ResponseError, FetchError, RequiredError } from 'type-script-fetch-vp-camel-case-masked-regression-test'; + +try { + const result = await api.someMethod(); +} catch (error) { + if (error instanceof ResponseError) { + // HTTP error response (4xx, 5xx) + console.error('HTTP Error:', error.response.status); + } else if (error instanceof FetchError) { + // Network or fetch error + console.error('Network Error:', error.cause); + } else if (error instanceof RequiredError) { + // Missing required parameter + console.error('Missing field:', error.field); + } +} +``` + +## API Reference + +This package exports the following: + +- **Configuration** - Client configuration class +- **BaseAPI** - Base class for all API clients +- **API Classes** - Generated API client classes (e.g., `UserApi`, `PostApi`) +- **Models** - Generated TypeScript interfaces for request/response types +- **Errors** - `ResponseError`, `FetchError`, `RequiredError` +- **Types** - TypeScript type definitions + +## Development + +### Building + +To build the package: + +```bash +npm install +npm run build +``` + +This will compile TypeScript to JavaScript in the `dist/` directory. + +### Building for ESM + +To build ES modules: + +```bash +npm run build +``` + +## TypeScript Support + +This package includes TypeScript type definitions. No additional `@types` package is needed. + +### TypeScript Configuration + +This package works with standard TypeScript configurations. If you're using a bundler-based setup, you may want to configure: + +```json +{ + "compilerOptions": { + "moduleResolution": "bundler" + } +} +``` + +### Type Imports + +```typescript +import type { User, CreateUserRequest } from 'type-script-fetch-vp-camel-case-masked-regression-test'; + +const user: User = { + id: 1, + name: 'John Doe', + email: 'john@example.com' +}; +``` + +## Browser Support + +This package uses the native Fetch API, which is supported in: + +- Chrome 42+ +- Firefox 39+ +- Safari 10.1+ +- Edge 14+ +- Node.js 18+ (native fetch) +- Node.js <18 (with `node-fetch` polyfill) + +For older browsers, you may need to include a fetch polyfill. + +## License + +This is an auto-generated API client. Please refer to your API documentation for license information. + +## Support + +For issues related to the API itself, please contact the API provider. + +For issues with this generated client, please check the OpenAPI specification used to generate it. + +--- + +**Generated by OpenAPI Generator** + +API Version: 1.0.0 diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/apis/DefaultApi.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/apis/DefaultApi.ts.golden new file mode 100644 index 000000000..5678c784d --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/apis/DefaultApi.ts.golden @@ -0,0 +1,76 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooEnvelope, FooEnvelope$Wire } from '../models/FooEnvelope'; +import { fooEnvelopeFromJSON, fooEnvelopeToJSON } from '../models/FooEnvelope'; +import type { Configuration, HTTPQuery, InitOverrideFunction } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, JSONApiResponse, RequiredError } from '../runtime/runtime'; + +export interface ApiCreateFooEnvelopeRequest { + body: FooEnvelope; +} + +export type CreateFooEnvelopeRawResponse = + | JSONApiResponse & { status: 200 } + | JSONApiResponse & { status: number }; + + +export interface DefaultApiInterface { + createFooEnvelopeRaw: (requestParameters: ApiCreateFooEnvelopeRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; + createFooEnvelope: (requestParameters: ApiCreateFooEnvelopeRequest, initOverrides?: RequestInit | InitOverrideFunction) => Promise; +} + +export class DefaultApi extends BaseAPI implements DefaultApiInterface { + /** + * Initialize the API client + */ + constructor(configuration?: Configuration) { + super(configuration ?? DefaultConfig); + } + + async createFooEnvelopeRaw(requestParameters: ApiCreateFooEnvelopeRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + if (requestParameters.body === undefined || requestParameters.body === null) { + throw new RequiredError( + 'body', + 'Required parameter "body" was null or undefined when calling createFooEnvelopeRaw().' + ); + } + // Build path with path parameters + const urlPath = `/foo`; + // Build query parameters + const queryParameters: HTTPQuery = {}; + // Build headers + const headerParameters: Record = { + 'Content-Type': 'application/json', + }; + + // Prepare request body + const requestBody = fooEnvelopeToJSON(requestParameters.body); + // Make request + const response = await this.request({ + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: requestBody, + }, initOverrides); + + // Handle responses + if (response.status === 200) { + return new JSONApiResponse(response, (json) => fooEnvelopeFromJSON(json as FooEnvelope$Wire)) as JSONApiResponse & { status: 200 }; + } + else { + return new JSONApiResponse(response) as JSONApiResponse & { status: number }; + } + } + + async createFooEnvelope(requestParameters: ApiCreateFooEnvelopeRequest, + initOverrides?: RequestInit | InitOverrideFunction): Promise { + const response = await this.createFooEnvelopeRaw(requestParameters, initOverrides); + return await response.value() as FooEnvelope; + } +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/apis/index.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/apis/index.ts.golden new file mode 100644 index 000000000..8d29803d1 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/apis/index.ts.golden @@ -0,0 +1,12 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export type { + ApiCreateFooEnvelopeRequest, + CreateFooEnvelopeRawResponse, + DefaultApiInterface, +} from './DefaultApi'; +export { DefaultApi } from './DefaultApi'; diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/index.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/index.ts.golden new file mode 100644 index 000000000..8c918d31d --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/index.ts.golden @@ -0,0 +1,9 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export * from './runtime/runtime'; +export * from './apis'; +export * from './models'; diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayload.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayload.ts.golden new file mode 100644 index 000000000..4b8eee5ec --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayload.ts.golden @@ -0,0 +1,44 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooAdjacentPayloadFooInlineObjectPayloadValue, FooAdjacentPayloadFooInlineObjectPayloadValue$Wire } from './FooAdjacentPayloadFooInlineObjectPayloadValue'; +import { fooAdjacentPayloadFooInlineObjectPayloadValueFromJSON, fooAdjacentPayloadFooInlineObjectPayloadValueToJSON } from './FooAdjacentPayloadFooInlineObjectPayloadValue'; +import type { FooArrayPart, FooArrayPart$Wire } from './FooArrayPart'; +import { fooArrayPartFromJSON, fooArrayPartToJSON } from './FooArrayPart'; + +export type FooAdjacentPayload$Wire = { type: 'foo_inline_none' } | { type: 'foo_inline_text'; payload_value: string } | { type: 'foo_inline_object'; payload_value: FooAdjacentPayloadFooInlineObjectPayloadValue$Wire } | { type: 'foo_inline_list'; payload_value: readonly FooArrayPart$Wire[] }; + +export type FooAdjacentPayload = { type: 'foo_inline_none' } | { type: 'foo_inline_text'; payloadValue: string } | { type: 'foo_inline_object'; payloadValue: FooAdjacentPayloadFooInlineObjectPayloadValue } | { type: 'foo_inline_list'; payloadValue: readonly FooArrayPart[] }; + +export function fooAdjacentPayloadFromJSON(json: FooAdjacentPayload$Wire): FooAdjacentPayload { + switch (json.type) { + case 'foo_inline_none': return { type: 'foo_inline_none' }; + case 'foo_inline_text': return { type: 'foo_inline_text', payloadValue: json.payload_value }; + case 'foo_inline_object': return { type: 'foo_inline_object', payloadValue: fooAdjacentPayloadFooInlineObjectPayloadValueFromJSON(json.payload_value) }; + case 'foo_inline_list': return { type: 'foo_inline_list', payloadValue: json.payload_value.map((item) => fooArrayPartFromJSON(item)) }; + } +} + +export function fooAdjacentPayloadToJSON(value: FooAdjacentPayload): FooAdjacentPayload$Wire { + switch (value.type) { + case 'foo_inline_none': return { type: 'foo_inline_none' }; + case 'foo_inline_text': return { type: 'foo_inline_text', payload_value: value.payloadValue }; + case 'foo_inline_object': return { type: 'foo_inline_object', payload_value: fooAdjacentPayloadFooInlineObjectPayloadValueToJSON(value.payloadValue) }; + case 'foo_inline_list': return { type: 'foo_inline_list', payload_value: value.payloadValue.map((item) => fooArrayPartToJSON(item)) }; + } +} + +export function isFooInlineText(value: FooAdjacentPayload): value is { type: 'foo_inline_text'; payloadValue: string } { + return value.type === 'foo_inline_text'; +} + +export function isFooInlineObject(value: FooAdjacentPayload): value is { type: 'foo_inline_object'; payloadValue: FooAdjacentPayloadFooInlineObjectPayloadValue } { + return value.type === 'foo_inline_object'; +} + +export function isFooInlineList(value: FooAdjacentPayload): value is { type: 'foo_inline_list'; payloadValue: readonly FooArrayPart[] } { + return value.type === 'foo_inline_list'; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayloadFooInlineNone.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayloadFooInlineNone.ts.golden new file mode 100644 index 000000000..7db3dfbe4 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayloadFooInlineNone.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooAdjacentPayloadFooInlineNone$Wire { + readonly type: 'foo_inline_none'; +} + +export interface FooAdjacentPayloadFooInlineNone { + readonly type: 'foo_inline_none'; +} + +export function fooAdjacentPayloadFooInlineNoneFromJSON(json: FooAdjacentPayloadFooInlineNone$Wire): FooAdjacentPayloadFooInlineNone { + return { + type: json.type, + }; +} + +export function fooAdjacentPayloadFooInlineNoneToJSON(value: FooAdjacentPayloadFooInlineNone): FooAdjacentPayloadFooInlineNone$Wire { + return { + type: value.type, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayloadFooInlineObjectPayloadValue.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayloadFooInlineObjectPayloadValue.ts.golden new file mode 100644 index 000000000..79b0ab894 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAdjacentPayloadFooInlineObjectPayloadValue.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooAdjacentPayloadFooInlineObjectPayloadValue$Wire { + readonly bar_value: string; +} + +export interface FooAdjacentPayloadFooInlineObjectPayloadValue { + readonly barValue: string; +} + +export function fooAdjacentPayloadFooInlineObjectPayloadValueFromJSON(json: FooAdjacentPayloadFooInlineObjectPayloadValue$Wire): FooAdjacentPayloadFooInlineObjectPayloadValue { + return { + barValue: json.bar_value, + }; +} + +export function fooAdjacentPayloadFooInlineObjectPayloadValueToJSON(value: FooAdjacentPayloadFooInlineObjectPayloadValue): FooAdjacentPayloadFooInlineObjectPayloadValue$Wire { + return { + bar_value: value.barValue, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAnyValue.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAnyValue.ts.golden new file mode 100644 index 000000000..2fe4c9c2e --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooAnyValue.ts.golden @@ -0,0 +1,7 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export type FooAnyValue = unknown; diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooArrayPart.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooArrayPart.ts.golden new file mode 100644 index 000000000..2f878161c --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooArrayPart.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooArrayPart$Wire { + readonly bar_text: string; +} + +export interface FooArrayPart { + readonly barText: string; +} + +export function fooArrayPartFromJSON(json: FooArrayPart$Wire): FooArrayPart { + return { + barText: json.bar_text, + }; +} + +export function fooArrayPartToJSON(value: FooArrayPart): FooArrayPart$Wire { + return { + bar_text: value.barText, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooClosedVariant.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooClosedVariant.ts.golden new file mode 100644 index 000000000..2c2393a47 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooClosedVariant.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooClosedVariant$Wire { + readonly type: 'foo_closed_variant'; +} + +export interface FooClosedVariant { + readonly type: 'foo_closed_variant'; +} + +export function fooClosedVariantFromJSON(json: FooClosedVariant$Wire): FooClosedVariant { + return { + type: json.type, + }; +} + +export function fooClosedVariantToJSON(value: FooClosedVariant): FooClosedVariant$Wire { + return { + type: value.type, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooEnvelope.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooEnvelope.ts.golden new file mode 100644 index 000000000..5b4446cdf --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooEnvelope.ts.golden @@ -0,0 +1,63 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooAdjacentPayload, FooAdjacentPayload$Wire } from './FooAdjacentPayload'; +import { fooAdjacentPayloadFromJSON, fooAdjacentPayloadToJSON } from './FooAdjacentPayload'; +import type { FooAnyValue } from './FooAnyValue'; +import type { FooMappedTagUnion, FooMappedTagUnion$Wire } from './FooMappedTagUnion'; +import { fooMappedTagUnionFromJSON, fooMappedTagUnionToJSON } from './FooMappedTagUnion'; +import type { FooNullableRefHolder, FooNullableRefHolder$Wire } from './FooNullableRefHolder'; +import { fooNullableRefHolderFromJSON, fooNullableRefHolderToJSON } from './FooNullableRefHolder'; +import type { FooTagOnlyUnion, FooTagOnlyUnion$Wire } from './FooTagOnlyUnion'; +import { fooTagOnlyUnionFromJSON, fooTagOnlyUnionToJSON } from './FooTagOnlyUnion'; +import type { FooTaggedRef, FooTaggedRef$Wire } from './FooTaggedRef'; +import { fooTaggedRefFromJSON, fooTaggedRefToJSON } from './FooTaggedRef'; +import type { FooUnionContent, FooUnionContent$Wire } from './FooUnionContent'; +import { fooUnionContentFromJSON, fooUnionContentToJSON } from './FooUnionContent'; + +export interface FooEnvelope$Wire { + readonly adjacent_payload: FooAdjacentPayload$Wire; + readonly any_value: FooAnyValue; + readonly mapped_tag_union: FooMappedTagUnion$Wire; + readonly nullable_ref_holder: FooNullableRefHolder$Wire; + readonly tag_only_union: FooTagOnlyUnion$Wire; + readonly tagged_ref: FooTaggedRef$Wire; + readonly union_content: FooUnionContent$Wire; +} + +export interface FooEnvelope { + readonly adjacentPayload: FooAdjacentPayload; + readonly anyValue: FooAnyValue; + readonly mappedTagUnion: FooMappedTagUnion; + readonly nullableRefHolder: FooNullableRefHolder; + readonly tagOnlyUnion: FooTagOnlyUnion; + readonly taggedRef: FooTaggedRef; + readonly unionContent: FooUnionContent; +} + +export function fooEnvelopeFromJSON(json: FooEnvelope$Wire): FooEnvelope { + return { + adjacentPayload: fooAdjacentPayloadFromJSON(json.adjacent_payload), + anyValue: json.any_value, + mappedTagUnion: fooMappedTagUnionFromJSON(json.mapped_tag_union), + nullableRefHolder: fooNullableRefHolderFromJSON(json.nullable_ref_holder), + tagOnlyUnion: fooTagOnlyUnionFromJSON(json.tag_only_union), + taggedRef: fooTaggedRefFromJSON(json.tagged_ref), + unionContent: fooUnionContentFromJSON(json.union_content), + }; +} + +export function fooEnvelopeToJSON(value: FooEnvelope): FooEnvelope$Wire { + return { + adjacent_payload: fooAdjacentPayloadToJSON(value.adjacentPayload), + any_value: value.anyValue, + mapped_tag_union: fooMappedTagUnionToJSON(value.mappedTagUnion), + nullable_ref_holder: fooNullableRefHolderToJSON(value.nullableRefHolder), + tag_only_union: fooTagOnlyUnionToJSON(value.tagOnlyUnion), + tagged_ref: fooTaggedRefToJSON(value.taggedRef), + union_content: fooUnionContentToJSON(value.unionContent), + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooMappedTagUnion.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooMappedTagUnion.ts.golden new file mode 100644 index 000000000..4c874bbf9 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooMappedTagUnion.ts.golden @@ -0,0 +1,30 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooOpenVariant, FooOpenVariant$Wire } from './FooOpenVariant'; +import { fooOpenVariantFromJSON, fooOpenVariantToJSON } from './FooOpenVariant'; + +export type FooMappedTagUnion$Wire = { type: 'foo_closed_variant' } | ({ type: 'foo_open_variant' } & FooOpenVariant$Wire); + +export type FooMappedTagUnion = { type: 'foo_closed_variant' } | ({ type: 'foo_open_variant' } & FooOpenVariant); + +export function fooMappedTagUnionFromJSON(json: FooMappedTagUnion$Wire): FooMappedTagUnion { + switch (json.type) { + case 'foo_closed_variant': return { type: 'foo_closed_variant' }; + case 'foo_open_variant': return { ...fooOpenVariantFromJSON(json), type: 'foo_open_variant' }; + } +} + +export function fooMappedTagUnionToJSON(value: FooMappedTagUnion): FooMappedTagUnion$Wire { + switch (value.type) { + case 'foo_closed_variant': return { type: 'foo_closed_variant' } as FooMappedTagUnion$Wire; + case 'foo_open_variant': return { ...fooOpenVariantToJSON(value), type: 'foo_open_variant' } as FooMappedTagUnion$Wire; + } +} + +export function isFooOpenVariant(value: FooMappedTagUnion): value is ({ type: 'foo_open_variant' } & FooOpenVariant) { + return value.type === 'foo_open_variant'; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableItem.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableItem.ts.golden new file mode 100644 index 000000000..fb5b14c61 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableItem.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooNullableItem$Wire { + readonly item_name: string; +} + +export interface FooNullableItem { + readonly itemName: string; +} + +export function fooNullableItemFromJSON(json: FooNullableItem$Wire): FooNullableItem { + return { + itemName: json.item_name, + }; +} + +export function fooNullableItemToJSON(value: FooNullableItem): FooNullableItem$Wire { + return { + item_name: value.itemName, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableItems.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableItems.ts.golden new file mode 100644 index 000000000..c0f1b8764 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableItems.ts.golden @@ -0,0 +1,28 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooNullableItem, FooNullableItem$Wire } from './FooNullableItem'; +import { fooNullableItemFromJSON, fooNullableItemToJSON } from './FooNullableItem'; + +export interface FooNullableItems$Wire { + readonly content: readonly FooNullableItem$Wire[] | null; +} + +export interface FooNullableItems { + readonly content: readonly FooNullableItem[] | null; +} + +export function fooNullableItemsFromJSON(json: FooNullableItems$Wire): FooNullableItems { + return { + content: json.content === null ? null : json.content.map((item) => fooNullableItemFromJSON(item)), + }; +} + +export function fooNullableItemsToJSON(value: FooNullableItems): FooNullableItems$Wire { + return { + content: value.content === null ? null : value.content.map((item) => fooNullableItemToJSON(item)), + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableRefHolder.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableRefHolder.ts.golden new file mode 100644 index 000000000..57bf2fd58 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooNullableRefHolder.ts.golden @@ -0,0 +1,28 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooNullableItems, FooNullableItems$Wire } from './FooNullableItems'; +import { fooNullableItemsFromJSON, fooNullableItemsToJSON } from './FooNullableItems'; + +export interface FooNullableRefHolder$Wire { + readonly maybe_items: FooNullableItems$Wire | null; +} + +export interface FooNullableRefHolder { + readonly maybeItems: FooNullableItems | null; +} + +export function fooNullableRefHolderFromJSON(json: FooNullableRefHolder$Wire): FooNullableRefHolder { + return { + maybeItems: json.maybe_items === null ? null : fooNullableItemsFromJSON(json.maybe_items), + }; +} + +export function fooNullableRefHolderToJSON(value: FooNullableRefHolder): FooNullableRefHolder$Wire { + return { + maybe_items: value.maybeItems === null ? null : fooNullableItemsToJSON(value.maybeItems), + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooObjectPayload.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooObjectPayload.ts.golden new file mode 100644 index 000000000..25a369249 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooObjectPayload.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooObjectPayload$Wire { + readonly object_name: string; +} + +export interface FooObjectPayload { + readonly objectName: string; +} + +export function fooObjectPayloadFromJSON(json: FooObjectPayload$Wire): FooObjectPayload { + return { + objectName: json.object_name, + }; +} + +export function fooObjectPayloadToJSON(value: FooObjectPayload): FooObjectPayload$Wire { + return { + object_name: value.objectName, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooOpenVariant.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooOpenVariant.ts.golden new file mode 100644 index 000000000..25ca69998 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooOpenVariant.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooOpenVariant$Wire { + readonly type: 'foo_open_variant'; +} + +export interface FooOpenVariant { + readonly type: 'foo_open_variant'; +} + +export function fooOpenVariantFromJSON(json: FooOpenVariant$Wire): FooOpenVariant { + return { + type: json.type, + }; +} + +export function fooOpenVariantToJSON(value: FooOpenVariant): FooOpenVariant$Wire { + return { + type: value.type, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooPayloadBody.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooPayloadBody.ts.golden new file mode 100644 index 000000000..51d02be89 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooPayloadBody.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooPayloadBody$Wire { + readonly bar_value: string; +} + +export interface FooPayloadBody { + readonly barValue: string; +} + +export function fooPayloadBodyFromJSON(json: FooPayloadBody$Wire): FooPayloadBody { + return { + barValue: json.bar_value, + }; +} + +export function fooPayloadBodyToJSON(value: FooPayloadBody): FooPayloadBody$Wire { + return { + bar_value: value.barValue, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnion.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnion.ts.golden new file mode 100644 index 000000000..573e386a7 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnion.ts.golden @@ -0,0 +1,32 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooObjectPayload, FooObjectPayload$Wire } from './FooObjectPayload'; +import { fooObjectPayloadFromJSON, fooObjectPayloadToJSON } from './FooObjectPayload'; + +export type FooTagOnlyUnion$Wire = { type: 'foo_unit_alpha' } | { type: 'foo_unit_beta' } | { type: 'foo_object_payload'; object_payload: FooObjectPayload$Wire }; + +export type FooTagOnlyUnion = { type: 'foo_unit_alpha' } | { type: 'foo_unit_beta' } | { type: 'foo_object_payload'; objectPayload: FooObjectPayload }; + +export function fooTagOnlyUnionFromJSON(json: FooTagOnlyUnion$Wire): FooTagOnlyUnion { + switch (json.type) { + case 'foo_unit_alpha': return { type: 'foo_unit_alpha' }; + case 'foo_unit_beta': return { type: 'foo_unit_beta' }; + case 'foo_object_payload': return { type: 'foo_object_payload', objectPayload: fooObjectPayloadFromJSON(json.object_payload) }; + } +} + +export function fooTagOnlyUnionToJSON(value: FooTagOnlyUnion): FooTagOnlyUnion$Wire { + switch (value.type) { + case 'foo_unit_alpha': return { type: 'foo_unit_alpha' }; + case 'foo_unit_beta': return { type: 'foo_unit_beta' }; + case 'foo_object_payload': return { type: 'foo_object_payload', object_payload: fooObjectPayloadToJSON(value.objectPayload) }; + } +} + +export function isFooObjectPayload(value: FooTagOnlyUnion): value is { type: 'foo_object_payload'; objectPayload: FooObjectPayload } { + return value.type === 'foo_object_payload'; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnionFooUnitAlpha.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnionFooUnitAlpha.ts.golden new file mode 100644 index 000000000..5df644c06 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnionFooUnitAlpha.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooTagOnlyUnionFooUnitAlpha$Wire { + readonly type: 'foo_unit_alpha'; +} + +export interface FooTagOnlyUnionFooUnitAlpha { + readonly type: 'foo_unit_alpha'; +} + +export function fooTagOnlyUnionFooUnitAlphaFromJSON(json: FooTagOnlyUnionFooUnitAlpha$Wire): FooTagOnlyUnionFooUnitAlpha { + return { + type: json.type, + }; +} + +export function fooTagOnlyUnionFooUnitAlphaToJSON(value: FooTagOnlyUnionFooUnitAlpha): FooTagOnlyUnionFooUnitAlpha$Wire { + return { + type: value.type, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnionFooUnitBeta.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnionFooUnitBeta.ts.golden new file mode 100644 index 000000000..436dc4439 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTagOnlyUnionFooUnitBeta.ts.golden @@ -0,0 +1,25 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export interface FooTagOnlyUnionFooUnitBeta$Wire { + readonly type: 'foo_unit_beta'; +} + +export interface FooTagOnlyUnionFooUnitBeta { + readonly type: 'foo_unit_beta'; +} + +export function fooTagOnlyUnionFooUnitBetaFromJSON(json: FooTagOnlyUnionFooUnitBeta$Wire): FooTagOnlyUnionFooUnitBeta { + return { + type: json.type, + }; +} + +export function fooTagOnlyUnionFooUnitBetaToJSON(value: FooTagOnlyUnionFooUnitBeta): FooTagOnlyUnionFooUnitBeta$Wire { + return { + type: value.type, + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTaggedRef.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTaggedRef.ts.golden new file mode 100644 index 000000000..fbea7f9ba --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooTaggedRef.ts.golden @@ -0,0 +1,28 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooPayloadBody, FooPayloadBody$Wire } from './FooPayloadBody'; +import { fooPayloadBodyFromJSON, fooPayloadBodyToJSON } from './FooPayloadBody'; + +export type FooTaggedRef$Wire = { type: 'foo_ref_payload'; bar_payload: FooPayloadBody$Wire }; + +export type FooTaggedRef = { type: 'foo_ref_payload'; barPayload: FooPayloadBody }; + +export function fooTaggedRefFromJSON(json: FooTaggedRef$Wire): FooTaggedRef { + switch (json.type) { + case 'foo_ref_payload': return { type: 'foo_ref_payload', barPayload: fooPayloadBodyFromJSON(json.bar_payload) }; + } +} + +export function fooTaggedRefToJSON(value: FooTaggedRef): FooTaggedRef$Wire { + switch (value.type) { + case 'foo_ref_payload': return { type: 'foo_ref_payload', bar_payload: fooPayloadBodyToJSON(value.barPayload) }; + } +} + +export function isFooRefPayload(value: FooTaggedRef): value is { type: 'foo_ref_payload'; barPayload: FooPayloadBody } { + return value.type === 'foo_ref_payload'; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooUnionContent.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooUnionContent.ts.golden new file mode 100644 index 000000000..bc0f1fe52 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/FooUnionContent.ts.golden @@ -0,0 +1,19 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +import type { FooArrayPart, FooArrayPart$Wire } from './FooArrayPart'; + +export type FooUnionContent$Wire = string | readonly FooArrayPart$Wire[]; + +export type FooUnionContent = string | readonly FooArrayPart[]; + +export function fooUnionContentFromJSON(json: FooUnionContent$Wire): FooUnionContent { + return json as unknown as FooUnionContent; +} + +export function fooUnionContentToJSON(value: FooUnionContent): FooUnionContent$Wire { + return value as unknown as FooUnionContent$Wire; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/index.ts.golden new file mode 100644 index 000000000..a5d0e0589 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/models/index.ts.golden @@ -0,0 +1,43 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export type { FooAdjacentPayload, FooAdjacentPayload$Wire } from './FooAdjacentPayload'; +export { fooAdjacentPayloadFromJSON, fooAdjacentPayloadToJSON, isFooInlineText, isFooInlineObject, isFooInlineList } from './FooAdjacentPayload'; +export type { FooAdjacentPayloadFooInlineNone, FooAdjacentPayloadFooInlineNone$Wire } from './FooAdjacentPayloadFooInlineNone'; +export { fooAdjacentPayloadFooInlineNoneFromJSON, fooAdjacentPayloadFooInlineNoneToJSON } from './FooAdjacentPayloadFooInlineNone'; +export type { FooAdjacentPayloadFooInlineObjectPayloadValue, FooAdjacentPayloadFooInlineObjectPayloadValue$Wire } from './FooAdjacentPayloadFooInlineObjectPayloadValue'; +export { fooAdjacentPayloadFooInlineObjectPayloadValueFromJSON, fooAdjacentPayloadFooInlineObjectPayloadValueToJSON } from './FooAdjacentPayloadFooInlineObjectPayloadValue'; +export type { FooAnyValue } from './FooAnyValue'; +export type { FooArrayPart, FooArrayPart$Wire } from './FooArrayPart'; +export { fooArrayPartFromJSON, fooArrayPartToJSON } from './FooArrayPart'; +export type { FooClosedVariant, FooClosedVariant$Wire } from './FooClosedVariant'; +export { fooClosedVariantFromJSON, fooClosedVariantToJSON } from './FooClosedVariant'; +export type { FooEnvelope, FooEnvelope$Wire } from './FooEnvelope'; +export { fooEnvelopeFromJSON, fooEnvelopeToJSON } from './FooEnvelope'; +export type { FooMappedTagUnion, FooMappedTagUnion$Wire } from './FooMappedTagUnion'; +export { fooMappedTagUnionFromJSON, fooMappedTagUnionToJSON, isFooOpenVariant } from './FooMappedTagUnion'; +export type { FooNullableItem, FooNullableItem$Wire } from './FooNullableItem'; +export { fooNullableItemFromJSON, fooNullableItemToJSON } from './FooNullableItem'; +export type { FooNullableItems, FooNullableItems$Wire } from './FooNullableItems'; +export { fooNullableItemsFromJSON, fooNullableItemsToJSON } from './FooNullableItems'; +export type { FooNullableRefHolder, FooNullableRefHolder$Wire } from './FooNullableRefHolder'; +export { fooNullableRefHolderFromJSON, fooNullableRefHolderToJSON } from './FooNullableRefHolder'; +export type { FooObjectPayload, FooObjectPayload$Wire } from './FooObjectPayload'; +export { fooObjectPayloadFromJSON, fooObjectPayloadToJSON } from './FooObjectPayload'; +export type { FooOpenVariant, FooOpenVariant$Wire } from './FooOpenVariant'; +export { fooOpenVariantFromJSON, fooOpenVariantToJSON } from './FooOpenVariant'; +export type { FooPayloadBody, FooPayloadBody$Wire } from './FooPayloadBody'; +export { fooPayloadBodyFromJSON, fooPayloadBodyToJSON } from './FooPayloadBody'; +export type { FooTagOnlyUnion, FooTagOnlyUnion$Wire } from './FooTagOnlyUnion'; +export { fooTagOnlyUnionFromJSON, fooTagOnlyUnionToJSON, isFooObjectPayload } from './FooTagOnlyUnion'; +export type { FooTagOnlyUnionFooUnitAlpha, FooTagOnlyUnionFooUnitAlpha$Wire } from './FooTagOnlyUnionFooUnitAlpha'; +export { fooTagOnlyUnionFooUnitAlphaFromJSON, fooTagOnlyUnionFooUnitAlphaToJSON } from './FooTagOnlyUnionFooUnitAlpha'; +export type { FooTagOnlyUnionFooUnitBeta, FooTagOnlyUnionFooUnitBeta$Wire } from './FooTagOnlyUnionFooUnitBeta'; +export { fooTagOnlyUnionFooUnitBetaFromJSON, fooTagOnlyUnionFooUnitBetaToJSON } from './FooTagOnlyUnionFooUnitBeta'; +export type { FooTaggedRef, FooTaggedRef$Wire } from './FooTaggedRef'; +export { fooTaggedRefFromJSON, fooTaggedRefToJSON, isFooRefPayload } from './FooTaggedRef'; +export type { FooUnionContent, FooUnionContent$Wire } from './FooUnionContent'; +export { fooUnionContentFromJSON, fooUnionContentToJSON } from './FooUnionContent'; diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/package.json.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/package.json.golden new file mode 100644 index 000000000..da81d5f4c --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/package.json.golden @@ -0,0 +1,31 @@ +{ + "description": "Masked fixture for TypeScript Fetch camelCase regression coverage.", + "devDependencies": { + "typescript": "latest", + "vite-plus": "latest" + }, + "exports": { + ".": { + "default": "./dist/index.mjs", + "types": "./dist/index.d.mts" + } + }, + "files": [ + "dist" + ], + "keywords": [ + "openapi", + "api-client", + "typescript", + "generated" + ], + "main": "./dist/index.mjs", + "name": "type-script-fetch-vp-camel-case-masked-regression-test", + "scripts": { + "build": "vp pack", + "check": "vp check --no-fmt" + }, + "type": "module", + "types": "./dist/index.d.mts", + "version": "1.0.0" +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/runtime/runtime.ts.golden new file mode 100644 index 000000000..398a8cf47 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/runtime/runtime.ts.golden @@ -0,0 +1,461 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * TypeScript Fetch VP CamelCase Masked Regression Test — 1.0.0 + * Masked fixture for TypeScript Fetch camelCase regression coverage. + */ +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string | (() => string | Promise); // parameter for basic security + password?: string | (() => string | Promise); // parameter for basic security + apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + constructor(private configuration: ConfigurationParameters = {}) {} + + set config(configuration: Configuration) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): (() => string | Promise) | undefined { + const username = this.configuration.username; + if (username) { + return typeof username === 'function' ? username : async () => username; + } + return undefined; + } + + get password(): (() => string | Promise) | undefined { + const password = this.configuration.password; + if (password) { + return typeof password === 'function' ? password : async () => password; + } + return undefined; + } + + get apiKey(): ((name: string) => string | Promise) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private static readonly jsonRegex = new RegExp('^(:?application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); + private middleware: Middleware[]; + + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response && (response.status >= 200 && response.status < 300)) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const token = await this.configuration.accessToken?.(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (!headers['Authorization']) { + const username = await this.configuration.username?.(); + const password = await this.configuration.password?.(); + if (username && password) { + headers['Authorization'] = `Basic ${btoa(`${username}:${password}`)}`; + } + } + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams: HTTPRequestInit = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + signal: context.signal, + }; + + const overriddenInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + let body: BodyInit | null | undefined; + if (isFormData(overriddenInit.body) + || (overriddenInit.body instanceof URLSearchParams) + || isBlob(overriddenInit.body)) { + body = overriddenInit.body; + } else if (this.isJsonMime(headers['Content-Type'])) { + body = JSON.stringify(overriddenInit.body); + } else { + body = overriddenInit.body as BodyInit | null | undefined; + } + + const init: RequestInit = { + ...overriddenInit, + body + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as new (configuration: Configuration) => T; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +} + +function isBlob(value: unknown): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: unknown): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name = "ResponseError" as const; + constructor(public response: Response, msg?: string) { + super(msg); + } +} + +export class FetchError extends Error { + override name = "FetchError" as const; + constructor(public cause: Error, msg?: string) { + super(msg); + } +} + +export class RequiredError extends Error { + override name = "RequiredError" as const; + constructor(public field: string, msg?: string) { + super(msg); + } +} + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = unknown; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | Blob | FormData | URLSearchParams; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody; signal?: AbortSignal }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; + signal?: AbortSignal; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function exists(json: Record, key: string): boolean { + const value = json[key]; + return value !== null && value !== undefined; +} + +export function mapValues(data: Record, fn: (item: T) => U): Record { + const result: Record = {}; + for (const key of Object.keys(data)) { + result[key] = fn(data[key]); + } + return result; +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export interface ResponseTransformer { + (json: unknown): T; +} + +class ApiResponseBase { + public readonly status: number; + public readonly ok: boolean; + public readonly statusText: string; + public readonly headers: Headers; + + constructor(public readonly raw: Response) { + this.status = raw.status; + this.ok = raw.ok; + this.statusText = raw.statusText; + this.headers = raw.headers; + } +} + +export class JSONApiResponse extends ApiResponseBase { + constructor(raw: Response, private transformer: ResponseTransformer = (jsonValue: unknown) => jsonValue as T) { + super(raw); + } + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return await this.raw.text(); + }; +} diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/tsconfig.esm.json.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/tsconfig.esm.json.golden new file mode 100644 index 000000000..bb8350cb6 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/tsconfig.esm.json.golden @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ES2020", + "outDir": "dist/esm" + }, + "extends": "./tsconfig.json" +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/tsconfig.json.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/tsconfig.json.golden new file mode 100644 index 000000000..4b3a8cf7f --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/tsconfig.json.golden @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": [ + "ES2020", + "DOM" + ], + "module": "ES2020", + "moduleResolution": "bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2020", + "typeRoots": [ + "node_modules/@types" + ] + }, + "exclude": [ + "dist", + "node_modules" + ], + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/vite.config.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/vite.config.ts.golden new file mode 100644 index 000000000..250639b98 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-camel-case-tagged-nullable-unions/vite.config.ts.golden @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + ignorePatterns: ['node_modules', 'dist'], + }, + pack: { + entry: ['index.ts'], + dts: true, + format: ['esm'], + }, +}); diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Category.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Category.ts.golden index 717c89114..b92b10ef6 100644 --- a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Category.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Category.ts.golden @@ -11,9 +11,9 @@ export interface Category { /** * Category ID */ - readonly id?: number; + readonly id?: number | null; /** * Category name */ - readonly name?: string; + readonly name?: string | null; } diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Order.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Order.ts.golden index ccde5c901..23e145b3d 100644 --- a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Order.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Order.ts.golden @@ -13,22 +13,22 @@ export interface Order { /** * Complete flag */ - readonly complete?: boolean; + readonly complete?: boolean | null; /** * Order ID */ - readonly id?: number; + readonly id?: number | null; /** * Pet ID */ - readonly pet_id?: number; + readonly pet_id?: number | null; /** * Quantity */ - readonly quantity?: number; + readonly quantity?: number | null; /** * Ship date */ - readonly ship_date?: string; - readonly status?: OrderStatus; + readonly ship_date?: string | null; + readonly status?: OrderStatus | null; } diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Pet.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Pet.ts.golden index 9c4388d0c..2f0234634 100644 --- a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Pet.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Pet.ts.golden @@ -12,11 +12,11 @@ import type { Tag } from './Tag'; * Pet model */ export interface Pet { - readonly category?: Category; + readonly category?: Category | null; /** * Pet ID */ - readonly id?: number; + readonly id?: number | null; /** * Pet name */ @@ -25,9 +25,9 @@ export interface Pet { * Photo URLs */ readonly photo_urls: readonly string[]; - readonly status?: PetStatus; + readonly status?: PetStatus | null; /** * Pet tags */ - readonly tags?: readonly Tag[]; + readonly tags?: readonly Tag[] | null; } diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Tag.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Tag.ts.golden index 3a51fd671..8ebf61160 100644 --- a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Tag.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/Tag.ts.golden @@ -11,9 +11,9 @@ export interface Tag { /** * Tag ID */ - readonly id?: number; + readonly id?: number | null; /** * Tag name */ - readonly name?: string; + readonly name?: string | null; } diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/UploadResponse.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/UploadResponse.ts.golden index 2795d1385..212c4c7a5 100644 --- a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/UploadResponse.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/UploadResponse.ts.golden @@ -11,13 +11,13 @@ export interface UploadResponse { /** * Response code */ - readonly code?: number; + readonly code?: number | null; /** * Response message */ - readonly message?: string; + readonly message?: string | null; /** * Response type */ - readonly type?: string; + readonly type?: string | null; } diff --git a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/User.ts.golden b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/User.ts.golden index 46b0ee297..e106f9774 100644 --- a/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/User.ts.golden +++ b/tests/golden/typescript/typescript-fetch/ts-toolchain-vp-petstore/models/User.ts.golden @@ -11,33 +11,33 @@ export interface User { /** * Email */ - readonly email?: string; + readonly email?: string | null; /** * First name */ - readonly first_name?: string; + readonly first_name?: string | null; /** * User ID */ - readonly id?: number; + readonly id?: number | null; /** * Last name */ - readonly last_name?: string; + readonly last_name?: string | null; /** * Password */ - readonly password?: string; + readonly password?: string | null; /** * Phone */ - readonly phone?: string; + readonly phone?: string | null; /** * User status */ - readonly user_status?: number; + readonly user_status?: number | null; /** * Username */ - readonly username?: string; + readonly username?: string | null; } diff --git a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/Event.ts.golden b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/Event.ts.golden index 4cb4e45ac..bafe54de3 100644 --- a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/Event.ts.golden +++ b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/Event.ts.golden @@ -13,8 +13,7 @@ * After the fix, these should generate properly prefixed names like * "EventKindUnspecified" instead of generic "Kind". */ -import type { EventMember1 } from './EventMember1'; -import type { EventMember2 } from './EventMember2'; -import type { EventMember3 } from './EventMember3'; +import type { EventEventCreated } from './EventEventCreated'; +import type { EventEventUpdated } from './EventEventUpdated'; -export type Event = EventMember1 | EventMember2 | EventMember3; +export type Event = { kind: 'EVENT_UNSPECIFIED' } | ({ kind: 'EVENT_CREATED' } & EventEventCreated) | ({ kind: 'EVENT_UPDATED' } & EventEventUpdated); diff --git a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.ts.golden b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.ts.golden similarity index 95% rename from tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.ts.golden rename to tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.ts.golden index b41a2acb0..538aa17af 100644 --- a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember2.ts.golden +++ b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventCreated.ts.golden @@ -16,7 +16,7 @@ /** * Created event with timestamp */ -export interface EventMember2 { +export interface EventEventCreated { readonly kind: 'EVENT_CREATED'; /** * When the event was created diff --git a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.ts.golden b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.ts.golden similarity index 94% rename from tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.ts.golden rename to tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.ts.golden index 46333b9e5..ba17f1796 100644 --- a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember1.ts.golden +++ b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUnspecified.ts.golden @@ -16,6 +16,6 @@ /** * Unspecified event variant (empty) */ -export interface EventMember1 { +export interface EventEventUnspecified { readonly kind: 'EVENT_UNSPECIFIED'; } diff --git a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.ts.golden b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.ts.golden similarity index 95% rename from tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.ts.golden rename to tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.ts.golden index 1b0b86781..1fecfbc6a 100644 --- a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventMember3.ts.golden +++ b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/EventEventUpdated.ts.golden @@ -16,7 +16,7 @@ /** * Updated event with new value */ -export interface EventMember3 { +export interface EventEventUpdated { readonly kind: 'EVENT_UPDATED'; /** * The new value after update diff --git a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/index.ts.golden index 0fd39e550..9a33d68a1 100644 --- a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/index.ts.golden +++ b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-inline-discriminator-only/models/index.ts.golden @@ -14,6 +14,6 @@ * "EventKindUnspecified" instead of generic "Kind". */ export type { Event } from './Event'; -export type { EventMember1 } from './EventMember1'; -export type { EventMember2 } from './EventMember2'; -export type { EventMember3 } from './EventMember3'; +export type { EventEventCreated } from './EventEventCreated'; +export type { EventEventUnspecified } from './EventEventUnspecified'; +export type { EventEventUpdated } from './EventEventUpdated'; diff --git a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-mixed-unit-and-allof/models/Setup.ts.golden b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-mixed-unit-and-allof/models/Setup.ts.golden index 240a281bb..725f2c8fc 100644 --- a/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-mixed-unit-and-allof/models/Setup.ts.golden +++ b/tests/golden/typescript/typescript-fetch/type-aliases-discriminated-union-mixed-unit-and-allof/models/Setup.ts.golden @@ -11,8 +11,7 @@ */ import type { SetupCustom } from './SetupCustom'; import type { SetupQuick } from './SetupQuick'; -import type { SetupSetupUnspecified } from './SetupSetupUnspecified'; /** Setup: quick (managed), custom, or unspecified. */ -export type Setup = ({ kind: 'SETUP_UNSPECIFIED' } & SetupSetupUnspecified) | ({ kind: 'SETUP_QUICK' } & SetupQuick) | ({ kind: 'SETUP_CUSTOM' } & SetupCustom); +export type Setup = { kind: 'SETUP_UNSPECIFIED' } | ({ kind: 'SETUP_QUICK' } & SetupQuick) | ({ kind: 'SETUP_CUSTOM' } & SetupCustom); diff --git a/tests/golden/typescript/typescript-fetch/type-aliases-intersection-with-nullable-reference/models/BazAllOf2.ts.golden b/tests/golden/typescript/typescript-fetch/type-aliases-intersection-with-nullable-reference/models/BazAllOf2.ts.golden index c81ad80ff..f8a6ae913 100644 --- a/tests/golden/typescript/typescript-fetch/type-aliases-intersection-with-nullable-reference/models/BazAllOf2.ts.golden +++ b/tests/golden/typescript/typescript-fetch/type-aliases-intersection-with-nullable-reference/models/BazAllOf2.ts.golden @@ -10,5 +10,5 @@ export interface BazAllOf2 { /** * Metadata (nullable) */ - readonly metaData?: Bar; + readonly metaData?: Bar | null; } diff --git a/tests/golden/typescript/typescript-fetch/type-aliases-union-with-any/models/Config.ts.golden b/tests/golden/typescript/typescript-fetch/type-aliases-union-with-any/models/Config.ts.golden index 6ba79a83d..db569eb7a 100644 --- a/tests/golden/typescript/typescript-fetch/type-aliases-union-with-any/models/Config.ts.golden +++ b/tests/golden/typescript/typescript-fetch/type-aliases-union-with-any/models/Config.ts.golden @@ -4,4 +4,4 @@ * Union Type with Any Test — 1.0.0 * Test fixture for union types that include the any type */ -export type Config = string | number | unknown; +export type Config = unknown; diff --git a/tests/golden_tests_typescript_fetch.rs b/tests/golden_tests_typescript_fetch.rs index f62bc1057..64444444d 100644 --- a/tests/golden_tests_typescript_fetch.rs +++ b/tests/golden_tests_typescript_fetch.rs @@ -602,4 +602,9 @@ generate_vp_golden_tests! { test_toolchain_vp_camel_case_golden: ("ts-toolchain-vp", "valid/type-aliases/discriminated-union-with-refs.yaml", "property_naming = \"camelCase\""), test_toolchain_vp_delete_response_golden: ("ts-toolchain-vp-delete-response", "valid/delete-with-response-schema.yaml", "property_naming = \"camelCase\""), test_toolchain_vp_enum_repr_golden: ("ts-toolchain-vp-enum-repr", "valid/enum-repr/enum-repr.yaml", "emit_enum_constants = true"), + test_toolchain_vp_camel_case_tagged_nullable_unions_golden: ( + "ts-toolchain-vp-camel-case-tagged-nullable-unions", + "valid/type-aliases/typescript-fetch-vp-camel-case-tagged-nullable-unions.yaml", + "emit_enum_constants = true\nemit_type_guards = true\nproperty_naming = \"camelCase\"" + ), }