chore: 0.17.2-alpha.4#145
Merged
Merged
Conversation
- Fix reqwest-middleware feature so it implies reqwest (was a compile error to enable reqwest-middleware alone — reported by an internal consumer). - Add MIGRATION.md covering the transport-shape refactor: Rust try_new returns Result, ReqwestClient::new does not exist, and the current TS DOM-collision workaround. TS DOM collision (#143) and codegen typecheck-blocks-emit (#144) filed as issues; not in this alpha.
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, reopen this pull request to trigger a review.
|
📖 Documentation Preview: https://reflectapi-docs-preview-pr-145.partly.workers.dev Updated automatically from commit ec5b020 |
Generated TypeScript now emits two files: generated.ts (the API surface) and generated.transport.ts (the transport contract). Bare Request/Response/Headers/Client live in the transport submodule, qualified by the import path, so they no longer shadow the DOM globals of the same name when imported from generated.ts. Internally, lib.ts imports Response under an alias (ClientResponse) so the unprefixed `new Response(...)` call inside __read_response_body reaches the platform global directly — no more `(globalThis as any)` cast. API change: typescript::generate now returns BTreeMap<String, String> rather than String. CLI and demo callers updated; in-place snapshot tests strip boilerplate so they still match. ClientInstance carries a single TS structural cast (Uint8Array -> BodyInit) to work around a TS 5 lib.dom typing of BufferSource that flags otherwise-valid fetch calls. Behaviour is unchanged.
…pets - Drop the misleading before/after pair where the convenience-constructor snippets were identical. Lead with "if your generated crate has a reqwest feature, Interface::try_new keeps working unchanged" and introduce the explicit form below. - Add a worked example for the reqwest_middleware::ClientWithMiddleware path (the common case with otel/retry layers and no feature-flagged generated crate). Matches what consumers wrote during the migration.
Previously, passing `--output some/dir/generated.ts` for TypeScript created a directory at that path containing generated.ts/generated.ts and generated.ts/generated.transport.ts because the multi-file branch treated the output path as a directory unconditionally. Now: if the output path looks like a file (not an existing directory, no trailing slash) and matches one of the codegen filenames, write that file at the requested path and place siblings in the parent directory. Backward-compatible with scripts that wrote --output .../generated.ts. If the file-looking path doesn't match any codegen filename (e.g. a custom name), error out with a migration hint suggesting --output <directory> rather than silently creating a directory there. Also update docs/src/clients/README.md to describe TS as two-file output.
When --output is a bare filename like `mystery.foo`, Path::parent()
returns Some("") rather than None, so the previous unwrap_or
fallback never fired and the error message read
`Pass a directory path (e.g. --output "")`. Useless suggestion.
Extract parent_or_dot() (the same .filter(empty).unwrap_or(".") that
the write path already used) and apply it in both places. Now the
error suggests `--output "."` for bare-filename inputs.
- Fix Rust trait snippet: Response is generic over Self::Error. - Replace loose "valid HTTP base" with the actual cannot_be_a_base check; correct re-export path (reflectapi::rt::UrlParseError). - Drop the defensive "No collision with standard-library names" Python line; reframe the Python section around what custom middleware / transport authors should target.
Pydantic can't represent serde(flatten) over a TypeVar at runtime — the inner T's wire fields were silently dropped at class-definition time, and ConfigDict(extra="ignore") then discarded them on parse. Silent data loss for IdentityData<I, D>, UpdateOrElse<T, C>, InsertManyOrElse<T, C>, and the like. Fix: walk the schema, find every concrete (struct, args) instantiation of a marked struct, and emit a specialized non-generic class. The existing flatten-of-concrete-type rendering then handles them correctly. Mangled names follow OriginalStruct_Arg1_Arg2 — short enough to avoid the existing 80-char hash-truncation path so all references (class def, namespace alias, method signatures, model_rebuild) stay consistent. Defence-in-depth: collect_flattened_fields' silent-skip arm now splits primitive (legitimate empty flatten for unit types) vs unresolved (codegen bug — bail). Any future regression that reintroduces the field-dropping behaviour will fail loudly instead of corrupting wire format. Tests: - Reproducer: TestUpdateOrElse<TestFlattenInner, TestFlattenIfElse> - Multiple distinct instantiations of the same generic - Optional<T> flatten (serde unwraps Option in flatten position) - End-to-end Pydantic round-trip via `uv run python` against the generated module — confirms the wire format actually parses Rust and TypeScript outputs are unaffected. Two pre-existing flatten tests had snapshots blessing the bug (empty class bodies for flatten-of-typevar); those snapshots are corrected.
…morphization The previous monomorphization pass refused to handle a marked struct whose own arg was another marked struct (e.g. UpdateOrElse<IdentityData<I, D>, C>) — it bailed loud rather than silently corrupt, but the bail blocked real consumers (10 endpoints in the reported core-server schema). Restructure: instead of a worklist that processes raw discoveries, walk type refs bottom-up. For each marked ref encountered, recursively normalize its args first (replacing inner marked refs with their already-mangled names), then register the outer with those normalized args as the lookup key. The rewriter still walks bottom-up, so it produces the same key — both sides agree. Also: name-length budget. Deeper nesting produces longer mangled names that would trip downstream class-name truncation, leaving class definitions / namespace aliases / type-ref dotted paths with two different hashes. Compute the post-PascalCase length up front and hash-truncate the suffix here when needed; downstream sees one stable name at every reference site. And: Typespace::remove_type leaves stale indices in its types_map, so calling it twice in a row would silently remove the wrong type (took out std::option::Option in one repro). Sort_types between removals to force a map rebuild — a workaround for an upstream behaviour we shouldn't paper over silently long-term. Test: TestUpdateOrElse<TestIdentityData<TestFlattenIdent, TestFlattenIdentData>, TestFlattenIfElse> — outer marked struct flattens an inner marked struct whose own params are concrete. Snapshot confirms class def, namespace alias, and method signature all reference the same name with consistent fields.
…tion Three issues caught by the same review: 1. CLI: fresh output directories for multi-file codegen (TS, Python) were rejected. The "looks like a directory" check required the path to already exist or end with `/`, so `--output ./brand-new-dir` was treated as a file path and bailed because the filename didn't match a codegen output. Now the rule is: if `output_path.file_name()` matches one of the emitted files AND the path isn't already a directory, write the matching file there with siblings in the parent. Otherwise treat as a directory and create it. 2. CLI: `--output -` printed the alphabetically-first file in the BTreeMap. For TS that's `generated.transport.ts` (a helper); for Python that's `__init__.py` (a 5-line shim). Pipelines expecting the actual API surface got the wrong file. Now each language has a declared "primary" filename; stdout selects that. 3. Python codegen monomorphization mangling collided when two distinct generic args shared a leaf name across modules. `module_a::Sample` and `module_b::Sample` both produced the same mangled key, fusing two distinct UpdateOrElse instantiations into one and silently keeping just the second type's fields. Switched the suffix to use full namespace paths (`::` -> `_`); the length-budget hash truncation already handles the resulting longer names consistently across class def / namespace alias / dotted-path reference. Tests: - reflectapi-cli/tests/output_paths.rs: fresh-dir for TS and Python, file-shaped path with siblings, stdout primary-file selection. - test_generic_flatten_leaf_collision: two TestUpdateOrElse instantiations with same-leaf args from different modules; snapshot confirms two distinct classes with each type's fields. - Pydantic round-trip test no longer hardcodes the mangled name — it discovers the class by walking the namespace and matching on the expected field set, so future mangling changes don't break it.
…enerics
Two related issues with the same root cause: a marked struct can be
referenced from inside another generic context, where its args are
the *outer* struct's TypeVars rather than concrete types. The
previous monomorphizer registered such refs (treating "I" or "D" as
real types), called instantiate which substituted I→I / D→D
(no-op), and emitted a struct with parameters=[] but raw TypeVar
field types still in place. The bail then fired with
target_type_name='D' at depth=0.
Two fixes:
1. is_concrete check in normalize_marked_refs: an arg ref with no
sub-args and no schema entry is a TypeVar from an enclosing
generic context. Refuse to register such instantiations — the
enclosing context will resolve them when its own concrete usage
is monomorphized.
2. Transitive marking. A generic struct G that itself has no
flatten-over-typevar still needs monomorphization if any field
references a marked struct using G's own TypeVars (e.g.
`Outer<T> { field: MarkedInner<T> }`). Otherwise concrete usages
of G would substitute the field to a now-concrete marked-struct
ref but G's body still holds the unsubstituted form, and after
removing the original marked struct the leftover ref dangles.
Transitive marking lets G's own concrete instantiations drive
the monomorphization chain. Iterate to fixed point so chains
like `A<T> → B<T> → MarkedC<T>` all get marked.
Test: TestWithMarkedInner<I, D> { body: TestIdentityData<I, D>; extra: bool }
instantiated with concrete (TestFlattenIdent, TestFlattenIdentData).
Before this fix: validation failed because the original
TestIdentityData was removed but TestWithMarkedInner still referenced
it. After: both structs transitively monomorphize; the wrapper's
body field references the inner mono'd type by mangled name.
Previous transitive marking and instantiation only considered
structs. Generic enums whose variant fields reference a marked
struct using the enum's own TypeVars (e.g.
`enum IngestRelation<I, D> { Insert(IdentityData<I, D>), ... }`)
slipped through: the original was kept, no concrete monomorphization
was emitted, and after step 6 removed the inner marked struct, the
enum's variant ref dangled.
Now both passes operate on Type::Struct and Type::Enum:
- transitive marking checks variant field type_refs
- normalize_marked_refs looks up by name and instantiates either
via Struct::instantiate or Enum::instantiate
- step 4 inserts the appropriate Type wrapper
Test: TestIngestRelation<I, D> with two tuple variants carrying
TestIdentityData<I, D> + a unit variant, instantiated concretely.
Snapshot confirms each tuple variant references the inner mono'd
struct by mangled name; the enum itself is monomorphized as a
discriminated union of variant classes + the bare unit literal.
Four items from a self-review pass:
1. Schema crate: Typespace::remove_type left stale indices in its
internal types_map after Vec::remove shifted slots. The next
lookup either panicked or silently returned the wrong type
(had observed: removing two marked structs took out
std::option::Option in one repro). Fixed in schema by
invalidating the map on remove; the python codegen pass no
longer needs its sort_types() workaround. Two regression tests
added in reflectapi-schema.
2. Python codegen: post-pass invariant assertion. Walks every
reachable TypeReference in debug builds and panics on either
(a) a ref to a removed marked type, or (b) a TypeVar leak
inside a parameters-empty body. Caught a real bug while
landing other items: when normalize_marked_refs replaced an
inner ref with its mangled name, the outer ref's is_concrete
check then rejected because the mangled name wasn't in the
schema yet. Fixed by tracking a parallel set of registered
mangled names and treating those as concrete.
3. Python codegen: mangled-name collision detection. Before
inserting monomorphized types, scan the concrete table for
distinct keys that produced the same mangled name (an
insert_type would silently no-op the second one). Errors
loudly with both keys.
4. Python codegen: transitive marking now considers TypeVars
nested at any depth in arg trees, not just immediate args.
Previously `Wrapper<I> { body: Marked<Option<I>> }` slipped
through because Marked's immediate arg was Option (not I), so
Wrapper wasn't marked, the inner Marked got removed, and the
wrapper's ref dangled. Test added.
Self-recursive marked-struct case (Tree<T> with #[flatten] +
Vec<Tree<T>>) was attempted but reflectapi's schema construction
itself overflows on recursive types — separate concern, not in
scope here. Note added in the test file.
Three test additions following the self-review: 1. mangle_monomorphized_name unit tests — direct assertions that the mangler produces distinct outputs for distinct inputs across the patterns we care about: same-leaf-different-namespace, arity, arg order, nested args. Plus a length-budget test that exercises the hash-truncation path against deep nesting. 2. recursive_marked_struct_terminates_and_renders — feeds a hand-built JSON schema into the codegen for a flatten-over- typevar struct that recursively contains itself (`Vec<Self<T>>`). Exercises the register-before-recurse guard in normalize_marked_refs. The reflectapi Rust derive can't construct such a type (overflows in schema-build), but the codegen pipeline itself terminates and renders correctly: FlatTreeItem with the flattened Item fields plus `children: list[FlatTreeItem]` (forward-ref via __future__). Earlier I dismissed this case as untestable — incorrectly. 3. test_generic_with_concrete_flatten_not_marked — boundary case: a generic struct whose flatten target is concrete (not a TypeVar). Confirms the marked-detection predicate doesn't over-trigger on any-flatten-on-a-generic-struct. Snapshot shows the class stays generic (Generic[T]) with the concrete flatten expanded inline; no monomorphization happens. Also updates the dismissive comment in serde.rs to point at the recursive-marked test in python.rs.
…dies
The earlier post-pass check flagged any zero-arg ref whose name
wasn't in the schema as a "TypeVar leak". That tripped on legitimate
schema shapes:
- chrono::DateTime<chrono::Utc> — chrono::Utc is a phantom
timezone marker that rides along as a type-arg of the
implemented-types-mapped chrono::DateTime. It's never looked up
as a standalone field type; the codegen translates the whole
DateTime to `datetime` directly. Demo schema reproduced this on
every DateTime field — ~280 false positives reported by an
internal consumer.
- Const-generic scalars: `[T; 2]` represents the 2 as a type-ref-
shaped node whose name is a numeric literal, not a real type.
Narrow the check: it only fires inside bodies of types this pass
actually monomorphized (the values of `concrete`). Original schema
bodies are not our concern — if they had problems, they had them
before. The dangling-marked-ref check stays unchanged (no false
positives, narrow definition).
Threads `registered: BTreeSet<String>` through to the assertion as
the set of mono'd names. Verified by:
- Demo schema codegen runs clean (no panics, 753-line output).
- New regression test
`invariant_check_ignores_phantom_type_args_in_unmodified_bodies`
builds a schema with Holder { ts: DateTime<Utc> } + a marked
Wrapper<T> and asserts codegen completes without invariant panic.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Field-fix alpha after testing alpha.3 in an internal consumer.
reqwest-middlewarefeature so it impliesreqwest(alpha.3 producedcannot find type ReqwestClientwhen onlyreqwest-middlewarewas enabled)../generated.transport(closes TS codegen: transport DTOs Request/Response/Headers shadow DOM globals #143). BareRequest/Response/Headersno longer shadow DOM globals when imported fromgenerated.ts; consumers writing custom transports import them from the submodule instead. Drops the(globalThis as any).Responsecast inside the runtime.MIGRATION.mdfor the transport-shape refactor: corrects theReqwestClient::newsnippet from feat(rt): move base URL onto Client transport, Request carries path #141's commit body (onlytry_newexists), explains the new TS file layout.API change
reflectapi::codegen::typescript::generatenow returnsBTreeMap<String, String>instead ofString. CLI and demo callers updated.Test plan
cargo build --workspacecleancargo test --workspacegreencargo clippy --all-targets --all-features -- -D warningscleancargo build -p reflectapi --no-default-features --features rt,reqwest-middlewarenow compileswrite_typescript_clienttest passes)v0.17.2-alpha.4to trigger release workflow