From ccb0962d2602011ae171af9782b9463d6ff56234 Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Fri, 13 Nov 2026 02:50:26 +0800 Subject: [PATCH] fix: preserve Go allOf object composition - Compose compatible object and map members as flat JSON instead of embedding anonymous map syntax that cannot compile or inheriting a sibling-hiding codec. - Retain named member entrypoints and promoted fields, naming anonymous maps and disambiguating repeated members and storage collisions without wire wrappers. - Project declared keys to their owning members so extension maps do not retain stale copies, and reject conflicting member encodings instead of overwriting. - Follow aliases and nested object facts with cached, deduplicated metadata so repeated composition references do not expand every path through the graph. - Offer composed and raw policies, preserving non-object, incompatible and cyclic intersections as json.RawMessage rather than emitting invalid or lossy models. - Retain ordinary encoding/json member behavior and typed HTTP request, success and lazy error payloads through structured model and method emission. - Run native Go consumers through the golden build entrypoint alongside exact render, versioned IR and golden contracts on untouched generated projects. - Keep ordinary object and dictionary codecs, shared lowering, other generators, generated dependencies and formatting redesign outside this Go repair. --- docs/src/SUMMARY.md | 1 + docs/src/go_config.md | 58 +++ just/golden-go.just | 10 +- scripts/generate-go-client-fixtures.sh | 24 ++ src/generators/go/http/codegen.rs | 1 + src/generators/go/http/composition.rs | 366 ++++++++++++++++++ src/generators/go/http/config.rs | 11 + src/generators/go/http/mod.rs | 1 + src/generators/go/http/sigil_emit.rs | 45 +-- .../fixtures/valid/go-allof-composition.yaml | 104 +++++ tests/generated-clients/go/.gitignore | 1 + tests/generated-clients/go/README.md | 12 + .../go/composition/composition_test.go | 274 +++++++++++++ tests/generated-clients/go/composition/go.mod | 7 + tests/generated-clients/go/fixtures.txt | 2 + tests/generated-clients/go/raw/go.mod | 7 + tests/generated-clients/go/raw/raw_test.go | 24 ++ tests/go_allof_composition.rs | 82 ++++ .../allof-composition/README.md.golden | 7 + .../allof-composition/apis/default.go.golden | 132 +++++++ .../go-http/allof-composition/go.mod.golden | 3 + .../models/additional_properties.go.golden | 9 + .../allof-composition/models/alias.go.golden | 8 + .../models/aliased.go.golden | 90 +++++ .../models/array_value.go.golden | 9 + .../allof-composition/models/base.go.golden | 60 +++ .../allof-composition/models/cycle.go.golden | 9 + .../allof-composition/models/holder.go.golden | 11 + .../models/incompatible.go.golden | 9 + .../allof-composition/models/maps.go.golden | 86 ++++ .../models/named_map.go.golden | 88 +++++ .../allof-composition/models/nested.go.golden | 91 +++++ .../models/open_all_of.go.golden | 88 +++++ .../allof-composition/models/other.go.golden | 9 + .../allof-composition/models/pair.go.golden | 90 +++++ .../models/repeated.go.golden | 86 ++++ .../allof-composition/models/scalar.go.golden | 9 + .../allof-composition/models/single.go.golden | 58 +++ .../models/storage_collision.go.golden | 88 +++++ .../models/string_field.go.golden | 9 + .../models/strings.go.golden | 59 +++ .../models/typed_map.go.golden | 88 +++++ .../allof-composition/runtime/auth.go.golden | 112 ++++++ .../runtime/client.go.golden | 98 +++++ .../runtime/errors.go.golden | 30 ++ .../go/go-http/allof-raw/README.md.golden | 7 + .../go-http/allof-raw/apis/default.go.golden | 132 +++++++ .../golden/go/go-http/allof-raw/go.mod.golden | 3 + .../models/additional_properties.go.golden | 9 + .../go-http/allof-raw/models/alias.go.golden | 8 + .../allof-raw/models/aliased.go.golden | 9 + .../allof-raw/models/array_value.go.golden | 9 + .../go-http/allof-raw/models/base.go.golden | 60 +++ .../go-http/allof-raw/models/cycle.go.golden | 9 + .../go-http/allof-raw/models/holder.go.golden | 11 + .../allof-raw/models/incompatible.go.golden | 9 + .../go-http/allof-raw/models/maps.go.golden | 9 + .../allof-raw/models/named_map.go.golden | 9 + .../go-http/allof-raw/models/nested.go.golden | 9 + .../allof-raw/models/open_all_of.go.golden | 9 + .../go-http/allof-raw/models/other.go.golden | 9 + .../go-http/allof-raw/models/pair.go.golden | 9 + .../allof-raw/models/repeated.go.golden | 9 + .../go-http/allof-raw/models/scalar.go.golden | 9 + .../go-http/allof-raw/models/single.go.golden | 9 + .../models/storage_collision.go.golden | 9 + .../allof-raw/models/string_field.go.golden | 9 + .../allof-raw/models/strings.go.golden | 59 +++ .../allof-raw/models/typed_map.go.golden | 9 + .../go-http/allof-raw/runtime/auth.go.golden | 112 ++++++ .../allof-raw/runtime/client.go.golden | 98 +++++ .../allof-raw/runtime/errors.go.golden | 30 ++ .../models/all_of_objects.go.golden | 86 ++++ .../models/foo.go.golden | 86 ++++ .../models/baz.go.golden | 92 +++++ tests/golden_tests_go_http.rs | 13 + 76 files changed, 3383 insertions(+), 33 deletions(-) create mode 100644 docs/src/go_config.md create mode 100644 scripts/generate-go-client-fixtures.sh create mode 100644 src/generators/go/http/composition.rs create mode 100644 tests/fixtures/valid/go-allof-composition.yaml create mode 100644 tests/generated-clients/go/.gitignore create mode 100644 tests/generated-clients/go/README.md create mode 100644 tests/generated-clients/go/composition/composition_test.go create mode 100644 tests/generated-clients/go/composition/go.mod create mode 100644 tests/generated-clients/go/fixtures.txt create mode 100644 tests/generated-clients/go/raw/go.mod create mode 100644 tests/generated-clients/go/raw/raw_test.go create mode 100644 tests/go_allof_composition.rs create mode 100644 tests/golden/go/go-http/allof-composition/README.md.golden create mode 100644 tests/golden/go/go-http/allof-composition/apis/default.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/go.mod.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/additional_properties.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/alias.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/aliased.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/array_value.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/base.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/cycle.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/holder.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/incompatible.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/maps.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/named_map.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/nested.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/open_all_of.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/other.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/pair.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/repeated.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/scalar.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/single.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/storage_collision.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/string_field.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/strings.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/models/typed_map.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/runtime/auth.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/runtime/client.go.golden create mode 100644 tests/golden/go/go-http/allof-composition/runtime/errors.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/README.md.golden create mode 100644 tests/golden/go/go-http/allof-raw/apis/default.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/go.mod.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/additional_properties.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/alias.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/aliased.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/array_value.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/base.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/cycle.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/holder.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/incompatible.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/maps.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/named_map.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/nested.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/open_all_of.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/other.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/pair.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/repeated.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/scalar.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/single.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/storage_collision.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/string_field.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/strings.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/models/typed_map.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/runtime/auth.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/runtime/client.go.golden create mode 100644 tests/golden/go/go-http/allof-raw/runtime/errors.go.golden diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 3bcaa16e0..501aa59ed 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -7,6 +7,7 @@ - [Multipart and Binary Bodies](multipart_and_binary.md) - [Response Headers](response_headers.md) - [Rust Generator Configuration](rust_config.md) +- [Go Generator Configuration](go_config.md) - [TypeScript Generator Configuration](ts_config.md) - [Python Generator Configuration](python_config.md) - [Java Generator Configuration](java_config.md) diff --git a/docs/src/go_config.md b/docs/src/go_config.md new file mode 100644 index 000000000..d4abaa31e --- /dev/null +++ b/docs/src/go_config.md @@ -0,0 +1,58 @@ +# Go Generator Configuration + +```toml +[go-http] +module_path = "example.com/sdk" +all_of_strategy = "composed" +``` + +## Object and map composition + +The default `composed` policy retains named object members as embedded Go fields +and gives anonymous map members named storage such as `AdditionalProperties`. +Generated JSON methods combine the members into one flat wire object rather +than inheriting one embedded member's JSON methods and losing its siblings. + +```go +value := models.Pair{ + Base: models.Base{KnownField: "fixed"}, + Other: models.Other{OtherField: 7}, +} +// json.Marshal(value): {"known_field":"fixed","other_field":7} +``` + +Existing named member entrypoints and promoted fields remain available. Repeated +member names or map-storage collisions receive numeric suffixes. Aliases and +nested object compositions are followed without rewriting ordinary model codecs. + +On reading, each member receives its own declared keys and globally unknown keys. +A sibling's declared keys are not duplicated into extension storage, so editing a +named field does not leave stale values in a map member. Typed members retain their +native `encoding/json` behavior. If member writes produce different JSON encodings +for the same key, marshaling returns an error instead of silently overwriting data. + +This is not a complete JSON Schema validator. It uses retained IR object facts and +conservative representation compatibility; ordinary member validation, optional +fields, null behavior and dynamic `any` number conversion remain unchanged. +In particular, `any` entries still follow Go's native floating-point conventions. +It does not infer missing shared-lowering facts or redesign ordinary dictionary +codecs. + +## Raw escape hatch + +Set `all_of_strategy = "raw"` to emit every intersection as an alias of +`json.RawMessage`, preserving JSON kinds, nulls and exact numeric literals without +typed member selection. The same raw representation is the default fallback for +non-object intersections, unproductive composition cycles and object/map types +that cannot be conservatively composed. No unconditional generation error or +invalid anonymous-map embedding is introduced for these shapes. + +```bash +openapi-nexus generate --input api.yaml --output sdk --generators go-http \ + --generator-config go-http.all_of_strategy=raw +``` + +Raw values intentionally do not enforce intersection constraints. Use them when +wire preservation is more useful than inferred typed composition. This policy +does not change untagged unions, ordinary objects, other generators or generated +dependencies. diff --git a/just/golden-go.just b/just/golden-go.just index 3d9a1f7ab..a6f62ec27 100644 --- a/just/golden-go.just +++ b/just/golden-go.just @@ -7,5 +7,13 @@ set working-directory := '..' UPDATE_GOLDEN=1 cargo test --test golden_tests_go_http -- --nocapture # Compile-check Go goldens with go build -@build: +@build: test ./scripts/golden-build-go.sh + +# Generate fresh dependencies before ordinary Go consumer execution. +@prepare: + bash ./scripts/generate-go-client-fixtures.sh + +@test: prepare + go -C tests/generated-clients/go/composition test ./... + go -C tests/generated-clients/go/raw test ./... diff --git a/scripts/generate-go-client-fixtures.sh b/scripts/generate-go-client-fixtures.sh new file mode 100644 index 000000000..9fc5a558b --- /dev/null +++ b/scripts/generate-go-client-fixtures.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" +project="$root/tests/generated-clients/go" +while read -r name fixture strategy; do + case "$name" in + ""|\#*) continue ;; + *[!a-zA-Z0-9_-]*) echo "invalid fixture name: $name" >&2; exit 1 ;; + esac + output="$project/generated/$name" + options=(--config "$root/tests/generated-clients/config.toml" + --input "$root/tests/fixtures/$fixture" --output "$output" --generators go-http) + if [ -n "$strategy" ]; then + case "$strategy" in + composed|raw) ;; + *) echo "invalid allOf strategy: $strategy" >&2; exit 1 ;; + esac + options+=(--generator-config "go-http.all_of_strategy=$strategy") + fi + # Only these fixed, ignored fixture output directories are disposable. + rm -rf "$output" + cargo run --quiet --manifest-path "$root/Cargo.toml" --bin openapi-nexus -- generate "${options[@]}" +done < "$project/fixtures.txt" diff --git a/src/generators/go/http/codegen.rs b/src/generators/go/http/codegen.rs index fd9329d00..28d3c6400 100644 --- a/src/generators/go/http/codegen.rs +++ b/src/generators/go/http/codegen.rs @@ -55,6 +55,7 @@ impl GoHttpCodeGenerator { &module_path, &header, &request_inputs, + self.config.all_of_strategy, )?); // APIs diff --git a/src/generators/go/http/composition.rs b/src/generators/go/http/composition.rs new file mode 100644 index 000000000..30087b88c --- /dev/null +++ b/src/generators/go/http/composition.rs @@ -0,0 +1,366 @@ +//! Preserve Go member entrypoints while composing one flat JSON object. + +use std::collections::{BTreeMap, BTreeSet}; + +use heck::ToPascalCase; +use sigil_stitch::lang::go::Go; +use sigil_stitch::prelude::*; + +use crate::ir::types::{ + IrEnumValueType, IrIntersection, IrPrimitive, IrSchema, IrSchemaKind, IrSpec, IrTypeExpr, +}; + +use super::config::AllOfStrategy; +use super::sigil_emit::{go_type_name, package_header}; + +struct Member<'a> { + field: String, + expr: &'a IrTypeExpr, + embedded: bool, + keys: BTreeSet, +} + +#[derive(Clone, Default)] +struct ObjectView { + keys: BTreeSet, + fields: Vec<(String, IrTypeExpr)>, + additional: Vec<(BTreeSet, IrTypeExpr)>, +} + +/// Follow only object/map identities. Non-object and unproductive cycles stay raw; +/// this deliberately does not pretend that arbitrary intersections are objects. +fn object_view( + expr: &IrTypeExpr, + ir: &IrSpec, + visiting: &mut BTreeSet, + cache: &mut BTreeMap, +) -> Option { + match expr { + IrTypeExpr::Map(values) => Some(ObjectView { + additional: vec![(BTreeSet::new(), *values.clone())], + ..Default::default() + }), + IrTypeExpr::Nullable(inner) => object_view(inner, ir, visiting, cache), + IrTypeExpr::Named(name) => { + if let Some(view) = cache.get(name) { + return Some(view.clone()); + } + if !visiting.insert(name.clone()) { + return None; + } + let result = match &ir.schemas.get(name)?.kind { + IrSchemaKind::Object(object) => { + let keys = object.properties.keys().cloned().collect::>(); + Some(ObjectView { + fields: object + .properties + .iter() + .map(|(key, property)| { + ( + key.clone(), + if property.nullable { + IrTypeExpr::Nullable(Box::new(property.type_expr.clone())) + } else { + property.type_expr.clone() + }, + ) + }) + .collect(), + additional: object + .additional_properties + .iter() + .map(|values| (keys.clone(), values.clone())) + .collect(), + keys, + }) + } + IrSchemaKind::Alias(inner) => object_view(inner, ir, visiting, cache), + IrSchemaKind::Intersection(intersection) => { + let mut result = ObjectView::default(); + for member in &intersection.members { + let view = object_view(member, ir, visiting, cache)?; + result.keys.extend(view.keys); + for field in view.fields { + if !result.fields.contains(&field) { + result.fields.push(field); + } + } + for additional in view.additional { + if !result.additional.contains(&additional) { + result.additional.push(additional); + } + } + } + Some(result) + } + _ => None, + }; + visiting.remove(name); + if let Some(view) = &result { + cache.insert(name.clone(), view.clone()); + } + result + } + _ => None, + } +} + +fn representation(expr: &IrTypeExpr, ir: &IrSpec, visiting: &mut BTreeSet) -> IrTypeExpr { + match expr.representation() { + IrTypeExpr::Named(name) if visiting.insert(name.clone()) => { + let result = match ir.schemas.get(name).map(|schema| &schema.kind) { + Some(IrSchemaKind::Alias(inner)) => representation(inner, ir, visiting), + Some(IrSchemaKind::Enum(value)) => match value.value_type { + IrEnumValueType::String => IrTypeExpr::Primitive(IrPrimitive::String), + IrEnumValueType::Integer => IrTypeExpr::Primitive(IrPrimitive::Integer), + IrEnumValueType::Number => IrTypeExpr::Primitive(IrPrimitive::Number), + IrEnumValueType::Mixed => IrTypeExpr::Any, + }, + _ => expr.clone(), + }; + visiting.remove(name); + result + } + IrTypeExpr::Nullable(inner) => { + IrTypeExpr::Nullable(Box::new(representation(inner, ir, visiting))) + } + IrTypeExpr::Array(inner) => { + IrTypeExpr::Array(Box::new(representation(inner, ir, visiting))) + } + IrTypeExpr::Map(inner) => IrTypeExpr::Map(Box::new(representation(inner, ir, visiting))), + IrTypeExpr::StringEnum(_) | IrTypeExpr::StringLiteral(_) => { + IrTypeExpr::Primitive(IrPrimitive::String) + } + other => other.clone(), + } +} + +/// A conservative representation check, not a full JSON Schema validator. +fn accepts(values: &IrTypeExpr, field: &IrTypeExpr) -> bool { + if values == field || matches!(values, IrTypeExpr::Any) { + return true; + } + match (values, field) { + (IrTypeExpr::Nullable(values), IrTypeExpr::Nullable(field)) => accepts(values, field), + (IrTypeExpr::Nullable(values), field) => accepts(values, field), + ( + IrTypeExpr::Primitive(IrPrimitive::Number | IrPrimitive::NumberWithFormat(_)), + IrTypeExpr::Primitive(IrPrimitive::Integer | IrPrimitive::IntegerWithFormat(_)), + ) => true, + _ => false, + } +} + +fn embedded_name(expr: &IrTypeExpr) -> Option { + match expr { + IrTypeExpr::Named(name) => Some(name.to_pascal_case()), + IrTypeExpr::Nullable(inner) => embedded_name(inner), + _ => None, + } +} + +fn plan<'a>(intersection: &'a IrIntersection, ir: &IrSpec) -> Option>> { + // Repeated references in a composition DAG must not expand metadata as a tree. + let mut cache = BTreeMap::new(); + let views = intersection + .members + .iter() + .map(|expr| object_view(expr, ir, &mut BTreeSet::new(), &mut cache)) + .collect::>>()?; + let fields = views + .iter() + .flat_map(|view| &view.fields) + .map(|(key, expr)| (key, representation(expr, ir, &mut BTreeSet::new()))) + .collect::>(); + for (index, (key, expr)) in fields.iter().enumerate() { + if fields[..index].iter().any(|(other_key, other)| { + key == other_key && !accepts(expr, other) && !accepts(other, expr) + }) { + return None; + } + } + for (known, values) in views.iter().flat_map(|view| &view.additional) { + let values = representation(values, ir, &mut BTreeSet::new()); + if fields + .iter() + .any(|(key, expr)| !known.contains(key.as_str()) && !accepts(&values, expr)) + { + return None; + } + } + let mut used = intersection + .members + .iter() + .filter_map(embedded_name) + .collect::>(); + let mut embedded = BTreeSet::new(); + intersection + .members + .iter() + .zip(views) + .map(|(expr, view)| { + let keys = view.keys; + let name = embedded_name(expr); + let keep_embedded = name + .as_ref() + .is_some_and(|name| embedded.insert(name.clone())); + let base = name.unwrap_or_else(|| "AdditionalProperties".into()); + let field = if keep_embedded { + base + } else { + let mut candidate = base.clone(); + let mut suffix = 2; + while !used.insert(candidate.clone()) { + candidate = format!("{base}{suffix}"); + suffix += 1; + } + candidate + }; + Some(Member { + field, + expr, + embedded: keep_embedded, + keys, + }) + }) + .collect() +} + +pub(super) fn emit( + schema: &IrSchema, + intersection: &IrIntersection, + ir: &IrSpec, + strategy: AllOfStrategy, +) -> Result { + let name = schema.name.to_pascal_case(); + let root = TypeName::primitive(&name); + let mut file = FileSpec::builder_with("composition.go", Go::new()).header(package_header()?); + let members = if strategy == AllOfStrategy::Raw { + None + } else { + plan(intersection, ir) + }; + let Some(members) = members.filter(|members| !members.is_empty()) else { + let mut alias = TypeSpec::builder(&name, TypeKind::TypeAlias) + .extends(TypeName::importable("encoding/json", "RawMessage")); + if let Some(doc) = &schema.description { + alias = alias.doc(doc); + } + return file.add_type(alias.build()?).build()?.render(100); + }; + let mut declaration = TypeSpec::builder(&name, TypeKind::Struct); + if let Some(doc) = &schema.description { + declaration = declaration.doc(doc); + } + for member in &members { + declaration = if member.embedded { + declaration.add_embedded(go_type_name(member.expr)) + } else { + declaration.add_field( + FieldSpec::builder(&member.field, go_type_name(member.expr)) + .tag("json:\"-\"") + .build()?, + ) + }; + } + file = file + .add_type(declaration.build()?) + .add_import(ImportSpec::named("encoding/json", "json")) + .add_import(ImportSpec::named("bytes", "bytes")) + .add_import(ImportSpec::named("fmt", "fmt")); + let marshal = FunSpec::builder("MarshalJSON") + .receiver(ParameterSpec::of("o", root.clone())) + .returns(vec![TypeName::slice(TypeName::primitive("byte")), TypeName::primitive("error")]) + .body(sigil_quote!(Go { + merged := make(map[string]json.RawMessage) + $for((index, member) in members.iter().enumerate()) { + $N(format!("memberData{index}")), err := json.Marshal(o.$N(&member.field)) + if err != nil { return nil, err } + var $N(format!("memberFields{index}")) map[string]json.RawMessage + if err := json.Unmarshal($N(format!("memberData{index}")), &$N(format!("memberFields{index}"))); err != nil { return nil, err } + for key, value := range $N(format!("memberFields{index}")) { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + } + return json.Marshal(merged) + })?).build()?; + let all_keys = members + .iter() + .flat_map(|member| &member.keys) + .collect::>(); + let unmarshal = FunSpec::builder("UnmarshalJSON") + .receiver(ParameterSpec::of("o", TypeName::pointer(root))) + .add_param(ParameterSpec::of("data", TypeName::slice(TypeName::primitive("byte")))) + .returns(vec![TypeName::primitive("error")]) + .body(sigil_quote!(Go { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { return err } + if fields == nil { return nil } + $for((index, member) in members.iter().enumerate()) { + $N(format!("projected{index}")) := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + $for(key in all_keys.iter().filter(|key| !member.keys.contains(key.as_str())); separator = "\n") { + case $S(key): + continue + } + } + $N(format!("projected{index}"))[key] = value + } + $N(format!("encoded{index}")), err := json.Marshal($N(format!("projected{index}"))) + if err != nil { return err } + if err := json.Unmarshal($N(format!("encoded{index}")), &o.$N(&member.field)); err != nil { return err } + } + return nil + })?).build()?; + file.add_function(marshal) + .add_function(unmarshal) + .build()? + .render(100) +} + +#[cfg(test)] +mod tests { + use super::*; + use sigil_stitch::assert_rendered; + + #[test] + fn go_map_storage_and_json_method_receiver_render_semantically() { + let declaration = TypeSpec::builder("Composed", TypeKind::Struct) + .add_embedded(TypeName::primitive("Base")) + .add_field( + FieldSpec::builder( + "AdditionalProperties", + TypeName::map(TypeName::primitive("string"), TypeName::primitive("any")), + ) + .tag("json:\"-\"") + .build() + .unwrap(), + ) + .build() + .unwrap() + .emit(&Go::new()) + .unwrap(); + let method = FunSpec::builder("MarshalJSON") + .receiver(ParameterSpec::of("o", TypeName::primitive("Composed"))) + .returns(vec![ + TypeName::slice(TypeName::primitive("byte")), + TypeName::primitive("error"), + ]) + .body(sigil_quote!(Go { return json.Marshal(o.AdditionalProperties) }).unwrap()) + .build() + .unwrap() + .emit(&Go::new(), DeclarationContext::TopLevel) + .unwrap(); + let block = sigil_quote!(Go { $C_each(declaration) $C(method) }).unwrap(); + assert_rendered!( + Go::new(), + width = 100, + block, + "type Composed struct {\n\tBase\n\tAdditionalProperties map[string]any `json:\"-\"`\n}\nfunc (o Composed) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(o.AdditionalProperties)\n}\n" + ); + } +} diff --git a/src/generators/go/http/config.rs b/src/generators/go/http/config.rs index 13fac7ce6..5def20bbc 100644 --- a/src/generators/go/http/config.rs +++ b/src/generators/go/http/config.rs @@ -9,6 +9,17 @@ pub struct GoHttpConfig { /// Go module path (e.g., "github.com/example/sdk") #[serde(default)] pub module_path: Option, + /// Compose object members by default, or retain every allOf as raw JSON. + #[serde(default)] + pub all_of_strategy: AllOfStrategy, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum AllOfStrategy { + #[default] + Composed, + Raw, } impl From for GoHttpConfig { diff --git a/src/generators/go/http/mod.rs b/src/generators/go/http/mod.rs index 99d673c48..138b5400a 100644 --- a/src/generators/go/http/mod.rs +++ b/src/generators/go/http/mod.rs @@ -1,4 +1,5 @@ pub mod codegen; +mod composition; pub mod config; pub mod runtime; pub mod sigil_emit; diff --git a/src/generators/go/http/sigil_emit.rs b/src/generators/go/http/sigil_emit.rs index 91ca04709..de27dfe7d 100644 --- a/src/generators/go/http/sigil_emit.rs +++ b/src/generators/go/http/sigil_emit.rs @@ -8,7 +8,7 @@ //! - `Enum` — typed string/int constants + named type alias. //! - `Alias` — `type X = Y` alias. //! - `Union` — `interface{}` alias plus variant structs (untagged, simplified). -//! - `Intersection` — struct composed by embedding each named member. +//! - `Intersection` — flat object/map composition or raw JSON fallback. //! - `TaggedUnion` — typed marker interface + one struct per variant. //! //! Imports for cross-schema references are tracked via sigil-stitch's @@ -22,8 +22,8 @@ use crate::generators::request_inputs::{ }; use crate::generators::{ArtifactKind, GeneratorError}; use crate::ir::types::{ - IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrProperty, IrSchema, - IrSchemaKind, IrSpec, IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle, + IrEnum, IrEnumValueType, IrObject, IrPrimitive, IrProperty, IrSchema, IrSchemaKind, IrSpec, + IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle, }; use heck::{ToPascalCase, ToSnakeCase}; use sigil_stitch::lang::go::Go; @@ -45,10 +45,11 @@ pub fn generate_model_files( module_path: &str, header: &str, request_inputs: &RequestInputPlan, + all_of_strategy: super::config::AllOfStrategy, ) -> Result, GeneratorError> { let mut files = Vec::new(); for (_name, schema) in &ir.schemas { - let body = emit_model_body(schema).map_err(|source| { + let body = emit_model_body(schema, ir, all_of_strategy).map_err(|source| { GeneratorError::emission(ArtifactKind::Model, &schema.name, source) })?; files.push(model_file(&schema.name, header, &body)); @@ -119,13 +120,17 @@ fn request_input_go_type(field: &RequestInputField) -> String { /// Dispatch on schema kind. Returns the rendered file body (package + imports /// + declarations) but not the pre-package header comment. -fn emit_model_body(schema: &IrSchema) -> Result { +fn emit_model_body( + schema: &IrSchema, + ir: &IrSpec, + all_of_strategy: super::config::AllOfStrategy, +) -> Result { let file_spec = match &schema.kind { IrSchemaKind::Object(obj) => emit_object(schema, obj), IrSchemaKind::Enum(en) => emit_enum(schema, en), IrSchemaKind::Alias(expr) => emit_alias(schema, expr), IrSchemaKind::Union(u) => emit_union(schema, u), - IrSchemaKind::Intersection(i) => emit_intersection(schema, i), + IrSchemaKind::Intersection(i) => super::composition::emit(schema, i, ir, all_of_strategy), IrSchemaKind::TaggedUnion(tu) => emit_tagged_union(schema, tu), }?; Ok(file_spec) @@ -369,30 +374,6 @@ fn emit_union( render_alias_file(&name, "any", schema.description.as_deref()) } -// --------------------------------------------------------------------------- -// Intersection -> struct with embedded members -// --------------------------------------------------------------------------- - -fn emit_intersection( - schema: &IrSchema, - inter: &IrIntersection, -) -> Result { - let name = schema.name.to_pascal_case(); - let mut tb = TypeSpec::builder(&name, TypeKind::Struct); - if let Some(doc) = &schema.description { - tb = tb.doc(doc); - } - for member in &inter.members { - tb = tb.add_embedded(go_type_name(member)); - } - - let fb = FileSpec::builder(&format!("{}.go", name)) - .header(package_header()?) - .add_type(tb.build()?); - let file = fb.build()?; - file.render(RENDER_WIDTH) -} - // --------------------------------------------------------------------------- // TaggedUnion -> marker interface + variant wrappers (simplified) // --------------------------------------------------------------------------- @@ -431,7 +412,7 @@ fn emit_tagged_union( // --------------------------------------------------------------------------- /// Build a `package models` header block. -fn package_header() -> Result { +pub(super) fn package_header() -> Result { sigil_quote!(Go { package $L(MODELS_PACKAGE) }) @@ -464,7 +445,7 @@ fn render_alias_file( /// All same-package references (named schemas, primitives) resolve to a plain /// `TypeName::primitive` with the Go identifier. Cross-package imports aren't /// needed within the `models/` tree. -fn go_type_name(expr: &IrTypeExpr) -> TypeName { +pub(super) fn go_type_name(expr: &IrTypeExpr) -> TypeName { match expr { IrTypeExpr::ScalarEnum { representation, .. } => go_type_name(representation), IrTypeExpr::Named(name) => TypeName::primitive(&name.to_pascal_case()), diff --git a/tests/fixtures/valid/go-allof-composition.yaml b/tests/fixtures/valid/go-allof-composition.yaml new file mode 100644 index 000000000..75b3f4a14 --- /dev/null +++ b/tests/fixtures/valid/go-allof-composition.yaml @@ -0,0 +1,104 @@ +openapi: 3.1.0 +info: {title: Go allOf composition, version: '1.0.0'} +paths: + /composition: + post: + operationId: echoComposition + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/Pair'} + responses: + '200': + description: Flat composed object + content: + application/json: + schema: {$ref: '#/components/schemas/Pair'} + '400': + description: Composed error + content: + application/json: + schema: {$ref: '#/components/schemas/Pair'} +components: + schemas: + Base: + type: object + required: [known_field] + properties: + known_field: {type: string} + additionalProperties: true + OpenAllOf: + allOf: + - {$ref: '#/components/schemas/Base'} + - {type: object, additionalProperties: true} + Other: + type: object + required: [other_field] + properties: + other_field: {type: integer} + Pair: + allOf: + - {$ref: '#/components/schemas/Base'} + - {$ref: '#/components/schemas/Other'} + Nested: + allOf: + - {$ref: '#/components/schemas/Pair'} + - {type: object, additionalProperties: true} + Alias: {$ref: '#/components/schemas/Base'} + Aliased: + allOf: + - {$ref: '#/components/schemas/Alias'} + - {$ref: '#/components/schemas/Other'} + Repeated: + allOf: + - {$ref: '#/components/schemas/Base'} + - {$ref: '#/components/schemas/Base'} + Strings: + type: object + additionalProperties: {type: string} + StringField: + type: object + required: [label] + properties: + label: {type: string} + TypedMap: + allOf: + - {$ref: '#/components/schemas/StringField'} + - {type: object, additionalProperties: {type: string}} + NamedMap: + allOf: + - {$ref: '#/components/schemas/StringField'} + - {$ref: '#/components/schemas/Strings'} + Maps: + allOf: + - {type: object, additionalProperties: true} + - {type: object, additionalProperties: true} + AdditionalProperties: + type: object + required: [label] + properties: + label: {type: string} + StorageCollision: + allOf: + - {type: object, additionalProperties: true} + - {$ref: '#/components/schemas/AdditionalProperties'} + Single: + allOf: [{$ref: '#/components/schemas/Base'}] + Scalar: + allOf: [{type: integer}, {type: number}] + ArrayValue: + allOf: [{type: array, items: {type: string}}] + Cycle: + allOf: [{$ref: '#/components/schemas/Cycle'}] + Incompatible: + allOf: + - {$ref: '#/components/schemas/StringField'} + - {type: object, additionalProperties: {type: integer}} + Holder: + type: object + required: [pair, nested, single] + properties: + pair: {$ref: '#/components/schemas/Pair'} + nested: {$ref: '#/components/schemas/Nested'} + single: {$ref: '#/components/schemas/Single'} diff --git a/tests/generated-clients/go/.gitignore b/tests/generated-clients/go/.gitignore new file mode 100644 index 000000000..9e0adcc10 --- /dev/null +++ b/tests/generated-clients/go/.gitignore @@ -0,0 +1 @@ +/generated/ diff --git a/tests/generated-clients/go/README.md b/tests/generated-clients/go/README.md new file mode 100644 index 000000000..c5b02c348 --- /dev/null +++ b/tests/generated-clients/go/README.md @@ -0,0 +1,12 @@ +# Go generated-client consumers + +Run `just golden-go::test` to generate fresh default/raw clients and execute these +ordinary Go tests. `just golden-go::build` includes them before compiling goldens. +The tests do not invoke generators, compilers or other runners, and generated +source and module manifests are not patched before execution. + +The composed consumer checks flat object/map allOf payloads, original embedded +member entrypoints, named and anonymous dictionaries, nested aliases, repeated +members, storage collisions, native errors, containing models, HTTP request/success/ +lazy error bodies and raw fallbacks. +The raw consumer checks exact numeric literals, nulls and non-object JSON kinds. diff --git a/tests/generated-clients/go/composition/composition_test.go b/tests/generated-clients/go/composition/composition_test.go new file mode 100644 index 000000000..d3a829dd9 --- /dev/null +++ b/tests/generated-clients/go/composition/composition_test.go @@ -0,0 +1,274 @@ +package composition_test + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + + "example.com/sdk/apis" + "example.com/sdk/models" + "example.com/sdk/runtime" +) + +func assertJSON(t *testing.T, expected string, value any) { + t.Helper() + data, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var want, got any + if err := json.Unmarshal([]byte(expected), &want); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(data, &got); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(want, got) { + t.Fatalf("want %s; got %s", expected, data) + } +} + +func TestHTTPRequestsAndSuccessResponsesKeepFlatComposition(t *testing.T) { + wire := `{"known_field":"fixed","other_field":7,"nil":null}` + captured := make(chan []byte, 1) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" || r.URL.Path != "/composition" { + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path) + } + body, err := io.ReadAll(r.Body) + if err != nil { + t.Error(err) + } + captured <- body + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, wire) + })) + defer server.Close() + var request models.Pair + if err := json.Unmarshal([]byte(wire), &request); err != nil { + t.Fatal(err) + } + response, err := apis.NewDefaultAPI(runtime.NewClient(server.URL)).EchoComposition(context.Background(), &request) + if err != nil { + t.Fatal(err) + } + assertJSON(t, wire, json.RawMessage(<-captured)) + assertJSON(t, wire, response.Status200) +} + +func TestLazyHTTPErrorsRetainTypedCompositionAndNativeDecodeFailures(t *testing.T) { + for _, wire := range []string{`{"known_field":"fixed","other_field":7,"nil":null}`, `{"known_field":false}`} { + t.Run(wire, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + _, _ = io.WriteString(w, wire) + })) + defer server.Close() + _, err := apis.NewDefaultAPI(runtime.NewClient(server.URL)).EchoComposition(context.Background(), &models.Pair{}) + var failure *apis.EchoCompositionError + if !errors.As(err, &failure) { + t.Fatalf("unexpected HTTP error %v", err) + } + detail, ok := failure.Detail.(*apis.EchoCompositionBadRequest) + if !ok { + t.Fatalf("unexpected detail %T", failure.Detail) + } + body, decodeErr := detail.Body() + if wire == `{"known_field":false}` { + if decodeErr == nil { + t.Fatal("native failure hidden") + } + } else { + if decodeErr != nil { + t.Fatal(decodeErr) + } + assertJSON(t, wire, body) + } + }) + } +} + +func TestObjectAndMapMembersKeepFlatWireShape(t *testing.T) { + wire := `{"known_field":"fixed","extra":{"nil":null},"knownField":"shadow"}` + var value models.OpenAllOf + if err := json.Unmarshal([]byte(wire), &value); err != nil { + t.Fatal(err) + } + if value.Base.KnownField != "fixed" { + t.Fatalf("lost named member: %#v", value) + } + assertJSON(t, wire, value) + assertJSON(t, `{"known_field":"fixed"}`, models.OpenAllOf{Base: models.Base{KnownField: "fixed"}}) +} + +func TestNamedMembersWithCustomCodecsDoNotHideSiblings(t *testing.T) { + wire := `{"known_field":"fixed","other_field":7,"nil":null}` + var value models.Pair + if err := json.Unmarshal([]byte(wire), &value); err != nil { + t.Fatal(err) + } + if value.Base.KnownField != "fixed" || value.Other.OtherField != 7 { + t.Fatalf("lost member: %#v", value) + } + if _, exists := value.Base.AdditionalProperties["other_field"]; exists { + t.Fatal("sibling captured as extension") + } + assertJSON(t, wire, value) + assertJSON(t, `{"known_field":"fixed","other_field":7}`, models.Pair{ + Base: models.Base{KnownField: "fixed"}, Other: models.Other{OtherField: 7}, + }) +} + +func TestNestedAndAliasCompositionsKeepTheirMemberEntrypoints(t *testing.T) { + wire := `{"known_field":"fixed","other_field":7,"nil":null}` + var nested models.Nested + if err := json.Unmarshal([]byte(wire), &nested); err != nil { + t.Fatal(err) + } + if nested.Pair.Other.OtherField != 7 { + t.Fatal("lost nested member") + } + assertJSON(t, wire, nested) + var aliased models.Aliased + if err := json.Unmarshal([]byte(wire), &aliased); err != nil { + t.Fatal(err) + } + if aliased.Alias.KnownField != "fixed" { + t.Fatal("lost alias member") + } + assertJSON(t, wire, aliased) +} + +func TestTypedAnonymousAndNamedDictionariesKeepValues(t *testing.T) { + wire := `{"label":"known","extra":"typed"}` + var direct models.TypedMap + if err := json.Unmarshal([]byte(wire), &direct); err != nil { + t.Fatal(err) + } + if direct.AdditionalProperties["extra"] != "typed" { + t.Fatal("lost typed map") + } + assertJSON(t, wire, direct) + var named models.NamedMap + if err := json.Unmarshal([]byte(wire), &named); err != nil { + t.Fatal(err) + } + if named.Strings.AdditionalProperties["extra"] != "typed" { + t.Fatal("lost named dictionary") + } + assertJSON(t, wire, named) + if err := json.Unmarshal([]byte(`{"label":"known","extra":2}`), &direct); err == nil { + t.Fatal("native map error hidden") + } +} + +func TestRepeatedMembersAndAnonymousMapsAreDisambiguated(t *testing.T) { + wire := `{"known_field":"fixed","nil":null}` + var repeated models.Repeated + if err := json.Unmarshal([]byte(wire), &repeated); err != nil { + t.Fatal(err) + } + if repeated.Base.KnownField != repeated.Base2.KnownField { + t.Fatal("duplicate member not decoded") + } + assertJSON(t, wire, repeated) + var maps models.Maps + if err := json.Unmarshal([]byte(`{"nil":null,"keep_key":[true]}`), &maps); err != nil { + t.Fatal(err) + } + if len(maps.AdditionalProperties) != 2 || len(maps.AdditionalProperties2) != 2 { + t.Fatal("missing map member") + } + assertJSON(t, `{"nil":null,"keep_key":[true]}`, maps) +} + +func TestStorageDoesNotShadowAnAuthoredMember(t *testing.T) { + var value models.StorageCollision + wire := `{"label":"known","extra":true}` + if err := json.Unmarshal([]byte(wire), &value); err != nil { + t.Fatal(err) + } + if value.AdditionalProperties.Label != "known" { + t.Fatal("authored member renamed") + } + if value.AdditionalProperties2["extra"] != true { + t.Fatal("missing map storage") + } + assertJSON(t, wire, value) +} + +func TestConflictingMemberWritesReturnAnErrorInsteadOfDroppingData(t *testing.T) { + value := models.Repeated{Base: models.Base{KnownField: "one"}, Base2: models.Base{KnownField: "two"}} + if _, err := json.Marshal(value); err == nil || !strings.Contains(err.Error(), "conflicting allOf property") { + t.Fatalf("expected collision error, got %v", err) + } +} + +func TestNamedFieldEditsDoNotLeaveStaleCopiesInDictionaryMembers(t *testing.T) { + var value models.OpenAllOf + if err := json.Unmarshal([]byte(`{"known_field":"old","extra":true}`), &value); err != nil { + t.Fatal(err) + } + if _, exists := value.AdditionalProperties["known_field"]; exists { + t.Fatal("declared field duplicated in map storage") + } + value.Base.KnownField = "new" + assertJSON(t, `{"known_field":"new","extra":true}`, value) +} + +func TestSingleMemberAndContainingModelsKeepFlatPayloads(t *testing.T) { + wire := `{"pair":{"known_field":"fixed","other_field":7},"nested":{"known_field":"fixed","other_field":7,"nil":null},"single":{"known_field":"fixed","nil":null}}` + var holder models.Holder + if err := json.Unmarshal([]byte(wire), &holder); err != nil { + t.Fatal(err) + } + assertJSON(t, wire, holder) +} + +func TestObjectCompositionRetainsNativeRootAndFieldFailures(t *testing.T) { + for _, wire := range []string{`[]`, `42`, `"bad"`, `{"known_field":2}`} { + var value models.Pair + if err := json.Unmarshal([]byte(wire), &value); err == nil { + t.Fatalf("accepted %s", wire) + } + } + value := models.Pair{Base: models.Base{KnownField: "retained"}} + if err := json.Unmarshal([]byte(`null`), &value); err != nil { + t.Fatal(err) + } + if value.Base.KnownField != "retained" { + t.Fatal("null changed existing object") + } +} + +func TestNonObjectAndCyclicIntersectionsUseRawJSONWithoutInvalidEmbedding(t *testing.T) { + var scalar models.Scalar + if err := json.Unmarshal([]byte(`9007199254740993`), &scalar); err != nil { + t.Fatal(err) + } + if string(scalar) != `9007199254740993` { + t.Fatal("raw number changed") + } + var array models.ArrayValue + if err := json.Unmarshal([]byte(`["keep",null]`), &array); err != nil { + t.Fatal(err) + } + assertJSON(t, `["keep",null]`, array) + var cycle models.Cycle + if err := json.Unmarshal([]byte(`{"nil":null}`), &cycle); err != nil { + t.Fatal(err) + } + assertJSON(t, `{"nil":null}`, cycle) + var incompatible models.Incompatible + if err := json.Unmarshal([]byte(`{"label":"kept","raw":null}`), &incompatible); err != nil { + t.Fatal(err) + } + assertJSON(t, `{"label":"kept","raw":null}`, incompatible) +} diff --git a/tests/generated-clients/go/composition/go.mod b/tests/generated-clients/go/composition/go.mod new file mode 100644 index 000000000..f8293ee01 --- /dev/null +++ b/tests/generated-clients/go/composition/go.mod @@ -0,0 +1,7 @@ +module example.com/composition-consumer + +go 1.21 + +require example.com/sdk v0.0.0 + +replace example.com/sdk => ../generated/composition diff --git a/tests/generated-clients/go/fixtures.txt b/tests/generated-clients/go/fixtures.txt new file mode 100644 index 000000000..2b513a9ce --- /dev/null +++ b/tests/generated-clients/go/fixtures.txt @@ -0,0 +1,2 @@ +composition valid/go-allof-composition.yaml +raw valid/go-allof-composition.yaml raw diff --git a/tests/generated-clients/go/raw/go.mod b/tests/generated-clients/go/raw/go.mod new file mode 100644 index 000000000..c59308ad1 --- /dev/null +++ b/tests/generated-clients/go/raw/go.mod @@ -0,0 +1,7 @@ +module example.com/composition-raw-consumer + +go 1.21 + +require example.com/sdk v0.0.0 + +replace example.com/sdk => ../generated/raw diff --git a/tests/generated-clients/go/raw/raw_test.go b/tests/generated-clients/go/raw/raw_test.go new file mode 100644 index 000000000..5ecef6fbf --- /dev/null +++ b/tests/generated-clients/go/raw/raw_test.go @@ -0,0 +1,24 @@ +package raw_test + +import ( + "encoding/json" + "testing" + + "example.com/sdk/models" +) + +func TestExplicitRawPolicyKeepsEveryJSONKindAndExactNumbers(t *testing.T) { + for _, wire := range []string{`null`, `42`, `[]`, `{"known_field":"fixed","large":9007199254740993,"nil":null}`} { + var value models.OpenAllOf + if err := json.Unmarshal([]byte(wire), &value); err != nil { + t.Fatal(err) + } + data, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + if string(data) != wire { + t.Fatalf("want %s; got %s", wire, data) + } + } +} diff --git a/tests/go_allof_composition.rs b/tests/go_allof_composition.rs new file mode 100644 index 000000000..bba244552 --- /dev/null +++ b/tests/go_allof_composition.rs @@ -0,0 +1,82 @@ +use openapi_nexus::codegen::traits::code_generator::CodeGenerator; +use openapi_nexus::generators::go::http::GoHttpCodeGenerator; +use openapi_nexus::ir::{ + lower::lower, + types::{IrSchemaKind, IrTypeExpr}, +}; +use openapi_nexus::parser::parse_content_yaml; + +#[test] +fn object_map_composition_retains_members_without_anonymous_map_embedding() { + let source = include_str!("fixtures/valid/go-allof-composition.yaml"); + for version in ["3.0.3", "3.1.0", "3.2.0"] { + let ir = lower(parse_content_yaml(&source.replacen("3.1.0", version, 1)).unwrap()).unwrap(); + let IrSchemaKind::Intersection(intersection) = &ir.schemas["OpenAllOf"].kind else { + panic!("allOf retained") + }; + assert!(matches!(&intersection.members[1], IrTypeExpr::Map(_))); + let files = GoHttpCodeGenerator::new(Default::default()) + .generate(&ir) + .unwrap(); + let model = &files + .iter() + .find(|f| f.filename == "open_all_of.go") + .unwrap() + .content; + assert!( + model.contains("AdditionalProperties map[string]any"), + "{model}" + ); + assert!(model.contains("MarshalJSON()"), "{model}"); + assert!(model.contains("UnmarshalJSON("), "{model}"); + assert!(!model.contains("\n\tmap["), "{model}"); + } +} + +#[test] +fn raw_policy_and_incompatible_shapes_keep_json_without_changing_ordinary_models() { + let source = include_str!("fixtures/valid/go-allof-composition.yaml"); + let ir = lower(parse_content_yaml(source).unwrap()).unwrap(); + let composed = GoHttpCodeGenerator::new(Default::default()) + .generate(&ir) + .unwrap(); + let raw = GoHttpCodeGenerator::new(toml::from_str("all_of_strategy = 'raw'").unwrap()) + .generate(&ir) + .unwrap(); + let content = |files: &[openapi_nexus::codegen::traits::file_writer::FileInfo], name: &str| { + files + .iter() + .find(|file| file.filename == name) + .unwrap() + .content + .clone() + }; + assert_eq!(content(&composed, "base.go"), content(&raw, "base.go")); + assert!(content(&raw, "open_all_of.go").contains("type OpenAllOf = json.RawMessage")); + for name in ["scalar.go", "array_value.go", "cycle.go", "incompatible.go"] { + assert!(content(&composed, name).contains("= json.RawMessage")); + } +} + +#[test] +fn repeated_composition_dags_share_metadata_instead_of_expanding_every_path() { + let mut source = "openapi: 3.1.0\ninfo: {title: Repeated composition, version: '1'}\npaths: {}\ncomponents:\n schemas:\n Root:\n type: object\n properties:\n wire_value: {type: string}\n".to_string(); + let mut previous = "Root".to_string(); + for depth in 0..22 { + let name = format!("Level{depth}"); + source.push_str(&format!(" {name}:\n allOf: [{{$ref: '#/components/schemas/{previous}'}}, {{$ref: '#/components/schemas/{previous}'}}]\n")); + previous = name; + } + let ir = lower(parse_content_yaml(&source).unwrap()).unwrap(); + let files = GoHttpCodeGenerator::new(Default::default()) + .generate(&ir) + .unwrap(); + let outer = &files + .iter() + .find(|file| file.filename == "level21.go") + .unwrap() + .content; + assert!(outer.contains("Level20")); + assert!(outer.contains("MarshalJSON()")); + assert!(!outer.contains("= json.RawMessage")); +} diff --git a/tests/golden/go/go-http/allof-composition/README.md.golden b/tests/golden/go/go-http/allof-composition/README.md.golden new file mode 100644 index 000000000..c69c413c6 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/README.md.golden @@ -0,0 +1,7 @@ +# Go allOf composition + +Generated Go SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `go-all-of-composition`. diff --git a/tests/golden/go/go-http/allof-composition/apis/default.go.golden b/tests/golden/go/go-http/allof-composition/apis/default.go.golden new file mode 100644 index 000000000..b111b2e81 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/apis/default.go.golden @@ -0,0 +1,132 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package apis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "example.com/sdk/models" + "example.com/sdk/runtime" +) + +// DefaultAPI groups operations under the corresponding tag. +type DefaultAPI struct { + client *runtime.Client +} + +// NewDefaultAPI constructs a DefaultAPI bound to client. +func NewDefaultAPI(client *runtime.Client) *DefaultAPI { + return &DefaultAPI{client: client} +} + +// EchoCompositionResponse carries the response from the corresponding operation. +type EchoCompositionResponse struct { + StatusCode int + Raw *http.Response + Status200 *models.Pair +} + +type EchoCompositionError struct { + StatusCode int + Status string + Header http.Header + RawBody []byte + Detail EchoCompositionErrorDetail +} + +func (e *EchoCompositionError) Error() string { + return fmt.Sprintf("api error: %s", e.Status) +} + +type EchoCompositionErrorDetail interface { + isEchoCompositionErrorDetail() +} + +type EchoCompositionBadRequest struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*EchoCompositionBadRequest) isEchoCompositionErrorDetail() {} + +func (d *EchoCompositionBadRequest) Body() (*models.Pair, error) { + var value models.Pair + if err := json.Unmarshal(d.RawBody, &value); err != nil { + return nil, err + } + return &value, nil +} +type EchoCompositionUnexpected struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*EchoCompositionUnexpected) isEchoCompositionErrorDetail() {} + +func (d *EchoCompositionUnexpected) Body() []byte { + return append([]byte(nil), d.RawBody...) +} + +// EchoComposition calls POST /composition. +func (a *DefaultAPI) EchoComposition(ctx context.Context, + body *models.Pair) (*EchoCompositionResponse, error) { + path := "/composition" + var bodyReader io.Reader + if body != nil { + buf, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("marshal body: %w", err) + } + bodyReader = bytes.NewReader(buf) + } + req, err := a.client.NewRequest(ctx, "POST", path, nil, bodyReader) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + httpResp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + resp := &EchoCompositionResponse{StatusCode: httpResp.StatusCode, Raw: httpResp} + if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { + responseBody, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + switch httpResp.StatusCode { + case 400: + detail := &EchoCompositionBadRequest{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &EchoCompositionError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + default: + detail := &EchoCompositionUnexpected{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &EchoCompositionError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + } + } + responseBody, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + + switch httpResp.StatusCode { + case 200: + var payload models.Pair + if err := json.Unmarshal(responseBody, &payload); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + resp.Status200 = &payload + default: + } + return resp, nil +} diff --git a/tests/golden/go/go-http/allof-composition/go.mod.golden b/tests/golden/go/go-http/allof-composition/go.mod.golden new file mode 100644 index 000000000..54d0d116e --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/go.mod.golden @@ -0,0 +1,3 @@ +module example.com/sdk + +go 1.21 diff --git a/tests/golden/go/go-http/allof-composition/models/additional_properties.go.golden b/tests/golden/go/go-http/allof-composition/models/additional_properties.go.golden new file mode 100644 index 000000000..ff7d0aaad --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/additional_properties.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type AdditionalProperties struct { + Label string `json:"label"` +} diff --git a/tests/golden/go/go-http/allof-composition/models/alias.go.golden b/tests/golden/go/go-http/allof-composition/models/alias.go.golden new file mode 100644 index 000000000..f38c244bf --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/alias.go.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type Alias = Base + diff --git a/tests/golden/go/go-http/allof-composition/models/aliased.go.golden b/tests/golden/go/go-http/allof-composition/models/aliased.go.golden new file mode 100644 index 000000000..5e25eda7d --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/aliased.go.golden @@ -0,0 +1,90 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type Aliased struct { + Alias + Other +} + +func (o Aliased) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.Alias) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.Other) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *Aliased) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "other_field": + continue + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.Alias); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "known_field": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.Other); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/array_value.go.golden b/tests/golden/go/go-http/allof-composition/models/array_value.go.golden new file mode 100644 index 000000000..d2f69ad63 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/array_value.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type ArrayValue = json.RawMessage diff --git a/tests/golden/go/go-http/allof-composition/models/base.go.golden b/tests/golden/go/go-http/allof-composition/models/base.go.golden new file mode 100644 index 000000000..ac0ebcb91 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/base.go.golden @@ -0,0 +1,60 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Base struct { + KnownField string `json:"known_field"` + // Additional properties not captured by defined fields. + AdditionalProperties map[string]any `json:"-"` +} + +func (o Base) MarshalJSON() ([]byte, error) { + type Plain Base + data, err := json.Marshal(Plain(o)) + if err != nil { + return nil, err + } + if len(o.AdditionalProperties) == 0 { + return data, nil + } + var base map[string]json.RawMessage + if err := json.Unmarshal(data, &base); err != nil { + return nil, err + } + for k, v := range o.AdditionalProperties { + raw, err := json.Marshal(v) + if err != nil { + return nil, err + } + base[k] = raw + } + return json.Marshal(base) +} + +func (o *Base) UnmarshalJSON(data []byte) error { + type Plain Base + if err := json.Unmarshal(data, (*Plain)(o)); err != nil { + return err + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + known := map[string]bool{"known_field": true} + o.AdditionalProperties = make(map[string]any) + for k, v := range raw { + if known[k] { + continue + } + var parsed any + if err := json.Unmarshal(v, &parsed); err != nil { + continue + } + o.AdditionalProperties[k] = parsed + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/cycle.go.golden b/tests/golden/go/go-http/allof-composition/models/cycle.go.golden new file mode 100644 index 000000000..e5e7259c8 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/cycle.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Cycle = json.RawMessage diff --git a/tests/golden/go/go-http/allof-composition/models/holder.go.golden b/tests/golden/go/go-http/allof-composition/models/holder.go.golden new file mode 100644 index 000000000..1ae91ff21 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/holder.go.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type Holder struct { + Nested Nested `json:"nested"` + Pair Pair `json:"pair"` + Single Single `json:"single"` +} diff --git a/tests/golden/go/go-http/allof-composition/models/incompatible.go.golden b/tests/golden/go/go-http/allof-composition/models/incompatible.go.golden new file mode 100644 index 000000000..c023ef8dd --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/incompatible.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Incompatible = json.RawMessage diff --git a/tests/golden/go/go-http/allof-composition/models/maps.go.golden b/tests/golden/go/go-http/allof-composition/models/maps.go.golden new file mode 100644 index 000000000..9b3be48bb --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/maps.go.golden @@ -0,0 +1,86 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type Maps struct { + AdditionalProperties map[string]any `json:"-"` + AdditionalProperties2 map[string]any `json:"-"` +} + +func (o Maps) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.AdditionalProperties) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.AdditionalProperties2) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *Maps) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.AdditionalProperties); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.AdditionalProperties2); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/named_map.go.golden b/tests/golden/go/go-http/allof-composition/models/named_map.go.golden new file mode 100644 index 000000000..d9e7de25e --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/named_map.go.golden @@ -0,0 +1,88 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type NamedMap struct { + StringField + Strings +} + +func (o NamedMap) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.StringField) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.Strings) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *NamedMap) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.StringField); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "label": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.Strings); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/nested.go.golden b/tests/golden/go/go-http/allof-composition/models/nested.go.golden new file mode 100644 index 000000000..6f7dfd167 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/nested.go.golden @@ -0,0 +1,91 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type Nested struct { + Pair + AdditionalProperties map[string]any `json:"-"` +} + +func (o Nested) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.Pair) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.AdditionalProperties) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *Nested) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.Pair); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "known_field": + continue + + case "other_field": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.AdditionalProperties); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/open_all_of.go.golden b/tests/golden/go/go-http/allof-composition/models/open_all_of.go.golden new file mode 100644 index 000000000..7e1fc4334 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/open_all_of.go.golden @@ -0,0 +1,88 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type OpenAllOf struct { + Base + AdditionalProperties map[string]any `json:"-"` +} + +func (o OpenAllOf) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.Base) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.AdditionalProperties) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *OpenAllOf) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.Base); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "known_field": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.AdditionalProperties); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/other.go.golden b/tests/golden/go/go-http/allof-composition/models/other.go.golden new file mode 100644 index 000000000..4d982617a --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/other.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type Other struct { + OtherField int `json:"other_field"` +} diff --git a/tests/golden/go/go-http/allof-composition/models/pair.go.golden b/tests/golden/go/go-http/allof-composition/models/pair.go.golden new file mode 100644 index 000000000..0ce0b769e --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/pair.go.golden @@ -0,0 +1,90 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type Pair struct { + Base + Other +} + +func (o Pair) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.Base) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.Other) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *Pair) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "other_field": + continue + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.Base); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "known_field": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.Other); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/repeated.go.golden b/tests/golden/go/go-http/allof-composition/models/repeated.go.golden new file mode 100644 index 000000000..9c99425bf --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/repeated.go.golden @@ -0,0 +1,86 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type Repeated struct { + Base + Base2 Base `json:"-"` +} + +func (o Repeated) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.Base) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.Base2) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *Repeated) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.Base); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.Base2); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/scalar.go.golden b/tests/golden/go/go-http/allof-composition/models/scalar.go.golden new file mode 100644 index 000000000..bdfd53fbf --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/scalar.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Scalar = json.RawMessage diff --git a/tests/golden/go/go-http/allof-composition/models/single.go.golden b/tests/golden/go/go-http/allof-composition/models/single.go.golden new file mode 100644 index 000000000..593d32beb --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/single.go.golden @@ -0,0 +1,58 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type Single struct { + Base +} + +func (o Single) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.Base) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *Single) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.Base); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/storage_collision.go.golden b/tests/golden/go/go-http/allof-composition/models/storage_collision.go.golden new file mode 100644 index 000000000..433824ce4 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/storage_collision.go.golden @@ -0,0 +1,88 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type StorageCollision struct { + AdditionalProperties + AdditionalProperties2 map[string]any `json:"-"` +} + +func (o StorageCollision) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.AdditionalProperties2) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.AdditionalProperties) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *StorageCollision) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "label": + continue + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.AdditionalProperties2); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.AdditionalProperties); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/string_field.go.golden b/tests/golden/go/go-http/allof-composition/models/string_field.go.golden new file mode 100644 index 000000000..8d3eb4892 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/string_field.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type StringField struct { + Label string `json:"label"` +} diff --git a/tests/golden/go/go-http/allof-composition/models/strings.go.golden b/tests/golden/go/go-http/allof-composition/models/strings.go.golden new file mode 100644 index 000000000..4ece70efe --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/strings.go.golden @@ -0,0 +1,59 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Strings struct { + // Additional properties not captured by defined fields. + AdditionalProperties map[string]string `json:"-"` +} + +func (o Strings) MarshalJSON() ([]byte, error) { + type Plain Strings + data, err := json.Marshal(Plain(o)) + if err != nil { + return nil, err + } + if len(o.AdditionalProperties) == 0 { + return data, nil + } + var base map[string]json.RawMessage + if err := json.Unmarshal(data, &base); err != nil { + return nil, err + } + for k, v := range o.AdditionalProperties { + raw, err := json.Marshal(v) + if err != nil { + return nil, err + } + base[k] = raw + } + return json.Marshal(base) +} + +func (o *Strings) UnmarshalJSON(data []byte) error { + type Plain Strings + if err := json.Unmarshal(data, (*Plain)(o)); err != nil { + return err + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + known := map[string]bool{} + o.AdditionalProperties = make(map[string]string) + for k, v := range raw { + if known[k] { + continue + } + var parsed string + if err := json.Unmarshal(v, &parsed); err != nil { + continue + } + o.AdditionalProperties[k] = parsed + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/models/typed_map.go.golden b/tests/golden/go/go-http/allof-composition/models/typed_map.go.golden new file mode 100644 index 000000000..7bc71913d --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/models/typed_map.go.golden @@ -0,0 +1,88 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type TypedMap struct { + StringField + AdditionalProperties map[string]string `json:"-"` +} + +func (o TypedMap) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.StringField) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.AdditionalProperties) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *TypedMap) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.StringField); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "label": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.AdditionalProperties); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/runtime/auth.go.golden b/tests/golden/go/go-http/allof-composition/runtime/auth.go.golden new file mode 100644 index 000000000..5454cdae1 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/runtime/auth.go.golden @@ -0,0 +1,112 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package runtime + +import ( + "encoding/base64" + "fmt" + "net/http" +) + +// Authenticator attaches credentials to an outgoing request. +// +// Return a non-nil error to abort the request before it leaves the client. +type Authenticator interface { + AuthenticateRequest(req *http.Request) error +} + +// BearerAuth sends `Authorization: Bearer `. +// +// Use the Token field for a static token, or TokenProvider for a +// function that returns the current token (evaluated per-request). +// If both are set, TokenProvider takes precedence. +type BearerAuth struct { + Token string + TokenProvider func() string +} + +func (b BearerAuth) AuthenticateRequest(req *http.Request) error { + token := b.Token + if b.TokenProvider != nil { + token = b.TokenProvider() + } + if token == "" { + return fmt.Errorf("bearer auth: empty token") + } + req.Header.Set("Authorization", "Bearer "+token) + return nil +} + +// APIKeyLocation enumerates where an API key is placed on the wire. +type APIKeyLocation int + +const ( + APIKeyInHeader APIKeyLocation = iota + APIKeyInQuery + APIKeyInCookie +) + +// APIKeyAuth attaches a named API key to a request. +// +// Use the Key field for a static key, or KeyProvider for a function +// that returns the current key (evaluated per-request). If both are +// set, KeyProvider takes precedence. +type APIKeyAuth struct { + Key string + KeyProvider func() string + Name string + Location APIKeyLocation +} + +func (a APIKeyAuth) AuthenticateRequest(req *http.Request) error { + key := a.Key + if a.KeyProvider != nil { + key = a.KeyProvider() + } + if key == "" || a.Name == "" { + return fmt.Errorf("api key auth: empty key or name") + } + switch a.Location { + case APIKeyInHeader: + req.Header.Set(a.Name, key) + case APIKeyInQuery: + q := req.URL.Query() + q.Set(a.Name, key) + req.URL.RawQuery = q.Encode() + case APIKeyInCookie: + req.AddCookie(&http.Cookie{Name: a.Name, Value: key}) + default: + return fmt.Errorf("api key auth: unknown location %d", a.Location) + } + return nil +} + +// BasicAuth sends `Authorization: Basic `. +// +// Use the Username/Password fields for static credentials, or +// UsernameProvider/PasswordProvider for functions that return the +// current credentials (evaluated per-request). If a provider is set, +// it takes precedence over the corresponding static field. +type BasicAuth struct { + Username string + Password string + UsernameProvider func() string + PasswordProvider func() string +} + +func (b BasicAuth) AuthenticateRequest(req *http.Request) error { + user := b.Username + if b.UsernameProvider != nil { + user = b.UsernameProvider() + } + pass := b.Password + if b.PasswordProvider != nil { + pass = b.PasswordProvider() + } + creds := user + ":" + pass + encoded := base64.StdEncoding.EncodeToString([]byte(creds)) + req.Header.Set("Authorization", "Basic "+encoded) + return nil +} diff --git a/tests/golden/go/go-http/allof-composition/runtime/client.go.golden b/tests/golden/go/go-http/allof-composition/runtime/client.go.golden new file mode 100644 index 000000000..18cc42752 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/runtime/client.go.golden @@ -0,0 +1,98 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package runtime + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// Client is the transport-layer HTTP client shared by all APIs. +type Client struct { + baseURL string + httpClient *http.Client + authenticator Authenticator + defaultHeaders http.Header +} + +// Option configures a Client. +type Option func(*Client) + +// NewClient builds a Client rooted at baseURL. Options override defaults. +func NewClient(baseURL string, opts ...Option) *Client { + c := &Client{ + baseURL: strings.TrimRight(baseURL, "/"), + httpClient: http.DefaultClient, + defaultHeaders: make(http.Header), + } + for _, opt := range opts { + opt(c) + } + return c +} + +// WithHTTPClient swaps the underlying *http.Client. +func WithHTTPClient(hc *http.Client) Option { + return func(c *Client) { + if hc != nil { + c.httpClient = hc + } + } +} + +// WithAuth installs an Authenticator applied to every request. +func WithAuth(a Authenticator) Option { + return func(c *Client) { c.authenticator = a } +} + +// WithDefaultHeader sets a header applied to every request. Per-request +// headers take precedence. +func WithDefaultHeader(key, value string) Option { + return func(c *Client) { c.defaultHeaders.Set(key, value) } +} + +// BaseURL returns the base URL configured on the client. +func (c *Client) BaseURL() string { return c.baseURL } + +// HTTPClient returns the underlying *http.Client. +func (c *Client) HTTPClient() *http.Client { return c.httpClient } + +// NewRequest builds an *http.Request joined to the client's base URL. Query +// pairs (flat key=value list) are appended when non-empty. +func (c *Client) NewRequest( + ctx context.Context, + method, path string, + query url.Values, + body io.Reader, +) (*http.Request, error) { + full := c.baseURL + path + if len(query) > 0 { + full += "?" + query.Encode() + } + req, err := http.NewRequestWithContext(ctx, method, full, body) + if err != nil { + return nil, fmt.Errorf("build request: %w", err) + } + for k, vals := range c.defaultHeaders { + for _, v := range vals { + req.Header.Add(k, v) + } + } + return req, nil +} + +// Do authenticates and executes a request. +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if c.authenticator != nil { + if err := c.authenticator.AuthenticateRequest(req); err != nil { + return nil, fmt.Errorf("authenticate: %w", err) + } + } + return c.httpClient.Do(req) +} diff --git a/tests/golden/go/go-http/allof-composition/runtime/errors.go.golden b/tests/golden/go/go-http/allof-composition/runtime/errors.go.golden new file mode 100644 index 000000000..86c94a902 --- /dev/null +++ b/tests/golden/go/go-http/allof-composition/runtime/errors.go.golden @@ -0,0 +1,30 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package runtime + +import ( + "errors" + "fmt" +) + +// APIError represents a non-2xx HTTP response from the API. +// +// Callers can type-assert via `errors.As` to inspect StatusCode and the raw +// response body. +type APIError struct { + StatusCode int + Status string + Body []byte +} + +func (e *APIError) Error() string { + return fmt.Sprintf("api error: %s", e.Status) +} + +// IsAPIError reports whether err is (or wraps) an *APIError. +func IsAPIError(err error) bool { + var apiErr *APIError + return errors.As(err, &apiErr) +} diff --git a/tests/golden/go/go-http/allof-raw/README.md.golden b/tests/golden/go/go-http/allof-raw/README.md.golden new file mode 100644 index 000000000..c69c413c6 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/README.md.golden @@ -0,0 +1,7 @@ +# Go allOf composition + +Generated Go SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `go-all-of-composition`. diff --git a/tests/golden/go/go-http/allof-raw/apis/default.go.golden b/tests/golden/go/go-http/allof-raw/apis/default.go.golden new file mode 100644 index 000000000..b111b2e81 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/apis/default.go.golden @@ -0,0 +1,132 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package apis + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "example.com/sdk/models" + "example.com/sdk/runtime" +) + +// DefaultAPI groups operations under the corresponding tag. +type DefaultAPI struct { + client *runtime.Client +} + +// NewDefaultAPI constructs a DefaultAPI bound to client. +func NewDefaultAPI(client *runtime.Client) *DefaultAPI { + return &DefaultAPI{client: client} +} + +// EchoCompositionResponse carries the response from the corresponding operation. +type EchoCompositionResponse struct { + StatusCode int + Raw *http.Response + Status200 *models.Pair +} + +type EchoCompositionError struct { + StatusCode int + Status string + Header http.Header + RawBody []byte + Detail EchoCompositionErrorDetail +} + +func (e *EchoCompositionError) Error() string { + return fmt.Sprintf("api error: %s", e.Status) +} + +type EchoCompositionErrorDetail interface { + isEchoCompositionErrorDetail() +} + +type EchoCompositionBadRequest struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*EchoCompositionBadRequest) isEchoCompositionErrorDetail() {} + +func (d *EchoCompositionBadRequest) Body() (*models.Pair, error) { + var value models.Pair + if err := json.Unmarshal(d.RawBody, &value); err != nil { + return nil, err + } + return &value, nil +} +type EchoCompositionUnexpected struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*EchoCompositionUnexpected) isEchoCompositionErrorDetail() {} + +func (d *EchoCompositionUnexpected) Body() []byte { + return append([]byte(nil), d.RawBody...) +} + +// EchoComposition calls POST /composition. +func (a *DefaultAPI) EchoComposition(ctx context.Context, + body *models.Pair) (*EchoCompositionResponse, error) { + path := "/composition" + var bodyReader io.Reader + if body != nil { + buf, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("marshal body: %w", err) + } + bodyReader = bytes.NewReader(buf) + } + req, err := a.client.NewRequest(ctx, "POST", path, nil, bodyReader) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + httpResp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + resp := &EchoCompositionResponse{StatusCode: httpResp.StatusCode, Raw: httpResp} + if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { + responseBody, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + switch httpResp.StatusCode { + case 400: + detail := &EchoCompositionBadRequest{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &EchoCompositionError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + default: + detail := &EchoCompositionUnexpected{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &EchoCompositionError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + } + } + responseBody, err := io.ReadAll(httpResp.Body) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + + switch httpResp.StatusCode { + case 200: + var payload models.Pair + if err := json.Unmarshal(responseBody, &payload); err != nil { + return nil, fmt.Errorf("decode response: %w", err) + } + resp.Status200 = &payload + default: + } + return resp, nil +} diff --git a/tests/golden/go/go-http/allof-raw/go.mod.golden b/tests/golden/go/go-http/allof-raw/go.mod.golden new file mode 100644 index 000000000..54d0d116e --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/go.mod.golden @@ -0,0 +1,3 @@ +module example.com/sdk + +go 1.21 diff --git a/tests/golden/go/go-http/allof-raw/models/additional_properties.go.golden b/tests/golden/go/go-http/allof-raw/models/additional_properties.go.golden new file mode 100644 index 000000000..ff7d0aaad --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/additional_properties.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type AdditionalProperties struct { + Label string `json:"label"` +} diff --git a/tests/golden/go/go-http/allof-raw/models/alias.go.golden b/tests/golden/go/go-http/allof-raw/models/alias.go.golden new file mode 100644 index 000000000..f38c244bf --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/alias.go.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type Alias = Base + diff --git a/tests/golden/go/go-http/allof-raw/models/aliased.go.golden b/tests/golden/go/go-http/allof-raw/models/aliased.go.golden new file mode 100644 index 000000000..830ba0858 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/aliased.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Aliased = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/array_value.go.golden b/tests/golden/go/go-http/allof-raw/models/array_value.go.golden new file mode 100644 index 000000000..d2f69ad63 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/array_value.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type ArrayValue = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/base.go.golden b/tests/golden/go/go-http/allof-raw/models/base.go.golden new file mode 100644 index 000000000..ac0ebcb91 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/base.go.golden @@ -0,0 +1,60 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Base struct { + KnownField string `json:"known_field"` + // Additional properties not captured by defined fields. + AdditionalProperties map[string]any `json:"-"` +} + +func (o Base) MarshalJSON() ([]byte, error) { + type Plain Base + data, err := json.Marshal(Plain(o)) + if err != nil { + return nil, err + } + if len(o.AdditionalProperties) == 0 { + return data, nil + } + var base map[string]json.RawMessage + if err := json.Unmarshal(data, &base); err != nil { + return nil, err + } + for k, v := range o.AdditionalProperties { + raw, err := json.Marshal(v) + if err != nil { + return nil, err + } + base[k] = raw + } + return json.Marshal(base) +} + +func (o *Base) UnmarshalJSON(data []byte) error { + type Plain Base + if err := json.Unmarshal(data, (*Plain)(o)); err != nil { + return err + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + known := map[string]bool{"known_field": true} + o.AdditionalProperties = make(map[string]any) + for k, v := range raw { + if known[k] { + continue + } + var parsed any + if err := json.Unmarshal(v, &parsed); err != nil { + continue + } + o.AdditionalProperties[k] = parsed + } + return nil +} diff --git a/tests/golden/go/go-http/allof-raw/models/cycle.go.golden b/tests/golden/go/go-http/allof-raw/models/cycle.go.golden new file mode 100644 index 000000000..e5e7259c8 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/cycle.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Cycle = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/holder.go.golden b/tests/golden/go/go-http/allof-raw/models/holder.go.golden new file mode 100644 index 000000000..1ae91ff21 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/holder.go.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type Holder struct { + Nested Nested `json:"nested"` + Pair Pair `json:"pair"` + Single Single `json:"single"` +} diff --git a/tests/golden/go/go-http/allof-raw/models/incompatible.go.golden b/tests/golden/go/go-http/allof-raw/models/incompatible.go.golden new file mode 100644 index 000000000..c023ef8dd --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/incompatible.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Incompatible = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/maps.go.golden b/tests/golden/go/go-http/allof-raw/models/maps.go.golden new file mode 100644 index 000000000..bd8eb3858 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/maps.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Maps = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/named_map.go.golden b/tests/golden/go/go-http/allof-raw/models/named_map.go.golden new file mode 100644 index 000000000..82e54546d --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/named_map.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type NamedMap = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/nested.go.golden b/tests/golden/go/go-http/allof-raw/models/nested.go.golden new file mode 100644 index 000000000..ac0a577ee --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/nested.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Nested = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/open_all_of.go.golden b/tests/golden/go/go-http/allof-raw/models/open_all_of.go.golden new file mode 100644 index 000000000..12dae252b --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/open_all_of.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type OpenAllOf = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/other.go.golden b/tests/golden/go/go-http/allof-raw/models/other.go.golden new file mode 100644 index 000000000..4d982617a --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/other.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type Other struct { + OtherField int `json:"other_field"` +} diff --git a/tests/golden/go/go-http/allof-raw/models/pair.go.golden b/tests/golden/go/go-http/allof-raw/models/pair.go.golden new file mode 100644 index 000000000..7984d3bc0 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/pair.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Pair = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/repeated.go.golden b/tests/golden/go/go-http/allof-raw/models/repeated.go.golden new file mode 100644 index 000000000..dbccd3ee9 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/repeated.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Repeated = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/scalar.go.golden b/tests/golden/go/go-http/allof-raw/models/scalar.go.golden new file mode 100644 index 000000000..bdfd53fbf --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/scalar.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Scalar = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/single.go.golden b/tests/golden/go/go-http/allof-raw/models/single.go.golden new file mode 100644 index 000000000..f0080957b --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/single.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Single = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/storage_collision.go.golden b/tests/golden/go/go-http/allof-raw/models/storage_collision.go.golden new file mode 100644 index 000000000..6d896d4dd --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/storage_collision.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type StorageCollision = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/models/string_field.go.golden b/tests/golden/go/go-http/allof-raw/models/string_field.go.golden new file mode 100644 index 000000000..8d3eb4892 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/string_field.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +type StringField struct { + Label string `json:"label"` +} diff --git a/tests/golden/go/go-http/allof-raw/models/strings.go.golden b/tests/golden/go/go-http/allof-raw/models/strings.go.golden new file mode 100644 index 000000000..4ece70efe --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/strings.go.golden @@ -0,0 +1,59 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type Strings struct { + // Additional properties not captured by defined fields. + AdditionalProperties map[string]string `json:"-"` +} + +func (o Strings) MarshalJSON() ([]byte, error) { + type Plain Strings + data, err := json.Marshal(Plain(o)) + if err != nil { + return nil, err + } + if len(o.AdditionalProperties) == 0 { + return data, nil + } + var base map[string]json.RawMessage + if err := json.Unmarshal(data, &base); err != nil { + return nil, err + } + for k, v := range o.AdditionalProperties { + raw, err := json.Marshal(v) + if err != nil { + return nil, err + } + base[k] = raw + } + return json.Marshal(base) +} + +func (o *Strings) UnmarshalJSON(data []byte) error { + type Plain Strings + if err := json.Unmarshal(data, (*Plain)(o)); err != nil { + return err + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + known := map[string]bool{} + o.AdditionalProperties = make(map[string]string) + for k, v := range raw { + if known[k] { + continue + } + var parsed string + if err := json.Unmarshal(v, &parsed); err != nil { + continue + } + o.AdditionalProperties[k] = parsed + } + return nil +} diff --git a/tests/golden/go/go-http/allof-raw/models/typed_map.go.golden b/tests/golden/go/go-http/allof-raw/models/typed_map.go.golden new file mode 100644 index 000000000..def884f7f --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/models/typed_map.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package models + +import "encoding/json" + +type TypedMap = json.RawMessage diff --git a/tests/golden/go/go-http/allof-raw/runtime/auth.go.golden b/tests/golden/go/go-http/allof-raw/runtime/auth.go.golden new file mode 100644 index 000000000..5454cdae1 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/runtime/auth.go.golden @@ -0,0 +1,112 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package runtime + +import ( + "encoding/base64" + "fmt" + "net/http" +) + +// Authenticator attaches credentials to an outgoing request. +// +// Return a non-nil error to abort the request before it leaves the client. +type Authenticator interface { + AuthenticateRequest(req *http.Request) error +} + +// BearerAuth sends `Authorization: Bearer `. +// +// Use the Token field for a static token, or TokenProvider for a +// function that returns the current token (evaluated per-request). +// If both are set, TokenProvider takes precedence. +type BearerAuth struct { + Token string + TokenProvider func() string +} + +func (b BearerAuth) AuthenticateRequest(req *http.Request) error { + token := b.Token + if b.TokenProvider != nil { + token = b.TokenProvider() + } + if token == "" { + return fmt.Errorf("bearer auth: empty token") + } + req.Header.Set("Authorization", "Bearer "+token) + return nil +} + +// APIKeyLocation enumerates where an API key is placed on the wire. +type APIKeyLocation int + +const ( + APIKeyInHeader APIKeyLocation = iota + APIKeyInQuery + APIKeyInCookie +) + +// APIKeyAuth attaches a named API key to a request. +// +// Use the Key field for a static key, or KeyProvider for a function +// that returns the current key (evaluated per-request). If both are +// set, KeyProvider takes precedence. +type APIKeyAuth struct { + Key string + KeyProvider func() string + Name string + Location APIKeyLocation +} + +func (a APIKeyAuth) AuthenticateRequest(req *http.Request) error { + key := a.Key + if a.KeyProvider != nil { + key = a.KeyProvider() + } + if key == "" || a.Name == "" { + return fmt.Errorf("api key auth: empty key or name") + } + switch a.Location { + case APIKeyInHeader: + req.Header.Set(a.Name, key) + case APIKeyInQuery: + q := req.URL.Query() + q.Set(a.Name, key) + req.URL.RawQuery = q.Encode() + case APIKeyInCookie: + req.AddCookie(&http.Cookie{Name: a.Name, Value: key}) + default: + return fmt.Errorf("api key auth: unknown location %d", a.Location) + } + return nil +} + +// BasicAuth sends `Authorization: Basic `. +// +// Use the Username/Password fields for static credentials, or +// UsernameProvider/PasswordProvider for functions that return the +// current credentials (evaluated per-request). If a provider is set, +// it takes precedence over the corresponding static field. +type BasicAuth struct { + Username string + Password string + UsernameProvider func() string + PasswordProvider func() string +} + +func (b BasicAuth) AuthenticateRequest(req *http.Request) error { + user := b.Username + if b.UsernameProvider != nil { + user = b.UsernameProvider() + } + pass := b.Password + if b.PasswordProvider != nil { + pass = b.PasswordProvider() + } + creds := user + ":" + pass + encoded := base64.StdEncoding.EncodeToString([]byte(creds)) + req.Header.Set("Authorization", "Basic "+encoded) + return nil +} diff --git a/tests/golden/go/go-http/allof-raw/runtime/client.go.golden b/tests/golden/go/go-http/allof-raw/runtime/client.go.golden new file mode 100644 index 000000000..18cc42752 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/runtime/client.go.golden @@ -0,0 +1,98 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package runtime + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// Client is the transport-layer HTTP client shared by all APIs. +type Client struct { + baseURL string + httpClient *http.Client + authenticator Authenticator + defaultHeaders http.Header +} + +// Option configures a Client. +type Option func(*Client) + +// NewClient builds a Client rooted at baseURL. Options override defaults. +func NewClient(baseURL string, opts ...Option) *Client { + c := &Client{ + baseURL: strings.TrimRight(baseURL, "/"), + httpClient: http.DefaultClient, + defaultHeaders: make(http.Header), + } + for _, opt := range opts { + opt(c) + } + return c +} + +// WithHTTPClient swaps the underlying *http.Client. +func WithHTTPClient(hc *http.Client) Option { + return func(c *Client) { + if hc != nil { + c.httpClient = hc + } + } +} + +// WithAuth installs an Authenticator applied to every request. +func WithAuth(a Authenticator) Option { + return func(c *Client) { c.authenticator = a } +} + +// WithDefaultHeader sets a header applied to every request. Per-request +// headers take precedence. +func WithDefaultHeader(key, value string) Option { + return func(c *Client) { c.defaultHeaders.Set(key, value) } +} + +// BaseURL returns the base URL configured on the client. +func (c *Client) BaseURL() string { return c.baseURL } + +// HTTPClient returns the underlying *http.Client. +func (c *Client) HTTPClient() *http.Client { return c.httpClient } + +// NewRequest builds an *http.Request joined to the client's base URL. Query +// pairs (flat key=value list) are appended when non-empty. +func (c *Client) NewRequest( + ctx context.Context, + method, path string, + query url.Values, + body io.Reader, +) (*http.Request, error) { + full := c.baseURL + path + if len(query) > 0 { + full += "?" + query.Encode() + } + req, err := http.NewRequestWithContext(ctx, method, full, body) + if err != nil { + return nil, fmt.Errorf("build request: %w", err) + } + for k, vals := range c.defaultHeaders { + for _, v := range vals { + req.Header.Add(k, v) + } + } + return req, nil +} + +// Do authenticates and executes a request. +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if c.authenticator != nil { + if err := c.authenticator.AuthenticateRequest(req); err != nil { + return nil, fmt.Errorf("authenticate: %w", err) + } + } + return c.httpClient.Do(req) +} diff --git a/tests/golden/go/go-http/allof-raw/runtime/errors.go.golden b/tests/golden/go/go-http/allof-raw/runtime/errors.go.golden new file mode 100644 index 000000000..86c94a902 --- /dev/null +++ b/tests/golden/go/go-http/allof-raw/runtime/errors.go.golden @@ -0,0 +1,30 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Go allOf composition — 1.0.0 + +package runtime + +import ( + "errors" + "fmt" +) + +// APIError represents a non-2xx HTTP response from the API. +// +// Callers can type-assert via `errors.As` to inspect StatusCode and the raw +// response body. +type APIError struct { + StatusCode int + Status string + Body []byte +} + +func (e *APIError) Error() string { + return fmt.Sprintf("api error: %s", e.Status) +} + +// IsAPIError reports whether err is (or wraps) an *APIError. +func IsAPIError(err error) bool { + var apiErr *APIError + return errors.As(err, &apiErr) +} diff --git a/tests/golden/go/go-http/comprehensive-schemas/models/all_of_objects.go.golden b/tests/golden/go/go-http/comprehensive-schemas/models/all_of_objects.go.golden index 4d2eaddc4..3918366e9 100644 --- a/tests/golden/go/go-http/comprehensive-schemas/models/all_of_objects.go.golden +++ b/tests/golden/go/go-http/comprehensive-schemas/models/all_of_objects.go.golden @@ -5,8 +5,94 @@ package models +import ( + "bytes" + "encoding/json" + "fmt" +) + // Intersection of object types (merge) type AllOfObjects struct { BasicObject AllOfObjectsAllOf2 } + +func (o AllOfObjects) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.BasicObject) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.AllOfObjectsAllOf2) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *AllOfObjects) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "metadata": + continue + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.BasicObject); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "active": + continue + + case "id": + continue + + case "name": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.AllOfObjectsAllOf2); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/type-aliases-intersection-allof/models/foo.go.golden b/tests/golden/go/go-http/type-aliases-intersection-allof/models/foo.go.golden index b1bcc170f..e20a706cf 100644 --- a/tests/golden/go/go-http/type-aliases-intersection-allof/models/foo.go.golden +++ b/tests/golden/go/go-http/type-aliases-intersection-allof/models/foo.go.golden @@ -5,7 +5,93 @@ package models +import ( + "bytes" + "encoding/json" + "fmt" +) + type Foo struct { Bar Baz } + +func (o Foo) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.Bar) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.Baz) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *Foo) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "createdAt": + continue + + case "email": + continue + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.Bar); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "id": + continue + + case "name": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.Baz); err != nil { + return err + } + return nil +} diff --git a/tests/golden/go/go-http/type-aliases-intersection-with-nullable-reference/models/baz.go.golden b/tests/golden/go/go-http/type-aliases-intersection-with-nullable-reference/models/baz.go.golden index 07616df42..0f524b07f 100644 --- a/tests/golden/go/go-http/type-aliases-intersection-with-nullable-reference/models/baz.go.golden +++ b/tests/golden/go/go-http/type-aliases-intersection-with-nullable-reference/models/baz.go.golden @@ -5,7 +5,99 @@ package models +import ( + "bytes" + "encoding/json" + "fmt" +) + type Baz struct { Foo BazAllOf2 } + +func (o Baz) MarshalJSON() ([]byte, error) { + merged := make(map[string]json.RawMessage) + memberData0, err := json.Marshal(o.Foo) + if err != nil { + return nil, err + } + var memberFields0 map[string]json.RawMessage + if err := json.Unmarshal(memberData0, &memberFields0); err != nil { + return nil, err + } + for key, value := range memberFields0 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + memberData1, err := json.Marshal(o.BazAllOf2) + if err != nil { + return nil, err + } + var memberFields1 map[string]json.RawMessage + if err := json.Unmarshal(memberData1, &memberFields1); err != nil { + return nil, err + } + for key, value := range memberFields1 { + if previous, exists := merged[key]; exists && !bytes.Equal(previous, value) { + return nil, fmt.Errorf("conflicting allOf property %q", key) + } + merged[key] = value + } + return json.Marshal(merged) +} + +func (o *Baz) UnmarshalJSON(data []byte) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if fields == nil { + return nil + } + projected0 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "metaData": + continue + } + projected0[key] = value + } + encoded0, err := json.Marshal(projected0) + if err != nil { + return err + } + if err := json.Unmarshal(encoded0, &o.Foo); err != nil { + return err + } + projected1 := make(map[string]json.RawMessage) + for key, value := range fields { + switch key { + case "count1": + continue + + case "count2": + continue + + case "createdAt": + continue + + case "id": + continue + + case "status": + continue + } + projected1[key] = value + } + encoded1, err := json.Marshal(projected1) + if err != nil { + return err + } + if err := json.Unmarshal(encoded1, &o.BazAllOf2); err != nil { + return err + } + return nil +} diff --git a/tests/golden_tests_go_http.rs b/tests/golden_tests_go_http.rs index 973f955e7..052448f5b 100644 --- a/tests/golden_tests_go_http.rs +++ b/tests/golden_tests_go_http.rs @@ -87,6 +87,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("enum-repr", "valid/enum-repr/enum-repr.yaml"), ("query-param-enum", "valid/query/query-param-enum.yaml"), ("server-path-prefix", "valid/server-path-prefix.yaml"), + ("allof-composition", "valid/go-allof-composition.yaml"), ]) } @@ -111,6 +112,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_allof_composition_golden: "allof-composition", test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", @@ -179,3 +181,14 @@ generate_golden_tests! { test_query_param_enum_golden: "query-param-enum", test_server_path_prefix_golden: "server-path-prefix", } + +#[test] +fn raw_allof_composition_golden() { + run_golden_test( + &GoHttpCodeGenerator::new(toml::from_str("all_of_strategy = 'raw'").unwrap()), + golden_dir(), + "allof-raw", + "valid/go-allof-composition.yaml", + UPDATE_HINT, + ); +}