diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c0692d0b..1b04006f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -185,6 +185,27 @@ jobs: if: contains(matrix.generator, 'java-') || contains(matrix.generator, 'kotlin-') - run: ${{ matrix.cmd }} + rust-client-tests: + name: Rust Client Tests (${{ matrix.backend }}) + runs-on: ubuntu-latest + needs: [basics] + strategy: + fail-fast: false + matrix: + backend: [reqwest, ureq, aioduct] + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: taiki-e/install-action@just + - uses: Swatinem/rust-cache@v2 + - run: just golden-rust::prepare ${{ matrix.backend }} + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rust-client-${{ matrix.backend }} + workspaces: | + tests/generated-clients/rust/${{ matrix.backend }} -> target + - run: cargo test --locked --manifest-path tests/generated-clients/rust/${{ matrix.backend }}/Cargo.toml + release: name: Release ${{ matrix.target }} runs-on: ${{ matrix.os }} diff --git a/.gitignore b/.gitignore index b4bbc6bc4..244aaf653 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ test-output/ examples/*/generated/ examples/*/target/ +tests/generated-clients/rust/*/generated/ # mdbook build output docs/book/ diff --git a/Cargo.toml b/Cargo.toml index 120d66a35..ab42599eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ uuid = { version = "1.19.0", features = ["v4", "v7", "serde"] } [package] name = "openapi-nexus" version.workspace = true -exclude = ["tests/golden/", "fixture-generators/"] +exclude = ["tests/golden/", "tests/generated-clients/", "fixture-generators/"] edition.workspace = true rust-version.workspace = true description.workspace = true diff --git a/Justfile b/Justfile index 55e141120..5ba0c7592 100644 --- a/Justfile +++ b/Justfile @@ -36,10 +36,12 @@ msrv: # Apply formatting fmt: cargo fmt --all + rustfmt --edition 2024 tests/generated-clients/rust/support/*.rs tests/generated-clients/rust/*/tests/*.rs # Check formatting fmt-check: cargo fmt --all -- --check + rustfmt --edition 2024 --check tests/generated-clients/rust/support/*.rs tests/generated-clients/rust/*/tests/*.rs # Run clippy with warnings as errors clippy: @@ -162,4 +164,4 @@ publish: # ---------- CI (run everything locally) ---------- # Run the full CI pipeline locally -ci: fmt-check clippy doc-check book msrv test-all coverage-lcov golden-check +ci: fmt-check clippy doc-check book msrv test-all coverage-lcov golden-check golden-rust::test diff --git a/docs/src/golden_testing.md b/docs/src/golden_testing.md index 04e62d861..382b2a26c 100644 --- a/docs/src/golden_testing.md +++ b/docs/src/golden_testing.md @@ -115,6 +115,31 @@ just golden-kotlin::build just golden-build-all # all languages ``` +## Rust Client Runtime Tests + +Generated Rust clients also have standalone consumer test projects under +`tests/generated-clients/rust/`. Unlike golden comparison, these tests call real +generated clients over local HTTP and check decoding, error metadata, response +dispatch, and strict non-nullable controls. + +```bash +just golden-rust::test # reqwest, ureq, and aioduct +just golden-rust::test-reqwest # one backend +``` + +Preparation runs the public generator CLI against the fixtures. Each consumer +project has its own checked-in manifest, lockfile, and ordinary Rust test files; +generated clients are unmodified path dependencies in an ignored directory. One +Cargo invocation runs all cases for a backend, with a persistent dependency cache. + +These projects are intentionally outside the generator workspace. `just test-all` +and generator coverage do not recursively build client crates. CI has a separate +Rust Client Tests matrix, and `just ci` includes these tests. They complement, +rather than replace, snapshot comparison and the all-golden compile checks. + +See [the consumer test README](https://github.com/rust-codegen-group/openapi-nexus/tree/main/tests/generated-clients/rust) +for direct Cargo/IDE use and the retained coverage per fixture. + ## Extra File Detection The test harness detects when a generator produces files that have no corresponding `.golden` file. This prevents regressions where new output silently goes untested. If a generator adds a new file, the test will fail with a clear message listing the unmatched files and instructing you to run `UPDATE_GOLDEN=1`. diff --git a/docs/src/ir_reference.md b/docs/src/ir_reference.md index 7d202ff27..ccfa3f07e 100644 --- a/docs/src/ir_reference.md +++ b/docs/src/ir_reference.md @@ -106,15 +106,32 @@ Alias(IrTypeExpr) pub enum IrTypeExpr { Named(String), Primitive(IrPrimitive), + StringLiteral(String), + StringEnum(Vec), Array(Box), Map(Box), Nullable(Box), Union(Vec), - Literal(serde_json::Value), - Unknown, + Any, } ``` +Nullability belongs to the node that declares it. OpenAPI 3.0 `nullable: true` +and OpenAPI 3.1/3.2 type sets containing `null` retain that distinction in nested +arrays and maps: + +| Schema meaning | IR shape | +| --- | --- | +| Map whose values may be null | `Map(Nullable(T))` | +| Array whose items may be null | `Array(Nullable(T))` | +| Array that may itself be null | `Nullable(Array(T))` | + +Array aliases and references to promoted inline items preserve these wrappers. +For object properties, the outer nullable wrapper is recorded in +`IrProperty.nullable`; nested wrappers remain in `type_expr`. A nullable +container does not make its contents nullable, and nullability is independent +of whether a property is required. + ## Operations Each `IrOperation` represents one HTTP method + path: diff --git a/just/golden-rust.just b/just/golden-rust.just index e38cdf64b..f30465add 100644 --- a/just/golden-rust.just +++ b/just/golden-rust.just @@ -31,3 +31,19 @@ build: build-reqwest build-ureq build-aioduct # Compile-check Rust aioduct goldens @build-aioduct: ./scripts/golden-build-rust.sh aioduct + +# Generate fresh clients and run their consumer test projects +test: test-reqwest test-ureq test-aioduct + +# Prepare generated dependencies for direct Cargo/IDE use +@prepare BACKEND: + bash ./scripts/generate-rust-client-fixtures.sh {{ BACKEND }} + +@test-reqwest: (prepare "reqwest") + cargo test --locked --manifest-path tests/generated-clients/rust/reqwest/Cargo.toml + +@test-ureq: (prepare "ureq") + cargo test --locked --manifest-path tests/generated-clients/rust/ureq/Cargo.toml + +@test-aioduct: (prepare "aioduct") + cargo test --locked --manifest-path tests/generated-clients/rust/aioduct/Cargo.toml diff --git a/scripts/generate-rust-client-fixtures.sh b/scripts/generate-rust-client-fixtures.sh new file mode 100644 index 000000000..ba5927e9d --- /dev/null +++ b/scripts/generate-rust-client-fixtures.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +backend="${1:?Usage: generate-rust-client-fixtures.sh }" +case "$backend" in + reqwest|ureq|aioduct) ;; + *) echo "unsupported Rust backend: $backend" >&2; exit 1 ;; +esac + +root="$(cd "$(dirname "$0")/.." && pwd)" +project="$root/tests/generated-clients/rust/$backend" + +for fixture in typed-error-responses rust-error-payload-shapes nested-nullable; do + output="$project/generated/$fixture" + # These fixed fixture directories contain only disposable generated output. + # Remove stale source files so a deleted declaration cannot pass unnoticed. + rm -rf "$output" + cargo run --quiet --manifest-path "$root/Cargo.toml" --bin openapi-nexus -- generate \ + --config "$root/tests/generated-clients/rust/config.toml" \ + --input "$root/tests/fixtures/valid/$fixture.yaml" \ + --output "$output" \ + --generators "rust-$backend" +done diff --git a/src/generators/typescript/fetch/sigil_emit.rs b/src/generators/typescript/fetch/sigil_emit.rs index bc2e769d6..fdda28d32 100644 --- a/src/generators/typescript/fetch/sigil_emit.rs +++ b/src/generators/typescript/fetch/sigil_emit.rs @@ -294,12 +294,7 @@ fn build_field_wire( }; let inner_ty = type_expr_to_typename_wire(&prop.type_expr, current_schema, convertible); let field_ty = if prop.nullable { - nullable_field_type_name( - &prop.type_expr, - inner_ty, - type_expr_str_wire(&prop.type_expr, convertible), - unknown_aliases, - ) + nullable_field_type_name(&prop.type_expr, inner_ty, unknown_aliases) } else { inner_ty }; @@ -328,12 +323,7 @@ fn build_field_camel( }; let inner_ty = type_expr_to_typename_for_schema(&prop.type_expr, current_schema); let field_ty = if prop.nullable { - nullable_field_type_name( - &prop.type_expr, - inner_ty, - type_expr_str(&prop.type_expr), - unknown_aliases, - ) + nullable_field_type_name(&prop.type_expr, inner_ty, unknown_aliases) } else { inner_ty }; @@ -527,18 +517,12 @@ fn type_name_is_unknown(type_name: &TypeName) -> bool { fn nullable_field_type_name( expr: &IrTypeExpr, inner_ty: TypeName, - rendered_inner: String, unknown_aliases: &HashSet, ) -> TypeName { if type_expr_collapses_to_unknown(expr, unknown_aliases) { inner_ty - } else if contains_any_named_ref(expr) { - union_typename(vec![inner_ty, TypeName::primitive("null")]) } else { - TypeName::raw(&simplify_union_strings(vec![ - rendered_inner, - "null".to_string(), - ])) + union_typename(vec![inner_ty, TypeName::primitive("null")]) } } @@ -2047,12 +2031,7 @@ fn build_field( }; let inner_ty = type_expr_to_typename_for_schema(&prop.type_expr, current_schema); let field_ty = if prop.nullable { - nullable_field_type_name( - &prop.type_expr, - inner_ty, - type_expr_str(&prop.type_expr), - unknown_aliases, - ) + nullable_field_type_name(&prop.type_expr, inner_ty, unknown_aliases) } else { inner_ty }; @@ -2089,9 +2068,6 @@ fn type_expr_to_typename_with_current(expr: &IrTypeExpr, current_schema: Option< IrTypeExpr::Array(inner) => { TypeName::readonly_array(type_expr_to_typename_nested(inner, current_schema)) } - IrTypeExpr::Nullable(inner) if !contains_any_named_ref(inner) => { - TypeName::raw(&type_expr_str(expr)) - } IrTypeExpr::Nullable(inner) => union_typename(vec![ type_expr_to_typename_with_current(inner, current_schema), TypeName::primitive("null"), @@ -2142,6 +2118,18 @@ fn type_expr_to_typename_nested(expr: &IrTypeExpr, current_schema: Option<&str>) IrTypeExpr::Array(inner) => { TypeName::array(type_expr_to_typename_nested(inner, current_schema)) } + // Array element unions must stay structured so the renderer can add + // parentheses; a raw `T | null` would render as `T | null[]`. + IrTypeExpr::Nullable(inner) => union_typename(vec![ + type_expr_to_typename_nested(inner, current_schema), + TypeName::primitive("null"), + ]), + IrTypeExpr::Union(members) => union_typename( + members + .iter() + .map(|member| type_expr_to_typename_nested(member, current_schema)) + .collect(), + ), other => type_expr_to_typename_with_current(other, current_schema), } } @@ -2223,3 +2211,49 @@ fn primitive_to_ts(p: &IrPrimitive) -> &'static str { IrPrimitive::Boolean => "boolean", } } + +#[cfg(test)] +mod render_tests { + use super::*; + use sigil_stitch::assert_rendered; + + #[test] + fn nullable_and_union_array_types_keep_precedence_and_imports() { + let string = IrTypeExpr::Primitive(IrPrimitive::String); + let cases = [ + ( + IrTypeExpr::Nullable(Box::new(IrTypeExpr::Array(Box::new(IrTypeExpr::Array( + Box::new(string.clone()), + ))))), + "export type Values = readonly string[][] | null;\n", + ), + ( + IrTypeExpr::Array(Box::new(IrTypeExpr::Nullable(Box::new(IrTypeExpr::Array( + Box::new(string.clone()), + ))))), + "export type Values = readonly (string[] | null)[];\n", + ), + ( + IrTypeExpr::Array(Box::new(IrTypeExpr::Union(vec![ + string, + IrTypeExpr::Primitive(IrPrimitive::Number), + ]))), + "export type Values = readonly (string | number)[];\n", + ), + ( + IrTypeExpr::Array(Box::new(IrTypeExpr::Nullable(Box::new(IrTypeExpr::Named( + "Detail".to_string(), + ))))), + "import type { Detail } from './Detail';\n\nexport type Values = readonly (Detail | null)[];\n", + ), + ]; + for (expr, expected) in cases { + let ty = type_expr_to_typename(&expr); + let block = sigil_quote!(TypeScript { + export type Values = $T(ty); + }) + .expect("array type render test quote builds"); + assert_rendered!(TypeScript::new(), width = 100, block, expected); + } + } +} diff --git a/src/ir/lower/v30.rs b/src/ir/lower/v30.rs index 386cefd66..d9e41ed7c 100644 --- a/src/ir/lower/v30.rs +++ b/src/ir/lower/v30.rs @@ -273,7 +273,10 @@ impl<'a> LowerCtx<'a> { if let Some(items) = &obj.items { let items_candidate = format!("{name}Items"); let inner = self.lower_schema_value_with_promotion(&items_candidate, items)?; - return Ok(IrSchemaKind::Alias(IrTypeExpr::Array(Box::new(inner)))); + return Ok(IrSchemaKind::Alias(with_schema_nullability( + obj, + IrTypeExpr::Array(Box::new(inner)), + ))); } // Simple type → alias @@ -690,11 +693,14 @@ impl<'a> LowerCtx<'a> { let mut schema = self.lower_object_schema_to_ir_schema(&name, obj)?; schema.is_component = self.in_component_phase; self.schemas.insert(name.clone(), schema); - Ok(IrTypeExpr::Named(name)) + Ok(with_schema_nullability(obj, IrTypeExpr::Named(name))) } else if is_array_with_promotable_items(obj) { let items_name = format!("{candidate_name}Item"); let inner = self.lower_array_items_with_promotion(&items_name, obj)?; - Ok(IrTypeExpr::Array(Box::new(inner))) + Ok(with_schema_nullability( + obj, + IrTypeExpr::Array(Box::new(inner)), + )) } else { Ok(self.lower_type_from_schema(obj)) } @@ -759,13 +765,7 @@ impl<'a> LowerCtx<'a> { } } }; - - // OAS 3.0 nullable: wrap in Nullable if nullable: true - if obj.nullable == Some(true) { - IrTypeExpr::Nullable(Box::new(base)) - } else { - base - } + with_schema_nullability(obj, base) } fn lower_schema_value_with_promotion( @@ -1105,6 +1105,20 @@ fn extract_component_name(ref_path: &str) -> String { ref_path.rsplit('/').next().unwrap_or(ref_path).to_string() } +/// Preserve nullability without changing the existing untyped-schema fallback. +fn nested_type_to_expr(obj: &ObjectSchema) -> IrTypeExpr { + let base = type_string_to_type_expr(obj.schema_type.as_deref().unwrap_or("string"), obj); + with_schema_nullability(obj, base) +} + +fn with_schema_nullability(obj: &ObjectSchema, base: IrTypeExpr) -> IrTypeExpr { + if obj.nullable == Some(true) { + IrTypeExpr::Nullable(Box::new(base)) + } else { + base + } +} + /// OAS 3.0: schema type is a single string, not a typed enum. fn type_string_to_type_expr(type_str: &str, obj: &ObjectSchema) -> IrTypeExpr { match type_str { @@ -1133,10 +1147,7 @@ fn type_string_to_type_expr(type_str: &str, obj: &ObjectSchema) -> IrTypeExpr { IrTypeExpr::Named(extract_schema_name(ref_path)), )), ObjectOrReference::Object(inner) => { - let inner_type = type_string_to_type_expr( - inner.schema_type.as_deref().unwrap_or("string"), - inner, - ); + let inner_type = nested_type_to_expr(inner); IrTypeExpr::Array(Box::new(inner_type)) } }, @@ -1163,10 +1174,7 @@ fn type_string_to_type_expr(type_str: &str, obj: &ObjectSchema) -> IrTypeExpr { IrTypeExpr::Named(extract_schema_name(ref_path)), )), ObjectOrReference::Object(inner) => { - let inner_type = type_string_to_type_expr( - inner.schema_type.as_deref().unwrap_or("string"), - inner, - ); + let inner_type = nested_type_to_expr(inner); IrTypeExpr::Map(Box::new(inner_type)) } }, diff --git a/src/ir/lower/v31.rs b/src/ir/lower/v31.rs index 692f59a15..56b1710e4 100644 --- a/src/ir/lower/v31.rs +++ b/src/ir/lower/v31.rs @@ -285,7 +285,10 @@ impl<'a> LowerCtx<'a> { if let Some(items) = &obj.items { let items_candidate = format!("{name}Items"); let inner = self.lower_schema_value_with_promotion(&items_candidate, items)?; - return Ok(IrSchemaKind::Alias(IrTypeExpr::Array(Box::new(inner)))); + return Ok(IrSchemaKind::Alias(with_schema_nullability( + obj, + IrTypeExpr::Array(Box::new(inner)), + ))); } // Simple type → alias @@ -695,7 +698,14 @@ impl<'a> LowerCtx<'a> { } Ok(IrTypeExpr::Named(extract_schema_name(ref_path))) } - ObjectOrReference::Object(obj) => Ok(self.lower_type_from_schema(obj)), + ObjectOrReference::Object(obj) => { + if let Some(inner) = nullable_wrapper_inner(obj) { + return Ok(IrTypeExpr::Nullable(Box::new( + self.lower_schema_ref(inner)?, + ))); + } + Ok(self.lower_type_from_schema(obj)) + } } } @@ -719,12 +729,15 @@ impl<'a> LowerCtx<'a> { let mut schema = self.lower_object_schema_to_ir_schema(&name, obj)?; schema.is_component = self.in_component_phase; self.schemas.insert(name.clone(), schema); - Ok(IrTypeExpr::Named(name)) + Ok(with_schema_nullability(obj, IrTypeExpr::Named(name))) } else if is_array_with_promotable_items(obj) { // Array with inline object items: promote the items schema let items_name = format!("{candidate_name}Item"); let inner = self.lower_array_items_with_promotion(&items_name, obj)?; - Ok(IrTypeExpr::Array(Box::new(inner))) + Ok(with_schema_nullability( + obj, + IrTypeExpr::Array(Box::new(inner)), + )) } else { Ok(self.lower_type_from_schema(obj)) } @@ -739,32 +752,9 @@ impl<'a> LowerCtx<'a> { candidate_name: &str, obj: &ObjectSchema, ) -> Result, LowerError> { - // Check oneOf and anyOf for nullable pattern - let members = if !obj.one_of.is_empty() { - &obj.one_of - } else if !obj.any_of.is_empty() { - &obj.any_of - } else { + let Some(inner_ref) = nullable_wrapper_inner(obj) else { return Ok(None); }; - - // Count null members and non-null members - let mut null_count = 0; - let mut non_null: Vec<&ObjectOrReference> = Vec::new(); - for m in members { - if is_null_schema(m) { - null_count += 1; - } else { - non_null.push(m); - } - } - - // Only handle the case: exactly 1 null + 1 non-null member → Nullable(inner) - if null_count != 1 || non_null.len() != 1 { - return Ok(None); - } - - let inner_ref = non_null[0]; let inner = self.lower_schema_ref_with_promotion(candidate_name, inner_ref)?; Ok(Some(IrTypeExpr::Nullable(Box::new(inner)))) } @@ -1228,21 +1218,7 @@ fn single_type_to_expr(t: SchemaType, obj: &ObjectSchema) -> IrTypeExpr { IrTypeExpr::Named(extract_schema_name(ref_path)), )), ObjectOrReference::Object(inner) => { - let inner_type = single_type_to_expr( - inner - .schema_type - .as_ref() - .map(|ts| match ts { - oas::SchemaTypeSet::Single(t) => *t, - oas::SchemaTypeSet::Multiple(ts) => ts - .iter() - .copied() - .find(|t| *t != oas::SchemaType::Null) - .unwrap_or(oas::SchemaType::String), - }) - .unwrap_or(oas::SchemaType::String), - inner, - ); + let inner_type = nested_type_to_expr(inner); IrTypeExpr::Array(Box::new(inner_type)) } }, @@ -1267,21 +1243,7 @@ fn single_type_to_expr(t: SchemaType, obj: &ObjectSchema) -> IrTypeExpr { IrTypeExpr::Named(extract_schema_name(ref_path)), )), ObjectOrReference::Object(inner) => { - let inner_type = single_type_to_expr( - inner - .schema_type - .as_ref() - .map(|ts| match ts { - oas::SchemaTypeSet::Single(t) => *t, - oas::SchemaTypeSet::Multiple(ts) => ts - .iter() - .copied() - .find(|t| *t != oas::SchemaType::Null) - .unwrap_or(oas::SchemaType::String), - }) - .unwrap_or(oas::SchemaType::String), - inner, - ); + let inner_type = nested_type_to_expr(inner); IrTypeExpr::Map(Box::new(inner_type)) } }, @@ -1294,6 +1256,35 @@ fn single_type_to_expr(t: SchemaType, obj: &ObjectSchema) -> IrTypeExpr { } } +/// Preserve the complete type set, including null, at nested schema nodes. +fn nested_type_to_expr(obj: &ObjectSchema) -> IrTypeExpr { + if let Some(inner) = nullable_wrapper_inner(obj) { + let inner = match inner { + ObjectOrReference::Ref { ref_path, .. } => { + IrTypeExpr::Named(extract_schema_name(ref_path)) + } + ObjectOrReference::Object(inner) => nested_type_to_expr(inner), + }; + return IrTypeExpr::Nullable(Box::new(inner)); + } + match &obj.schema_type { + Some(types) => type_set_to_type_expr(types, obj), + None => single_type_to_expr(SchemaType::String, obj), + } +} + +fn with_schema_nullability(obj: &ObjectSchema, base: IrTypeExpr) -> IrTypeExpr { + if obj + .schema_type + .as_ref() + .is_some_and(|types| types.contains(SchemaType::Null)) + { + IrTypeExpr::Nullable(Box::new(base)) + } else { + base + } +} + fn classify_enum_values(values: &[serde_json::Value]) -> IrEnumValueType { let mut has_string = false; let mut has_number = false; @@ -1335,6 +1326,20 @@ fn should_promote_to_named(obj: &ObjectSchema) -> bool { || !obj.one_of.is_empty() } +/// Share the supported two-member nullable pattern across schema depths. +fn nullable_wrapper_inner(obj: &ObjectSchema) -> Option<&ObjectOrReference> { + let members = if !obj.one_of.is_empty() { + &obj.one_of + } else { + &obj.any_of + }; + match members.as_slice() { + [left, right] if is_null_schema(left) && !is_null_schema(right) => Some(right), + [left, right] if !is_null_schema(left) && is_null_schema(right) => Some(left), + _ => None, + } +} + /// Check if a schema reference is a null type (used for nullable oneOf detection). fn is_null_schema(schema_ref: &ObjectOrReference) -> bool { match schema_ref { @@ -1353,7 +1358,7 @@ fn is_array_with_promotable_items(obj: &ObjectSchema) -> bool { let is_array = obj .schema_type .as_ref() - .is_some_and(|t| matches!(t, oas::SchemaTypeSet::Single(oas::SchemaType::Array))); + .is_some_and(|t| t.is_array_or_nullable_array()); if !is_array { return false; } diff --git a/src/ir/lower/v32.rs b/src/ir/lower/v32.rs index 4291c6411..3bae3435c 100644 --- a/src/ir/lower/v32.rs +++ b/src/ir/lower/v32.rs @@ -282,7 +282,10 @@ impl<'a> LowerCtx<'a> { if let Some(items) = &obj.items { let items_candidate = format!("{name}Items"); let inner = self.lower_schema_value_with_promotion(&items_candidate, items)?; - return Ok(IrSchemaKind::Alias(IrTypeExpr::Array(Box::new(inner)))); + return Ok(IrSchemaKind::Alias(with_schema_nullability( + obj, + IrTypeExpr::Array(Box::new(inner)), + ))); } // Simple type → alias @@ -692,7 +695,14 @@ impl<'a> LowerCtx<'a> { } Ok(IrTypeExpr::Named(extract_schema_name(ref_path))) } - ObjectOrReference::Object(obj) => Ok(self.lower_type_from_schema(obj)), + ObjectOrReference::Object(obj) => { + if let Some(inner) = nullable_wrapper_inner(obj) { + return Ok(IrTypeExpr::Nullable(Box::new( + self.lower_schema_ref(inner)?, + ))); + } + Ok(self.lower_type_from_schema(obj)) + } } } @@ -716,12 +726,15 @@ impl<'a> LowerCtx<'a> { let mut schema = self.lower_object_schema_to_ir_schema(&name, obj)?; schema.is_component = self.in_component_phase; self.schemas.insert(name.clone(), schema); - Ok(IrTypeExpr::Named(name)) + Ok(with_schema_nullability(obj, IrTypeExpr::Named(name))) } else if is_array_with_promotable_items(obj) { // Array with inline object items: promote the items schema let items_name = format!("{candidate_name}Item"); let inner = self.lower_array_items_with_promotion(&items_name, obj)?; - Ok(IrTypeExpr::Array(Box::new(inner))) + Ok(with_schema_nullability( + obj, + IrTypeExpr::Array(Box::new(inner)), + )) } else { Ok(self.lower_type_from_schema(obj)) } @@ -736,32 +749,9 @@ impl<'a> LowerCtx<'a> { candidate_name: &str, obj: &ObjectSchema, ) -> Result, LowerError> { - // Check oneOf and anyOf for nullable pattern - let members = if !obj.one_of.is_empty() { - &obj.one_of - } else if !obj.any_of.is_empty() { - &obj.any_of - } else { + let Some(inner_ref) = nullable_wrapper_inner(obj) else { return Ok(None); }; - - // Count null members and non-null members - let mut null_count = 0; - let mut non_null: Vec<&ObjectOrReference> = Vec::new(); - for m in members { - if is_null_schema(m) { - null_count += 1; - } else { - non_null.push(m); - } - } - - // Only handle the case: exactly 1 null + 1 non-null member → Nullable(inner) - if null_count != 1 || non_null.len() != 1 { - return Ok(None); - } - - let inner_ref = non_null[0]; let inner = self.lower_schema_ref_with_promotion(candidate_name, inner_ref)?; Ok(Some(IrTypeExpr::Nullable(Box::new(inner)))) } @@ -1235,21 +1225,7 @@ fn single_type_to_expr(t: SchemaType, obj: &ObjectSchema) -> IrTypeExpr { IrTypeExpr::Named(extract_schema_name(ref_path)), )), ObjectOrReference::Object(inner) => { - let inner_type = single_type_to_expr( - inner - .schema_type - .as_ref() - .map(|ts| match ts { - oas::SchemaTypeSet::Single(t) => *t, - oas::SchemaTypeSet::Multiple(ts) => ts - .iter() - .copied() - .find(|t| *t != oas::SchemaType::Null) - .unwrap_or(oas::SchemaType::String), - }) - .unwrap_or(oas::SchemaType::String), - inner, - ); + let inner_type = nested_type_to_expr(inner); IrTypeExpr::Array(Box::new(inner_type)) } }, @@ -1274,21 +1250,7 @@ fn single_type_to_expr(t: SchemaType, obj: &ObjectSchema) -> IrTypeExpr { IrTypeExpr::Named(extract_schema_name(ref_path)), )), ObjectOrReference::Object(inner) => { - let inner_type = single_type_to_expr( - inner - .schema_type - .as_ref() - .map(|ts| match ts { - oas::SchemaTypeSet::Single(t) => *t, - oas::SchemaTypeSet::Multiple(ts) => ts - .iter() - .copied() - .find(|t| *t != oas::SchemaType::Null) - .unwrap_or(oas::SchemaType::String), - }) - .unwrap_or(oas::SchemaType::String), - inner, - ); + let inner_type = nested_type_to_expr(inner); IrTypeExpr::Map(Box::new(inner_type)) } }, @@ -1301,6 +1263,35 @@ fn single_type_to_expr(t: SchemaType, obj: &ObjectSchema) -> IrTypeExpr { } } +/// Preserve the complete type set, including null, at nested schema nodes. +fn nested_type_to_expr(obj: &ObjectSchema) -> IrTypeExpr { + if let Some(inner) = nullable_wrapper_inner(obj) { + let inner = match inner { + ObjectOrReference::Ref { ref_path, .. } => { + IrTypeExpr::Named(extract_schema_name(ref_path)) + } + ObjectOrReference::Object(inner) => nested_type_to_expr(inner), + }; + return IrTypeExpr::Nullable(Box::new(inner)); + } + match &obj.schema_type { + Some(types) => type_set_to_type_expr(types, obj), + None => single_type_to_expr(SchemaType::String, obj), + } +} + +fn with_schema_nullability(obj: &ObjectSchema, base: IrTypeExpr) -> IrTypeExpr { + if obj + .schema_type + .as_ref() + .is_some_and(|types| types.contains(SchemaType::Null)) + { + IrTypeExpr::Nullable(Box::new(base)) + } else { + base + } +} + fn classify_enum_values(values: &[serde_json::Value]) -> IrEnumValueType { let mut has_string = false; let mut has_number = false; @@ -1342,6 +1333,20 @@ fn should_promote_to_named(obj: &ObjectSchema) -> bool { || !obj.one_of.is_empty() } +/// Share the supported two-member nullable pattern across schema depths. +fn nullable_wrapper_inner(obj: &ObjectSchema) -> Option<&ObjectOrReference> { + let members = if !obj.one_of.is_empty() { + &obj.one_of + } else { + &obj.any_of + }; + match members.as_slice() { + [left, right] if is_null_schema(left) && !is_null_schema(right) => Some(right), + [left, right] if !is_null_schema(left) && is_null_schema(right) => Some(left), + _ => None, + } +} + /// Check if a schema reference is a null type (used for nullable oneOf detection). fn is_null_schema(schema_ref: &ObjectOrReference) -> bool { match schema_ref { @@ -1360,7 +1365,7 @@ fn is_array_with_promotable_items(obj: &ObjectSchema) -> bool { let is_array = obj .schema_type .as_ref() - .is_some_and(|t| matches!(t, oas::SchemaTypeSet::Single(oas::SchemaType::Array))); + .is_some_and(|t| t.is_array_or_nullable_array()); if !is_array { return false; } diff --git a/tests/fixtures/valid/nested-nullable.yaml b/tests/fixtures/valid/nested-nullable.yaml new file mode 100644 index 000000000..b1eb58005 --- /dev/null +++ b/tests/fixtures/valid/nested-nullable.yaml @@ -0,0 +1,133 @@ +openapi: 3.0.3 +info: + title: Nested Nullable API + version: 1.0.0 +paths: + /values: + get: + tags: [Values] + operationId: getValues + responses: + '200': + description: Nullable model properties. + content: + application/json: + schema: + $ref: '#/components/schemas/NullablePayload' + '400': + description: Map with nullable array values. + content: + application/json: + schema: + type: object + additionalProperties: + type: array + nullable: true + items: + type: string + '401': + description: Array with nullable map items. + content: + application/json: + schema: + type: array + items: + type: object + nullable: true + additionalProperties: + type: string + '402': + description: Nullable array with non-nullable string items. + content: + application/json: + schema: + type: array + nullable: true + items: + type: string + '403': + description: Non-nullable map with non-nullable arrays. + content: + application/json: + schema: + type: object + additionalProperties: + type: array + items: + type: string + '404': + description: Named model alias payload. + content: + application/json: + schema: + $ref: '#/components/schemas/DetailAlias' + '410': + description: Array of named model aliases. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DetailAlias' +components: + schemas: + MaybeMatrix: + type: array + nullable: true + items: + type: array + items: + type: string + MatrixContainer: + type: object + required: [matrix] + properties: + matrix: + type: array + nullable: true + items: + type: array + items: + type: string + Detail: + type: object + required: [message] + properties: + message: + type: string + DetailAlias: + $ref: '#/components/schemas/Detail' + NullablePayload: + type: object + required: [mapValues, arrayItems, arrayMaps, nullableArray, strictValues] + properties: + mapValues: + type: object + additionalProperties: + type: array + nullable: true + items: + type: string + arrayItems: + type: array + items: + type: string + nullable: true + arrayMaps: + type: array + items: + type: object + nullable: true + additionalProperties: + type: string + nullableArray: + type: array + nullable: true + items: + type: string + strictValues: + type: object + additionalProperties: + type: array + items: + type: string diff --git a/tests/generated-clients/rust/README.md b/tests/generated-clients/rust/README.md new file mode 100644 index 000000000..206a69e30 --- /dev/null +++ b/tests/generated-clients/rust/README.md @@ -0,0 +1,50 @@ +# Rust generated-client tests + +These are standalone consumer test projects, not tests of the generator crate. +Their ordinary Cargo manifests depend on freshly generated clients through path +dependencies. Test source is checked in as Rust files; no Rust test invokes Cargo, +assembles another test program, or changes a generated manifest. + +## Run + +From the repository root: + +```bash +just golden-rust::test # all three backends +just golden-rust::test-reqwest # one backend +``` + +The recipe uses the public CLI to generate three fixtures into the backend's +ignored `generated/` directory, then runs `cargo test --locked` once for that +consumer project. It inherits the active toolchain, `RUSTFLAGS`, Cargo settings, +and test concurrency settings. Generated client files are never edited. Each +backend retains its target directory, so dependencies are not rebuilt per case. + +After generation, the project can be used directly from Cargo or an IDE: + +```bash +just golden-rust::prepare reqwest +cargo test --locked --manifest-path tests/generated-clients/rust/reqwest/Cargo.toml +cargo test --locked --manifest-path tests/generated-clients/rust/reqwest/Cargo.toml --test nested_nullable +``` + +Regenerate after changing the generator or a fixture. Preparation removes the +three managed fixture output directories before regenerating them, preventing +stale declarations from masking missing output. The manifests and lockfiles are +checked in; do not edit the ignored generated clients to make a test pass. + +## Coverage + +| Fixture | Checks | +| --- | --- | +| `typed-error-responses` | HTTP errors, metadata, uniform errors, success dispatch, body handling, and layout; existing backend-specific cases are retained | +| `nested-nullable` | Seven shared HTTP decoding cases per backend, including strict controls and invalid-body preservation | +| `rust-error-payload-shapes` | Compiles as a dev-dependency of every consumer project, retaining the previous composite-payload compile checks | + +The reqwest project also retains compile-time propagation checks for uniform +errors, thiserror, and Snafu; ureq retains its synchronous propagation check. + +The root `just test-all` and `just coverage-lcov` cover the generator workspace, +not these projects. CI runs a separate Rust Client Tests matrix, and `just ci` +includes `just golden-rust::test`. Golden comparison and all-golden compilation +remain separate checks. JVM runtime tests are unchanged by this separation. diff --git a/tests/generated-clients/rust/aioduct/Cargo.lock b/tests/generated-clients/rust/aioduct/Cargo.lock new file mode 100644 index 000000000..144b04c85 --- /dev/null +++ b/tests/generated-clients/rust/aioduct/Cargo.lock @@ -0,0 +1,1494 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aioduct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c269f3acce4b5b8491308ad6f009cfc38df8466b872899bc8792050955aadfa1" +dependencies = [ + "base64", + "bytes", + "coarsetime", + "futures-channel", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "memchr", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "socket2", + "thiserror", + "tokio", + "url", + "webpki-roots", + "x509-parser", +] + +[[package]] +name = "aioduct-client-tests" +version = "0.0.0" +dependencies = [ + "aioduct", + "nested-nullable-api", + "rust-error-payload-shapes-api", + "tokio", + "typed-error-responses-api", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "coarsetime" +version = "0.1.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae63702c5627c75addbfb1ea9d1b3842205a6ffd43e4cce884a2a9eb1828fd7" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nested-nullable-api" +version = "0.1.0" +dependencies = [ + "aioduct", + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-error-payload-shapes-api" +version = "0.1.0" +dependencies = [ + "aioduct", + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" +dependencies = [ + "log", + "serde", + "thiserror", + "xml", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-error-responses-api" +version = "0.1.0" +dependencies = [ + "aioduct", + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasix" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae86f02046da16a333a9129d31451423e1657737ecdafed4193838a5f54c5cfe" +dependencies = [ + "wasi", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "xml" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure 0.14.0", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure 0.14.0", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/generated-clients/rust/aioduct/Cargo.toml b/tests/generated-clients/rust/aioduct/Cargo.toml new file mode 100644 index 000000000..7abe3cf8c --- /dev/null +++ b/tests/generated-clients/rust/aioduct/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "aioduct-client-tests" +version = "0.0.0" +edition = "2024" +publish = false + +[workspace] +resolver = "3" + +[dev-dependencies] +typed-error-responses-api = { path = "generated/typed-error-responses" } +rust-error-payload-shapes-api = { path = "generated/rust-error-payload-shapes" } +nested-nullable-api = { path = "generated/nested-nullable" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +aioduct = { version = "0.2", features = ["tokio", "rustls", "rustls-ring", "json"] } diff --git a/tests/generated-clients/rust/aioduct/tests/http_error.rs b/tests/generated-clients/rust/aioduct/tests/http_error.rs new file mode 100644 index 000000000..736605b9d --- /dev/null +++ b/tests/generated-clients/rust/aioduct/tests/http_error.rs @@ -0,0 +1,9 @@ +use typed_error_responses_api::runtime::error::ApiCallError; + +#[test] +fn api_call_error_is_pointer_sized() { + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::>() + ); +} diff --git a/tests/generated-clients/rust/aioduct/tests/nested_nullable.rs b/tests/generated-clients/rust/aioduct/tests/nested_nullable.rs new file mode 100644 index 000000000..5c251c2b1 --- /dev/null +++ b/tests/generated-clients/rust/aioduct/tests/nested_nullable.rs @@ -0,0 +1,17 @@ +use nested_nullable_api::apis::{GetValuesError, GetValuesResponse, ValuesApi}; +use nested_nullable_api::runtime::client::Client; + +#[path = "../../support/one_response_server.rs"] +mod server; + +#[path = "../../support/nested_nullable.rs"] +mod cases; + +async fn request(status: &str, body: &'static str) -> Result { + let url = server::spawn_one_response_server(status, body); + let client = + Client::::new( + &url, + ); + ValuesApi::new(&client).get_values().await +} diff --git a/tests/generated-clients/rust/config.toml b/tests/generated-clients/rust/config.toml new file mode 100644 index 000000000..beab351c0 --- /dev/null +++ b/tests/generated-clients/rust/config.toml @@ -0,0 +1 @@ +# Explicit empty configuration: use generator defaults, not local discovery. diff --git a/tests/generated-clients/rust/reqwest/Cargo.lock b/tests/generated-clients/rust/reqwest/Cargo.lock new file mode 100644 index 000000000..8ebe03f3d --- /dev/null +++ b/tests/generated-clients/rust/reqwest/Cargo.lock @@ -0,0 +1,1615 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "h2" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nested-nullable-api" +version = "0.1.0" +dependencies = [ + "reqwest", + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest-client-tests" +version = "0.0.0" +dependencies = [ + "nested-nullable-api", + "rust-error-payload-shapes-api", + "snafu", + "thiserror", + "tokio", + "typed-error-responses-api", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rust-error-payload-shapes-api" +version = "0.1.0" +dependencies = [ + "reqwest", + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" +dependencies = [ + "log", + "serde", + "thiserror", + "xml", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "snafu" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45cb604038abb7b926b679887b3226d8d0f23874b66623625a0454be425a4b7" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287f59010008f0d7cf5e3b03196d666c1acc46c8d3e9cf34c28a1a7157601e72" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-error-responses-api" +version = "0.1.0" +dependencies = [ + "reqwest", + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", +] + +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xml" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/generated-clients/rust/reqwest/Cargo.toml b/tests/generated-clients/rust/reqwest/Cargo.toml new file mode 100644 index 000000000..fcae4bc9b --- /dev/null +++ b/tests/generated-clients/rust/reqwest/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "reqwest-client-tests" +version = "0.0.0" +edition = "2024" +publish = false + +[workspace] +resolver = "3" + +[dev-dependencies] +typed-error-responses-api = { path = "generated/typed-error-responses" } +rust-error-payload-shapes-api = { path = "generated/rust-error-payload-shapes" } +nested-nullable-api = { path = "generated/nested-nullable" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +thiserror = "2" +snafu = "0.9.1" diff --git a/tests/generated-clients/rust/reqwest/tests/http_error.rs b/tests/generated-clients/rust/reqwest/tests/http_error.rs new file mode 100644 index 000000000..555f8d4cf --- /dev/null +++ b/tests/generated-clients/rust/reqwest/tests/http_error.rs @@ -0,0 +1,278 @@ +#[path = "../../support/incomplete_body_server.rs"] +mod incomplete_server; +#[path = "../../support/one_response_server.rs"] +mod server; + +use incomplete_server::spawn_incomplete_success_body_server; +use server::spawn_one_response_server; + +use typed_error_responses_api::apis::{ + CreateResourceError, CreateResourceWithWildcardSuccessError, ResourcesApi, +}; +use typed_error_responses_api::models::CreateResourceRequest; +use typed_error_responses_api::runtime::client::Client; +use typed_error_responses_api::runtime::error::{ApiCallError, Error}; + +#[test] +fn api_call_error_is_pointer_sized() { + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::>() + ); +} + +#[allow(dead_code)] +async fn propagate_uniformly( + api: &ResourcesApi<'_>, + request: &CreateResourceRequest, +) -> Result<(), ApiCallError> { + api.create_resource(request).await?; + api.check_no_success_body().await?; + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +#[error("SDK request failed: {source}")] +struct ThisErrorAppError { + #[source] + source: ApiCallError, +} + +impl From for ThisErrorAppError +where + E: Into, +{ + fn from(error: E) -> Self { + Self { + source: error.into(), + } + } +} + +#[allow(dead_code)] +async fn propagate_with_thiserror( + api: &ResourcesApi<'_>, + request: &CreateResourceRequest, +) -> Result<(), ThisErrorAppError> { + api.create_resource(request).await?; + api.check_no_success_body().await?; + Ok(()) +} + +#[derive(Debug, snafu::Snafu)] +#[snafu(context(false))] +struct SnafuAppError { + #[snafu(source(from(generic)))] + source: ApiCallError, +} + +#[allow(dead_code)] +async fn propagate_with_snafu( + api: &ResourcesApi<'_>, + request: &CreateResourceRequest, +) -> Result<(), SnafuAppError> { + api.create_resource(request).await?; + api.check_no_success_body().await?; + Ok(()) +} + +#[tokio::test] +async fn http_503_returns_operation_error_with_typed_body() { + let body = r#"{"message":"temporarily unavailable","retryable":true}"#; + let base_url = spawn_one_response_server("503 Service Unavailable", body); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + let request = CreateResourceRequest { + name: "resource".to_string(), + }; + + let err = api + .create_resource(&request) + .await + .expect_err("HTTP 503 must not be returned as success"); + assert_eq!(err.retry_after_header(), Some("120")); + + match err { + CreateResourceError::ServerError(api_error) => { + assert_eq!(api_error.status_code(), 503); + assert_eq!(api_error.raw_body(), body.as_bytes()); + assert_eq!( + api_error + .headers() + .get("retry-after") + .and_then(|value| value.to_str().ok()), + Some("120") + ); + assert_eq!( + api_error + .headers() + .get("x-trace-id") + .and_then(|value| value.to_str().ok()), + Some("typed-error-test") + ); + let typed_body = api_error + .body() + .expect("documented 5XX error body should decode lazily"); + assert_eq!(typed_body.message, "temporarily unavailable"); + assert!(typed_body.retryable); + } + other => panic!("expected ServerError variant, got {other:?}"), + } +} + +#[tokio::test] +async fn invalid_error_body_keeps_http_error_and_raw_body() { + let body = r#"{"message":42,"retryable":true}"#; + let base_url = spawn_one_response_server("503 Service Unavailable", body); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + let request = CreateResourceRequest { + name: "resource".to_string(), + }; + + let err = api + .create_resource(&request) + .await + .expect_err("HTTP 503 must remain an operation error even when its body is invalid"); + + match err { + CreateResourceError::ServerError(api_error) => { + assert_eq!(api_error.status_code(), 503); + assert_eq!(api_error.raw_body(), body.as_bytes()); + assert!(matches!(api_error.body(), Err(Error::Deserialize(_)))); + + let error: ApiCallError = CreateResourceError::ServerError(api_error).into(); + assert_eq!(error.operation_id(), "createResource"); + assert_eq!(error.status_code(), Some(503)); + assert_eq!(error.raw_body(), Some(body.as_bytes())); + assert!(matches!( + std::error::Error::source(&error).and_then(|source| source.downcast_ref::()), + Some(Error::Deserialize(_)) + )); + } + other => panic!("expected ServerError variant, got {other:?}"), + } +} + +#[tokio::test] +async fn operation_error_converts_to_uniform_api_call_error() { + let body = r#"{"message":"temporarily unavailable","retryable":true}"#; + let base_url = spawn_one_response_server("503 Service Unavailable", body); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + let request = CreateResourceRequest { + name: "resource".to_string(), + }; + + let error: ApiCallError = api + .create_resource(&request) + .await + .expect_err("HTTP 503 must be returned as an operation error") + .into(); + + assert_eq!(error.operation_id(), "createResource"); + assert_eq!(error.status_code(), Some(503)); + assert_eq!(error.raw_body(), Some(body.as_bytes())); + assert_eq!( + error + .headers() + .and_then(|headers| headers.get("retry-after")) + .and_then(|value| value.to_str().ok()), + Some("120") + ); + assert_eq!( + error.to_string(), + "operation createResource failed with HTTP status 503" + ); + assert!(std::error::Error::source(&error).is_none()); +} + +#[test] +fn transport_error_converts_to_uniform_api_call_error() { + let error: ApiCallError = + CreateResourceError::Transport(Error::Unsupported("test transport failure")).into(); + + assert_eq!(error.operation_id(), "createResource"); + assert_eq!(error.status_code(), None); + assert!(error.headers().is_none()); + assert!(error.raw_body().is_none()); + assert!(matches!( + std::error::Error::source(&error).and_then(|source| source.downcast_ref::()), + Some(Error::Unsupported("test transport failure")) + )); +} + +#[tokio::test] +async fn exact_success_status_wins_over_2xx_wildcard() { + let body = r#"{"id":"exact-created"}"#; + let base_url = spawn_one_response_server("201 Created", body); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + let request = CreateResourceRequest { + name: "resource".to_string(), + }; + + let response = api + .create_resource_with_wildcard_success(&request) + .await + .expect("HTTP 201 should be a successful exact response"); + + let created = response + .created + .expect("201 must match the exact branch before 2XX wildcard"); + assert_eq!(created.id, "exact-created"); + assert!(response.status_2xx.is_none()); + assert_eq!( + response + .headers + .get("x-request-id") + .and_then(|value| value.to_str().ok()), + Some("typed-response-test") + ); +} + +#[tokio::test] +async fn unexpected_non_success_status_uses_unexpected_detail() { + let body = r#"{"message":"teapot"}"#; + let base_url = spawn_one_response_server("418 I'm a teapot", body); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + let request = CreateResourceRequest { + name: "resource".to_string(), + }; + + let err = api + .create_resource_with_wildcard_success(&request) + .await + .expect_err("undocumented HTTP 418 must not be returned as success"); + + match err { + CreateResourceWithWildcardSuccessError::Unexpected(api_error) => { + assert_eq!(api_error.status_code(), 418); + assert_eq!(api_error.raw_body(), body.as_bytes()); + } + other => panic!("expected Unexpected variant, got {other:?}"), + } +} + +#[tokio::test] +async fn success_without_documented_body_does_not_drain_body() { + let base_url = spawn_incomplete_success_body_server(); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + + let response = api + .check_no_success_body() + .await + .expect("success without a documented body must not read an incomplete body"); + + assert_eq!(response.status_code, 200); + assert_eq!(response.x_request_id_header(), Some("typed-response-test")); + assert_eq!( + response + .headers + .get("x-request-id") + .and_then(|value| value.to_str().ok()), + Some("typed-response-test") + ); +} diff --git a/tests/generated-clients/rust/reqwest/tests/nested_nullable.rs b/tests/generated-clients/rust/reqwest/tests/nested_nullable.rs new file mode 100644 index 000000000..03b6ac0b3 --- /dev/null +++ b/tests/generated-clients/rust/reqwest/tests/nested_nullable.rs @@ -0,0 +1,14 @@ +use nested_nullable_api::apis::{GetValuesError, GetValuesResponse, ValuesApi}; +use nested_nullable_api::runtime::client::Client; + +#[path = "../../support/one_response_server.rs"] +mod server; + +#[path = "../../support/nested_nullable.rs"] +mod cases; + +async fn request(status: &str, body: &'static str) -> Result { + let url = server::spawn_one_response_server(status, body); + let client = Client::new(&url); + ValuesApi::new(&client).get_values().await +} diff --git a/tests/generated-clients/rust/support/incomplete_body_server.rs b/tests/generated-clients/rust/support/incomplete_body_server.rs new file mode 100644 index 000000000..69f9a4f96 --- /dev/null +++ b/tests/generated-clients/rust/support/incomplete_body_server.rs @@ -0,0 +1,22 @@ +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::thread; + +pub fn spawn_incomplete_success_body_server() -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("test server should bind"); + let addr = listener + .local_addr() + .expect("test server address should be readable"); + thread::spawn(move || { + let (mut stream, _) = listener + .accept() + .expect("test server should accept one request"); + let mut request = [0_u8; 4096]; + let _ = stream.read(&mut request); + let response = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nX-Request-Id: typed-response-test\r\nContent-Length: 999999\r\nConnection: close\r\n\r\n"; + stream + .write_all(response.as_bytes()) + .expect("test response headers should be written"); + }); + format!("http://{addr}") +} diff --git a/tests/generated-clients/rust/support/nested_nullable.rs b/tests/generated-clients/rust/support/nested_nullable.rs new file mode 100644 index 000000000..2493149dd --- /dev/null +++ b/tests/generated-clients/rust/support/nested_nullable.rs @@ -0,0 +1,163 @@ +//! Shared consumer tests against each backend's freshly generated client. + +use super::request; + +use nested_nullable_api::apis::GetValuesError; +use nested_nullable_api::runtime::error::{ApiCallError, ApiError, Error}; + +fn check_metadata(error: &ApiError, status: u16, body: &str) { + assert_eq!(error.status_code(), status); + assert_eq!(error.raw_body(), body.as_bytes()); + assert_eq!( + error.headers().get("x-trace-id").unwrap().to_str().unwrap(), + "typed-error-test" + ); +} + +#[tokio::test] +async fn map_values_decode_both_arrays_and_null() { + let body = r#"{"present":["detail"],"absent":null}"#; + let error = request("400 Bad Request", body) + .await + .expect_err("400 is an operation error"); + let GetValuesError::BadRequest(error) = error else { + panic!("wrong HTTP error variant") + }; + check_metadata(&error, 400, body); + let decoded = error.body().expect("nullable map value decodes"); + assert_eq!(decoded["present"], Some(vec!["detail".to_string()])); + assert_eq!(decoded["absent"], None); +} + +#[tokio::test] +async fn array_items_decode_both_maps_and_null() { + let body = r#"[null,{"detail":"message"}]"#; + let error = request("401 Unauthorized", body) + .await + .expect_err("401 is an operation error"); + let GetValuesError::Unauthorized(error) = error else { + panic!("wrong HTTP error variant") + }; + check_metadata(&error, 401, body); + let decoded = error.body().expect("nullable map item decodes"); + assert_eq!(decoded[0], None); + assert_eq!(decoded[1].as_ref().unwrap()["detail"], "message"); +} + +#[tokio::test] +async fn nullable_container_is_not_confused_with_nullable_items() { + for body in ["null", r#"["detail"]"#] { + let error = request("402 Payment Required", body) + .await + .expect_err("402 is an operation error"); + let GetValuesError::Status402(error) = error else { + panic!("wrong HTTP error variant") + }; + check_metadata(&error, 402, body); + let expected = if body == "null" { + None + } else { + Some(vec!["detail".to_string()]) + }; + assert_eq!(error.body().expect("nullable array decodes"), &expected); + } +} + +fn check_decode_failure(error: &ApiError) { + assert!(matches!(error.body(), Err(Error::Deserialize(_)))); +} + +#[tokio::test] +async fn invalid_payloads_keep_the_http_error_headers_and_raw_bytes() { + for (status, body, code) in [ + ("400 Bad Request", r#"{"detail":[42]}"#, 400), + ("400 Bad Request", r#"{"detail":[null]}"#, 400), + ("401 Unauthorized", r#"[{"detail":null}]"#, 401), + ("402 Payment Required", "[null]", 402), + ("403 Forbidden", r#"{"detail":null}"#, 403), + ("404 Not Found", r#"{"message":42}"#, 404), + ("410 Gone", r#"[{"message":null}]"#, 410), + ] { + let error = request(status, body) + .await + .expect_err("invalid body is still an HTTP error"); + match &error { + GetValuesError::BadRequest(error) => check_decode_failure(error), + GetValuesError::Unauthorized(error) => check_decode_failure(error), + GetValuesError::Status402(error) => check_decode_failure(error), + GetValuesError::Forbidden(error) => check_decode_failure(error), + GetValuesError::NotFound(error) => check_decode_failure(error), + GetValuesError::Status410(error) => check_decode_failure(error), + other => panic!("unexpected error: {other:?}"), + } + let uniform: ApiCallError = error.into(); + assert_eq!(uniform.operation_id(), "getValues"); + assert_eq!(uniform.status_code(), Some(code)); + assert_eq!(uniform.raw_body(), Some(body.as_bytes())); + assert_eq!( + uniform + .headers() + .unwrap() + .get("x-trace-id") + .unwrap() + .to_str() + .unwrap(), + "typed-error-test" + ); + } +} + +#[tokio::test] +async fn model_aliases_decode_alone_and_inside_arrays() { + let error = request("404 Not Found", r#"{"message":"missing"}"#) + .await + .expect_err("404 is an operation error"); + let GetValuesError::NotFound(error) = error else { + panic!("wrong HTTP error variant") + }; + assert_eq!( + error.body().expect("model alias decodes").message, + "missing" + ); + + let error = request("410 Gone", r#"[{"message":"expired"}]"#) + .await + .expect_err("410 is an operation error"); + let GetValuesError::Status410(error) = error else { + panic!("wrong HTTP error variant") + }; + assert_eq!( + error.body().expect("array of model aliases decodes")[0].message, + "expired" + ); +} + +#[tokio::test] +async fn non_nullable_values_still_decode_without_optional_wrappers() { + let body = r#"{"detail":["message"]}"#; + let error = request("403 Forbidden", body) + .await + .expect_err("403 is an operation error"); + let GetValuesError::Forbidden(error) = error else { + panic!("wrong HTTP error variant") + }; + assert_eq!( + error.body().expect("strict payload decodes")["detail"], + vec!["message"] + ); +} + +#[tokio::test] +async fn success_models_keep_nested_nullability() { + let body = r#"{"mapValues":{"detail":null},"arrayItems":[null,"detail"],"arrayMaps":[null,{"detail":"message"}],"nullableArray":null,"strictValues":{"detail":["message"]}}"#; + let response = request("200 OK", body) + .await + .expect("success model decodes"); + let model = response.data.expect("success payload exists"); + assert_eq!(model.map_values["detail"], None); + assert_eq!(model.array_items, vec![None, Some("detail".to_string())]); + assert_eq!(model.array_maps[0], None); + assert_eq!(model.array_maps[1].as_ref().unwrap()["detail"], "message"); + assert_eq!(model.nullable_array, None); + assert_eq!(model.strict_values["detail"], vec!["message"]); +} diff --git a/tests/generated-clients/rust/support/one_response_server.rs b/tests/generated-clients/rust/support/one_response_server.rs new file mode 100644 index 000000000..e81b34b2b --- /dev/null +++ b/tests/generated-clients/rust/support/one_response_server.rs @@ -0,0 +1,26 @@ +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::thread; + +pub fn spawn_one_response_server(status: &str, body: &'static str) -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("test server should bind"); + let addr = listener + .local_addr() + .expect("test server address should be readable"); + let status = status.to_string(); + thread::spawn(move || { + let (mut stream, _) = listener + .accept() + .expect("test server should accept one request"); + let mut request = [0_u8; 4096]; + let _ = stream.read(&mut request); + let response = format!( + "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nRetry-After: 120\r\nX-Request-Id: typed-response-test\r\nX-Trace-Id: typed-error-test\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream + .write_all(response.as_bytes()) + .expect("test response should be written"); + }); + format!("http://{addr}") +} diff --git a/tests/generated-clients/rust/ureq/Cargo.lock b/tests/generated-clients/rust/ureq/Cargo.lock new file mode 100644 index 000000000..487bed454 --- /dev/null +++ b/tests/generated-clients/rust/ureq/Cargo.lock @@ -0,0 +1,943 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nested-nullable-api" +version = "0.1.0" +dependencies = [ + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", + "ureq", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys", +] + +[[package]] +name = "rust-error-payload-shapes-api" +version = "0.1.0" +dependencies = [ + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", + "ureq", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" +dependencies = [ + "log", + "serde", + "thiserror", + "xml", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typed-error-responses-api" +version = "0.1.0" +dependencies = [ + "serde", + "serde-xml-rs", + "serde_json", + "serde_repr", + "ureq", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64", + "cookie_store", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-client-tests" +version = "0.0.0" +dependencies = [ + "nested-nullable-api", + "rust-error-payload-shapes-api", + "tokio", + "typed-error-responses-api", +] + +[[package]] +name = "ureq-proto" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xml" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f45bb2c13fec6a6cb4c0f76a7e94839e110a14ec803ec2940777a94c347bc52" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/tests/generated-clients/rust/ureq/Cargo.toml b/tests/generated-clients/rust/ureq/Cargo.toml new file mode 100644 index 000000000..2f0cb3e3d --- /dev/null +++ b/tests/generated-clients/rust/ureq/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ureq-client-tests" +version = "0.0.0" +edition = "2024" +publish = false + +[workspace] +resolver = "3" + +[dev-dependencies] +typed-error-responses-api = { path = "generated/typed-error-responses" } +rust-error-payload-shapes-api = { path = "generated/rust-error-payload-shapes" } +nested-nullable-api = { path = "generated/nested-nullable" } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/tests/generated-clients/rust/ureq/tests/http_error.rs b/tests/generated-clients/rust/ureq/tests/http_error.rs new file mode 100644 index 000000000..4ec9cbbeb --- /dev/null +++ b/tests/generated-clients/rust/ureq/tests/http_error.rs @@ -0,0 +1,121 @@ +#[path = "../../support/incomplete_body_server.rs"] +mod incomplete_server; +#[path = "../../support/one_response_server.rs"] +mod server; + +use incomplete_server::spawn_incomplete_success_body_server; +use server::spawn_one_response_server; + +use typed_error_responses_api::apis::{CreateResourceError, ResourcesApi}; +use typed_error_responses_api::models::CreateResourceRequest; +use typed_error_responses_api::runtime::client::Client; +use typed_error_responses_api::runtime::error::ApiCallError; + +#[test] +fn api_call_error_is_pointer_sized() { + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::>() + ); +} + +#[allow(dead_code)] +fn propagate_uniformly( + api: &ResourcesApi<'_>, + request: &CreateResourceRequest, +) -> Result<(), ApiCallError> { + api.create_resource(request)?; + api.check_no_success_body()?; + Ok(()) +} + +#[test] +fn http_503_returns_operation_error_with_typed_body() { + let body = r#"{"message":"temporarily unavailable","retryable":true}"#; + let base_url = spawn_one_response_server("503 Service Unavailable", body); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + let request = CreateResourceRequest { + name: "resource".to_string(), + }; + + let err = api + .create_resource(&request) + .expect_err("HTTP 503 must not be returned as transport or success"); + assert_eq!(err.retry_after_header(), Some("120")); + + match err { + CreateResourceError::ServerError(api_error) => { + assert_eq!(api_error.status_code(), 503); + assert_eq!(api_error.raw_body(), body.as_bytes()); + assert_eq!( + api_error + .headers() + .get("retry-after") + .and_then(|value| value.to_str().ok()), + Some("120") + ); + assert_eq!( + api_error + .headers() + .get("x-trace-id") + .and_then(|value| value.to_str().ok()), + Some("typed-error-test") + ); + let typed_body = api_error + .body() + .expect("documented 5XX error body should decode"); + assert_eq!(typed_body.message, "temporarily unavailable"); + assert!(typed_body.retryable); + } + other => panic!("expected ServerError variant, got {other:?}"), + } +} + +#[test] +fn operation_error_converts_to_uniform_api_call_error() { + let body = r#"{"message":"temporarily unavailable","retryable":true}"#; + let base_url = spawn_one_response_server("503 Service Unavailable", body); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + let request = CreateResourceRequest { + name: "resource".to_string(), + }; + + let error: ApiCallError = api + .create_resource(&request) + .expect_err("HTTP 503 must be returned as an operation error") + .into(); + + assert_eq!(error.operation_id(), "createResource"); + assert_eq!(error.status_code(), Some(503)); + assert_eq!(error.raw_body(), Some(body.as_bytes())); + assert_eq!( + error + .headers() + .and_then(|headers| headers.get("retry-after")) + .and_then(|value| value.to_str().ok()), + Some("120") + ); +} + +#[test] +fn success_without_documented_body_does_not_drain_body() { + let base_url = spawn_incomplete_success_body_server(); + let client = Client::new(&base_url); + let api = ResourcesApi::new(&client); + + let response = api + .check_no_success_body() + .expect("success without a documented body must not read an incomplete body"); + + assert_eq!(response.status_code, 200); + assert_eq!(response.x_request_id_header(), Some("typed-response-test")); + assert_eq!( + response + .headers + .get("x-request-id") + .and_then(|value| value.to_str().ok()), + Some("typed-response-test") + ); +} diff --git a/tests/generated-clients/rust/ureq/tests/nested_nullable.rs b/tests/generated-clients/rust/ureq/tests/nested_nullable.rs new file mode 100644 index 000000000..fa112cef7 --- /dev/null +++ b/tests/generated-clients/rust/ureq/tests/nested_nullable.rs @@ -0,0 +1,14 @@ +use nested_nullable_api::apis::{GetValuesError, GetValuesResponse, ValuesApi}; +use nested_nullable_api::runtime::client::Client; + +#[path = "../../support/one_response_server.rs"] +mod server; + +#[path = "../../support/nested_nullable.rs"] +mod cases; + +async fn request(status: &str, body: &'static str) -> Result { + let url = server::spawn_one_response_server(status, body); + let client = Client::new(&url); + ValuesApi::new(&client).get_values() +} diff --git a/tests/golden/go/go-http/nested-nullable/README.md.golden b/tests/golden/go/go-http/nested-nullable/README.md.golden new file mode 100644 index 000000000..f862b4465 --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/README.md.golden @@ -0,0 +1,7 @@ +# Nested Nullable API + +Generated Go SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `nested-nullable-api`. diff --git a/tests/golden/go/go-http/nested-nullable/apis/values.go.golden b/tests/golden/go/go-http/nested-nullable/apis/values.go.golden new file mode 100644 index 000000000..9141625b3 --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/apis/values.go.golden @@ -0,0 +1,211 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package apis + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "example.com/sdk/models" + "example.com/sdk/runtime" +) + +// ValuesAPI groups operations under the corresponding tag. +type ValuesAPI struct { + client *runtime.Client +} + +// NewValuesAPI constructs a ValuesAPI bound to client. +func NewValuesAPI(client *runtime.Client) *ValuesAPI { + return &ValuesAPI{client: client} +} + +// GetValuesResponse carries the response from the corresponding operation. +type GetValuesResponse struct { + StatusCode int + Raw *http.Response + Status200 *models.NullablePayload +} + +type GetValuesError struct { + StatusCode int + Status string + Header http.Header + RawBody []byte + Detail GetValuesErrorDetail +} + +func (e *GetValuesError) Error() string { + return fmt.Sprintf("api error: %s", e.Status) +} + +type GetValuesErrorDetail interface { + isGetValuesErrorDetail() +} + +type GetValuesBadRequest struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*GetValuesBadRequest) isGetValuesErrorDetail() {} + +func (d *GetValuesBadRequest) Body() (map[string]*[]string, error) { + var value map[string]*[]string + if err := json.Unmarshal(d.RawBody, &value); err != nil { + return nil, err + } + return value, nil +} +type GetValuesUnauthorized struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*GetValuesUnauthorized) isGetValuesErrorDetail() {} + +func (d *GetValuesUnauthorized) Body() ([]*map[string]string, error) { + var value []*map[string]string + if err := json.Unmarshal(d.RawBody, &value); err != nil { + return nil, err + } + return value, nil +} +type GetValuesStatus402 struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*GetValuesStatus402) isGetValuesErrorDetail() {} + +func (d *GetValuesStatus402) Body() (*[]string, error) { + var value []string + if err := json.Unmarshal(d.RawBody, &value); err != nil { + return nil, err + } + return &value, nil +} +type GetValuesForbidden struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*GetValuesForbidden) isGetValuesErrorDetail() {} + +func (d *GetValuesForbidden) Body() (map[string][]string, error) { + var value map[string][]string + if err := json.Unmarshal(d.RawBody, &value); err != nil { + return nil, err + } + return value, nil +} +type GetValuesNotFound struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*GetValuesNotFound) isGetValuesErrorDetail() {} + +func (d *GetValuesNotFound) Body() (*models.DetailAlias, error) { + var value models.DetailAlias + if err := json.Unmarshal(d.RawBody, &value); err != nil { + return nil, err + } + return &value, nil +} +type GetValuesStatus410 struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*GetValuesStatus410) isGetValuesErrorDetail() {} + +func (d *GetValuesStatus410) Body() ([]models.DetailAlias, error) { + var value []models.DetailAlias + if err := json.Unmarshal(d.RawBody, &value); err != nil { + return nil, err + } + return value, nil +} +type GetValuesUnexpected struct { + StatusCode int + Header http.Header + RawBody []byte +} + +func (*GetValuesUnexpected) isGetValuesErrorDetail() {} + +func (d *GetValuesUnexpected) Body() []byte { + return append([]byte(nil), d.RawBody...) +} + +// GetValues calls GET /values. +func (a *ValuesAPI) GetValues(ctx context.Context) (*GetValuesResponse, error) { + path := "/values" + req, err := a.client.NewRequest(ctx, "GET", path, nil, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + httpResp, err := a.client.Do(req) + if err != nil { + return nil, err + } + defer httpResp.Body.Close() + resp := &GetValuesResponse{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 := &GetValuesBadRequest{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &GetValuesError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + case 401: + detail := &GetValuesUnauthorized{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &GetValuesError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + case 402: + detail := &GetValuesStatus402{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &GetValuesError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + case 403: + detail := &GetValuesForbidden{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &GetValuesError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + case 404: + detail := &GetValuesNotFound{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &GetValuesError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + case 410: + detail := &GetValuesStatus410{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &GetValuesError{StatusCode: httpResp.StatusCode, Status: httpResp.Status, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...), Detail: detail} + default: + detail := &GetValuesUnexpected{StatusCode: httpResp.StatusCode, Header: httpResp.Header.Clone(), RawBody: append([]byte(nil), responseBody...)} + return nil, &GetValuesError{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.NullablePayload + 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/nested-nullable/go.mod.golden b/tests/golden/go/go-http/nested-nullable/go.mod.golden new file mode 100644 index 000000000..54d0d116e --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/go.mod.golden @@ -0,0 +1,3 @@ +module example.com/sdk + +go 1.21 diff --git a/tests/golden/go/go-http/nested-nullable/models/detail.go.golden b/tests/golden/go/go-http/nested-nullable/models/detail.go.golden new file mode 100644 index 000000000..abd70589a --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/models/detail.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package models + +type Detail struct { + Message string `json:"message"` +} diff --git a/tests/golden/go/go-http/nested-nullable/models/detail_alias.go.golden b/tests/golden/go/go-http/nested-nullable/models/detail_alias.go.golden new file mode 100644 index 000000000..6d3963278 --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/models/detail_alias.go.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package models + +type DetailAlias = Detail + diff --git a/tests/golden/go/go-http/nested-nullable/models/matrix_container.go.golden b/tests/golden/go/go-http/nested-nullable/models/matrix_container.go.golden new file mode 100644 index 000000000..7f0563a93 --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/models/matrix_container.go.golden @@ -0,0 +1,9 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package models + +type MatrixContainer struct { + Matrix *[][]string `json:"matrix,omitempty"` +} diff --git a/tests/golden/go/go-http/nested-nullable/models/maybe_matrix.go.golden b/tests/golden/go/go-http/nested-nullable/models/maybe_matrix.go.golden new file mode 100644 index 000000000..b32bf59b1 --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/models/maybe_matrix.go.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package models + +type MaybeMatrix = *[][]string + diff --git a/tests/golden/go/go-http/nested-nullable/models/nullable_payload.go.golden b/tests/golden/go/go-http/nested-nullable/models/nullable_payload.go.golden new file mode 100644 index 000000000..15fe128bc --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/models/nullable_payload.go.golden @@ -0,0 +1,13 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package models + +type NullablePayload struct { + ArrayItems []*string `json:"arrayItems"` + ArrayMaps []*map[string]string `json:"arrayMaps"` + MapValues map[string]*[]string `json:"mapValues"` + NullableArray *[]string `json:"nullableArray,omitempty"` + StrictValues map[string][]string `json:"strictValues"` +} diff --git a/tests/golden/go/go-http/nested-nullable/runtime/auth.go.golden b/tests/golden/go/go-http/nested-nullable/runtime/auth.go.golden new file mode 100644 index 000000000..131f1a519 --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/runtime/auth.go.golden @@ -0,0 +1,112 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 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/nested-nullable/runtime/client.go.golden b/tests/golden/go/go-http/nested-nullable/runtime/client.go.golden new file mode 100644 index 000000000..5908a2ad1 --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/runtime/client.go.golden @@ -0,0 +1,98 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 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/nested-nullable/runtime/errors.go.golden b/tests/golden/go/go-http/nested-nullable/runtime/errors.go.golden new file mode 100644 index 000000000..92be703b9 --- /dev/null +++ b/tests/golden/go/go-http/nested-nullable/runtime/errors.go.golden @@ -0,0 +1,30 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 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/java/java-okhttp/nested-nullable/README.md.golden b/tests/golden/java/java-okhttp/nested-nullable/README.md.golden new file mode 100644 index 000000000..2a6a33be8 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/README.md.golden @@ -0,0 +1,7 @@ +# Nested Nullable API + +Generated Java SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `nested-nullable-api`. diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesBadRequest.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesBadRequest.java.golden new file mode 100644 index 000000000..8cd081680 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesBadRequest.java.golden @@ -0,0 +1,40 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public final class GetValuesBadRequest implements GetValuesExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public GetValuesBadRequest(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + + private String textBody() { + String contentTypeHeader = this.headers.get("Content-Type"); + okhttp3.MediaType contentType = contentTypeHeader != null ? okhttp3.MediaType.parse(contentTypeHeader) : null; + java.nio.charset.Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + return new String(this.rawBody, charset); + } + public Map> body() { + String text = textBody(); + return new Gson().fromJson(text.isEmpty() ? "null" : text, new TypeToken>>() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesException.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesException.java.golden new file mode 100644 index 000000000..f823d7115 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesException.java.golden @@ -0,0 +1,33 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import com.example.sdk.runtime.ApiException; +import okhttp3.Headers; + +public final class GetValuesException extends ApiException { + private final GetValuesExceptionDetail detail; + private final Headers headers; + private final byte[] rawBody; + + public GetValuesException(int statusCode, String status, String body, Headers headers, byte[] rawBody, GetValuesExceptionDetail detail) { + super(statusCode, status, body); + this.headers = headers; + this.rawBody = rawBody.clone(); + this.detail = detail; + } + + public GetValuesExceptionDetail detail() { + return this.detail; + } + + public Headers headers() { + return this.headers; + } + + public byte[] rawBody() { + return this.rawBody.clone(); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesExceptionDetail.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesExceptionDetail.java.golden new file mode 100644 index 000000000..52aa228a4 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesExceptionDetail.java.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +public sealed interface GetValuesExceptionDetail permits GetValuesBadRequest, GetValuesUnauthorized, GetValuesStatus402, GetValuesForbidden, GetValuesNotFound, GetValuesStatus410, GetValuesUnexpected { + int statusCode(); + okhttp3.Headers headers(); + byte[] rawBody(); +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesForbidden.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesForbidden.java.golden new file mode 100644 index 000000000..2e31d7f6b --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesForbidden.java.golden @@ -0,0 +1,40 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public final class GetValuesForbidden implements GetValuesExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public GetValuesForbidden(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + + private String textBody() { + String contentTypeHeader = this.headers.get("Content-Type"); + okhttp3.MediaType contentType = contentTypeHeader != null ? okhttp3.MediaType.parse(contentTypeHeader) : null; + java.nio.charset.Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + return new String(this.rawBody, charset); + } + public Map> body() { + String text = textBody(); + return new Gson().fromJson(text.isEmpty() ? "null" : text, new TypeToken>>() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesNotFound.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesNotFound.java.golden new file mode 100644 index 000000000..dc8260047 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesNotFound.java.golden @@ -0,0 +1,40 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public final class GetValuesNotFound implements GetValuesExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public GetValuesNotFound(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + + private String textBody() { + String contentTypeHeader = this.headers.get("Content-Type"); + okhttp3.MediaType contentType = contentTypeHeader != null ? okhttp3.MediaType.parse(contentTypeHeader) : null; + java.nio.charset.Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + return new String(this.rawBody, charset); + } + public DetailAlias body() { + String text = textBody(); + return new Gson().fromJson(text.isEmpty() ? "null" : text, new TypeToken() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesResponse.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesResponse.java.golden new file mode 100644 index 000000000..730aedb33 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesResponse.java.golden @@ -0,0 +1,38 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import okhttp3.Response; + +/** + * GetValuesResponse carries the response from getValues. + */ +public class GetValuesResponse { + private final int statusCode; + private final Response raw; + private final NullablePayload status200; + + public GetValuesResponse(int statusCode, Response raw, NullablePayload status200) { + this.statusCode = statusCode; + this.raw = raw; + this.status200 = status200; + } + + public int getStatusCode() { + return this.statusCode; + } + + public Response getRaw() { + return this.raw; + } + + public NullablePayload getStatus200() { + return this.status200; + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesStatus402.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesStatus402.java.golden new file mode 100644 index 000000000..fd071f779 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesStatus402.java.golden @@ -0,0 +1,40 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public final class GetValuesStatus402 implements GetValuesExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public GetValuesStatus402(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + + private String textBody() { + String contentTypeHeader = this.headers.get("Content-Type"); + okhttp3.MediaType contentType = contentTypeHeader != null ? okhttp3.MediaType.parse(contentTypeHeader) : null; + java.nio.charset.Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + return new String(this.rawBody, charset); + } + public List body() { + String text = textBody(); + return new Gson().fromJson(text.isEmpty() ? "null" : text, new TypeToken>() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesStatus410.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesStatus410.java.golden new file mode 100644 index 000000000..ff4466de3 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesStatus410.java.golden @@ -0,0 +1,40 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public final class GetValuesStatus410 implements GetValuesExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public GetValuesStatus410(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + + private String textBody() { + String contentTypeHeader = this.headers.get("Content-Type"); + okhttp3.MediaType contentType = contentTypeHeader != null ? okhttp3.MediaType.parse(contentTypeHeader) : null; + java.nio.charset.Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + return new String(this.rawBody, charset); + } + public List body() { + String text = textBody(); + return new Gson().fromJson(text.isEmpty() ? "null" : text, new TypeToken>() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesUnauthorized.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesUnauthorized.java.golden new file mode 100644 index 000000000..6c52cbe0d --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesUnauthorized.java.golden @@ -0,0 +1,40 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import com.example.sdk.models.*; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public final class GetValuesUnauthorized implements GetValuesExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public GetValuesUnauthorized(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + + private String textBody() { + String contentTypeHeader = this.headers.get("Content-Type"); + okhttp3.MediaType contentType = contentTypeHeader != null ? okhttp3.MediaType.parse(contentTypeHeader) : null; + java.nio.charset.Charset charset = contentType != null ? contentType.charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + return new String(this.rawBody, charset); + } + public List> body() { + String text = textBody(); + return new Gson().fromJson(text.isEmpty() ? "null" : text, new TypeToken>>() {}.getType()); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesUnexpected.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesUnexpected.java.golden new file mode 100644 index 000000000..6e99e05fe --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/GetValuesUnexpected.java.golden @@ -0,0 +1,22 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +public final class GetValuesUnexpected implements GetValuesExceptionDetail { + private final int statusCode; + private final okhttp3.Headers headers; + private final byte[] rawBody; + + public GetValuesUnexpected(int statusCode, okhttp3.Headers headers, byte[] rawBody) { + this.statusCode = statusCode; + this.headers = headers; + this.rawBody = rawBody.clone(); + } + + public int statusCode() { return this.statusCode; } + public okhttp3.Headers headers() { return this.headers; } + public byte[] rawBody() { return this.rawBody.clone(); } + public byte[] body() { return this.rawBody.clone(); } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/apis/ValuesApi.java.golden b/tests/golden/java/java-okhttp/nested-nullable/apis/ValuesApi.java.golden new file mode 100644 index 000000000..b13fbe891 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/apis/ValuesApi.java.golden @@ -0,0 +1,73 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.example.sdk.models.*; +import com.example.sdk.runtime.ApiClient; +import com.example.sdk.runtime.ApiException; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import okhttp3.Request; +import okhttp3.Response; + +/** + * ValuesApi groups operations under the Values tag. + */ +public class ValuesApi { + private final ApiClient client; + private final Gson gson = new Gson(); + + public ValuesApi(ApiClient client) { + this.client = client; + } + + /** + * getValues GET /values. + */ + public GetValuesResponse getValues() throws IOException { + String path = "/values"; + Request request = client.newRequest("GET", path, null, null); + Response response = client.execute(request); + + if (!response.isSuccessful()) { + okhttp3.ResponseBody responseBody = response.body(); + java.nio.charset.Charset responseCharset = responseBody != null && responseBody.contentType() != null ? responseBody.contentType().charset(StandardCharsets.UTF_8) : StandardCharsets.UTF_8; + byte[] responseBytes = responseBody != null ? responseBody.bytes() : new byte[0]; + String responseText = new String(responseBytes, responseCharset); + GetValuesExceptionDetail detail; + switch (response.code()) { + case 400: + detail = new GetValuesBadRequest(response.code(), response.headers(), responseBytes);break; + case 401: + detail = new GetValuesUnauthorized(response.code(), response.headers(), responseBytes);break; + case 402: + detail = new GetValuesStatus402(response.code(), response.headers(), responseBytes);break; + case 403: + detail = new GetValuesForbidden(response.code(), response.headers(), responseBytes);break; + case 404: + detail = new GetValuesNotFound(response.code(), response.headers(), responseBytes);break; + case 410: + detail = new GetValuesStatus410(response.code(), response.headers(), responseBytes);break; + default: + detail = new GetValuesUnexpected(response.code(), response.headers(), responseBytes); + } + throw new GetValuesException(response.code(), response.message(), responseText, response.headers(), responseBytes, detail); + } + byte[] responseBytes = response.body() != null ? response.body().bytes() : new byte[0]; + String responseText = new String(responseBytes, StandardCharsets.UTF_8); + NullablePayload status200 = null; + if (response.code() == 200) { + status200 = gson.fromJson(responseText.isEmpty() ? "null" : responseText, new TypeToken() {}.getType()); + } + return new GetValuesResponse(response.code(), response, status200); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/build.gradle.golden b/tests/golden/java/java-okhttp/nested-nullable/build.gradle.golden new file mode 100644 index 000000000..4b3a3bddf --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/build.gradle.golden @@ -0,0 +1,29 @@ +plugins { + id("java-library") +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +sourceSets { + main { + java { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/models/Detail.java.golden b/tests/golden/java/java-okhttp/nested-nullable/models/Detail.java.golden new file mode 100644 index 000000000..d4dbf6246 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/models/Detail.java.golden @@ -0,0 +1,17 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models; + +public class Detail { + private String message; + + public Detail(String message) { + this.message = message; + } + + public String getMessage() { + return this.message; + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/models/DetailAlias.java.golden b/tests/golden/java/java-okhttp/nested-nullable/models/DetailAlias.java.golden new file mode 100644 index 000000000..c6e5203e6 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/models/DetailAlias.java.golden @@ -0,0 +1,17 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models; + +public class DetailAlias { + private Detail value; + + public DetailAlias(Detail value) { + this.value = value; + } + + public Detail getValue() { + return this.value; + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/models/MatrixContainer.java.golden b/tests/golden/java/java-okhttp/nested-nullable/models/MatrixContainer.java.golden new file mode 100644 index 000000000..2ceb45507 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/models/MatrixContainer.java.golden @@ -0,0 +1,19 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models; + +import java.util.List; + +public class MatrixContainer { + private List> matrix; + + public MatrixContainer(List> matrix) { + this.matrix = matrix; + } + + public List> getMatrix() { + return this.matrix; + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/models/MaybeMatrix.java.golden b/tests/golden/java/java-okhttp/nested-nullable/models/MaybeMatrix.java.golden new file mode 100644 index 000000000..f1ccefd62 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/models/MaybeMatrix.java.golden @@ -0,0 +1,19 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models; + +import java.util.List; + +public class MaybeMatrix { + private List> value; + + public MaybeMatrix(List> value) { + this.value = value; + } + + public List> getValue() { + return this.value; + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/models/NullablePayload.java.golden b/tests/golden/java/java-okhttp/nested-nullable/models/NullablePayload.java.golden new file mode 100644 index 000000000..829d7ac9a --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/models/NullablePayload.java.golden @@ -0,0 +1,46 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models; + +import java.util.List; +import java.util.Map; + +public class NullablePayload { + private List arrayItems; + private List> arrayMaps; + private Map> mapValues; + private List nullableArray; + private Map> strictValues; + + public NullablePayload(List arrayItems, List> arrayMaps, + Map> mapValues, List nullableArray, + Map> strictValues) { + this.arrayItems = arrayItems; + this.arrayMaps = arrayMaps; + this.mapValues = mapValues; + this.nullableArray = nullableArray; + this.strictValues = strictValues; + } + + public List getArrayItems() { + return this.arrayItems; + } + + public List> getArrayMaps() { + return this.arrayMaps; + } + + public Map> getMapValues() { + return this.mapValues; + } + + public List getNullableArray() { + return this.nullableArray; + } + + public Map> getStrictValues() { + return this.strictValues; + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiClient.java.golden b/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiClient.java.golden new file mode 100644 index 000000000..ee0da46d8 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiClient.java.golden @@ -0,0 +1,78 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime; + +import java.io.IOException; +import java.util.Map; + +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class ApiClient { + private static final MediaType JSON = MediaType.get("application/json"); + + private final String baseUrl; + private final OkHttpClient client; + private final Authenticator authenticator; + private final Map defaultHeaders; + + public ApiClient(String baseUrl) { + this(baseUrl, new OkHttpClient(), null, Map.of()); + } + + public ApiClient(String baseUrl, OkHttpClient client, Authenticator authenticator, Map defaultHeaders) { + this.baseUrl = baseUrl; + this.client = client; + this.authenticator = authenticator; + this.defaultHeaders = defaultHeaders; + } + + public Request newRequest(String method, String path, Map query, String body) { + RequestBody requestBody = null; + if (body != null) { + requestBody = RequestBody.create(body, JSON); + } + return buildRequest(method, path, query, requestBody); + } + + public Request newRequestWithBody(String method, String path, Map query, RequestBody body) { + return buildRequest(method, path, query, body); + } + + private Request buildRequest(String method, String path, Map query, RequestBody requestBody) { + String url = baseUrl.replaceAll("/+$", "") + path; + HttpUrl parsed = HttpUrl.parse(url); + if (parsed == null) { + throw new IllegalArgumentException("Invalid URL: " + url); + } + HttpUrl.Builder urlBuilder = parsed.newBuilder(); + if (query != null) { + for (Map.Entry entry : query.entrySet()) { + urlBuilder.addQueryParameter(entry.getKey(), entry.getValue()); + } + } + + Request.Builder builder = new Request.Builder() + .url(urlBuilder.build()) + .method(method, requestBody); + + for (Map.Entry entry : defaultHeaders.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } + if (authenticator != null) { + authenticator.authenticate(builder); + } + + return builder.build(); + } + + public Response execute(Request request) throws IOException { + return client.newCall(request).execute(); + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiException.java.golden b/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiException.java.golden new file mode 100644 index 000000000..815e23329 --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiException.java.golden @@ -0,0 +1,30 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime; + +public class ApiException extends RuntimeException { + private final int statusCode; + private final String status; + private final String body; + + public ApiException(int statusCode, String status, String body) { + super("API error " + statusCode + ": " + status); + this.statusCode = statusCode; + this.status = status; + this.body = body; + } + + public int getStatusCode() { + return statusCode; + } + + public String getStatus() { + return status; + } + + public String getBody() { + return body; + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiKeyAuth.java.golden b/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiKeyAuth.java.golden new file mode 100644 index 000000000..edc54dcdc --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiKeyAuth.java.golden @@ -0,0 +1,41 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime; + +import java.util.function.Supplier; +import okhttp3.Request; + +public class ApiKeyAuth implements Authenticator { + private final String key; + private final Supplier keyProvider; + private final String name; + private final ApiKeyLocation location; + + public ApiKeyAuth(String key, String name, ApiKeyLocation location) { + this.key = key; + this.keyProvider = null; + this.name = name; + this.location = location; + } + + public ApiKeyAuth(Supplier keyProvider, String name, ApiKeyLocation location) { + this.key = null; + this.keyProvider = keyProvider; + this.name = name; + this.location = location; + } + + @Override + public void authenticate(Request.Builder builder) { + String k = keyProvider != null ? keyProvider.get() : key; + if (location == ApiKeyLocation.HEADER) { + builder.header(name, k); + } else if (location == ApiKeyLocation.QUERY) { + builder.url(builder.build().url().newBuilder() + .addQueryParameter(name, k) + .build()); + } + } +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiKeyLocation.java.golden b/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiKeyLocation.java.golden new file mode 100644 index 000000000..07bebbcfd --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/runtime/ApiKeyLocation.java.golden @@ -0,0 +1,10 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime; + +public enum ApiKeyLocation { + HEADER, + QUERY +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/runtime/Authenticator.java.golden b/tests/golden/java/java-okhttp/nested-nullable/runtime/Authenticator.java.golden new file mode 100644 index 000000000..93cb3ac4f --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/runtime/Authenticator.java.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime; + +import okhttp3.Request; + +public interface Authenticator { + void authenticate(Request.Builder builder); +} diff --git a/tests/golden/java/java-okhttp/nested-nullable/runtime/BearerAuth.java.golden b/tests/golden/java/java-okhttp/nested-nullable/runtime/BearerAuth.java.golden new file mode 100644 index 000000000..2d79f7c5a --- /dev/null +++ b/tests/golden/java/java-okhttp/nested-nullable/runtime/BearerAuth.java.golden @@ -0,0 +1,29 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime; + +import java.util.function.Supplier; +import okhttp3.Request; + +public class BearerAuth implements Authenticator { + private final String token; + private final Supplier tokenProvider; + + public BearerAuth(String token) { + this.token = token; + this.tokenProvider = null; + } + + public BearerAuth(Supplier tokenProvider) { + this.token = null; + this.tokenProvider = tokenProvider; + } + + @Override + public void authenticate(Request.Builder builder) { + String t = tokenProvider != null ? tokenProvider.get() : token; + builder.header("Authorization", "Bearer " + t); + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/README.md.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/README.md.golden new file mode 100644 index 000000000..c2a7072b7 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/README.md.golden @@ -0,0 +1,7 @@ +# Nested Nullable API + +Generated Kotlin SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `nested-nullable-api`. diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/apis/ValuesApi.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/apis/ValuesApi.kt.golden new file mode 100644 index 000000000..cad24832f --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/apis/ValuesApi.kt.golden @@ -0,0 +1,176 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.apis + +import com.example.sdk.models.* +import com.example.sdk.runtime.ApiClient +import com.example.sdk.runtime.ApiException +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Response + +/** + * GetValuesResponse carries the response from getValues. + */ +data class GetValuesResponse(val statusCode: Int, val raw: Response, + val status200: NullablePayload? = null) { +} + +sealed interface GetValuesExceptionDetail { + val statusCode: Int + val headers: okhttp3.Headers + fun rawBody(): ByteArray +} + +class GetValuesException( + val detail: GetValuesExceptionDetail, + statusCode: Int, + status: String, + body: String, + val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : ApiException(statusCode, status, body) { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() +} + +class GetValuesBadRequest( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetValuesExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): Map?> = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken?>>() {}.type) +} + +class GetValuesUnauthorized( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetValuesExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): List?> = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken?>>() {}.type) +} + +class GetValuesStatus402( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetValuesExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): List? = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken?>() {}.type) +} + +class GetValuesForbidden( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetValuesExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): Map> = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken>>() {}.type) +} + +class GetValuesNotFound( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetValuesExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): DetailAlias = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken() {}.type) +} + +class GetValuesStatus410( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetValuesExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + private fun textBody(): String { + val charset = headers["Content-Type"]?.toMediaTypeOrNull()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + return rawBodyBytes.toString(charset) + } + fun body(): List = Gson().fromJson(textBody().ifEmpty { "null" }, object : TypeToken>() {}.type) +} + +class GetValuesUnexpected( + override val statusCode: Int, + override val headers: okhttp3.Headers, + rawBodyBytes: ByteArray, +) : GetValuesExceptionDetail { + private val rawBodyBytes: ByteArray = rawBodyBytes.copyOf() + override fun rawBody(): ByteArray = this.rawBodyBytes.copyOf() + fun body(): ByteArray = rawBody() +} + +/** + * ValuesApi groups operations under the Values tag. + */ +class ValuesApi(private val client: ApiClient) { + private val gson: Gson = Gson() + + /** + * getValues GET /values. + */ + fun getValues(): GetValuesResponse { + val path = "/values" + val request = client.newRequest("GET", path, null, null) + val response = client.execute(request) + + if (!response.isSuccessful) { + val responseBody = response.body + val responseCharset = responseBody?.contentType()?.charset(Charsets.UTF_8) ?: Charsets.UTF_8 + val responseBytes = responseBody?.bytes() ?: ByteArray(0) + val responseText = responseBytes.toString(responseCharset) + val detail: GetValuesExceptionDetail = when (response.code) { + 400 -> GetValuesBadRequest(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + 401 -> GetValuesUnauthorized(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + 402 -> GetValuesStatus402(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + 403 -> GetValuesForbidden(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + 404 -> GetValuesNotFound(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + 410 -> GetValuesStatus410(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + else -> { + GetValuesUnexpected(statusCode = response.code, headers = response.headers, rawBodyBytes = responseBytes) + } + } + throw GetValuesException(detail = detail, statusCode = response.code, status = response.message, body = responseText, headers = response.headers, rawBodyBytes = responseBytes) + } + + val responseBytes = response.body?.bytes() ?: ByteArray(0) + val responseText = responseBytes.toString(Charsets.UTF_8) + var status200: NullablePayload? = null + + if (response.code == 200) { + status200 = gson.fromJson(responseText.ifEmpty { "null" }, object : TypeToken() {}.type) + } + return GetValuesResponse(statusCode = response.code, raw = response, status200 = status200) + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/build.gradle.kts.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/build.gradle.kts.golden new file mode 100644 index 000000000..093728ca0 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/build.gradle.kts.golden @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") version "2.2.21" +} + +group = "com.example.sdk" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") +} + +kotlin { + jvmToolchain(17) +} + +sourceSets { + main { + kotlin { + srcDirs("apis", "models", "runtime") + } + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/Detail.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/Detail.kt.golden new file mode 100644 index 000000000..78297384b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/Detail.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models + +data class Detail(val message: String) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/DetailAlias.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/DetailAlias.kt.golden new file mode 100644 index 000000000..9d11c879b --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/DetailAlias.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models + +typealias DetailAlias = Detail diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/MatrixContainer.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/MatrixContainer.kt.golden new file mode 100644 index 000000000..e5153e05a --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/MatrixContainer.kt.golden @@ -0,0 +1,8 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models + +data class MatrixContainer(val matrix: List>? = null) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/MaybeMatrix.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/MaybeMatrix.kt.golden new file mode 100644 index 000000000..c47a055a8 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/MaybeMatrix.kt.golden @@ -0,0 +1,7 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models + +typealias MaybeMatrix = List>? diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/NullablePayload.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/NullablePayload.kt.golden new file mode 100644 index 000000000..a4d893d24 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/models/NullablePayload.kt.golden @@ -0,0 +1,10 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.models + +data class NullablePayload(val arrayItems: List, val arrayMaps: List?>, + val mapValues: Map?>, val nullableArray: List? = null, + val strictValues: Map>) { +} diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/ApiClient.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/ApiClient.kt.golden new file mode 100644 index 000000000..94074d264 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/ApiClient.kt.golden @@ -0,0 +1,61 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response + +class ApiClient( + private val baseUrl: String, + private val client: OkHttpClient = OkHttpClient(), + private val authenticator: Authenticator? = null, + private val defaultHeaders: Map = emptyMap(), +) { + fun newRequest( + method: String, + path: String, + query: Map? = null, + body: String? = null, + ): Request { + return buildRequest(method, path, query, body?.toRequestBody("application/json".toMediaType())) + } + + fun newRequestWithBody( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + return buildRequest(method, path, query, body) + } + + private fun buildRequest( + method: String, + path: String, + query: Map? = null, + body: okhttp3.RequestBody?, + ): Request { + val urlBuilder: HttpUrl.Builder = (baseUrl.trimEnd('/') + path).toHttpUrl().newBuilder() + query?.forEach { (k, v) -> urlBuilder.addQueryParameter(k, v) } + + val builder = Request.Builder() + .url(urlBuilder.build()) + .method(method, body) + + defaultHeaders.forEach { (k, v) -> builder.header(k, v) } + authenticator?.authenticate(builder) + + return builder.build() + } + + fun execute(request: Request): Response { + return client.newCall(request).execute() + } +} diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/ApiException.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/ApiException.kt.golden new file mode 100644 index 000000000..1c161505d --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/ApiException.kt.golden @@ -0,0 +1,11 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime + +open class ApiException( + val statusCode: Int, + val status: String, + val body: String, +) : RuntimeException("API error $statusCode: $status") diff --git a/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/Auth.kt.golden b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/Auth.kt.golden new file mode 100644 index 000000000..840a69287 --- /dev/null +++ b/tests/golden/kotlin/kotlin-okhttp/nested-nullable/runtime/Auth.kt.golden @@ -0,0 +1,70 @@ +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +package com.example.sdk.runtime + +import okhttp3.Request + +interface Authenticator { + fun authenticate(builder: Request.Builder) +} + +class BearerAuth : Authenticator { + private val token: String? + private val tokenProvider: (() -> String)? + + constructor(token: String) { + this.token = token + this.tokenProvider = null + } + + constructor(tokenProvider: () -> String) { + this.token = null + this.tokenProvider = tokenProvider + } + + override fun authenticate(builder: Request.Builder) { + val t = tokenProvider?.invoke() ?: token!! + builder.header("Authorization", "Bearer $t") + } +} + +class ApiKeyAuth : Authenticator { + private val key: String? + private val keyProvider: (() -> String)? + private val name: String + private val location: ApiKeyLocation + + constructor(key: String, name: String, location: ApiKeyLocation) { + this.key = key + this.keyProvider = null + this.name = name + this.location = location + } + + constructor(keyProvider: () -> String, name: String, location: ApiKeyLocation) { + this.key = null + this.keyProvider = keyProvider + this.name = name + this.location = location + } + + override fun authenticate(builder: Request.Builder) { + val k = keyProvider?.invoke() ?: key!! + when (location) { + ApiKeyLocation.HEADER -> builder.header(name, k) + ApiKeyLocation.QUERY -> { + val url = builder.build().url.newBuilder() + .addQueryParameter(name, k) + .build() + builder.url(url) + } + } + } +} + +enum class ApiKeyLocation { + HEADER, + QUERY, +} diff --git a/tests/golden/python/python-httpx/nested-nullable/README.md.golden b/tests/golden/python/python-httpx/nested-nullable/README.md.golden new file mode 100644 index 000000000..9ea0941f9 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/README.md.golden @@ -0,0 +1,7 @@ +# Nested Nullable API + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `nested_nullable_api`. diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/__init__.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/__init__.py.golden new file mode 100644 index 000000000..4e28d09b0 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/apis/__init__.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/apis/__init__.py.golden new file mode 100644 index 000000000..575f9b76c --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from .values_api import ValuesApi as ValuesApi diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/apis/values_api.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/apis/values_api.py.golden new file mode 100644 index 000000000..f0e338b9f --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/apis/values_api.py.golden @@ -0,0 +1,200 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping + +from ..models.detail_alias import DetailAlias +from ..models.nullable_payload import NullablePayload +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class GetValuesBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: dict[str, list[str] | None] | None = None + self._body_error: Exception | None = None + @property + def body(self) -> dict[str, list[str] | None]: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesUnauthorized: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: list[dict[str, str] | None] | None = None + self._body_error: Exception | None = None + @property + def body(self) -> list[dict[str, str] | None]: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesStatus402: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: list[str] | None | None = None + self._body_error: Exception | None = None + @property + def body(self) -> list[str] | None: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesForbidden: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: dict[str, list[str]] | None = None + self._body_error: Exception | None = None + @property + def body(self) -> dict[str, list[str]]: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesNotFound: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: DetailAlias | None = None + self._body_error: Exception | None = None + @property + def body(self) -> DetailAlias: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesStatus410: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: list[DetailAlias] | None = None + self._body_error: Exception | None = None + @property + def body(self) -> list[DetailAlias]: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type GetValuesErrorDetail = ( + GetValuesBadRequest + | GetValuesUnauthorized + | GetValuesStatus402 + | GetValuesForbidden + | GetValuesNotFound + | GetValuesStatus410 + | GetValuesUnexpected +) + +class GetValuesError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: GetValuesErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: GetValuesErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class ValuesApi: + def __init__(self, client: Client) -> None: + self._client = client + + def get_values_with_http_info(self) -> ApiResponse[NullablePayload]: + path = "/values" + response = self._client.request("GET", path) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = GetValuesBadRequest(response.status_code, response.headers, response.content) + elif response.status_code == 401: + detail = GetValuesUnauthorized(response.status_code, response.headers, response.content) + elif response.status_code == 402: + detail = GetValuesStatus402(response.status_code, response.headers, response.content) + elif response.status_code == 403: + detail = GetValuesForbidden(response.status_code, response.headers, response.content) + elif response.status_code == 404: + detail = GetValuesNotFound(response.status_code, response.headers, response.content) + elif response.status_code == 410: + detail = GetValuesStatus410(response.status_code, response.headers, response.content) + else: + detail = GetValuesUnexpected(response.status_code, response.headers, response.content) + raise GetValuesError(response.status_code, response.reason_phrase, response.content, detail, headers=response.headers, response=response) + data = NullablePayload.from_dict(response.json()) + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def get_values(self) -> NullablePayload: + return self.get_values_with_http_info().data diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/__init__.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/__init__.py.golden new file mode 100644 index 000000000..ef0b5b072 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/__init__.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from .detail import Detail as Detail +from .detail_alias import DetailAlias as DetailAlias +from .matrix_container import MatrixContainer as MatrixContainer +from .maybe_matrix import MaybeMatrix as MaybeMatrix +from .nullable_payload import NullablePayload as NullablePayload diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/detail.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/detail.py.golden new file mode 100644 index 000000000..a383192db --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/detail.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class Detail: + message: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["message"] = self.message + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Detail: + return cls( + message=data["message"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/detail_alias.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/detail_alias.py.golden new file mode 100644 index 000000000..60267ea3d --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/detail_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +from .detail import Detail + +type DetailAlias = (Detail) diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/matrix_container.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/matrix_container.py.golden new file mode 100644 index 000000000..894a57f87 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/matrix_container.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class MatrixContainer: + matrix: list[list[str]] | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.matrix is not None: + result["matrix"] = self.matrix + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> MatrixContainer: + return cls( + matrix=data.get("matrix"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/maybe_matrix.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/maybe_matrix.py.golden new file mode 100644 index 000000000..3ac72707c --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/maybe_matrix.py.golden @@ -0,0 +1,7 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +type MaybeMatrix = (list[list[str]] | None) diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/nullable_payload.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/nullable_payload.py.golden new file mode 100644 index 000000000..32bfce308 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/models/nullable_payload.py.golden @@ -0,0 +1,35 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NullablePayload: + array_items: list[str | None] + array_maps: list[dict[str, str] | None] + map_values: dict[str, list[str] | None] + strict_values: dict[str, list[str]] + nullable_array: list[str] | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["arrayItems"] = self.array_items + result["arrayMaps"] = self.array_maps + result["mapValues"] = self.map_values + result["strictValues"] = self.strict_values + if self.nullable_array is not None: + result["nullableArray"] = self.nullable_array + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullablePayload: + return cls( + array_items=data["arrayItems"], # type: ignore[assignment] + array_maps=data["arrayMaps"], # type: ignore[assignment] + map_values=data["mapValues"], # type: ignore[assignment] + strict_values=data["strictValues"], # type: ignore[assignment] + nullable_array=data.get("nullableArray"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/py.typed.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/__init__.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/__init__.py.golden new file mode 100644 index 000000000..9995d4081 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/auth.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/auth.py.golden new file mode 100644 index 000000000..a1c4c12f8 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/client.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/client.py.golden new file mode 100644 index 000000000..2c0fbf5ce --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/client.py.golden @@ -0,0 +1,84 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +"""HTTP client wrapping httpx.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +import httpx + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: httpx.Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: httpx.Client | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or httpx.Client() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + content: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> httpx.Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + return self._http_client.request( + method, + url, + params=params, + json=json, + content=content, + data=data, + files=files, + headers=req_headers, + ) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/errors.py.golden b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/errors.py.golden new file mode 100644 index 000000000..686ddfc44 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/nested_nullable_api/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-httpx/nested-nullable/pyproject.toml.golden b/tests/golden/python/python-httpx/nested-nullable/pyproject.toml.golden new file mode 100644 index 000000000..afda41074 --- /dev/null +++ b/tests/golden/python/python-httpx/nested-nullable/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "nested_nullable_api" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["httpx>=0.27"] diff --git a/tests/golden/python/python-requests/nested-nullable/README.md.golden b/tests/golden/python/python-requests/nested-nullable/README.md.golden new file mode 100644 index 000000000..9ea0941f9 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/README.md.golden @@ -0,0 +1,7 @@ +# Nested Nullable API + +Generated Python SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `nested_nullable_api`. diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/__init__.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/__init__.py.golden new file mode 100644 index 000000000..4e28d09b0 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from .runtime import ApiKeyAuth as ApiKeyAuth +from .runtime import Authenticator as Authenticator +from .runtime import BearerAuth as BearerAuth +from .runtime import ApiResponse as ApiResponse +from .runtime import Client as Client +from .runtime import ApiError as ApiError diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/apis/__init__.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/apis/__init__.py.golden new file mode 100644 index 000000000..575f9b76c --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/apis/__init__.py.golden @@ -0,0 +1,5 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from .values_api import ValuesApi as ValuesApi diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/apis/values_api.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/apis/values_api.py.golden new file mode 100644 index 000000000..4a6836332 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/apis/values_api.py.golden @@ -0,0 +1,200 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +import json + +from collections.abc import Mapping + +from ..models.detail_alias import DetailAlias +from ..models.nullable_payload import NullablePayload +from ..runtime.client import ApiResponse, Client +from ..runtime.errors import ApiError + +class GetValuesBadRequest: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: dict[str, list[str] | None] | None = None + self._body_error: Exception | None = None + @property + def body(self) -> dict[str, list[str] | None]: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesUnauthorized: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: list[dict[str, str] | None] | None = None + self._body_error: Exception | None = None + @property + def body(self) -> list[dict[str, str] | None]: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesStatus402: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: list[str] | None | None = None + self._body_error: Exception | None = None + @property + def body(self) -> list[str] | None: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesForbidden: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: dict[str, list[str]] | None = None + self._body_error: Exception | None = None + @property + def body(self) -> dict[str, list[str]]: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesNotFound: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: DetailAlias | None = None + self._body_error: Exception | None = None + @property + def body(self) -> DetailAlias: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesStatus410: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + self._body_loaded: bool = False + self._body_value: list[DetailAlias] | None = None + self._body_error: Exception | None = None + @property + def body(self) -> list[DetailAlias]: + if not self._body_loaded: + try: + self._body_value = json.loads(self.raw_body.decode("utf-8")) # type: ignore[return-value] + except Exception as exc: + self._body_error = exc + self._body_loaded = True + if self._body_error is not None: + raise self._body_error + if self._body_value is None: + raise RuntimeError("error body was not decoded") + return self._body_value + +class GetValuesUnexpected: + def __init__(self, status_code: int, headers: Mapping[str, str], raw_body: bytes) -> None: + self.status_code: int = status_code + self.headers: Mapping[str, str] = headers + self.raw_body: bytes = raw_body + @property + def body(self) -> bytes: + return self.raw_body + +type GetValuesErrorDetail = ( + GetValuesBadRequest + | GetValuesUnauthorized + | GetValuesStatus402 + | GetValuesForbidden + | GetValuesNotFound + | GetValuesStatus410 + | GetValuesUnexpected +) + +class GetValuesError(ApiError): + def __init__(self, status_code: int, status: str, body: bytes, detail: GetValuesErrorDetail, headers: Mapping[str, str] | None = None, response: object | None = None) -> None: + self.detail: GetValuesErrorDetail = detail + super().__init__(status_code, status, body, headers=headers, response=response) + + +class ValuesApi: + def __init__(self, client: Client) -> None: + self._client = client + + def get_values_with_http_info(self) -> ApiResponse[NullablePayload]: + path = "/values" + response = self._client.request("GET", path) + if not (200 <= response.status_code < 300): + if response.status_code == 400: + detail = GetValuesBadRequest(response.status_code, response.headers, response.content) + elif response.status_code == 401: + detail = GetValuesUnauthorized(response.status_code, response.headers, response.content) + elif response.status_code == 402: + detail = GetValuesStatus402(response.status_code, response.headers, response.content) + elif response.status_code == 403: + detail = GetValuesForbidden(response.status_code, response.headers, response.content) + elif response.status_code == 404: + detail = GetValuesNotFound(response.status_code, response.headers, response.content) + elif response.status_code == 410: + detail = GetValuesStatus410(response.status_code, response.headers, response.content) + else: + detail = GetValuesUnexpected(response.status_code, response.headers, response.content) + raise GetValuesError(response.status_code, response.reason, response.content, detail, headers=response.headers, response=response) + data = NullablePayload.from_dict(response.json()) + return ApiResponse(data=data, status_code=response.status_code, headers=response.headers, raw=response) + + def get_values(self) -> NullablePayload: + return self.get_values_with_http_info().data diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/__init__.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/__init__.py.golden new file mode 100644 index 000000000..ef0b5b072 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/__init__.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from .detail import Detail as Detail +from .detail_alias import DetailAlias as DetailAlias +from .matrix_container import MatrixContainer as MatrixContainer +from .maybe_matrix import MaybeMatrix as MaybeMatrix +from .nullable_payload import NullablePayload as NullablePayload diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/detail.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/detail.py.golden new file mode 100644 index 000000000..a383192db --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/detail.py.golden @@ -0,0 +1,22 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class Detail: + message: str + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["message"] = self.message + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> Detail: + return cls( + message=data["message"], # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/detail_alias.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/detail_alias.py.golden new file mode 100644 index 000000000..60267ea3d --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/detail_alias.py.golden @@ -0,0 +1,9 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +from .detail import Detail + +type DetailAlias = (Detail) diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/matrix_container.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/matrix_container.py.golden new file mode 100644 index 000000000..894a57f87 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/matrix_container.py.golden @@ -0,0 +1,23 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class MatrixContainer: + matrix: list[list[str]] | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + if self.matrix is not None: + result["matrix"] = self.matrix + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> MatrixContainer: + return cls( + matrix=data.get("matrix"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/maybe_matrix.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/maybe_matrix.py.golden new file mode 100644 index 000000000..3ac72707c --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/maybe_matrix.py.golden @@ -0,0 +1,7 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +type MaybeMatrix = (list[list[str]] | None) diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/nullable_payload.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/nullable_payload.py.golden new file mode 100644 index 000000000..32bfce308 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/models/nullable_payload.py.golden @@ -0,0 +1,35 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from __future__ import annotations + +from dataclasses import dataclass + +@dataclass +class NullablePayload: + array_items: list[str | None] + array_maps: list[dict[str, str] | None] + map_values: dict[str, list[str] | None] + strict_values: dict[str, list[str]] + nullable_array: list[str] | None = None + + def to_dict(self) -> dict[str, object]: + result: dict[str, object] = {} + result["arrayItems"] = self.array_items + result["arrayMaps"] = self.array_maps + result["mapValues"] = self.map_values + result["strictValues"] = self.strict_values + if self.nullable_array is not None: + result["nullableArray"] = self.nullable_array + return result + + @classmethod + def from_dict(cls, data: dict[str, object]) -> NullablePayload: + return cls( + array_items=data["arrayItems"], # type: ignore[assignment] + array_maps=data["arrayMaps"], # type: ignore[assignment] + map_values=data["mapValues"], # type: ignore[assignment] + strict_values=data["strictValues"], # type: ignore[assignment] + nullable_array=data.get("nullableArray"), # type: ignore[assignment] + ) diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/py.typed.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/py.typed.golden new file mode 100644 index 000000000..e69de29bb diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/__init__.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/__init__.py.golden new file mode 100644 index 000000000..9995d4081 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/__init__.py.golden @@ -0,0 +1,10 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +from .auth import ApiKeyAuth as ApiKeyAuth +from .auth import Authenticator as Authenticator +from .auth import BearerAuth as BearerAuth +from .client import ApiResponse as ApiResponse +from .client import Client as Client +from .errors import ApiError as ApiError diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/auth.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/auth.py.golden new file mode 100644 index 000000000..a1c4c12f8 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/auth.py.golden @@ -0,0 +1,50 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +"""Authentication helpers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable + + +class Authenticator(ABC): + """Base class for request authenticators.""" + + @abstractmethod + def auth_headers(self) -> dict[str, str]: + """Return headers to attach to every request.""" + ... + + +class BearerAuth(Authenticator): + """Bearer token authentication. + + Accepts a static token string or a callable that returns the + current token (evaluated on every request). + """ + + def __init__(self, token: str | Callable[[], str]) -> None: + self._token = token + + def auth_headers(self) -> dict[str, str]: + token = self._token() if callable(self._token) else self._token + return {"Authorization": f"Bearer {token}"} + + +class ApiKeyAuth(Authenticator): + """API key authentication via a custom header. + + Accepts a static key string or a callable that returns the + current key (evaluated on every request). + """ + + def __init__(self, header_name: str, api_key: str | Callable[[], str]) -> None: + self._header_name = header_name + self._api_key = api_key + + def auth_headers(self) -> dict[str, str]: + key = self._api_key() if callable(self._api_key) else self._api_key + return {self._header_name: key} diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/client.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/client.py.golden new file mode 100644 index 000000000..2a4ed3a04 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/client.py.golden @@ -0,0 +1,116 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +"""HTTP client wrapping requests.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Generic, TypeVar, cast + +import requests + +from .auth import Authenticator + +ResponseData = TypeVar("ResponseData") + + +class Response: + """Thin wrapper over requests.Response with strict types.""" + + def __init__(self, raw: requests.Response) -> None: + self._raw = raw + + @property + def status_code(self) -> int: + return self._raw.status_code # type: ignore[return-value] + + @property + def reason(self) -> str: + return self._raw.reason or "" + + @property + def content(self) -> bytes: + return self._raw.content # type: ignore[return-value] + + @property + def headers(self) -> Mapping[str, str]: + return cast(Mapping[str, str], self._raw.headers) + + @property + def text(self) -> str: + return self._raw.text + + def json(self) -> Any: # type: ignore[explicit-override] + return self._raw.json() # type: ignore[reportUnknownMemberType] + + +@dataclass(frozen=True) +class ApiResponse(Generic[ResponseData]): + """Decoded response data together with its HTTP metadata.""" + + data: ResponseData + status_code: int + headers: Mapping[str, str] + raw: Response + + +class Client: + """Synchronous HTTP client for the generated SDK.""" + + def __init__( + self, + base_url: str, + *, + http_client: requests.Session | None = None, + authenticator: Authenticator | None = None, + ) -> None: + self._base_url = base_url.rstrip("/") + self._http_client = http_client or requests.Session() + self._authenticator = authenticator + + def request( + self, + method: str, + path: str, + *, + params: dict[str, str] | None = None, + json: Any = None, + data: Any = None, + files: Any = None, + headers: dict[str, str] | None = None, + ) -> Response: + """Send an HTTP request and return the raw response.""" + url = f"{self._base_url}{path}" + req_headers: dict[str, str] = {"Accept": "application/json"} + if json is not None: + req_headers["Content-Type"] = "application/json" + if headers: + req_headers.update(headers) + if self._authenticator is not None: + req_headers.update(self._authenticator.auth_headers()) + raw = cast( + requests.Response, + self._http_client.request( # type: ignore[reportUnknownMemberType] + method, + url, + params=params, + json=json, + data=data, + files=files, + headers=req_headers, + ), + ) + return Response(raw) + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._http_client.close() + + def __enter__(self) -> Client: + return self + + def __exit__(self, *args: object) -> None: + self.close() diff --git a/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/errors.py.golden b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/errors.py.golden new file mode 100644 index 000000000..686ddfc44 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/nested_nullable_api/runtime/errors.py.golden @@ -0,0 +1,34 @@ +# Code generated by openapi-nexus. DO NOT EDIT. +# +# Nested Nullable API — 1.0.0 + +"""API error type.""" + +from __future__ import annotations + +from collections.abc import Mapping + + +class ApiError(Exception): + """Raised when the server returns a non-success status code.""" + + def __init__( + self, + status_code: int, + status: str, + body: bytes, + *, + headers: Mapping[str, str] | None = None, + response: object | None = None, + ) -> None: + self.status_code: int = status_code + self.status: str = status + self.raw_body: bytes = body + self.body: bytes = body + self.headers: Mapping[str, str] | None = headers + self.response: object | None = response + super().__init__(f"{status_code} {status}") + + @property + def text(self) -> str: + return self.raw_body.decode("utf-8", errors="replace") diff --git a/tests/golden/python/python-requests/nested-nullable/pyproject.toml.golden b/tests/golden/python/python-requests/nested-nullable/pyproject.toml.golden new file mode 100644 index 000000000..0272e6898 --- /dev/null +++ b/tests/golden/python/python-requests/nested-nullable/pyproject.toml.golden @@ -0,0 +1,10 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "nested_nullable_api" +version = "1.0.0" +description = "Generated Python SDK." +requires-python = ">=3.12" +dependencies = ["requests>=2.32"] diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/Cargo.toml.golden b/tests/golden/rust/rust-aioduct/nested-nullable/Cargo.toml.golden new file mode 100644 index 000000000..a84eaf2cc --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "nested-nullable-api" +version = "0.1.0" +edition = "2024" +description = "Generated Rust SDK." + +[dependencies] +aioduct = { version = "0.2", features = ["tokio", "rustls", "rustls-ring", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde-xml-rs = "0.8.2" +serde_repr = "0.1" diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/README.md.golden b/tests/golden/rust/rust-aioduct/nested-nullable/README.md.golden new file mode 100644 index 000000000..534032ae1 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/README.md.golden @@ -0,0 +1,7 @@ +# Nested Nullable API + +Generated Rust SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `nested-nullable-api`. diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/apis/mod.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/apis/mod.rs.golden new file mode 100644 index 000000000..51a610ed2 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/apis/mod.rs.golden @@ -0,0 +1,7 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +mod values; +pub use values::*; diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/apis/values.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/apis/values.rs.golden new file mode 100644 index 000000000..ccce5645d --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/apis/values.rs.golden @@ -0,0 +1,139 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use crate::runtime::client::Client; +use crate::runtime::error::{ApiCallError, ApiError, Error}; + +/// API operations under the "Values" tag. +pub struct ValuesApi<'a, R: aioduct::RuntimePoll, C: aioduct::ConnectorSend> { + client: &'a Client, +} + +impl<'a, R: aioduct::RuntimePoll, C: aioduct::ConnectorSend> ValuesApi<'a, R, C> { + /// Create a new `ValuesApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// GET /values + pub async fn get_values( + &self, + ) -> Result { + let path = "/values".to_string(); + let req = self.client.get(&path)?; + let resp = req.send().await.map_err(Error::from)?; + let status_code = resp.status().as_u16(); + let response_headers = resp.headers().clone(); + let body_bytes = resp.bytes().await.map_err(Error::from)?; + let mut result = GetValuesResponse { status_code, headers: response_headers.clone(), data: None }; + match status_code { + 200 => { + result.data = Some(serde_json::from_slice(&body_bytes).map_err(Error::Deserialize)?); + } + _ => {} + } + if !(200..300).contains(&status_code) { + match status_code { + 400 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::BadRequest(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 401 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::Unauthorized(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 402 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::Status402(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 403 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::Forbidden(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 404 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::NotFound(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 410 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::Status410(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + _ => { + let body = Ok::, Error>(body_bytes.to_vec()); + return Err(GetValuesError::Unexpected(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + } + } + Ok(result) + } +} + +/// Response from `get_values`. +#[derive(Debug)] +pub struct GetValuesResponse { + pub status_code: u16, + pub headers: aioduct::HeaderMap, + pub data: Option, +} + +/// Error from `get_values`. +#[derive(Debug)] +pub enum GetValuesError { + BadRequest(ApiError>>>), + Unauthorized(ApiError>>>), + Status402(ApiError>>), + Forbidden(ApiError>>), + NotFound(ApiError), + Status410(ApiError>), + Unexpected(ApiError>), + Transport(Error), +} + +impl From for GetValuesError { + fn from(error: Error) -> Self { + Self::Transport(error) + } +} + +impl From for ApiCallError { + fn from(error: GetValuesError) -> Self { + match error { + GetValuesError::BadRequest(error) => Self::from_api_error("getValues", error), + GetValuesError::Unauthorized(error) => Self::from_api_error("getValues", error), + GetValuesError::Status402(error) => Self::from_api_error("getValues", error), + GetValuesError::Forbidden(error) => Self::from_api_error("getValues", error), + GetValuesError::NotFound(error) => Self::from_api_error("getValues", error), + GetValuesError::Status410(error) => Self::from_api_error("getValues", error), + GetValuesError::Unexpected(error) => Self::from_api_error("getValues", error), + GetValuesError::Transport(error) => Self::from_runtime_error("getValues", error), + } + } +} + +impl std::fmt::Display for GetValuesError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::BadRequest(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Unauthorized(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Status402(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Forbidden(err) => write!(f, "HTTP error {}", err.status_code()), + Self::NotFound(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Status410(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Unexpected(err) => write!(f, "unexpected HTTP error {}", err.status_code()), + Self::Transport(err) => std::fmt::Display::fmt(err, f), + } + } +} + +impl std::error::Error for GetValuesError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Transport(err) => Some(err), + _ => None, + } + } +} diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/lib.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/lib.rs.golden new file mode 100644 index 000000000..1c579197e --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/lib.rs.golden @@ -0,0 +1,10 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/models/detail.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/detail.rs.golden new file mode 100644 index 000000000..fb5ca68ad --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/detail.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Detail { + pub message: String, +} diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/models/detail_alias.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/detail_alias.rs.golden new file mode 100644 index 000000000..c10402563 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/detail_alias.rs.golden @@ -0,0 +1,6 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub type DetailAlias = super::Detail; diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/models/matrix_container.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/matrix_container.rs.golden new file mode 100644 index 000000000..fd9ed3009 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/matrix_container.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MatrixContainer { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub matrix: Option>>, +} diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/models/maybe_matrix.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/maybe_matrix.rs.golden new file mode 100644 index 000000000..a307bebbb --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/maybe_matrix.rs.golden @@ -0,0 +1,6 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub type MaybeMatrix = Option>>; diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/models/mod.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/mod.rs.golden new file mode 100644 index 000000000..b1db7d34f --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/mod.rs.golden @@ -0,0 +1,15 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +mod detail; +pub use detail::*; +mod detail_alias; +pub use detail_alias::*; +mod matrix_container; +pub use matrix_container::*; +mod maybe_matrix; +pub use maybe_matrix::*; +mod nullable_payload; +pub use nullable_payload::*; diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/models/nullable_payload.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/nullable_payload.rs.golden new file mode 100644 index 000000000..328ceefd8 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/models/nullable_payload.rs.golden @@ -0,0 +1,21 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NullablePayload { + #[serde(rename = "arrayItems")] + pub array_items: Vec>, + #[serde(rename = "arrayMaps")] + pub array_maps: Vec>>, + #[serde(rename = "mapValues")] + pub map_values: std::collections::HashMap>>, + #[serde(rename = "nullableArray")] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub nullable_array: Option>, + #[serde(rename = "strictValues")] + pub strict_values: std::collections::HashMap>, +} diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/auth.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/auth.rs.golden new file mode 100644 index 000000000..e5e4aae38 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/auth.rs.golden @@ -0,0 +1,181 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use std::sync::Arc; + +use crate::runtime::error::Error; + +/// Trait for authenticating requests. +pub trait Authenticator: + Send + Sync + std::fmt::Debug +{ + /// Apply authentication to the request builder. + fn authenticate<'a>( + &self, + req: aioduct::RequestBuilderSend<'a, R, C>, + ) -> Result, Error>; +} + +/// Bearer token authentication. +/// +/// Use [`BearerAuth::new`] with a static token, or +/// [`BearerAuth::from_provider`] with a function that returns the +/// current token (evaluated on every request). +pub struct BearerAuth { + token: TokenSource, +} + +enum TokenSource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for TokenSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(t) => f.debug_tuple("Static").field(&t).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for TokenSource { + fn clone(&self) -> Self { + match self { + Self::Static(t) => Self::Static(t.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for BearerAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BearerAuth").field("token", &self.token).finish() + } +} + +impl Clone for BearerAuth { + fn clone(&self) -> Self { + Self { + token: self.token.clone(), + } + } +} + +impl BearerAuth { + /// Create a new bearer token authenticator with a static token. + pub fn new(token: impl Into) -> Self { + Self { + token: TokenSource::Static(token.into()), + } + } + + /// Create a bearer token authenticator that evaluates the given + /// function on every request to obtain the current token. + pub fn from_provider(f: impl Fn() -> String + Send + Sync + 'static) -> Self { + Self { + token: TokenSource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for BearerAuth { + fn authenticate<'a>( + &self, + req: aioduct::RequestBuilderSend<'a, R, C>, + ) -> Result, Error> { + let token = match &self.token { + TokenSource::Static(t) => t.clone(), + TokenSource::Dynamic(f) => f(), + }; + Ok(req.bearer_auth(&token)) + } +} + +/// API key authentication. +/// +/// Use [`ApiKeyAuth::new`] with a static key, or +/// [`ApiKeyAuth::from_provider`] with a function that returns the +/// current key (evaluated on every request). +pub struct ApiKeyAuth { + header_name: String, + api_key: KeySource, +} + +enum KeySource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for KeySource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(k) => f.debug_tuple("Static").field(&k).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for KeySource { + fn clone(&self) -> Self { + match self { + Self::Static(k) => Self::Static(k.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for ApiKeyAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ApiKeyAuth") + .field("header_name", &self.header_name) + .field("api_key", &self.api_key) + .finish() + } +} + +impl Clone for ApiKeyAuth { + fn clone(&self) -> Self { + Self { + header_name: self.header_name.clone(), + api_key: self.api_key.clone(), + } + } +} + +impl ApiKeyAuth { + /// Create a new API key authenticator with a static key. + pub fn new(header_name: impl Into, api_key: impl Into) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Static(api_key.into()), + } + } + + /// Create an API key authenticator that evaluates the given + /// function on every request to obtain the current key. + pub fn from_provider( + header_name: impl Into, + f: impl Fn() -> String + Send + Sync + 'static, + ) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for ApiKeyAuth { + fn authenticate<'a>( + &self, + req: aioduct::RequestBuilderSend<'a, R, C>, + ) -> Result, Error> { + let key = match &self.api_key { + KeySource::Static(k) => k.clone(), + KeySource::Dynamic(f) => f(), + }; + Ok(req.header_str(&self.header_name, &key)?) + } +} diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/client.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/client.rs.golden new file mode 100644 index 000000000..868524c11 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/client.rs.golden @@ -0,0 +1,111 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use std::fmt; +use std::sync::Arc; + +use crate::runtime::auth::Authenticator; +use crate::runtime::error::Error; + +/// Async HTTP client wrapping `aioduct::HttpEngineSend`. +pub struct Client { + inner: aioduct::HttpEngineSend, + base_url: String, + authenticator: Option>>, +} + +impl Client { + /// Create a new client with the given base URL. + pub fn new(base_url: &str) -> Self { + Self { + inner: aioduct::HttpEngineSend::::with_rustls(), + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } +} + +impl Client { + /// Create a client with a custom `aioduct::HttpEngineSend`. + pub fn with_client(inner: aioduct::HttpEngineSend, base_url: &str) -> Self { + Self { + inner, + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Attach an authenticator to the client. + pub fn with_auth(mut self, auth: Arc>) -> Self { + self.authenticator = Some(auth); + self + } + + fn full_url(&self, path: &str) -> String { + format!("{}{}", self.base_url, path) + } + + /// Build a GET request. + pub fn get(&self, path: &str) -> Result, Error> { + let mut req = self.inner.get(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a POST request. + pub fn post(&self, path: &str) -> Result, Error> { + let mut req = self.inner.post(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a PUT request. + pub fn put(&self, path: &str) -> Result, Error> { + let mut req = self.inner.put(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a DELETE request. + pub fn delete(&self, path: &str) -> Result, Error> { + let mut req = self.inner.delete(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a PATCH request. + pub fn patch(&self, path: &str) -> Result, Error> { + let mut req = self.inner.patch(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } + + /// Build a HEAD request. + pub fn head(&self, path: &str) -> Result, Error> { + let mut req = self.inner.head(&self.full_url(path))?; + if let Some(auth) = &self.authenticator { + req = auth.authenticate(req)?; + } + Ok(req) + } +} + +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Client") + .field("base_url", &self.base_url) + .finish() + } +} diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/error.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/error.rs.golden new file mode 100644 index 000000000..e53c33105 --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/error.rs.golden @@ -0,0 +1,229 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use std::fmt; + +/// Error types for the generated SDK. +#[derive(Debug)] +pub enum Error { + /// HTTP/transport error from aioduct. + Http(aioduct::Error), + /// JSON deserialization error. + Deserialize(serde_json::Error), + /// XML serialization error. + Xml(serde_xml_rs::Error), + /// Unsupported generated operation. + Unsupported(&'static str), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Http(e) => write!(f, "HTTP error: {e}"), + Error::Deserialize(e) => write!(f, "Deserialization error: {e}"), + Error::Xml(e) => write!(f, "XML serialization error: {e}"), + Error::Unsupported(e) => write!(f, "Unsupported operation: {e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Http(e) => Some(e), + Error::Deserialize(e) => Some(e), + Error::Xml(e) => Some(e), + Error::Unsupported(_) => None, + } + } +} + +impl From for Error { + fn from(e: aioduct::Error) -> Self { + Error::Http(e) + } +} + +impl From for Error { + fn from(e: aioduct::SendError) -> Self { + Error::Http(e.into()) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Deserialize(e) + } +} + +impl From for Error { + fn from(e: serde_xml_rs::Error) -> Self { + Error::Xml(e) + } +} + +/// Typed HTTP error response payload. +#[derive(Debug)] +pub struct ApiError { + status_code: u16, + headers: aioduct::HeaderMap, + raw_body: Vec, + body: Result, +} + +impl ApiError { + pub fn new( + status_code: u16, + headers: aioduct::HeaderMap, + raw_body: Vec, + body: Result, + ) -> Self { + Self { + status_code, + headers, + raw_body, + body, + } + } + + pub fn status_code(&self) -> u16 { + self.status_code + } + + pub fn headers(&self) -> &aioduct::HeaderMap { + &self.headers + } + + pub fn raw_body(&self) -> &[u8] { + &self.raw_body + } + + pub fn body(&self) -> Result<&T, &Error> { + self.body.as_ref() + } +} + +#[derive(Debug)] +struct ApiCallHttpError { + status_code: u16, + headers: aioduct::HeaderMap, + raw_body: Vec, + body_error: Option, +} + +impl ApiCallHttpError { + fn from_api_error(error: ApiError) -> Self { + Self { + status_code: error.status_code, + headers: error.headers, + raw_body: error.raw_body, + body_error: error.body.err(), + } + } +} + +#[derive(Debug)] +enum ApiCallErrorKind { + Http(ApiCallHttpError), + Runtime(Error), +} + +#[derive(Debug)] +struct ApiCallErrorInner { + operation_id: &'static str, + kind: ApiCallErrorKind, +} + +impl ApiCallErrorInner { + fn from_api_error(operation_id: &'static str, error: ApiError) -> Self { + Self { + operation_id, + kind: ApiCallErrorKind::Http(ApiCallHttpError::from_api_error(error)), + } + } + + fn from_runtime_error(operation_id: &'static str, error: Error) -> Self { + Self { + operation_id, + kind: ApiCallErrorKind::Runtime(error), + } + } +} + +/// Type-erased error from any generated API operation. +#[derive(Debug)] +pub struct ApiCallError { + inner: Box, +} + +impl ApiCallError { + pub(crate) fn from_api_error(operation_id: &'static str, error: ApiError) -> Self { + Self { + inner: Box::new(ApiCallErrorInner::from_api_error(operation_id, error)), + } + } + + pub(crate) fn from_runtime_error(operation_id: &'static str, error: Error) -> Self { + Self { + inner: Box::new(ApiCallErrorInner::from_runtime_error(operation_id, error)), + } + } + + /// Operation identifier, generated from the HTTP method and path when OpenAPI omits one. + pub fn operation_id(&self) -> &'static str { + self.inner.operation_id + } + + /// HTTP response status, if the operation reached the server and received an error response. + pub fn status_code(&self) -> Option { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(error.status_code), + ApiCallErrorKind::Runtime(_) => None, + } + } + + /// Native HTTP response headers, if an error response was received. + pub fn headers(&self) -> Option<&aioduct::HeaderMap> { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(&error.headers), + ApiCallErrorKind::Runtime(_) => None, + } + } + + /// Raw HTTP response body, if an error response was received. + pub fn raw_body(&self) -> Option<&[u8]> { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(&error.raw_body), + ApiCallErrorKind::Runtime(_) => None, + } + } +} + +impl std::fmt::Display for ApiCallError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => { + write!(f, "operation {} failed with HTTP status {}", self.inner.operation_id, error.status_code) + } + ApiCallErrorKind::Runtime(error) => { + write!(f, "operation {} failed: {error}", self.inner.operation_id) + } + } + } +} + +impl std::error::Error for ApiCallError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + if let ApiCallErrorKind::Http(error) = &self.inner.kind { + if let Some(error) = &error.body_error { + return Some(error); + } + } + if let ApiCallErrorKind::Runtime(error) = &self.inner.kind { + return Some(error); + } + None + } +} diff --git a/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/mod.rs.golden b/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/mod.rs.golden new file mode 100644 index 000000000..6d821c61c --- /dev/null +++ b/tests/golden/rust/rust-aioduct/nested-nullable/src/runtime/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/Cargo.toml.golden b/tests/golden/rust/rust-reqwest/nested-nullable/Cargo.toml.golden new file mode 100644 index 000000000..a0b6fbd85 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "nested-nullable-api" +version = "0.1.0" +edition = "2024" +description = "Generated Rust SDK." + +[dependencies] +reqwest = { version = "0.12", features = ["json", "multipart"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde-xml-rs = "0.8.2" +serde_repr = "0.1" diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/README.md.golden b/tests/golden/rust/rust-reqwest/nested-nullable/README.md.golden new file mode 100644 index 000000000..534032ae1 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/README.md.golden @@ -0,0 +1,7 @@ +# Nested Nullable API + +Generated Rust SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `nested-nullable-api`. diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/apis/mod.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/apis/mod.rs.golden new file mode 100644 index 000000000..51a610ed2 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/apis/mod.rs.golden @@ -0,0 +1,7 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +mod values; +pub use values::*; diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/apis/values.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/apis/values.rs.golden new file mode 100644 index 000000000..1c46519c5 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/apis/values.rs.golden @@ -0,0 +1,139 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use crate::runtime::client::Client; +use crate::runtime::error::{ApiCallError, ApiError, Error}; + +/// API operations under the "Values" tag. +pub struct ValuesApi<'a> { + client: &'a Client, +} + +impl<'a> ValuesApi<'a> { + /// Create a new `ValuesApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// GET /values + pub async fn get_values( + &self, + ) -> Result { + let path = "/values".to_string(); + let req = self.client.request(reqwest::Method::GET, &path).await?; + let resp = self.client.send(req).await?; + let status_code = resp.status().as_u16(); + let response_headers = resp.headers().clone(); + let body_bytes = resp.bytes().await.map_err(Error::Network)?; + let mut result = GetValuesResponse { status_code, headers: response_headers.clone(), data: None }; + match status_code { + 200 => { + result.data = Some(serde_json::from_slice(&body_bytes).map_err(Error::Deserialize)?); + } + _ => {} + } + if !(200..300).contains(&status_code) { + match status_code { + 400 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::BadRequest(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 401 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::Unauthorized(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 402 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::Status402(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 403 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::Forbidden(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 404 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::NotFound(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 410 => { + let body = serde_json::from_slice(&body_bytes).map_err(Error::Deserialize); + return Err(GetValuesError::Status410(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + _ => { + let body = Ok::, Error>(body_bytes.to_vec()); + return Err(GetValuesError::Unexpected(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + } + } + Ok(result) + } +} + +/// Response from `get_values`. +#[derive(Debug)] +pub struct GetValuesResponse { + pub status_code: u16, + pub headers: reqwest::header::HeaderMap, + pub data: Option, +} + +/// Error from `get_values`. +#[derive(Debug)] +pub enum GetValuesError { + BadRequest(ApiError>>>), + Unauthorized(ApiError>>>), + Status402(ApiError>>), + Forbidden(ApiError>>), + NotFound(ApiError), + Status410(ApiError>), + Unexpected(ApiError>), + Transport(Error), +} + +impl From for GetValuesError { + fn from(error: Error) -> Self { + Self::Transport(error) + } +} + +impl From for ApiCallError { + fn from(error: GetValuesError) -> Self { + match error { + GetValuesError::BadRequest(error) => Self::from_api_error("getValues", error), + GetValuesError::Unauthorized(error) => Self::from_api_error("getValues", error), + GetValuesError::Status402(error) => Self::from_api_error("getValues", error), + GetValuesError::Forbidden(error) => Self::from_api_error("getValues", error), + GetValuesError::NotFound(error) => Self::from_api_error("getValues", error), + GetValuesError::Status410(error) => Self::from_api_error("getValues", error), + GetValuesError::Unexpected(error) => Self::from_api_error("getValues", error), + GetValuesError::Transport(error) => Self::from_runtime_error("getValues", error), + } + } +} + +impl std::fmt::Display for GetValuesError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::BadRequest(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Unauthorized(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Status402(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Forbidden(err) => write!(f, "HTTP error {}", err.status_code()), + Self::NotFound(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Status410(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Unexpected(err) => write!(f, "unexpected HTTP error {}", err.status_code()), + Self::Transport(err) => std::fmt::Display::fmt(err, f), + } + } +} + +impl std::error::Error for GetValuesError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Transport(err) => Some(err), + _ => None, + } + } +} diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/lib.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/lib.rs.golden new file mode 100644 index 000000000..1c579197e --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/lib.rs.golden @@ -0,0 +1,10 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/models/detail.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/detail.rs.golden new file mode 100644 index 000000000..fb5ca68ad --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/detail.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Detail { + pub message: String, +} diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/models/detail_alias.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/detail_alias.rs.golden new file mode 100644 index 000000000..c10402563 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/detail_alias.rs.golden @@ -0,0 +1,6 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub type DetailAlias = super::Detail; diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/models/matrix_container.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/matrix_container.rs.golden new file mode 100644 index 000000000..fd9ed3009 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/matrix_container.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MatrixContainer { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub matrix: Option>>, +} diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/models/maybe_matrix.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/maybe_matrix.rs.golden new file mode 100644 index 000000000..a307bebbb --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/maybe_matrix.rs.golden @@ -0,0 +1,6 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub type MaybeMatrix = Option>>; diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/models/mod.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/mod.rs.golden new file mode 100644 index 000000000..b1db7d34f --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/mod.rs.golden @@ -0,0 +1,15 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +mod detail; +pub use detail::*; +mod detail_alias; +pub use detail_alias::*; +mod matrix_container; +pub use matrix_container::*; +mod maybe_matrix; +pub use maybe_matrix::*; +mod nullable_payload; +pub use nullable_payload::*; diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/models/nullable_payload.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/nullable_payload.rs.golden new file mode 100644 index 000000000..328ceefd8 --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/models/nullable_payload.rs.golden @@ -0,0 +1,21 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NullablePayload { + #[serde(rename = "arrayItems")] + pub array_items: Vec>, + #[serde(rename = "arrayMaps")] + pub array_maps: Vec>>, + #[serde(rename = "mapValues")] + pub map_values: std::collections::HashMap>>, + #[serde(rename = "nullableArray")] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub nullable_array: Option>, + #[serde(rename = "strictValues")] + pub strict_values: std::collections::HashMap>, +} diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/auth.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/auth.rs.golden new file mode 100644 index 000000000..c771451bc --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/auth.rs.golden @@ -0,0 +1,188 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use std::sync::Arc; + +use reqwest::header::{HeaderMap, HeaderValue}; + +use crate::runtime::error::Error; + +/// Trait for authenticating HTTP requests. +pub trait Authenticator: Send + Sync + std::fmt::Debug { + /// Apply authentication to the given headers. + fn authenticate(&self, headers: &mut HeaderMap) -> Result<(), Error>; +} + +/// Bearer token authentication. +/// +/// Use [`BearerAuth::new`] with a static token, or +/// [`BearerAuth::from_provider`] with a function that returns the +/// current token (evaluated on every request). +pub struct BearerAuth { + token: TokenSource, +} + +enum TokenSource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for TokenSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(t) => f.debug_tuple("Static").field(&t).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for TokenSource { + fn clone(&self) -> Self { + match self { + Self::Static(t) => Self::Static(t.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for BearerAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BearerAuth").field("token", &self.token).finish() + } +} + +impl Clone for BearerAuth { + fn clone(&self) -> Self { + Self { + token: self.token.clone(), + } + } +} + +impl BearerAuth { + /// Create a new bearer token authenticator with a static token. + pub fn new(token: impl Into) -> Self { + Self { + token: TokenSource::Static(token.into()), + } + } + + /// Create a bearer token authenticator that evaluates the given + /// function on every request to obtain the current token. + pub fn from_provider(f: impl Fn() -> String + Send + Sync + 'static) -> Self { + Self { + token: TokenSource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for BearerAuth { + fn authenticate(&self, headers: &mut HeaderMap) -> Result<(), Error> { + let token = match &self.token { + TokenSource::Static(t) => t.clone(), + TokenSource::Dynamic(f) => f(), + }; + let value = HeaderValue::from_str(&format!("Bearer {}", token)) + .map_err(|e| Error::Api { + status: reqwest::StatusCode::INTERNAL_SERVER_ERROR, + body: format!("invalid auth header: {e}"), + })?; + headers.insert(reqwest::header::AUTHORIZATION, value); + Ok(()) + } +} + +/// API key authentication via header. +/// +/// Use [`ApiKeyAuth::new`] with a static key, or +/// [`ApiKeyAuth::from_provider`] with a function that returns the +/// current key (evaluated on every request). +pub struct ApiKeyAuth { + header_name: String, + api_key: KeySource, +} + +enum KeySource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for KeySource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(k) => f.debug_tuple("Static").field(&k).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for KeySource { + fn clone(&self) -> Self { + match self { + Self::Static(k) => Self::Static(k.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for ApiKeyAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ApiKeyAuth") + .field("header_name", &self.header_name) + .field("api_key", &self.api_key) + .finish() + } +} + +impl Clone for ApiKeyAuth { + fn clone(&self) -> Self { + Self { + header_name: self.header_name.clone(), + api_key: self.api_key.clone(), + } + } +} + +impl ApiKeyAuth { + /// Create a new API key authenticator with a static key. + pub fn new(header_name: impl Into, api_key: impl Into) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Static(api_key.into()), + } + } + + /// Create an API key authenticator that evaluates the given + /// function on every request to obtain the current key. + pub fn from_provider( + header_name: impl Into, + f: impl Fn() -> String + Send + Sync + 'static, + ) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for ApiKeyAuth { + fn authenticate(&self, headers: &mut HeaderMap) -> Result<(), Error> { + let key = match &self.api_key { + KeySource::Static(k) => k.clone(), + KeySource::Dynamic(f) => f(), + }; + let name = reqwest::header::HeaderName::from_bytes(self.header_name.as_bytes()) + .map_err(|e| Error::Api { + status: reqwest::StatusCode::INTERNAL_SERVER_ERROR, + body: format!("invalid header name: {e}"), + })?; + let value = HeaderValue::from_str(&key).map_err(|e| Error::Api { + status: reqwest::StatusCode::INTERNAL_SERVER_ERROR, + body: format!("invalid header value: {e}"), + })?; + headers.insert(name, value); + Ok(()) + } +} diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/client.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/client.rs.golden new file mode 100644 index 000000000..56b97cd8f --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/client.rs.golden @@ -0,0 +1,72 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use std::sync::Arc; + +use reqwest::header::HeaderMap; + +use crate::runtime::auth::Authenticator; +use crate::runtime::error::Error; + +/// HTTP client wrapper around reqwest. +#[derive(Debug, Clone)] +pub struct Client { + inner: reqwest::Client, + base_url: String, + authenticator: Option>, +} + +impl Client { + /// Create a new client with the given base URL. + pub fn new(base_url: &str) -> Self { + Self { + inner: reqwest::Client::new(), + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Create a new client with a custom reqwest::Client. + pub fn with_client(inner: reqwest::Client, base_url: &str) -> Self { + Self { + inner, + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Set the authenticator for this client. + pub fn with_auth(mut self, auth: impl Authenticator + 'static) -> Self { + self.authenticator = Some(Arc::new(auth)); + self + } + + /// Build a request with authentication applied. + pub async fn request( + &self, + method: reqwest::Method, + path: &str, + ) -> Result { + let url = format!("{}{}", self.base_url, path); + let mut builder = self.inner.request(method, &url); + + if let Some(auth) = &self.authenticator { + let mut headers = HeaderMap::new(); + auth.authenticate(&mut headers)?; + builder = builder.headers(headers); + } + + Ok(builder) + } + + /// Send a request and return the response. + pub async fn send( + &self, + request: reqwest::RequestBuilder, + ) -> Result { + let response = request.send().await.map_err(Error::Network)?; + Ok(response) + } +} diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/error.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/error.rs.golden new file mode 100644 index 000000000..ea8632c2f --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/error.rs.golden @@ -0,0 +1,228 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +/// Errors that can occur during API calls. +#[derive(Debug)] +pub enum Error { + /// Network-level error from reqwest. + Network(reqwest::Error), + /// API returned a non-success status code. + Api { + status: reqwest::StatusCode, + body: String, + }, + /// Failed to deserialize the response body. + Deserialize(serde_json::Error), + /// Failed to serialize the request body as XML. + Xml(serde_xml_rs::Error), + /// Unsupported generated operation. + Unsupported(&'static str), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Network(e) => write!(f, "network error: {e}"), + Error::Api { status, body } => write!(f, "API error {status}: {body}"), + Error::Deserialize(e) => write!(f, "deserialization error: {e}"), + Error::Xml(e) => write!(f, "XML serialization error: {e}"), + Error::Unsupported(e) => write!(f, "Unsupported operation: {e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Network(e) => Some(e), + Error::Deserialize(e) => Some(e), + Error::Xml(e) => Some(e), + Error::Api { .. } => None, + Error::Unsupported(_) => None, + } + } +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Error::Network(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Deserialize(e) + } +} + +impl From for Error { + fn from(e: serde_xml_rs::Error) -> Self { + Error::Xml(e) + } +} + +/// Typed HTTP error response payload. +#[derive(Debug)] +pub struct ApiError { + status_code: u16, + headers: reqwest::header::HeaderMap, + raw_body: Vec, + body: Result, +} + +impl ApiError { + pub fn new( + status_code: u16, + headers: reqwest::header::HeaderMap, + raw_body: Vec, + body: Result, + ) -> Self { + Self { + status_code, + headers, + raw_body, + body, + } + } + + pub fn status_code(&self) -> u16 { + self.status_code + } + + pub fn headers(&self) -> &reqwest::header::HeaderMap { + &self.headers + } + + pub fn raw_body(&self) -> &[u8] { + &self.raw_body + } + + pub fn body(&self) -> Result<&T, &Error> { + self.body.as_ref() + } +} + +#[derive(Debug)] +struct ApiCallHttpError { + status_code: u16, + headers: reqwest::header::HeaderMap, + raw_body: Vec, + body_error: Option, +} + +impl ApiCallHttpError { + fn from_api_error(error: ApiError) -> Self { + Self { + status_code: error.status_code, + headers: error.headers, + raw_body: error.raw_body, + body_error: error.body.err(), + } + } +} + +#[derive(Debug)] +enum ApiCallErrorKind { + Http(ApiCallHttpError), + Runtime(Error), +} + +#[derive(Debug)] +struct ApiCallErrorInner { + operation_id: &'static str, + kind: ApiCallErrorKind, +} + +impl ApiCallErrorInner { + fn from_api_error(operation_id: &'static str, error: ApiError) -> Self { + Self { + operation_id, + kind: ApiCallErrorKind::Http(ApiCallHttpError::from_api_error(error)), + } + } + + fn from_runtime_error(operation_id: &'static str, error: Error) -> Self { + Self { + operation_id, + kind: ApiCallErrorKind::Runtime(error), + } + } +} + +/// Type-erased error from any generated API operation. +#[derive(Debug)] +pub struct ApiCallError { + inner: Box, +} + +impl ApiCallError { + pub(crate) fn from_api_error(operation_id: &'static str, error: ApiError) -> Self { + Self { + inner: Box::new(ApiCallErrorInner::from_api_error(operation_id, error)), + } + } + + pub(crate) fn from_runtime_error(operation_id: &'static str, error: Error) -> Self { + Self { + inner: Box::new(ApiCallErrorInner::from_runtime_error(operation_id, error)), + } + } + + /// Operation identifier, generated from the HTTP method and path when OpenAPI omits one. + pub fn operation_id(&self) -> &'static str { + self.inner.operation_id + } + + /// HTTP response status, if the operation reached the server and received an error response. + pub fn status_code(&self) -> Option { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(error.status_code), + ApiCallErrorKind::Runtime(_) => None, + } + } + + /// Native HTTP response headers, if an error response was received. + pub fn headers(&self) -> Option<&reqwest::header::HeaderMap> { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(&error.headers), + ApiCallErrorKind::Runtime(_) => None, + } + } + + /// Raw HTTP response body, if an error response was received. + pub fn raw_body(&self) -> Option<&[u8]> { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(&error.raw_body), + ApiCallErrorKind::Runtime(_) => None, + } + } +} + +impl std::fmt::Display for ApiCallError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => { + write!(f, "operation {} failed with HTTP status {}", self.inner.operation_id, error.status_code) + } + ApiCallErrorKind::Runtime(error) => { + write!(f, "operation {} failed: {error}", self.inner.operation_id) + } + } + } +} + +impl std::error::Error for ApiCallError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + if let ApiCallErrorKind::Http(error) = &self.inner.kind { + if let Some(error) = &error.body_error { + return Some(error); + } + } + if let ApiCallErrorKind::Runtime(error) = &self.inner.kind { + return Some(error); + } + None + } +} diff --git a/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/mod.rs.golden b/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/mod.rs.golden new file mode 100644 index 000000000..6d821c61c --- /dev/null +++ b/tests/golden/rust/rust-reqwest/nested-nullable/src/runtime/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/rust/rust-ureq/nested-nullable/Cargo.toml.golden b/tests/golden/rust/rust-ureq/nested-nullable/Cargo.toml.golden new file mode 100644 index 000000000..efa57d4db --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/Cargo.toml.golden @@ -0,0 +1,12 @@ +[package] +name = "nested-nullable-api" +version = "0.1.0" +edition = "2024" +description = "Generated Rust SDK." + +[dependencies] +ureq = { version = "3", features = ["json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde-xml-rs = "0.8.2" +serde_repr = "0.1" diff --git a/tests/golden/rust/rust-ureq/nested-nullable/README.md.golden b/tests/golden/rust/rust-ureq/nested-nullable/README.md.golden new file mode 100644 index 000000000..534032ae1 --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/README.md.golden @@ -0,0 +1,7 @@ +# Nested Nullable API + +Generated Rust SDK. + +Version: `1.0.0` + +Generated by [openapi-nexus](https://github.com/rust-codegen-group/openapi-nexus) for `nested-nullable-api`. diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/apis/mod.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/apis/mod.rs.golden new file mode 100644 index 000000000..51a610ed2 --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/apis/mod.rs.golden @@ -0,0 +1,7 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +mod values; +pub use values::*; diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/apis/values.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/apis/values.rs.golden new file mode 100644 index 000000000..595881f73 --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/apis/values.rs.golden @@ -0,0 +1,139 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use crate::runtime::client::Client; +use crate::runtime::error::{ApiCallError, ApiError, Error}; + +/// API operations under the "Values" tag. +pub struct ValuesApi<'a> { + client: &'a Client, +} + +impl<'a> ValuesApi<'a> { + /// Create a new `ValuesApi` bound to the given client. + pub fn new(client: &'a Client) -> Self { + Self { + client, + } + } + + /// GET /values + pub fn get_values( + &self, + ) -> Result { + let path = "/values".to_string(); + let req = self.client.get(&path); + let resp = req.call().map_err(Error::Http)?; + let status_code = resp.status().as_u16(); + let response_headers = resp.headers().clone(); + let body_bytes = resp.into_body().read_to_vec().map_err(Error::Http)?; + let mut result = GetValuesResponse { status_code, headers: response_headers.clone(), data: None }; + match status_code { + 200 => { + result.data = Some(serde_json::from_slice(body_bytes.as_slice()).map_err(Error::Deserialize)?); + } + _ => {} + } + if !(200..300).contains(&status_code) { + match status_code { + 400 => { + let body = serde_json::from_slice(body_bytes.as_slice()).map_err(Error::Deserialize); + return Err(GetValuesError::BadRequest(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 401 => { + let body = serde_json::from_slice(body_bytes.as_slice()).map_err(Error::Deserialize); + return Err(GetValuesError::Unauthorized(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 402 => { + let body = serde_json::from_slice(body_bytes.as_slice()).map_err(Error::Deserialize); + return Err(GetValuesError::Status402(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 403 => { + let body = serde_json::from_slice(body_bytes.as_slice()).map_err(Error::Deserialize); + return Err(GetValuesError::Forbidden(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 404 => { + let body = serde_json::from_slice(body_bytes.as_slice()).map_err(Error::Deserialize); + return Err(GetValuesError::NotFound(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + 410 => { + let body = serde_json::from_slice(body_bytes.as_slice()).map_err(Error::Deserialize); + return Err(GetValuesError::Status410(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + _ => { + let body = Ok::, Error>(body_bytes.to_vec()); + return Err(GetValuesError::Unexpected(ApiError::new(status_code, response_headers.clone(), body_bytes.to_vec(), body))); + } + } + } + Ok(result) + } +} + +/// Response from `get_values`. +#[derive(Debug)] +pub struct GetValuesResponse { + pub status_code: u16, + pub headers: ureq::http::HeaderMap, + pub data: Option, +} + +/// Error from `get_values`. +#[derive(Debug)] +pub enum GetValuesError { + BadRequest(ApiError>>>), + Unauthorized(ApiError>>>), + Status402(ApiError>>), + Forbidden(ApiError>>), + NotFound(ApiError), + Status410(ApiError>), + Unexpected(ApiError>), + Transport(Error), +} + +impl From for GetValuesError { + fn from(error: Error) -> Self { + Self::Transport(error) + } +} + +impl From for ApiCallError { + fn from(error: GetValuesError) -> Self { + match error { + GetValuesError::BadRequest(error) => Self::from_api_error("getValues", error), + GetValuesError::Unauthorized(error) => Self::from_api_error("getValues", error), + GetValuesError::Status402(error) => Self::from_api_error("getValues", error), + GetValuesError::Forbidden(error) => Self::from_api_error("getValues", error), + GetValuesError::NotFound(error) => Self::from_api_error("getValues", error), + GetValuesError::Status410(error) => Self::from_api_error("getValues", error), + GetValuesError::Unexpected(error) => Self::from_api_error("getValues", error), + GetValuesError::Transport(error) => Self::from_runtime_error("getValues", error), + } + } +} + +impl std::fmt::Display for GetValuesError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::BadRequest(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Unauthorized(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Status402(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Forbidden(err) => write!(f, "HTTP error {}", err.status_code()), + Self::NotFound(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Status410(err) => write!(f, "HTTP error {}", err.status_code()), + Self::Unexpected(err) => write!(f, "unexpected HTTP error {}", err.status_code()), + Self::Transport(err) => std::fmt::Display::fmt(err, f), + } + } +} + +impl std::error::Error for GetValuesError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Transport(err) => Some(err), + _ => None, + } + } +} diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/lib.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/lib.rs.golden new file mode 100644 index 000000000..1c579197e --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/lib.rs.golden @@ -0,0 +1,10 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +#![allow(clippy::all)] + +pub mod apis; +pub mod models; +pub mod runtime; diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/models/detail.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/models/detail.rs.golden new file mode 100644 index 000000000..fb5ca68ad --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/models/detail.rs.golden @@ -0,0 +1,11 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Detail { + pub message: String, +} diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/models/detail_alias.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/models/detail_alias.rs.golden new file mode 100644 index 000000000..c10402563 --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/models/detail_alias.rs.golden @@ -0,0 +1,6 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub type DetailAlias = super::Detail; diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/models/matrix_container.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/models/matrix_container.rs.golden new file mode 100644 index 000000000..fd9ed3009 --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/models/matrix_container.rs.golden @@ -0,0 +1,12 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MatrixContainer { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub matrix: Option>>, +} diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/models/maybe_matrix.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/models/maybe_matrix.rs.golden new file mode 100644 index 000000000..a307bebbb --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/models/maybe_matrix.rs.golden @@ -0,0 +1,6 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub type MaybeMatrix = Option>>; diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/models/mod.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/models/mod.rs.golden new file mode 100644 index 000000000..b1db7d34f --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/models/mod.rs.golden @@ -0,0 +1,15 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +mod detail; +pub use detail::*; +mod detail_alias; +pub use detail_alias::*; +mod matrix_container; +pub use matrix_container::*; +mod maybe_matrix; +pub use maybe_matrix::*; +mod nullable_payload; +pub use nullable_payload::*; diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/models/nullable_payload.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/models/nullable_payload.rs.golden new file mode 100644 index 000000000..328ceefd8 --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/models/nullable_payload.rs.golden @@ -0,0 +1,21 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NullablePayload { + #[serde(rename = "arrayItems")] + pub array_items: Vec>, + #[serde(rename = "arrayMaps")] + pub array_maps: Vec>>, + #[serde(rename = "mapValues")] + pub map_values: std::collections::HashMap>>, + #[serde(rename = "nullableArray")] + #[serde(skip_serializing_if = "Option::is_none", default)] + pub nullable_array: Option>, + #[serde(rename = "strictValues")] + pub strict_values: std::collections::HashMap>, +} diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/auth.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/auth.rs.golden new file mode 100644 index 000000000..cd4e16a52 --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/auth.rs.golden @@ -0,0 +1,168 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use std::sync::Arc; + +/// Trait for authenticating requests. +pub trait Authenticator: Send + Sync + std::fmt::Debug { + /// Return header key-value pairs to apply to the request. + fn auth_headers(&self) -> Vec<(&str, String)>; +} + +/// Bearer token authentication. +/// +/// Use [`BearerAuth::new`] with a static token, or +/// [`BearerAuth::from_provider`] with a function that returns the +/// current token (evaluated on every request). +pub struct BearerAuth { + token: TokenSource, +} + +enum TokenSource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for TokenSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(t) => f.debug_tuple("Static").field(&t).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for TokenSource { + fn clone(&self) -> Self { + match self { + Self::Static(t) => Self::Static(t.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for BearerAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BearerAuth").field("token", &self.token).finish() + } +} + +impl Clone for BearerAuth { + fn clone(&self) -> Self { + Self { + token: self.token.clone(), + } + } +} + +impl BearerAuth { + /// Create a new bearer token authenticator with a static token. + pub fn new(token: impl Into) -> Self { + Self { + token: TokenSource::Static(token.into()), + } + } + + /// Create a bearer token authenticator that evaluates the given + /// function on every request to obtain the current token. + pub fn from_provider(f: impl Fn() -> String + Send + Sync + 'static) -> Self { + Self { + token: TokenSource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for BearerAuth { + fn auth_headers(&self) -> Vec<(&str, String)> { + let token = match &self.token { + TokenSource::Static(t) => t.clone(), + TokenSource::Dynamic(f) => f(), + }; + vec![("Authorization", format!("Bearer {}", token))] + } +} + +/// API key authentication. +/// +/// Use [`ApiKeyAuth::new`] with a static key, or +/// [`ApiKeyAuth::from_provider`] with a function that returns the +/// current key (evaluated on every request). +pub struct ApiKeyAuth { + header_name: String, + api_key: KeySource, +} + +enum KeySource { + Static(String), + Dynamic(Arc String + Send + Sync>), +} + +impl std::fmt::Debug for KeySource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Static(k) => f.debug_tuple("Static").field(&k).finish(), + Self::Dynamic(_) => f.debug_tuple("Dynamic").field(&"").finish(), + } + } +} + +impl Clone for KeySource { + fn clone(&self) -> Self { + match self { + Self::Static(k) => Self::Static(k.clone()), + Self::Dynamic(f) => Self::Dynamic(Arc::clone(f)), + } + } +} + +impl std::fmt::Debug for ApiKeyAuth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ApiKeyAuth") + .field("header_name", &self.header_name) + .field("api_key", &self.api_key) + .finish() + } +} + +impl Clone for ApiKeyAuth { + fn clone(&self) -> Self { + Self { + header_name: self.header_name.clone(), + api_key: self.api_key.clone(), + } + } +} + +impl ApiKeyAuth { + /// Create a new API key authenticator with a static key. + pub fn new(header_name: impl Into, api_key: impl Into) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Static(api_key.into()), + } + } + + /// Create an API key authenticator that evaluates the given + /// function on every request to obtain the current key. + pub fn from_provider( + header_name: impl Into, + f: impl Fn() -> String + Send + Sync + 'static, + ) -> Self { + Self { + header_name: header_name.into(), + api_key: KeySource::Dynamic(Arc::new(f)), + } + } +} + +impl Authenticator for ApiKeyAuth { + fn auth_headers(&self) -> Vec<(&str, String)> { + let key = match &self.api_key { + KeySource::Static(k) => k.clone(), + KeySource::Dynamic(f) => f(), + }; + vec![(&self.header_name, key)] + } +} diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/client.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/client.rs.golden new file mode 100644 index 000000000..f8b783b59 --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/client.rs.golden @@ -0,0 +1,93 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use std::fmt; +use std::sync::Arc; + +use crate::runtime::auth::Authenticator; + +/// Synchronous HTTP client wrapping `ureq::Agent`. +pub struct Client { + inner: ureq::Agent, + base_url: String, + authenticator: Option>, +} + +impl Client { + /// Create a new client with the given base URL. + pub fn new(base_url: &str) -> Self { + Self { + inner: ureq::Agent::new_with_defaults(), + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Create a client with a custom `ureq::Agent`. + pub fn with_agent(inner: ureq::Agent, base_url: &str) -> Self { + Self { + inner, + base_url: base_url.trim_end_matches('/').to_string(), + authenticator: None, + } + } + + /// Attach an authenticator to the client. + pub fn with_auth(mut self, auth: Arc) -> Self { + self.authenticator = Some(auth); + self + } + + fn full_url(&self, path: &str) -> String { + format!("{}{}", self.base_url, path) + } + + fn apply_auth(&self, mut req: ureq::RequestBuilder) -> ureq::RequestBuilder { + if let Some(auth) = &self.authenticator { + for (key, value) in auth.auth_headers() { + req = req.header(key, &value); + } + } + req.config().http_status_as_error(false).build() + } + + /// Build a GET request. + pub fn get(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.get(&self.full_url(path))) + } + + /// Build a POST request. + pub fn post(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.post(&self.full_url(path))) + } + + /// Build a PUT request. + pub fn put(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.put(&self.full_url(path))) + } + + /// Build a DELETE request. + pub fn delete(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.delete(&self.full_url(path))) + } + + /// Build a PATCH request. + pub fn patch(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.patch(&self.full_url(path))) + } + + /// Build a HEAD request. + pub fn head(&self, path: &str) -> ureq::RequestBuilder { + self.apply_auth(self.inner.head(&self.full_url(path))) + } +} + +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Client") + .field("base_url", &self.base_url) + .finish() + } +} diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/error.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/error.rs.golden new file mode 100644 index 000000000..6f556103c --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/error.rs.golden @@ -0,0 +1,227 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +use std::fmt; + +/// Error types for the generated SDK. +#[derive(Debug)] +pub enum Error { + /// HTTP transport error. + Http(ureq::Error), + /// I/O error while reading response body. + Io(std::io::Error), + /// JSON deserialization error. + Deserialize(serde_json::Error), + /// XML serialization error. + Xml(serde_xml_rs::Error), + /// Unsupported generated operation. + Unsupported(&'static str), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Http(e) => write!(f, "HTTP error: {e}"), + Error::Io(e) => write!(f, "IO error: {e}"), + Error::Deserialize(e) => write!(f, "Deserialization error: {e}"), + Error::Xml(e) => write!(f, "XML serialization error: {e}"), + Error::Unsupported(e) => write!(f, "Unsupported operation: {e}"), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Http(e) => Some(e), + Error::Io(e) => Some(e), + Error::Deserialize(e) => Some(e), + Error::Xml(e) => Some(e), + Error::Unsupported(_) => None, + } + } +} + +impl From for Error { + fn from(e: ureq::Error) -> Self { + Error::Http(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Deserialize(e) + } +} + +impl From for Error { + fn from(e: serde_xml_rs::Error) -> Self { + Error::Xml(e) + } +} + +/// Typed HTTP error response payload. +#[derive(Debug)] +pub struct ApiError { + status_code: u16, + headers: ureq::http::HeaderMap, + raw_body: Vec, + body: Result, +} + +impl ApiError { + pub fn new( + status_code: u16, + headers: ureq::http::HeaderMap, + raw_body: Vec, + body: Result, + ) -> Self { + Self { + status_code, + headers, + raw_body, + body, + } + } + + pub fn status_code(&self) -> u16 { + self.status_code + } + + pub fn headers(&self) -> &ureq::http::HeaderMap { + &self.headers + } + + pub fn raw_body(&self) -> &[u8] { + &self.raw_body + } + + pub fn body(&self) -> Result<&T, &Error> { + self.body.as_ref() + } +} + +#[derive(Debug)] +struct ApiCallHttpError { + status_code: u16, + headers: ureq::http::HeaderMap, + raw_body: Vec, + body_error: Option, +} + +impl ApiCallHttpError { + fn from_api_error(error: ApiError) -> Self { + Self { + status_code: error.status_code, + headers: error.headers, + raw_body: error.raw_body, + body_error: error.body.err(), + } + } +} + +#[derive(Debug)] +enum ApiCallErrorKind { + Http(ApiCallHttpError), + Runtime(Error), +} + +#[derive(Debug)] +struct ApiCallErrorInner { + operation_id: &'static str, + kind: ApiCallErrorKind, +} + +impl ApiCallErrorInner { + fn from_api_error(operation_id: &'static str, error: ApiError) -> Self { + Self { + operation_id, + kind: ApiCallErrorKind::Http(ApiCallHttpError::from_api_error(error)), + } + } + + fn from_runtime_error(operation_id: &'static str, error: Error) -> Self { + Self { + operation_id, + kind: ApiCallErrorKind::Runtime(error), + } + } +} + +/// Type-erased error from any generated API operation. +#[derive(Debug)] +pub struct ApiCallError { + inner: Box, +} + +impl ApiCallError { + pub(crate) fn from_api_error(operation_id: &'static str, error: ApiError) -> Self { + Self { + inner: Box::new(ApiCallErrorInner::from_api_error(operation_id, error)), + } + } + + pub(crate) fn from_runtime_error(operation_id: &'static str, error: Error) -> Self { + Self { + inner: Box::new(ApiCallErrorInner::from_runtime_error(operation_id, error)), + } + } + + /// Operation identifier, generated from the HTTP method and path when OpenAPI omits one. + pub fn operation_id(&self) -> &'static str { + self.inner.operation_id + } + + /// HTTP response status, if the operation reached the server and received an error response. + pub fn status_code(&self) -> Option { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(error.status_code), + ApiCallErrorKind::Runtime(_) => None, + } + } + + /// Native HTTP response headers, if an error response was received. + pub fn headers(&self) -> Option<&ureq::http::HeaderMap> { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(&error.headers), + ApiCallErrorKind::Runtime(_) => None, + } + } + + /// Raw HTTP response body, if an error response was received. + pub fn raw_body(&self) -> Option<&[u8]> { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => Some(&error.raw_body), + ApiCallErrorKind::Runtime(_) => None, + } + } +} + +impl std::fmt::Display for ApiCallError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.inner.kind { + ApiCallErrorKind::Http(error) => { + write!(f, "operation {} failed with HTTP status {}", self.inner.operation_id, error.status_code) + } + ApiCallErrorKind::Runtime(error) => { + write!(f, "operation {} failed: {error}", self.inner.operation_id) + } + } + } +} + +impl std::error::Error for ApiCallError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + if let ApiCallErrorKind::Http(error) = &self.inner.kind { + if let Some(error) = &error.body_error { + return Some(error); + } + } + if let ApiCallErrorKind::Runtime(error) = &self.inner.kind { + return Some(error); + } + None + } +} diff --git a/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/mod.rs.golden b/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/mod.rs.golden new file mode 100644 index 000000000..6d821c61c --- /dev/null +++ b/tests/golden/rust/rust-ureq/nested-nullable/src/runtime/mod.rs.golden @@ -0,0 +1,8 @@ +// @generated +// Code generated by openapi-nexus. DO NOT EDIT. +// +// Nested Nullable API — 1.0.0 + +pub mod auth; +pub mod client; +pub mod error; diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/README.md.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/README.md.golden new file mode 100644 index 000000000..ff7db7b52 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/README.md.golden @@ -0,0 +1,248 @@ +# nested-nullable-api + +Generated API client + +**Version:** 1.0.0 + +## Overview + +This package provides a TypeScript/JavaScript client for the Nested Nullable API API. It uses the native [Fetch API](https://fetch.spec.whatwg.org/) for HTTP requests and works in both Node.js and browser environments. + +## Features + +- ✨ **Type-safe** - Full TypeScript support with generated types +- 🚀 **Modern** - Uses native Fetch API, no external HTTP dependencies +- 🔧 **Configurable** - Flexible configuration options +- 🎯 **Middleware** - Support for request/response interceptors +- 📦 **Tree-shakeable** - Import only what you need +- 🌐 **Universal** - Works in Node.js and browsers + +## Installation + +### From npm (published package) + +```bash +npm install nested-nullable-api +``` + +### From local path (development) + +Add the package to your `package.json` using the `file:` protocol: + +```json +{ + "dependencies": { + "nested-nullable-api": "file:../../path/to/generated/package" + } +} +``` + +Then run: + +```bash +npm install +``` + +## Quick Start + +```typescript +import { Configuration, ValuesApi } from 'nested-nullable-api'; + +// Create a configuration +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'Authorization': 'Bearer YOUR_TOKEN' + } +}); + +// Initialize the API client +const api = new ValuesApi(config); + +// Make API calls +try { + const result = await api.someMethod(); + console.log(result); +} catch (error) { + console.error('API Error:', error); +} +``` + +## Configuration + +The `Configuration` class accepts the following options: + +```typescript +interface ConfigurationParameters { + /** Base URL for API requests */ + basePath?: string; + + /** Custom fetch implementation */ + fetchApi?: typeof fetch; + + /** Request/response middleware */ + middleware?: Middleware[]; + + /** Custom query string serializer */ + queryParamsStringify?: (params: HTTPQuery) => string; + + /** Default headers for all requests */ + headers?: Record; + + /** Credentials mode for requests */ + credentials?: RequestCredentials; +} +``` + +### Example with custom configuration + +```typescript +const config = new Configuration({ + basePath: 'https://api.example.com', + headers: { + 'X-API-Key': 'your-api-key', + 'Content-Type': 'application/json' + }, + credentials: 'include' +}); +``` + +## Middleware + +Add custom middleware to intercept requests and responses: + +```typescript +import { Configuration, Middleware } from 'nested-nullable-api'; + +const loggingMiddleware: Middleware = { + pre: async (context) => { + console.log('Request:', context.url); + return context; + }, + post: async (context) => { + console.log('Response:', context.response.status); + return context.response; + }, + onError: async (context) => { + console.error('Error:', context.error); + return undefined; + } +}; + +const config = new Configuration({ + basePath: 'https://api.example.com', + middleware: [loggingMiddleware] +}); +``` + +## Error Handling + +The client throws typed errors for different failure scenarios: + +```typescript +import { ResponseError, FetchError, RequiredError } from 'nested-nullable-api'; + +try { + const result = await api.someMethod(); +} catch (error) { + if (error instanceof ResponseError) { + // HTTP error response (4xx, 5xx) + console.error('HTTP Error:', error.response.status); + } else if (error instanceof FetchError) { + // Network or fetch error + console.error('Network Error:', error.cause); + } else if (error instanceof RequiredError) { + // Missing required parameter + console.error('Missing field:', error.field); + } +} +``` + +## API Reference + +This package exports the following: + +- **Configuration** - Client configuration class +- **BaseAPI** - Base class for all API clients +- **API Classes** - Generated API client classes (e.g., `UserApi`, `PostApi`) +- **Models** - Generated TypeScript interfaces for request/response types +- **Errors** - `ResponseError`, `FetchError`, `RequiredError` +- **Types** - TypeScript type definitions + +## Development + +### Building + +To build the package: + +```bash +npm install +npm run build +``` + +This will compile TypeScript to JavaScript in the `dist/` directory. + +### Building for ESM + +To build ES modules: + +```bash +npm run build +``` + +## TypeScript Support + +This package includes TypeScript type definitions. No additional `@types` package is needed. + +### TypeScript Configuration + +This package works with standard TypeScript configurations. If you're using a bundler-based setup, you may want to configure: + +```json +{ + "compilerOptions": { + "moduleResolution": "bundler" + } +} +``` + +### Type Imports + +```typescript +import type { User, CreateUserRequest } from 'nested-nullable-api'; + +const user: User = { + id: 1, + name: 'John Doe', + email: 'john@example.com' +}; +``` + +## Browser Support + +This package uses the native Fetch API, which is supported in: + +- Chrome 42+ +- Firefox 39+ +- Safari 10.1+ +- Edge 14+ +- Node.js 18+ (native fetch) +- Node.js <18 (with `node-fetch` polyfill) + +For older browsers, you may need to include a fetch polyfill. + +## License + +This is an auto-generated API client. Please refer to your API documentation for license information. + +## Support + +For issues related to the API itself, please contact the API provider. + +For issues with this generated client, please check the OpenAPI specification used to generate it. + +--- + +**Generated by OpenAPI Generator** + +API Version: 1.0.0 diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/apis/ValuesApi.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/apis/ValuesApi.ts.golden new file mode 100644 index 000000000..d67d5e7e4 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/apis/ValuesApi.ts.golden @@ -0,0 +1,118 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +import type { DetailAlias } from '../models/DetailAlias'; +import type { NullablePayload } from '../models/NullablePayload'; +import type { Configuration, HTTPQuery, HttpSuccessResponse, InitOverrideFunction } from '../runtime/runtime'; +import { BaseAPI, DefaultConfig, JSONApiResponse, ResponseError } from '../runtime/runtime'; + +export type GetValuesRawResponse = + | JSONApiResponse & { status: 200 } + | JSONApiResponse & HttpSuccessResponse; + + +export type GetValuesErrorDetail = + { kind: 'badRequest'; status: 400; raw: Response; value(): Promise> } + | { kind: 'unauthorized'; status: 401; raw: Response; value(): Promise<(Record +| null)[]> } + | { kind: 'status402'; status: 402; raw: Response; value(): Promise } + | { kind: 'forbidden'; status: 403; raw: Response; value(): Promise> } + | { kind: 'notFound'; status: 404; raw: Response; value(): Promise } + | { kind: 'status410'; status: 410; raw: Response; value(): Promise } + | { kind: 'unexpected'; status: number; raw: Response; value(): Promise }; + +export class GetValuesError extends ResponseError { + readonly detail: GetValuesErrorDetail; + + constructor(response: Response, detail: GetValuesErrorDetail, msg = 'Response returned an error code') { + super(response, msg); + this.detail = detail; + } +} + + +export interface ValuesApiInterface { + getValuesRaw: (initOverrides?: RequestInit | InitOverrideFunction) => Promise; + getValues: (initOverrides?: RequestInit | InitOverrideFunction) => Promise; +} + +export class ValuesApi extends BaseAPI implements ValuesApiInterface { + /** + * Initialize the API client + */ + constructor(configuration?: Configuration) { + super(configuration ?? DefaultConfig); + } + + async getValuesRaw(initOverrides?: RequestInit + | InitOverrideFunction): Promise { + // Build path with path parameters + const urlPath = `/values`; + // Build query parameters + const queryParameters: HTTPQuery = {}; + // Build headers + const headerParameters: Record = { + }; + + // Make request + const response = await this.request({ + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + body: undefined, + }, initOverrides); + + // Handle responses + if (!(response.status >= 200 && response.status < 300)) { + const errorRaw = response.clone(); + if (response.status === 400) { + let errorValue: Promise> | undefined; + throw new GetValuesError(response, { kind: 'badRequest', status: response.status as 400, raw: response, value: () => errorValue ??= new JSONApiResponse>(errorRaw.clone()).value() }); + } + else if (response.status === 401) { + let errorValue: Promise<(Record | null)[]> | undefined; + throw new GetValuesError(response, { kind: 'unauthorized', status: response.status as 401, raw: response, value: () => errorValue ??= new JSONApiResponse<(Record + | null)[]>(errorRaw.clone()).value() }); + } + else if (response.status === 402) { + let errorValue: Promise | undefined; + throw new GetValuesError(response, { kind: 'status402', status: response.status as 402, raw: response, value: () => errorValue ??= new JSONApiResponse(errorRaw.clone()).value() }); + } + else if (response.status === 403) { + let errorValue: Promise> | undefined; + throw new GetValuesError(response, { kind: 'forbidden', status: response.status as 403, raw: response, value: () => errorValue ??= new JSONApiResponse>(errorRaw.clone()).value() }); + } + else if (response.status === 404) { + let errorValue: Promise | undefined; + throw new GetValuesError(response, { kind: 'notFound', status: response.status as 404, raw: response, value: () => errorValue ??= new JSONApiResponse(errorRaw.clone()).value() }); + } + else if (response.status === 410) { + let errorValue: Promise | undefined; + throw new GetValuesError(response, { kind: 'status410', status: response.status as 410, raw: response, value: () => errorValue ??= new JSONApiResponse(errorRaw.clone()).value() }); + } + else { + let errorValue: Promise | undefined; + throw new GetValuesError(response, { kind: 'unexpected', status: response.status, raw: response, value: () => errorValue ??= errorRaw.clone().blob() }); + } + } + if (response.status === 200) { + return new JSONApiResponse(response) as JSONApiResponse & { status: 200 }; + } + else { + return new JSONApiResponse(response) as JSONApiResponse &HttpSuccessResponse; + } + } + + async getValues(initOverrides?: RequestInit | InitOverrideFunction): Promise { + const response = await this.getValuesRaw(initOverrides); + return await response.value() as NullablePayload; + } +} diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/apis/index.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/apis/index.ts.golden new file mode 100644 index 000000000..d17c6df89 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/apis/index.ts.golden @@ -0,0 +1,7 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +export type { GetValuesRawResponse, GetValuesErrorDetail, ValuesApiInterface } from './ValuesApi'; +export { ValuesApi, GetValuesError } from './ValuesApi'; diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/index.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/index.ts.golden new file mode 100644 index 000000000..38e11717d --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/index.ts.golden @@ -0,0 +1,8 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +export * from './runtime/runtime'; +export * from './apis'; +export * from './models'; diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/models/Detail.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/models/Detail.ts.golden new file mode 100644 index 000000000..45d72a596 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/models/Detail.ts.golden @@ -0,0 +1,8 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +export interface Detail { + readonly message: string; +} diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/models/DetailAlias.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/models/DetailAlias.ts.golden new file mode 100644 index 000000000..00527526c --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/models/DetailAlias.ts.golden @@ -0,0 +1,8 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +import type { Detail } from './Detail'; + +export type DetailAlias = Detail; diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/models/MatrixContainer.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/models/MatrixContainer.ts.golden new file mode 100644 index 000000000..016b9c67a --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/models/MatrixContainer.ts.golden @@ -0,0 +1,8 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +export interface MatrixContainer { + readonly matrix: readonly string[][] | null; +} diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/models/MaybeMatrix.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/models/MaybeMatrix.ts.golden new file mode 100644 index 000000000..03875bf05 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/models/MaybeMatrix.ts.golden @@ -0,0 +1,6 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +export type MaybeMatrix = readonly string[][] | null; diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/models/NullablePayload.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/models/NullablePayload.ts.golden new file mode 100644 index 000000000..8cd6d1986 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/models/NullablePayload.ts.golden @@ -0,0 +1,12 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +export interface NullablePayload { + readonly arrayItems: readonly (string | null)[]; + readonly arrayMaps: readonly (Record | null)[]; + readonly mapValues: Record; + readonly nullableArray: readonly string[] | null; + readonly strictValues: Record; +} diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/models/index.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/models/index.ts.golden new file mode 100644 index 000000000..5e49436b7 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/models/index.ts.golden @@ -0,0 +1,10 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +export type { Detail } from './Detail'; +export type { DetailAlias } from './DetailAlias'; +export type { MatrixContainer } from './MatrixContainer'; +export type { MaybeMatrix } from './MaybeMatrix'; +export type { NullablePayload } from './NullablePayload'; diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/package.json.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/package.json.golden new file mode 100644 index 000000000..6e4680435 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/package.json.golden @@ -0,0 +1,26 @@ +{ + "description": "TypeScript client for Nested Nullable API", + "exports": { + ".": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "keywords": [ + "openapi", + "api-client", + "typescript", + "generated" + ], + "main": "./dist/index.js", + "name": "nested-nullable-api", + "scripts": { + "build": "tsc" + }, + "type": "module", + "types": "./dist/index.d.ts", + "version": "1.0.0" +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/runtime/runtime.ts.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/runtime/runtime.ts.golden new file mode 100644 index 000000000..2cb8475dc --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/runtime/runtime.ts.golden @@ -0,0 +1,490 @@ +/** + * @generated by openapi-nexus. Do not edit. + * + * Nested Nullable API — 1.0.0 + */ +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string | (() => string | Promise); // parameter for basic security + password?: string | (() => string | Promise); // parameter for basic security + apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + private configuration: ConfigurationParameters; + + constructor(configuration: ConfigurationParameters = {}) { + this.configuration = configuration; + } + + set config(configuration: ConfigurationParameters) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): (() => string | Promise) | undefined { + const username = this.configuration.username; + if (username) { + return typeof username === 'function' ? username : async () => username; + } + return undefined; + } + + get password(): (() => string | Promise) | undefined { + const password = this.configuration.password; + if (password) { + return typeof password === 'function' ? password : async () => password; + } + return undefined; + } + + get apiKey(): ((name: string) => string | Promise) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private static readonly jsonRegex = new RegExp('^(:?application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); + protected configuration: Configuration; + private middleware: Middleware[]; + + constructor(configuration: Configuration = DefaultConfig) { + this.configuration = configuration; + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + return await this.fetchApi(url, init); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const token = await this.configuration.accessToken?.(); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (!headers['Authorization']) { + const username = await this.configuration.username?.(); + const password = await this.configuration.password?.(); + if (username && password) { + headers['Authorization'] = `Basic ${btoa(`${username}:${password}`)}`; + } + } + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams: HTTPRequestInit = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + signal: context.signal, + }; + + const overriddenInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + let body: BodyInit | null | undefined; + if (isFormData(overriddenInit.body) + || (overriddenInit.body instanceof URLSearchParams) + || isBlob(overriddenInit.body)) { + body = overriddenInit.body; + } else if (this.isJsonMime(headers['Content-Type'])) { + body = JSON.stringify(overriddenInit.body); + } else { + body = overriddenInit.body as BodyInit | null | undefined; + } + + const init: RequestInit = { + ...overriddenInit, + body + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as new (configuration: Configuration) => T; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +} + +function isBlob(value: unknown): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: unknown): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name = "ResponseError" as const; + public response: Response; + + constructor(response: Response, msg?: string) { + super(msg); + this.response = response; + } +} + +export class FetchError extends Error { + override name = "FetchError" as const; + public cause: Error; + + constructor(cause: Error, msg?: string) { + super(msg); + this.cause = cause; + } +} + +export class RequiredError extends Error { + override name = "RequiredError" as const; + public field: string; + + constructor(field: string, msg?: string) { + super(msg); + this.field = field; + } +} + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = unknown; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | Blob | FormData | URLSearchParams; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody; signal?: AbortSignal }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; + signal?: AbortSignal; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function exists(json: Record, key: string): boolean { + const value = json[key]; + return value !== null && value !== undefined; +} + +export function mapValues(data: Record, fn: (item: T) => U): Record { + const result: Record = {}; + for (const key of Object.keys(data)) { + result[key] = fn(data[key]); + } + return result; +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export type HttpSuccessStatus = + | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 + | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 + | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 + | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 + | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 + | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 + | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 + | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 + | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 + | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299; + +export type HttpSuccessResponse = { status: HttpSuccessStatus }; + +export interface ResponseTransformer { + (json: unknown): T; +} + +class ApiResponseBase { + public readonly status: number; + public readonly ok: boolean; + public readonly statusText: string; + public readonly headers: Headers; + public readonly raw: Response; + + constructor(raw: Response) { + this.raw = raw; + this.status = raw.status; + this.ok = raw.ok; + this.statusText = raw.statusText; + this.headers = raw.headers; + } +} + +export class JSONApiResponse extends ApiResponseBase { + private transformer: ResponseTransformer; + + constructor(raw: Response, transformer: ResponseTransformer = (jsonValue: unknown) => jsonValue as T) { + super(raw); + this.transformer = transformer; + } + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse extends ApiResponseBase { + constructor(raw: Response) { + super(raw); + } + + async value(): Promise { + return await this.raw.text(); + }; +} diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/tsconfig.esm.json.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/tsconfig.esm.json.golden new file mode 100644 index 000000000..bb8350cb6 --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/tsconfig.esm.json.golden @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ES2020", + "outDir": "dist/esm" + }, + "extends": "./tsconfig.json" +} \ No newline at end of file diff --git a/tests/golden/typescript/typescript-fetch/nested-nullable/tsconfig.json.golden b/tests/golden/typescript/typescript-fetch/nested-nullable/tsconfig.json.golden new file mode 100644 index 000000000..4b3a8cf7f --- /dev/null +++ b/tests/golden/typescript/typescript-fetch/nested-nullable/tsconfig.json.golden @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": [ + "ES2020", + "DOM" + ], + "module": "ES2020", + "moduleResolution": "bundler", + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2020", + "typeRoots": [ + "node_modules/@types" + ] + }, + "exclude": [ + "dist", + "node_modules" + ], + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/tests/golden_tests_go_http.rs b/tests/golden_tests_go_http.rs index 4f6edd849..8b6956e3a 100644 --- a/tests/golden_tests_go_http.rs +++ b/tests/golden_tests_go_http.rs @@ -80,6 +80,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("additional-properties", "valid/additional-properties/additional-properties.yaml"), ("enum-repr", "valid/enum-repr/enum-repr.yaml"), @@ -109,6 +110,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/golden_tests_java_okhttp.rs b/tests/golden_tests_java_okhttp.rs index 04d243954..499ebd790 100644 --- a/tests/golden_tests_java_okhttp.rs +++ b/tests/golden_tests_java_okhttp.rs @@ -81,6 +81,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("additional-properties", "valid/additional-properties/additional-properties.yaml"), ("enum-repr", "valid/enum-repr/enum-repr.yaml"), @@ -111,6 +112,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/golden_tests_kotlin_okhttp.rs b/tests/golden_tests_kotlin_okhttp.rs index 93ecd1398..a6e201b4c 100644 --- a/tests/golden_tests_kotlin_okhttp.rs +++ b/tests/golden_tests_kotlin_okhttp.rs @@ -83,6 +83,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("additional-properties", "valid/additional-properties/additional-properties.yaml"), ("enum-repr", "valid/enum-repr/enum-repr.yaml"), @@ -113,6 +114,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/golden_tests_python_httpx.rs b/tests/golden_tests_python_httpx.rs index 522c78ba8..735ec290c 100644 --- a/tests/golden_tests_python_httpx.rs +++ b/tests/golden_tests_python_httpx.rs @@ -80,6 +80,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("additional-properties", "valid/additional-properties/additional-properties.yaml"), ("enum-repr", "valid/enum-repr/enum-repr.yaml"), @@ -109,6 +110,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/golden_tests_python_requests.rs b/tests/golden_tests_python_requests.rs index 9bf5e7ff1..81bbb5429 100644 --- a/tests/golden_tests_python_requests.rs +++ b/tests/golden_tests_python_requests.rs @@ -80,6 +80,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("additional-properties", "valid/additional-properties/additional-properties.yaml"), ("enum-repr", "valid/enum-repr/enum-repr.yaml"), @@ -109,6 +110,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/golden_tests_rust_aioduct.rs b/tests/golden_tests_rust_aioduct.rs index 7c7c1408c..fab425dec 100644 --- a/tests/golden_tests_rust_aioduct.rs +++ b/tests/golden_tests_rust_aioduct.rs @@ -63,6 +63,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("recursive-json-all-optional-properties", "valid/recursive-json/all-optional-properties.yaml"), ("recursive-json-array-of-inline-objects", "valid/recursive-json/array-of-inline-objects.yaml"), @@ -109,6 +110,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_minimal_golden: "minimal", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/golden_tests_rust_reqwest.rs b/tests/golden_tests_rust_reqwest.rs index 17951c28b..a54364923 100644 --- a/tests/golden_tests_rust_reqwest.rs +++ b/tests/golden_tests_rust_reqwest.rs @@ -63,6 +63,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("recursive-json-all-optional-properties", "valid/recursive-json/all-optional-properties.yaml"), ("recursive-json-array-of-inline-objects", "valid/recursive-json/array-of-inline-objects.yaml"), @@ -109,6 +110,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_minimal_golden: "minimal", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/golden_tests_rust_ureq.rs b/tests/golden_tests_rust_ureq.rs index ff8f96b84..392c48508 100644 --- a/tests/golden_tests_rust_ureq.rs +++ b/tests/golden_tests_rust_ureq.rs @@ -63,6 +63,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("recursive-json-all-optional-properties", "valid/recursive-json/all-optional-properties.yaml"), ("recursive-json-array-of-inline-objects", "valid/recursive-json/array-of-inline-objects.yaml"), @@ -109,6 +110,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_minimal_golden: "minimal", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/golden_tests_typescript_fetch.rs b/tests/golden_tests_typescript_fetch.rs index 984ed783d..638f665be 100644 --- a/tests/golden_tests_typescript_fetch.rs +++ b/tests/golden_tests_typescript_fetch.rs @@ -81,6 +81,7 @@ fn get_golden_test_cases() -> HashMap<&'static str, &'static str> { ("response-body-multi-status-responses", "valid/response-body/multi-status-responses.yaml"), ("response-body-no-response-body", "valid/response-body/no-response-body.yaml"), ("typed-error-responses", "valid/typed-error-responses.yaml"), + ("nested-nullable", "valid/nested-nullable.yaml"), ("additional-properties", "valid/additional-properties/additional-properties.yaml"), ("enum-repr", "valid/enum-repr/enum-repr.yaml"), @@ -110,6 +111,7 @@ macro_rules! generate_golden_tests { } generate_golden_tests! { + test_nested_nullable_golden: "nested-nullable", test_petstore_golden: "petstore", test_comprehensive_schemas_golden: "comprehensive-schemas", diff --git a/tests/nested_nullable_generation.rs b/tests/nested_nullable_generation.rs new file mode 100644 index 000000000..44cb05312 --- /dev/null +++ b/tests/nested_nullable_generation.rs @@ -0,0 +1,22 @@ +//! Source contracts through the public generator boundary. + +use openapi_nexus::generators::typescript::fetch::TypeScriptFetchCodeGenerator; +use openapi_nexus::test_utils::{generate_files, read_fixture}; + +#[test] +fn typescript_nullable_matrices_keep_array_precedence() { + let generator = TypeScriptFetchCodeGenerator::new(toml::value::Table::new()); + let files = generate_files(&generator, &read_fixture("valid/nested-nullable.yaml")) + .expect("nullable matrix fixture generates"); + assert!( + files["models/MaybeMatrix.ts"] + .contains("export type MaybeMatrix = readonly string[][] | null;"), + "nullable matrix alias has incorrect array syntax: {}", + files["models/MaybeMatrix.ts"] + ); + assert!( + files["models/MatrixContainer.ts"].contains("readonly matrix: readonly string[][] | null;"), + "nullable matrix field has incorrect array syntax: {}", + files["models/MatrixContainer.ts"] + ); +} diff --git a/tests/nested_nullable_lowering.rs b/tests/nested_nullable_lowering.rs new file mode 100644 index 000000000..6cc9497a6 --- /dev/null +++ b/tests/nested_nullable_lowering.rs @@ -0,0 +1,234 @@ +//! Nested schema semantics through the public parser/lowering boundary. + +use openapi_nexus::ir::types::{IrPrimitive, IrSchemaKind, IrTypeExpr}; + +fn response_type(version: &str, schema: &str) -> IrTypeExpr { + let schema = schema + .lines() + .map(|line| format!(" {line}")) + .collect::>() + .join("\n"); + let source = format!( + "openapi: {version}\ninfo:\n title: Nested Nullable\n version: '1'\npaths:\n /values:\n get:\n operationId: getValues\n responses:\n '400':\n description: Error\n content:\n application/json:\n schema:\n{schema}\n" + ); + let parsed = openapi_nexus::parser::parse_content_yaml(&source).expect("schema parses"); + let ir = openapi_nexus::ir::lower::lower(parsed).expect("schema lowers"); + ir.operations[0].responses[0].content["application/json"].clone() +} + +#[test] +fn oas30_map_values_keep_nullable_arrays() { + let actual = response_type( + "3.0.3", + "type: object\nadditionalProperties:\n type: array\n nullable: true\n items:\n type: string", + ); + assert_eq!( + actual, + IrTypeExpr::Map(Box::new(IrTypeExpr::Nullable(Box::new(IrTypeExpr::Array( + Box::new(IrTypeExpr::Primitive(IrPrimitive::String)), + ))))) + ); +} + +fn assert_nullable_map_values(version: &str) { + assert_eq!( + response_type( + version, + "type: object\nadditionalProperties:\n type: [array, 'null']\n items:\n type: string", + ), + IrTypeExpr::Map(Box::new(IrTypeExpr::Nullable(Box::new(IrTypeExpr::Array( + Box::new(IrTypeExpr::Primitive(IrPrimitive::String)), + ))))) + ); +} + +#[test] +fn oas31_map_values_keep_nullable_arrays() { + assert_nullable_map_values("3.1.0"); +} + +#[test] +fn oas32_map_values_keep_nullable_arrays() { + assert_nullable_map_values("3.2.0"); +} + +#[test] +fn nested_nullability_stays_on_the_declared_node() { + let string = IrTypeExpr::Primitive(IrPrimitive::String); + let cases = [ + ( + "type: array\nitems:\n type: string\n nullable: true", + "type: array\nitems:\n type: [string, 'null']", + IrTypeExpr::Array(Box::new(IrTypeExpr::Nullable(Box::new(string.clone())))), + ), + ( + "type: array\nitems:\n type: object\n nullable: true\n additionalProperties:\n type: string", + "type: array\nitems:\n type: [object, 'null']\n additionalProperties:\n type: string", + IrTypeExpr::Array(Box::new(IrTypeExpr::Nullable(Box::new(IrTypeExpr::Map( + Box::new(string.clone()), + ))))), + ), + ( + "type: array\nnullable: true\nitems:\n type: object\n additionalProperties:\n type: string", + "type: [array, 'null']\nitems:\n type: object\n additionalProperties:\n type: string", + IrTypeExpr::Nullable(Box::new(IrTypeExpr::Array(Box::new(IrTypeExpr::Map( + Box::new(string.clone()), + ))))), + ), + ( + "type: object\nnullable: true\nadditionalProperties:\n type: array\n items:\n type: string", + "type: [object, 'null']\nadditionalProperties:\n type: array\n items:\n type: string", + IrTypeExpr::Nullable(Box::new(IrTypeExpr::Map(Box::new(IrTypeExpr::Array( + Box::new(string.clone()), + ))))), + ), + ( + "type: object\nadditionalProperties:\n type: array\n items:\n type: string", + "type: object\nadditionalProperties:\n type: array\n items:\n type: string", + IrTypeExpr::Map(Box::new(IrTypeExpr::Array(Box::new(string.clone())))), + ), + ]; + for (oas30, oas31, expected) in cases { + for (version, schema) in [("3.0.3", oas30), ("3.1.0", oas31), ("3.2.0", oas31)] { + assert_eq!( + response_type(version, schema), + expected, + "{version}: {schema}" + ); + } + } +} + +#[test] +fn promoted_array_items_keep_container_and_item_nullability() { + for (version, schema) in [ + ( + "3.0.3", + "type: array\nnullable: true\nitems:\n type: object\n nullable: true\n properties:\n message:\n type: string", + ), + ( + "3.1.0", + "type: [array, 'null']\nitems:\n type: [object, 'null']\n properties:\n message:\n type: string", + ), + ( + "3.2.0", + "type: [array, 'null']\nitems:\n type: [object, 'null']\n properties:\n message:\n type: string", + ), + ] { + let actual = response_type(version, schema); + let IrTypeExpr::Nullable(array) = actual else { + panic!("{version}: nullable container lost: {actual:?}") + }; + let IrTypeExpr::Array(item) = *array else { + panic!("expected array") + }; + assert!( + matches!(*item, IrTypeExpr::Nullable(ref inner) if matches!(**inner, IrTypeExpr::Named(_))), + "{version}: nullable promoted item lost: {item:?}" + ); + } +} + +#[test] +fn named_nullable_array_aliases_keep_the_container_wrapper() { + for (version, declaration) in [ + ("3.0.3", "type: array\n nullable: true"), + ("3.1.0", "type: [array, 'null']"), + ("3.2.0", "type: [array, 'null']"), + ] { + let yaml = format!( + "openapi: {version}\ninfo:\n title: Nullable Alias\n version: '1'\npaths: {{}}\ncomponents:\n schemas:\n MaybeValues:\n {declaration}\n items:\n type: string\n" + ); + let parsed = openapi_nexus::parser::parse_content_yaml(&yaml).expect("alias schema parses"); + let ir = openapi_nexus::ir::lower::lower(parsed).expect("alias schema lowers"); + let IrSchemaKind::Alias(alias) = &ir.schemas["MaybeValues"].kind else { + panic!("expected array alias") + }; + assert_eq!( + alias, + &IrTypeExpr::Nullable(Box::new(IrTypeExpr::Array(Box::new( + IrTypeExpr::Primitive(IrPrimitive::String) + )))), + "{version}" + ); + } +} + +#[test] +fn nested_type_sets_preserve_all_non_null_members() { + for version in ["3.1.0", "3.2.0"] { + let actual = response_type( + version, + "type: array\nitems:\n type: [string, integer, 'null']", + ); + assert_eq!( + actual, + IrTypeExpr::Array(Box::new(IrTypeExpr::Nullable(Box::new(IrTypeExpr::Union( + vec![ + IrTypeExpr::Primitive(IrPrimitive::String), + IrTypeExpr::Primitive(IrPrimitive::Integer), + ] + ))))), + "{version}" + ); + } +} + +#[test] +fn nested_nullable_union_wrappers_match_top_level_semantics() { + for version in ["3.1.0", "3.2.0"] { + for keyword in ["oneOf", "anyOf"] { + for (container, expected) in [ + ( + "type: array\nitems:", + IrTypeExpr::Array(Box::new(IrTypeExpr::Nullable(Box::new( + IrTypeExpr::Primitive(IrPrimitive::String), + )))), + ), + ( + "type: object\nadditionalProperties:", + IrTypeExpr::Map(Box::new(IrTypeExpr::Nullable(Box::new( + IrTypeExpr::Primitive(IrPrimitive::String), + )))), + ), + ] { + let schema = + format!("{container}\n {keyword}:\n - type: 'null'\n - type: string"); + assert_eq!( + response_type(version, &schema), + expected, + "{version}: {keyword}: {container}" + ); + } + } + } +} + +#[test] +fn named_map_values_keep_nullable_union_wrappers() { + for version in ["3.1.0", "3.2.0"] { + for keyword in ["oneOf", "anyOf"] { + for members in [ + "[{type: 'null'}, {type: string}]", + "[{type: string}, {type: 'null'}]", + ] { + let yaml = format!( + "openapi: {version}\ninfo:\n title: Named Nullable Map\n version: '1'\npaths: {{}}\ncomponents:\n schemas:\n NullableStrings:\n type: object\n additionalProperties:\n {keyword}: {members}\n" + ); + let parsed = + openapi_nexus::parser::parse_content_yaml(&yaml).expect("named map parses"); + let ir = openapi_nexus::ir::lower::lower(parsed).expect("named map lowers"); + let IrSchemaKind::Object(map) = &ir.schemas["NullableStrings"].kind else { + panic!("named map should keep its object schema"); + }; + assert_eq!( + map.additional_properties, + Some(IrTypeExpr::Nullable(Box::new(IrTypeExpr::Primitive( + IrPrimitive::String + )))), + "{version}: {keyword}: {members}" + ); + } + } + } +} diff --git a/tests/typed_http_error_runtime.rs b/tests/typed_http_error_runtime.rs deleted file mode 100644 index 899456e46..000000000 --- a/tests/typed_http_error_runtime.rs +++ /dev/null @@ -1,661 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::{SystemTime, UNIX_EPOCH}; - -use openapi_nexus::codegen::traits::code_generator::CodeGenerator; -use openapi_nexus::codegen::traits::file_writer::FileWriter; -use openapi_nexus::generators::rust::aioduct::RustAioductCodeGenerator; -use openapi_nexus::generators::rust::reqwest::RustReqwestCodeGenerator; -use openapi_nexus::generators::rust::ureq::RustUreqCodeGenerator; -use openapi_nexus::test_utils::{generate_files, read_fixture}; - -fn empty_config() -> toml::value::Table { - toml::value::Table::new() -} - -struct TempDir { - path: PathBuf, -} - -impl TempDir { - fn new(name: &str) -> Self { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock should be after UNIX_EPOCH") - .as_nanos(); - let path = std::env::temp_dir().join(format!( - "openapi-nexus-{name}-{}-{nanos}", - std::process::id() - )); - fs::create_dir_all(&path).expect("temporary crate directory should be created"); - Self { path } - } -} - -impl Drop for TempDir { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.path); - } -} - -fn write_generated_crate(files: &HashMap, root: &Path) { - for (relative_path, contents) in files { - let path = root.join(relative_path); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).expect("generated parent directory should be created"); - } - fs::write(path, contents).expect("generated file should be written"); - } -} - -const RUST_TEST_SERVER: &str = r##" -use std::io::{Read, Write}; -use std::net::TcpListener; -use std::thread; - -fn spawn_one_response_server(status: &str, body: &'static str) -> String { - let listener = TcpListener::bind("127.0.0.1:0").expect("test server should bind"); - let addr = listener.local_addr().expect("test server address should be readable"); - let status = status.to_string(); - thread::spawn(move || { - let (mut stream, _) = listener.accept().expect("test server should accept one request"); - let mut request = [0_u8; 4096]; - let _ = stream.read(&mut request); - let response = format!( - "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nRetry-After: 120\r\nX-Request-Id: typed-response-test\r\nX-Trace-Id: typed-error-test\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len() - ); - stream - .write_all(response.as_bytes()) - .expect("test response should be written"); - }); - format!("http://{addr}") -} - -fn spawn_incomplete_success_body_server() -> String { - let listener = TcpListener::bind("127.0.0.1:0").expect("test server should bind"); - let addr = listener.local_addr().expect("test server address should be readable"); - thread::spawn(move || { - let (mut stream, _) = listener.accept().expect("test server should accept one request"); - let mut request = [0_u8; 4096]; - let _ = stream.read(&mut request); - let response = "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nX-Request-Id: typed-response-test\r\nContent-Length: 999999\r\nConnection: close\r\n\r\n"; - stream - .write_all(response.as_bytes()) - .expect("test response headers should be written"); - }); - format!("http://{addr}") -} -"##; - -fn add_reqwest_runtime_test(root: &Path) { - let cargo_toml = root.join("Cargo.toml"); - let mut manifest = - fs::read_to_string(&cargo_toml).expect("generated Cargo.toml should be readable"); - manifest.push_str( - r#" - -[dev-dependencies] -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } -thiserror = "2" -snafu = "0.9.1" -"#, - ); - fs::write(cargo_toml, manifest).expect("generated Cargo.toml should be updated"); - - let tests_dir = root.join("tests"); - fs::create_dir_all(&tests_dir).expect("generated tests directory should be created"); - fs::write( - tests_dir.join("http_error.rs"), - [ - RUST_TEST_SERVER, - r##" -use typed_error_responses_api::apis::{ - CreateResourceError, CreateResourceWithWildcardSuccessError, ResourcesApi, -}; -use typed_error_responses_api::models::CreateResourceRequest; -use typed_error_responses_api::runtime::client::Client; -use typed_error_responses_api::runtime::error::{ApiCallError, Error}; - -#[test] -fn api_call_error_is_pointer_sized() { - assert_eq!( - std::mem::size_of::(), - std::mem::size_of::>() - ); -} - -#[allow(dead_code)] -async fn propagate_uniformly( - api: &ResourcesApi<'_>, - request: &CreateResourceRequest, -) -> Result<(), ApiCallError> { - api.create_resource(request).await?; - api.check_no_success_body().await?; - Ok(()) -} - -#[derive(Debug, thiserror::Error)] -#[error("SDK request failed: {source}")] -struct ThisErrorAppError { - #[source] - source: ApiCallError, -} - -impl From for ThisErrorAppError -where - E: Into, -{ - fn from(error: E) -> Self { - Self { - source: error.into(), - } - } -} - -#[allow(dead_code)] -async fn propagate_with_thiserror( - api: &ResourcesApi<'_>, - request: &CreateResourceRequest, -) -> Result<(), ThisErrorAppError> { - api.create_resource(request).await?; - api.check_no_success_body().await?; - Ok(()) -} - -#[derive(Debug, snafu::Snafu)] -#[snafu(context(false))] -struct SnafuAppError { - #[snafu(source(from(generic)))] - source: ApiCallError, -} - -#[allow(dead_code)] -async fn propagate_with_snafu( - api: &ResourcesApi<'_>, - request: &CreateResourceRequest, -) -> Result<(), SnafuAppError> { - api.create_resource(request).await?; - api.check_no_success_body().await?; - Ok(()) -} - -#[tokio::test] -async fn http_503_returns_operation_error_with_typed_body() { - let body = r#"{"message":"temporarily unavailable","retryable":true}"#; - let base_url = spawn_one_response_server("503 Service Unavailable", body); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - let request = CreateResourceRequest { - name: "resource".to_string(), - }; - - let err = api - .create_resource(&request) - .await - .expect_err("HTTP 503 must not be returned as success"); - assert_eq!(err.retry_after_header(), Some("120")); - - match err { - CreateResourceError::ServerError(api_error) => { - assert_eq!(api_error.status_code(), 503); - assert_eq!(api_error.raw_body(), body.as_bytes()); - assert_eq!( - api_error - .headers() - .get("retry-after") - .and_then(|value| value.to_str().ok()), - Some("120") - ); - assert_eq!( - api_error - .headers() - .get("x-trace-id") - .and_then(|value| value.to_str().ok()), - Some("typed-error-test") - ); - let typed_body = api_error - .body() - .expect("documented 5XX error body should decode lazily"); - assert_eq!(typed_body.message, "temporarily unavailable"); - assert!(typed_body.retryable); - } - other => panic!("expected ServerError variant, got {other:?}"), - } -} - -#[tokio::test] -async fn invalid_error_body_keeps_http_error_and_raw_body() { - let body = r#"{"message":42,"retryable":true}"#; - let base_url = spawn_one_response_server("503 Service Unavailable", body); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - let request = CreateResourceRequest { - name: "resource".to_string(), - }; - - let err = api - .create_resource(&request) - .await - .expect_err("HTTP 503 must remain an operation error even when its body is invalid"); - - match err { - CreateResourceError::ServerError(api_error) => { - assert_eq!(api_error.status_code(), 503); - assert_eq!(api_error.raw_body(), body.as_bytes()); - assert!(matches!(api_error.body(), Err(Error::Deserialize(_)))); - - let error: ApiCallError = CreateResourceError::ServerError(api_error).into(); - assert_eq!(error.operation_id(), "createResource"); - assert_eq!(error.status_code(), Some(503)); - assert_eq!(error.raw_body(), Some(body.as_bytes())); - assert!(matches!( - std::error::Error::source(&error).and_then(|source| source.downcast_ref::()), - Some(Error::Deserialize(_)) - )); - } - other => panic!("expected ServerError variant, got {other:?}"), - } -} - -#[tokio::test] -async fn operation_error_converts_to_uniform_api_call_error() { - let body = r#"{"message":"temporarily unavailable","retryable":true}"#; - let base_url = spawn_one_response_server("503 Service Unavailable", body); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - let request = CreateResourceRequest { - name: "resource".to_string(), - }; - - let error: ApiCallError = api - .create_resource(&request) - .await - .expect_err("HTTP 503 must be returned as an operation error") - .into(); - - assert_eq!(error.operation_id(), "createResource"); - assert_eq!(error.status_code(), Some(503)); - assert_eq!(error.raw_body(), Some(body.as_bytes())); - assert_eq!( - error - .headers() - .and_then(|headers| headers.get("retry-after")) - .and_then(|value| value.to_str().ok()), - Some("120") - ); - assert_eq!( - error.to_string(), - "operation createResource failed with HTTP status 503" - ); - assert!(std::error::Error::source(&error).is_none()); -} - -#[test] -fn transport_error_converts_to_uniform_api_call_error() { - let error: ApiCallError = - CreateResourceError::Transport(Error::Unsupported("test transport failure")).into(); - - assert_eq!(error.operation_id(), "createResource"); - assert_eq!(error.status_code(), None); - assert!(error.headers().is_none()); - assert!(error.raw_body().is_none()); - assert!(matches!( - std::error::Error::source(&error).and_then(|source| source.downcast_ref::()), - Some(Error::Unsupported("test transport failure")) - )); -} - -#[tokio::test] -async fn exact_success_status_wins_over_2xx_wildcard() { - let body = r#"{"id":"exact-created"}"#; - let base_url = spawn_one_response_server("201 Created", body); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - let request = CreateResourceRequest { - name: "resource".to_string(), - }; - - let response = api - .create_resource_with_wildcard_success(&request) - .await - .expect("HTTP 201 should be a successful exact response"); - - let created = response - .created - .expect("201 must match the exact branch before 2XX wildcard"); - assert_eq!(created.id, "exact-created"); - assert!(response.status_2xx.is_none()); - assert_eq!( - response - .headers - .get("x-request-id") - .and_then(|value| value.to_str().ok()), - Some("typed-response-test") - ); -} - -#[tokio::test] -async fn unexpected_non_success_status_uses_unexpected_detail() { - let body = r#"{"message":"teapot"}"#; - let base_url = spawn_one_response_server("418 I'm a teapot", body); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - let request = CreateResourceRequest { - name: "resource".to_string(), - }; - - let err = api - .create_resource_with_wildcard_success(&request) - .await - .expect_err("undocumented HTTP 418 must not be returned as success"); - - match err { - CreateResourceWithWildcardSuccessError::Unexpected(api_error) => { - assert_eq!(api_error.status_code(), 418); - assert_eq!(api_error.raw_body(), body.as_bytes()); - } - other => panic!("expected Unexpected variant, got {other:?}"), - } -} - -#[tokio::test] -async fn success_without_documented_body_does_not_drain_body() { - let base_url = spawn_incomplete_success_body_server(); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - - let response = api - .check_no_success_body() - .await - .expect("success without a documented body must not read an incomplete body"); - - assert_eq!(response.status_code, 200); - assert_eq!(response.x_request_id_header(), Some("typed-response-test")); - assert_eq!( - response - .headers - .get("x-request-id") - .and_then(|value| value.to_str().ok()), - Some("typed-response-test") - ); -} -"##, - ] - .join("\n"), - ) - .expect("generated runtime test should be written"); -} - -fn add_ureq_runtime_test(root: &Path) { - let tests_dir = root.join("tests"); - fs::create_dir_all(&tests_dir).expect("generated tests directory should be created"); - fs::write( - tests_dir.join("http_error.rs"), - [ - RUST_TEST_SERVER, - r##" -use typed_error_responses_api::apis::{CreateResourceError, ResourcesApi}; -use typed_error_responses_api::models::CreateResourceRequest; -use typed_error_responses_api::runtime::client::Client; -use typed_error_responses_api::runtime::error::ApiCallError; - -#[test] -fn api_call_error_is_pointer_sized() { - assert_eq!( - std::mem::size_of::(), - std::mem::size_of::>() - ); -} - -#[allow(dead_code)] -fn propagate_uniformly( - api: &ResourcesApi<'_>, - request: &CreateResourceRequest, -) -> Result<(), ApiCallError> { - api.create_resource(request)?; - api.check_no_success_body()?; - Ok(()) -} - -#[test] -fn http_503_returns_operation_error_with_typed_body() { - let body = r#"{"message":"temporarily unavailable","retryable":true}"#; - let base_url = spawn_one_response_server("503 Service Unavailable", body); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - let request = CreateResourceRequest { - name: "resource".to_string(), - }; - - let err = api - .create_resource(&request) - .expect_err("HTTP 503 must not be returned as transport or success"); - assert_eq!(err.retry_after_header(), Some("120")); - - match err { - CreateResourceError::ServerError(api_error) => { - assert_eq!(api_error.status_code(), 503); - assert_eq!(api_error.raw_body(), body.as_bytes()); - assert_eq!( - api_error - .headers() - .get("retry-after") - .and_then(|value| value.to_str().ok()), - Some("120") - ); - assert_eq!( - api_error - .headers() - .get("x-trace-id") - .and_then(|value| value.to_str().ok()), - Some("typed-error-test") - ); - let typed_body = api_error - .body() - .expect("documented 5XX error body should decode"); - assert_eq!(typed_body.message, "temporarily unavailable"); - assert!(typed_body.retryable); - } - other => panic!("expected ServerError variant, got {other:?}"), - } -} - -#[test] -fn operation_error_converts_to_uniform_api_call_error() { - let body = r#"{"message":"temporarily unavailable","retryable":true}"#; - let base_url = spawn_one_response_server("503 Service Unavailable", body); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - let request = CreateResourceRequest { - name: "resource".to_string(), - }; - - let error: ApiCallError = api - .create_resource(&request) - .expect_err("HTTP 503 must be returned as an operation error") - .into(); - - assert_eq!(error.operation_id(), "createResource"); - assert_eq!(error.status_code(), Some(503)); - assert_eq!(error.raw_body(), Some(body.as_bytes())); - assert_eq!( - error - .headers() - .and_then(|headers| headers.get("retry-after")) - .and_then(|value| value.to_str().ok()), - Some("120") - ); -} - -#[test] -fn success_without_documented_body_does_not_drain_body() { - let base_url = spawn_incomplete_success_body_server(); - let client = Client::new(&base_url); - let api = ResourcesApi::new(&client); - - let response = api - .check_no_success_body() - .expect("success without a documented body must not read an incomplete body"); - - assert_eq!(response.status_code, 200); - assert_eq!(response.x_request_id_header(), Some("typed-response-test")); - assert_eq!( - response - .headers - .get("x-request-id") - .and_then(|value| value.to_str().ok()), - Some("typed-response-test") - ); -} -"##, - ] - .join("\n"), - ) - .expect("generated runtime test should be written"); -} - -fn add_aioduct_layout_test(root: &Path) { - let tests_dir = root.join("tests"); - fs::create_dir_all(&tests_dir).expect("generated tests directory should be created"); - fs::write( - tests_dir.join("api_call_error_layout.rs"), - r#"use typed_error_responses_api::runtime::error::ApiCallError; - -#[test] -fn api_call_error_is_pointer_sized() { - assert_eq!( - std::mem::size_of::(), - std::mem::size_of::>() - ); -} -"#, - ) - .expect("generated layout test should be written"); -} - -#[test] -fn reqwest_http_503_is_not_success_and_preserves_error_detail() { - let fixture = read_fixture("valid/typed-error-responses.yaml"); - let files = generate_files(&RustReqwestCodeGenerator::new(empty_config()), &fixture) - .expect("typed error fixture should generate"); - let temp = TempDir::new("typed-http-error-reqwest"); - write_generated_crate(&files, &temp.path); - add_reqwest_runtime_test(&temp.path); - - let output = Command::new("cargo") - .arg("test") - .arg("--quiet") - .current_dir(&temp.path) - .output() - .expect("generated crate cargo test should run"); - - assert!( - output.status.success(), - "generated reqwest runtime test failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} - -#[test] -fn ureq_http_503_is_not_transport_and_preserves_error_detail() { - let fixture = read_fixture("valid/typed-error-responses.yaml"); - let files = generate_files(&RustUreqCodeGenerator::new(empty_config()), &fixture) - .expect("typed error fixture should generate"); - let temp = TempDir::new("typed-http-error-ureq"); - write_generated_crate(&files, &temp.path); - add_ureq_runtime_test(&temp.path); - - let output = Command::new("cargo") - .arg("test") - .arg("--quiet") - .current_dir(&temp.path) - .output() - .expect("generated crate cargo test should run"); - - assert!( - output.status.success(), - "generated ureq runtime test failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} - -#[test] -fn aioduct_api_call_error_is_pointer_sized() { - let fixture = read_fixture("valid/typed-error-responses.yaml"); - let files = generate_files(&RustAioductCodeGenerator::new(empty_config()), &fixture) - .expect("typed error fixture should generate"); - let temp = TempDir::new("api-call-error-layout-aioduct"); - write_generated_crate(&files, &temp.path); - add_aioduct_layout_test(&temp.path); - - let output = Command::new("cargo") - .arg("test") - .arg("--quiet") - .current_dir(&temp.path) - .output() - .expect("generated crate cargo test should run"); - - assert!( - output.status.success(), - "generated aioduct layout test failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} - -fn check_error_payload_crate(generator: &(impl CodeGenerator + FileWriter)) { - let fixture = read_fixture("valid/rust-error-payload-shapes.yaml"); - let files = generate_files(generator, &fixture).expect("composite error fixture generates"); - let temp = TempDir::new(&format!("error-payload-{}", generator.generator_type())); - write_generated_crate(&files, &temp.path); - - let output = Command::new("cargo") - .arg("check") - .arg("--quiet") - .current_dir(&temp.path) - .output() - .expect("generated error payload crate cargo check should run"); - assert!( - output.status.success(), - "{} error payload crate failed to compile\nstatus: {}\nstdout:\n{}\nstderr:\n{}", - generator.generator_type(), - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} - -// Value: protects=reqwest generated composite error payloads compile; -// fails_when=payload type syntax, imports, aliases, or decode inference is invalid; -// why_new=existing compiled fixtures only cover named error model payloads; -// seam=CodeGenerator::generate and cargo check; no_claim=live HTTP decoding. -#[test] -fn reqwest_composite_error_payloads_compile() { - check_error_payload_crate(&RustReqwestCodeGenerator::new(empty_config())); -} - -// Value: protects=ureq generated composite error payloads compile; -// fails_when=payload type syntax, imports, aliases, or decode inference is invalid; -// why_new=existing compiled fixtures only cover named error model payloads; -// seam=CodeGenerator::generate and cargo check; no_claim=live HTTP decoding. -#[test] -fn ureq_composite_error_payloads_compile() { - check_error_payload_crate(&RustUreqCodeGenerator::new(empty_config())); -} - -// Value: protects=aioduct generated composite error payloads compile; -// fails_when=payload types or generic backend bodies cannot type-check; -// why_new=existing aioduct layout test only covers named error payloads; -// seam=CodeGenerator::generate and cargo check; no_claim=live HTTP decoding. -#[test] -fn aioduct_composite_error_payloads_compile() { - check_error_payload_crate(&RustAioductCodeGenerator::new(empty_config())); -}