@@ -338,6 +338,21 @@ const TEST_TABLE: &[Feature] = &[
338338 status : Status :: Supported ,
339339 fixture : Some ( "type_name_collisions" ) ,
340340 } ,
341+ Feature {
342+ element : "naming.prelude-value-names" ,
343+ status : Status :: Supported ,
344+ fixture : Some ( "prelude_value_names" ) ,
345+ } ,
346+ Feature {
347+ element : "naming.prelude-type-names" ,
348+ status : Status :: Unsupported ,
349+ fixture : Some ( "unsupported_prelude_shadowing" ) ,
350+ } ,
351+ Feature {
352+ element : "naming.prelude-type-names.target-scoped" ,
353+ status : Status :: Supported ,
354+ fixture : Some ( "prelude_result_name" ) ,
355+ } ,
341356 // Document-level (server/client generation). The axum server generator now
342357 // covers a slice of paths/parameters/requestBody/responses; the blocking
343358 // reqwest client generator additionally covers securitySchemes. Those slices
@@ -501,6 +516,7 @@ const CLIENT_UNSUPPORTED_FIXTURES: &[&str] = &[
501516/// flat crate-root layout in which the server and client share one file and the
502517/// same per-operation types alongside the component models.
503518const COMBINED_FIXTURES : & [ & str ] = & [
519+ "combined_prelude_value_names" ,
504520 "combined_server_client" ,
505521 "combined_response_name_collision" ,
506522 "combined_x_rust_derive" ,
@@ -630,6 +646,8 @@ generated_tests!(
630646 number_formats,
631647 object_additional_properties,
632648 object_deny_unknown_fields,
649+ prelude_result_name,
650+ prelude_value_names,
633651 schema_defaults,
634652 object_nested_inline,
635653 object_optional_required,
@@ -1157,6 +1175,7 @@ macro_rules! combined_generated_tests {
11571175}
11581176
11591177combined_generated_tests ! (
1178+ combined_prelude_value_names,
11601179 combined_server_client,
11611180 combined_response_name_collision,
11621181 combined_x_rust_derive,
@@ -1426,6 +1445,82 @@ fn operation_types_that_take_client_reserved_names_fail() {
14261445 ) ;
14271446}
14281447
1448+ /// A model that takes the name of a prelude type the file writes without a path
1449+ /// must stop generation. Such a model emits one item and duplicates nothing, so
1450+ /// no other collision check sees it. It shadows the prelude instead, and the
1451+ /// generated file stops compiling.
1452+ #[ test]
1453+ fn a_model_that_shadows_a_prelude_type_fails ( ) {
1454+ let fixture = tests_dir ( ) . join ( "fixtures" ) . join ( "unsupported_prelude_shadowing.yaml" ) ;
1455+ let err = oapi_codegen:: generate_models_string ( & fixture) . expect_err ( "a model named `Option` must stop generation" ) ;
1456+ let oapi_codegen:: Error :: Validation { problems } = & err else {
1457+ panic ! ( "expected an aggregated Validation error, got: {err:?}" ) ;
1458+ } ;
1459+ // One run reports every shadowed name, so the author fixes them together.
1460+ assert_eq ! ( problems. len( ) , 4 , "expected every shadowed name, got: {problems:?}" ) ;
1461+ let report = err. to_string ( ) ;
1462+ for name in [ "Option" , "String" , "Vec" , "Box" ] {
1463+ assert ! ( report. contains( name) , "the report must name `{name}`, got: {report}" ) ;
1464+ }
1465+ }
1466+
1467+ /// The remedy must name a rename, because the prelude name is fixed.
1468+ #[ test]
1469+ fn the_prelude_shadowing_hint_offers_a_rename ( ) {
1470+ let fixture = tests_dir ( ) . join ( "fixtures" ) . join ( "unsupported_prelude_shadowing.yaml" ) ;
1471+ let err = oapi_codegen:: generate_models_string ( & fixture) . expect_err ( "a shadowing model must stop generation" ) ;
1472+ let oapi_codegen:: Error :: Validation { problems } = & err else {
1473+ panic ! ( "expected an aggregated Validation error, got: {err:?}" ) ;
1474+ } ;
1475+ let first = problems. first ( ) . expect ( "at least one problem" ) ;
1476+ let oapi_codegen:: Error :: PreludeShadowing { hint, used_for, .. } = first else {
1477+ panic ! ( "expected PreludeShadowing, got: {first:?}" ) ;
1478+ } ;
1479+ assert ! (
1480+ hint. contains( "x-rust-name" ) ,
1481+ "the hint must offer a rename, got: {hint}"
1482+ ) ;
1483+ assert ! ( !used_for. is_empty( ) , "the problem must say what needs the name" ) ;
1484+ }
1485+
1486+ /// A prelude name is only held when the run writes it. Models name no `Result`,
1487+ /// so a model of that name generates. A server writes `Result` in every method
1488+ /// signature, so the same document fails there.
1489+ #[ test]
1490+ fn prelude_name_check_is_target_scoped ( ) {
1491+ let fixture = tests_dir ( ) . join ( "fixtures" ) . join ( "prelude_result_name.yaml" ) ;
1492+ oapi_codegen:: generate_models_string ( & fixture) . expect ( "models alone name no `Result`" ) ;
1493+ let err = oapi_codegen:: generate ( & fixture, & server_config ( ) ) . expect_err ( "a server writes `Result` in every method" ) ;
1494+ assert ! (
1495+ matches!( err, oapi_codegen:: Error :: PreludeShadowing { .. } ) ,
1496+ "expected PreludeShadowing, got: {err:?}" ,
1497+ ) ;
1498+ }
1499+
1500+ /// A value name is not a type name. `Ok`, `Err`, `Some`, and `None` name values,
1501+ /// and a *braced* `struct` takes a type name only, so those four must generate.
1502+ /// The generated file under `tests/generated` compiles as proof.
1503+ ///
1504+ /// This covers models-only output. `combined_prelude_value_names` covers the
1505+ /// harder case, where a server and a client write `Ok(..)`, `Err(..)`, `Some(..)`,
1506+ /// and `None` around models of those names. Both rest on
1507+ /// `every_generated_struct_is_braced`.
1508+ #[ test]
1509+ fn a_model_named_after_a_prelude_value_generates ( ) {
1510+ let fixture = tests_dir ( ) . join ( "fixtures" ) . join ( "prelude_value_names.yaml" ) ;
1511+ let code = oapi_codegen:: generate_models_string ( & fixture) . expect ( "a prelude value name is free" ) ;
1512+ assert ! (
1513+ code. contains( "pub struct Ok" ) && code. contains( "pub struct None" ) ,
1514+ "the models must keep their names, got: {code}" ,
1515+ ) ;
1516+ // The same file still writes `Option` for an optional field, which proves the
1517+ // four names left it alone.
1518+ assert ! (
1519+ code. contains( "pub maybe: Option<String>" ) ,
1520+ "the file must still name the prelude `Option`, got: {code}" ,
1521+ ) ;
1522+ }
1523+
14291524/// A reserved name is only reserved when its target is requested. The server-only
14301525/// `Api` trait must not block an operation whose response enum takes that name in a
14311526/// client-only run.
@@ -2108,6 +2203,48 @@ fn fixtures_and_test_table_agree() {
21082203 }
21092204}
21102205
2206+ /// Every generated `struct` must be braced, never a tuple or a unit `struct`.
2207+ ///
2208+ /// This is what keeps `Ok`, `Err`, `Some`, and `None` free as schema names. A
2209+ /// braced `struct` takes a type name only. A tuple or unit `struct` takes the
2210+ /// value name of that identifier too, so a model named `Ok` would then shadow the
2211+ /// prelude variant, and every `Ok(..)` the generators write would stop compiling.
2212+ ///
2213+ /// The invariant is implicit in the emitter, which writes `pub struct #name {..}`
2214+ /// at every site. This test states it, so a newtype added later fails here rather
2215+ /// than in a consumer's build.
2216+ #[ test]
2217+ fn every_generated_struct_is_braced ( ) {
2218+ let dir = tests_dir ( ) . join ( "generated" ) ;
2219+ let entries = std:: fs:: read_dir ( & dir) . expect ( "read generated dir" ) ;
2220+ let mut checked = 0_usize ;
2221+ for entry in entries {
2222+ let path = entry. expect ( "dir entry" ) . path ( ) ;
2223+ if !path. extension ( ) . is_some_and ( |ext| {
2224+ return ext == "rs" ;
2225+ } ) {
2226+ continue ;
2227+ }
2228+ let source = std:: fs:: read_to_string ( & path) . expect ( "read generated file" ) ;
2229+ for line in source. lines ( ) {
2230+ let Some ( rest) = line. trim_start ( ) . strip_prefix ( "pub struct " ) else {
2231+ continue ;
2232+ } ;
2233+ checked += 1 ;
2234+ // A braced struct opens its body, or its generics, before anything
2235+ // else. A tuple struct opens `(` and a unit struct ends at `;`.
2236+ let tail = rest. trim_end ( ) ;
2237+ assert ! (
2238+ tail. ends_with( '{' ) ,
2239+ "`{}` in {} is not a braced struct, which would take a prelude value name" ,
2240+ tail,
2241+ path. display( ) ,
2242+ ) ;
2243+ }
2244+ }
2245+ assert ! ( checked > 0 , "no generated struct was checked, so the scan is broken" ) ;
2246+ }
2247+
21112248/// The test table itself must be well-formed: unique elements, and every
21122249/// supported/unsupported row backed by a fixture.
21132250#[ test]
0 commit comments