diff --git a/README.md b/README.md index 26435bb9..026863d1 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ - [Crates.io](https://crates.io/crates/reflectapi) - Package information and versions - [API Documentation](https://docs.rs/reflectapi) - Complete API reference - [User Guide](docs/src/SUMMARY.md) - Tutorials and examples (build locally with `mdbook serve` in `docs/`) -- [Architecture](docs/architecture.md) - System design and internals +- [Architecture](docs/src/architecture.md) - System design and internals ## Development notes diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index db281d02..00000000 --- a/docs/architecture.md +++ /dev/null @@ -1,533 +0,0 @@ -# ReflectAPI Architecture - -## 1. Overview - -ReflectAPI is a Rust framework for defining web API services and generating type-safe clients in multiple languages. Developers write Rust handler functions with typed inputs and outputs. Derive macros capture type metadata into a JSON schema (`reflectapi.json`). Code generation backends produce idiomatic clients for TypeScript, Rust, and Python, plus OpenAPI specifications. - -Handler functions follow a fixed signature convention: - -```rust -async fn handler(state: State, input: Input, headers: Headers) -> Result -``` - -- **Input** — the request body type (implements `reflectapi::Input + DeserializeOwned`) -- **Headers** — typed header extraction (implements `reflectapi::Input`; struct fields map to HTTP header names) -- **Output** — the success response type (implements `reflectapi::Output + Serialize`) -- **Error** — the error response type (implements `reflectapi::Output + Serialize + StatusCode`) -- **`reflectapi::Empty`** — sentinel for no input/output body -- **`reflectapi::Infallible`** — sentinel for handlers that cannot fail - -The `Input` and `Output` traits (defined in `reflectapi/src/traits.rs`) are the mechanism by which types self-register into the schema. Each trait provides a `reflectapi_input_type(schema: &mut Typespace) -> TypeReference` (or `_output_type`) method that the derive macros implement. - -The system is a multi-stage pipeline: - -1. **Derive macros** extract type metadata at compile time into a `Schema`. -2. The schema is serialized to JSON (`reflectapi.json`), forming the contract between server and clients. -3. At codegen time, the schema is deserialized, normalized through a pipeline of transformations, and fed to language-specific backends that emit client code. - -## 2. Crate Structure - -``` -reflectapi/ Workspace root - reflectapi-schema/ Schema types, semantic IR, normalization pipeline - reflectapi-derive/ Proc macros: #[derive(Input, Output)] - reflectapi/ Builder, codegen backends, runtime (axum, serialization) - reflectapi-cli/ CLI tool wrapping codegen (clap-based) - reflectapi-demo/ Demo application with snapshot tests (insta) - reflectapi-python-runtime/ Python runtime support library (Pydantic helpers, client base classes) - docs/ User-facing documentation (mdbook) -``` - -### reflectapi-schema - -The foundational crate. Contains: - -- **Schema types** (`Schema`, `Function`, `Type`, `Struct`, `Enum`, `Field`, `Variant`, `TypeReference`, `Representation`, `Primitive`, `Typespace`) -- the serializable representation of an API. -- **SymbolId and SymbolKind** -- stable unique identifiers for all schema symbols. -- **ensure_symbol_ids()** -- post-deserialization ID assignment. -- **NormalizationPipeline** -- multi-stage schema transformation (type consolidation, naming, circular dependency resolution). -- **Normalizer** -- converts `Schema` into validated `SemanticSchema`. -- **SemanticSchema** -- immutable, fully-resolved intermediate representation. -- **Visitor/VisitMut** -- generic traversal utilities. -- **Substitute/Instantiate** -- generic type parameter substitution. -- **Rename/Glob** -- type renaming and glob-based filtering. - -### reflectapi-derive - -Procedural macros that implement `#[derive(Input)]` and `#[derive(Output)]`. These macros parse Rust struct/enum definitions (including serde attributes) and emit code that populates the `Schema` at compile time. - -Key source files: -- `lib.rs` -- macro entry points -- `parser.rs` -- serde attribute parsing -- `context.rs` -- code generation context -- `derive.rs` -- the derive implementation -- `tokenizable_schema.rs` -- converts schema types to token streams - -Beyond serde attributes, the macros recognize `#[reflectapi(...)]` attributes: - -**Type-level:** `type = "..."` (override reflected type), `input_type` / `output_type` (per-direction override), `discriminant` (use Rust enum discriminant values), `derive(...)` (add derives to codegen config). - -**Field-level:** `skip` / `input_skip` / `output_skip` (omit from schema), `type = "..."` / `input_type` / `output_type` (override field type), `input_transform` / `output_transform` (transform callbacks). - -### reflectapi - -The main user-facing crate. Contains: -- **Builder** (`src/builder/`) -- runtime schema construction and handler registration. -- **Codegen backends** -- `typescript.rs`, `rust.rs`, `python.rs`, `openapi.rs` in `src/codegen/`. -- **Runtime integration** -- axum handler adapters, serialization modes (JSON, msgpack). -- **Format utilities** (`src/codegen/format.rs`) -- invokes prettier, rustfmt, or ruff on generated output. - -### reflectapi-cli - -A thin CLI wrapper using `clap`. Reads a `reflectapi.json` schema file and invokes the appropriate codegen backend: - -``` -reflectapi codegen --language typescript --schema reflectapi.json --output ./clients/ts -``` - -Flags control output formatting, type checking, tag-based endpoint filtering, shared Rust module imports, and tracing instrumentation. - -### reflectapi-demo - -A working demo server with snapshot tests. The `reflectapi.json` file and generated clients (TypeScript, Rust, Python) are checked in and validated by `cargo insta` snapshot tests. The `assert_snapshot!` macro in `src/tests/assert.rs` generates **5 snapshots** per test: JSON schema, TypeScript, Rust, OpenAPI, and Python. Tests are organized in `basic.rs`, `enums.rs`, `generics.rs`, `serde.rs`, and `namespace.rs`. Compile-fail and compile-pass tests use `trybuild`. - -### reflectapi-python-runtime - -Python package providing base classes and utilities for generated Python clients: `AsyncClientBase`, `ClientBase`, `ApiResponse`, `ReflectapiOption`, mock testing utilities, and middleware support. - -## 3. Schema Types - -The schema is a JSON-serializable representation of a Rust API. The core types live in `reflectapi-schema/src/lib.rs`. - -### Schema - -Top-level container holding the API definition: - -```rust -pub struct Schema { - pub id: SymbolId, // Assigned by ensure_symbol_ids() - pub name: String, - pub description: String, - pub functions: Vec, - pub input_types: Typespace, // Types used in request positions - pub output_types: Typespace, // Types used in response positions -} -``` - -Separating `input_types` from `output_types` allows the same Rust type name to carry different shapes in request and response contexts. The `TypeConsolidationStage` merges these during normalization. - -### Function - -An API endpoint: - -```rust -pub struct Function { - pub id: SymbolId, - pub name: String, // e.g. "users.login" - pub path: String, // URL path, e.g. "/api/v1" - pub description: String, - pub deprecation_note: Option, - pub input_type: Option, - pub input_headers: Option, - pub output_type: Option, - pub error_type: Option, - pub serialization: Vec, // Json, Msgpack - pub readonly: bool, - pub tags: BTreeSet, -} -``` - -### Type - -A sum type covering all possible type definitions: - -```rust -pub enum Type { - Primitive(Primitive), // Opaque types (String, i32, Uuid, etc.) - Struct(Struct), // Named/unnamed fields, tuple structs - Enum(Enum), // Tagged/untagged unions with variants -} -``` - -### TypeReference - -A reference to a type, including generic arguments: - -```rust -pub struct TypeReference { - pub name: String, // Fully-qualified name, e.g. "std::vec::Vec" - pub arguments: Vec, // Generic type arguments -} -``` - -### Representation - -Captures serde's enum tagging strategy: - -```rust -pub enum Representation { - External, // {"variant": {...}} - Internal { tag: String }, // {"type": "variant", ...} - Adjacent { tag: String, content: String }, // {"t": "variant", "c": {...}} - None, // untagged -} -``` - -### Primitive and the Fallback Mechanism - -`Primitive` types are opaque to codegen — they have no inspectable fields. The `fallback` field enables codegen backends to resolve types they do not natively handle: - -```rust -pub struct Primitive { - pub name: String, - pub parameters: Vec, - pub fallback: Option, // e.g., NonZeroU8 -> u8, BTreeMap -> HashMap -} -``` - -`TypeReference::fallback_recursively()` follows the fallback chain until reaching a type the backend supports. Examples: `Arc` falls back to `T`, `BTreeMap` falls back to `HashMap`, `HashSet` falls back to `Vec`. - -### reflectapi::Option\ - -A three-state type (`Undefined | None | Some(T)`) defined in `reflectapi/src/option.rs`. Essential for PATCH-style APIs where "field absent" differs from "field set to null": - -- **Undefined** — field not present in the request body -- **None** — field explicitly set to `null` -- **Some(T)** — field present with a value - -Each codegen backend represents this distinctly: TypeScript uses `T | null | undefined`, Python uses `ReflectapiOption` from the runtime library. - -### Fields, Field, Variant - -Fields can be `Named`, `Unnamed` (tuple), or `None` (unit). Each `Field` carries: -- `name` / `serde_name` -- the Rust name and the wire name (after serde rename) -- `type_ref` -- the field's type -- `required` -- whether the field must be present -- `flattened` -- whether `#[serde(flatten)]` is applied -- `description` / `deprecation_note` -- documentation metadata - -## 4. Semantic IR Pipeline - -The pipeline transforms a deserialized `Schema` into a validated, immutable `SemanticSchema` suitable for code generation. - -### Pipeline Overview - -``` - reflectapi.json - | - v - +--------------------------+ - | JSON Deserialization | - +--------------------------+ - | - v - +--------------------------+ - | ensure_symbol_ids() | Assign stable SymbolIds - +--------------------------+ - | - v - +-------------------------------+ - | PipelineBuilder | Configures which stages run - | .consolidation(Standard|Skip)| - | .naming(Standard|Skip|Custom)| - | .circular_dependency_strategy| - | .add_stage(custom) | - +-------------------------------+ - | .build() - +--------------------------------------------------+ - | standard() | for_codegen() - | (all defaults) | (Skip, Skip) - v v - +---------------------------+ +------------------------------+ - | 1. TypeConsolidation | | Schema::consolidate_types() | - | 2. NamingResolution | | (caller runs separately) | - | 3. CircularDependency | +------------------------------+ - +---------------------------+ | - | v - | +------------------------------+ - | | CircularDependency only | - | +------------------------------+ - | | - +------------------+-----------------------+ - | - v - +-------------------+ - | Normalizer | - | 1. Discovery | Register symbols - | 2. Resolution | Resolve type references - | 3. Dependencies | Build dependency graph - | 4. Validation | Semantic checks - | 5. IR Build | Construct SemanticSchema - +-------------------+ - | - v - +-------------------+ - | SemanticSchema | Immutable, validated IR - +-------------------+ -``` - -Pipelines are configured via `PipelineBuilder`. The convenience methods `NormalizationPipeline::standard()` and `for_codegen()` delegate to the builder internally. The Python backend uses `PipelineBuilder` directly: - -```rust -PipelineBuilder::new() - .consolidation(Consolidation::Skip) - .naming(Naming::Skip) - .build() -``` - -### SymbolId - -Every symbol in the schema (types, functions, fields, variants) receives a stable, unique identifier: - -```rust -pub struct SymbolId { - pub kind: SymbolKind, // Struct, Enum, Endpoint, Field, Variant, Primitive, TypeAlias - pub path: Vec, // e.g. ["api", "User"] - pub disambiguator: u32, // Distinguishes same-name types across input/output -} -``` - -The `SymbolId` is the primary key for all lookups throughout the pipeline and in the final `SemanticSchema`. The `disambiguator` field handles the case where input and output typespaces define different types with the same fully-qualified name. - -### ensure_symbol_ids() - -When a schema is deserialized from JSON, all `SymbolId` fields have their default ("unknown") values. `ensure_symbol_ids()` walks the schema and assigns stable IDs based on canonical names: - -1. Pre-registers well-known stdlib types (`String`, `Vec`, `Option`, `HashMap`, etc.) from `STDLIB_TYPES`. -2. Assigns IDs to functions based on their names. -3. Assigns IDs to types in each typespace, using separate seen-maps so cross-typespace conflicts are detected. -4. When the same FQN appears in both typespaces with different type definitions, the output typespace's copy receives a disambiguated ID (`disambiguator = 1`). -5. Assigns IDs to struct fields and enum variants as children of their parent's path. - -### NormalizationPipeline and PipelineBuilder - -Pipelines are assembled via `PipelineBuilder`, which controls three dimensions: - -- **`Consolidation`** (`Standard` | `Skip`) -- whether to run `TypeConsolidationStage`. -- **`Naming`** (`Standard` | `Skip` | `Custom(Box)`) -- whether/how to run naming resolution. -- **`ResolutionStrategy`** (`Intelligent` | `Boxing` | `ForwardDeclarations` | `OptionalBreaking` | `ReferenceCounted`) -- passed to `CircularDependencyResolutionStage`. - -Additional custom stages can be appended via `add_stage()`. The convenience methods `NormalizationPipeline::standard()` and `for_codegen()` delegate to `PipelineBuilder` internally and remain the recommended shorthand for common configurations. - -The standard pipeline applies three stages in sequence: - -**Stage 1: TypeConsolidationStage** -- Merges `input_types` and `output_types` into a single unified `input_types` collection. When a simple type name appears in both typespaces, both copies are renamed with `input.` / `output.` prefixes and all type references throughout the schema are rewritten to point to the new names. Types that appear in only one typespace are added as-is. - -**Stage 2: NamingResolutionStage** -- Strips module path prefixes from type names (e.g. `myapp::api::User` becomes `User`). When stripping creates a conflict (two different types both resolve to `User`), a unique name is generated by incorporating the last meaningful module component (e.g. `ApiUser` vs `BillingUser`). Common module names (`model`, `proto`) are skipped when building the prefix. - -**Stage 3: CircularDependencyResolutionStage** -- Detects circular type dependencies using Tarjan's strongly-connected components (SCC) algorithm. Supports multiple resolution strategies: -- `Intelligent` (default) -- self-references use boxing; multi-type cycles use forward declarations. -- `Boxing` -- a no-op because Rust schemas already encode `Box` in their type references. -- `ForwardDeclarations`, `OptionalBreaking`, `ReferenceCounted` -- defined in `ResolutionStrategy` but their `apply_*` methods return `Ok(())` without modifying the schema. - -Downstream codegen backends query the detected cycles to emit forward-reference annotations regardless of the resolution strategy. - -### Normalizer - -The `Normalizer` orchestrates the full conversion from `Schema` to `SemanticSchema`. It accepts a custom pipeline via `normalize_with_pipeline()`, or uses `NormalizationPipeline::standard()` by default. Codegen backends that handle their own naming build a pipeline with `PipelineBuilder` (skipping consolidation and naming) and pass it to `normalize_with_pipeline()`. - -1. **Phase 0**: Calls `ensure_symbol_ids()` and runs the selected pipeline. -2. **Phase 1 (Symbol Discovery)**: Registers all types, functions, fields, and variants in the `SymbolTable`. -3. **Phase 2 (Type Resolution)**: Resolves `TypeReference` names to `SymbolId` targets. -4. **Phase 3 (Dependency Analysis)**: Builds dependency graph and performs topological sorting. -5. **Phase 4 (Semantic Validation)**: Checks for semantic errors. -6. **Phase 5 (IR Construction)**: Builds the `SemanticSchema` with `BTreeMap`-ordered collections. - -### SemanticSchema - -The output of the pipeline: - -```rust -pub struct SemanticSchema { - pub id: SymbolId, - pub name: String, - pub description: String, - pub functions: BTreeMap, - pub types: BTreeMap, - pub symbol_table: SymbolTable, -} -``` - -Key properties compared to `Schema`: -- **Fully resolved** -- all type references are `ResolvedTypeReference` with `SymbolId` targets. -- **Deterministically ordered** -- `BTreeMap`/`BTreeSet` everywhere for stable codegen output. -- **Single typespace** -- input/output distinction has been resolved by `TypeConsolidationStage`. -- **Dependency graph** -- the `SymbolTable` contains dependency edges and supports topological sorting. - -## 5. Code Generation Backends - -All backends live in `reflectapi/src/codegen/`. The Python backend uses `SemanticSchema` for type iteration ordering and the consolidated raw `Schema` for rendering. TypeScript, Rust, and OpenAPI backends read the `Schema` directly. - -### TypeScript - -Uses `std::fmt::Write` for code generation. Key design decisions: - -- **Interfaces** for struct types with named fields. -- **Type aliases** for tuple structs and transparent wrappers. -- **Intersection types** (`&`) for `#[serde(flatten)]` -- flattened fields are rendered as separate type references joined with `&`. -- **Discriminated unions** for tagged enums. -- **`NullToEmptyObject`** helper to handle the interaction between Rust `()` (which maps to `null`) and intersection types. -- Output is formatted with `prettier`. - -### Rust - -Mirrors the source Rust types, re-emitting struct/enum definitions with appropriate `#[serde(...)]` attributes. Key features: - -- Preserves `#[serde(flatten)]` on fields that had it in the original. -- Handles shared modules -- types from specified module prefixes are imported rather than regenerated. -- Generates a client module with async functions for each endpoint. -- Output is formatted with `rustfmt`. - -### Python - -Generates Pydantic v2 models with namespace classes mirroring the Rust module structure. Uses `SemanticSchema` for type iteration ordering via `PipelineBuilder::new().consolidation(Consolidation::Skip).naming(Naming::Skip).build()` (skips NamingResolution since Python handles its own naming via `improve_class_name`). The consolidated raw `Schema` provides concrete type data for rendering. - -- **BaseModel classes** for structs, with `ConfigDict(extra="ignore", populate_by_name=True)`. -- **Namespace alias classes** mirror the Rust module hierarchy for dotted access (e.g., `auth.UsersSignInRequest`). Type definitions are at module top-level with flat PascalCase names; namespace classes provide aliases. -- **Discriminated unions** (`Union[..., Field(discriminator="tag")]`) for internally-tagged enums. -- **RootModel** wrappers with `model_validator`/`model_serializer` for externally-tagged and adjacently-tagged enums. -- **Per-variant model expansion** for `#[serde(flatten)]` with internally-tagged enums (see Section 6). -- **Field descriptions** via `Field(description="...")` propagated from the schema. -- **Typed error returns** — `ApiResponse[OutputType, ErrorType]` in method signatures. -- **Field aliases** via `Field(serialization_alias=..., validation_alias=...)` for serde-renamed fields and underscore-prefixed fields. -- **Literal types** for discriminator fields, with alias handling for Python reserved words (e.g., `type` becomes `type_`). -- **TypeVar collision resolution** — renames TypeVars that collide with class names (e.g., `Identity` → `_T_Identity`). -- Python reserved words are sanitized in field names, method names, and parameters. -- Output is formatted with `ruff`. - -### OpenAPI - -Generates an OpenAPI 3.1 specification (JSON). Key features: - -- **`$ref`** for type references, with inline expansion for simple cases. -- **`allOf`** composition for `#[serde(flatten)]` -- the parent object schema and each flattened type schema are combined under `allOf`. -- **`oneOf`** for enum variants (tagged and untagged). -- **`const`** values for discriminator tag fields in internally-tagged enum variants. - -## 6. Flattened Type Handling - -`#[serde(flatten)]` merges fields from one type into another at the wire level. This requires distinct code generation strategies per target language. Internally-tagged enums compound this: the tag field and variant fields merge flat into the parent struct. - -### Wire Format - -Given this Rust code: - -```rust -#[derive(Serialize, Deserialize)] -struct Request { - id: String, - #[serde(flatten)] - payload: Action, -} - -#[derive(Serialize, Deserialize)] -#[serde(tag = "type")] -enum Action { - Create { name: String }, - Delete { reason: Option }, -} -``` - -Serde produces this JSON for `Request { id: "1", payload: Action::Create { name: "foo" } }`: - -```json -{"id": "1", "type": "Create", "name": "foo"} -``` - -All fields are merged flat -- there is no nesting. The codegen backends must produce types that match this exact wire format. - -### TypeScript - -Uses intersection types to compose the parent fields with the flattened type: - -```typescript -type Request = { - id: string; -} & NullToEmptyObject; -``` - -### Python (struct flatten) - -For flattened structs (not enums), all fields from the flattened type are expanded inline into the parent Pydantic model: - -```python -class Request(BaseModel): - id: str - # Fields from FlattenedStruct expanded here: - extra_field: str -``` - -### Python (internally-tagged enum flatten) - -When a struct flattens an internally-tagged enum, per-variant models are generated that merge the parent's fields, the tag discriminator, and the variant's fields: - -```python -class RequestCreate(BaseModel): - """'Create' variant of Request""" - id: str - type_: Literal['Create'] = Field(default="Create", serialization_alias='type', validation_alias='type') - name: str - -class RequestDelete(BaseModel): - """'Delete' variant of Request""" - id: str - type_: Literal['Delete'] = Field(default="Delete", serialization_alias='type', validation_alias='type') - reason: str | None = None - -class Request(RootModel): - root: Annotated[ - Union[RequestCreate, RequestDelete], - Field(discriminator="type_"), - ] -``` - -Each per-variant model contains all parent struct fields, the `Literal`-typed tag discriminator, and the variant-specific fields. The parent type becomes a `RootModel` with a discriminated union. - -The implementation handles several edge cases: -- **Unnamed variants** with a single field: the inner struct's fields are expanded. -- **Box-wrapped variants**: `Box` is unwrapped to get the inner type. -- **Optional flattened fields**: all expanded fields become optional. -- **Multiple flattened structs**: non-enum flattened structs are also expanded into each variant model. - -For other enum representations (external, adjacent, untagged), the flattened enum is emitted as a regular typed field. This preserves data but does not match the flat wire format that serde produces. - -### OpenAPI - -Uses `allOf` composition: - -```json -{ - "allOf": [ - { "$ref": "#/components/schemas/Action" }, - { - "type": "object", - "properties": { "id": { "type": "string" } }, - "required": ["id"] - } - ] -} -``` - -For enums, `oneOf` is used for the variant schemas with `const` values on tag fields. - -### Rust - -Preserves the original `#[serde(flatten)]` attribute on the field, since the generated Rust client uses serde directly. - -## 7. Limitations and Design Gaps - -### TypeScript, Rust, and OpenAPI backends use raw Schema - -The Python backend uses `SemanticSchema` for iteration ordering via `PipelineBuilder` (with consolidation and naming skipped). The TypeScript, Rust, and OpenAPI backends still consume the raw `Schema` directly. Migrating them to `SemanticSchema` would provide deterministic ordering and dependency-aware topological sorting. - -### Flattened enum handling varies by representation - -The Python backend generates per-variant models for flattened internally-tagged enums (wire-compatible). For externally-tagged, adjacently-tagged, and untagged enums, the flattened field is emitted as a regular nested field, which does not match the flat wire format. TypeScript uses intersection types for all representations. OpenAPI uses `allOf` composition. The backends do not share a common strategy. - -### CircularDependencyResolutionStage - -Cycle detection uses Tarjan's SCC algorithm. The `Boxing` strategy is a no-op because Rust schemas already encode `Box`. The `ForwardDeclarations`, `OptionalBreaking`, and `ReferenceCounted` strategies are defined in `ResolutionStrategy` but their `apply_*` methods return `Ok(())` without modifying the schema. - -### Python codegen coverage - -Validated against a production 284-endpoint API (59K-line generated client, 284 endpoints). Remaining gap: externally-tagged enums generate more boilerplate than TypeScript equivalents (multiple classes + RootModel + model_validator vs inline union syntax). diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 1e1f60f8..8532f05e 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,10 +1,10 @@ # Summary -[Introduction](./introduction.md) +- [Introduction](./introduction.md) +- [Architecture](./architecture.md) # Getting Started - [Quick Start](./getting-started/quick-start.md) - [Installation](./getting-started/installation.md) - [Client Libraries](./clients/README.md) - diff --git a/docs/src/architecture.md b/docs/src/architecture.md new file mode 100644 index 00000000..19957fa2 --- /dev/null +++ b/docs/src/architecture.md @@ -0,0 +1,137 @@ +# Architecture + +## Overview + +ReflectAPI has three layers: + +1. Rust types and handler functions define the API surface. +2. Reflection builds a `Schema`, which is the interchange format between the server side and code generators. +3. Codegen backends transform that schema into language-specific clients or an OpenAPI document. + +The workspace is split accordingly: + +- `reflectapi-schema`: schema types, symbol IDs, normalization pipeline, semantic IR +- `reflectapi-derive`: `#[derive(Input, Output)]` macros +- `reflectapi`: reflection traits, builder, runtime integrations, codegen backends +- `reflectapi-cli`: CLI wrapper around codegen +- `reflectapi-demo`: snapshot and integration tests +- `reflectapi-python-runtime`: runtime support for generated Python clients + +## Reflection Model + +Reflection starts from the `Input` and `Output` traits in `reflectapi/src/traits.rs`. Derived implementations and hand-written impls register types into a `Typespace` and return `TypeReference`s that point at those definitions. + +The top-level `Schema` in `reflectapi-schema/src/lib.rs` contains: + +- `functions`: endpoint definitions +- `input_types`: types seen in request positions +- `output_types`: types seen in response positions + +Input and output types stay separate at schema-construction time so the same Rust name can have different request and response shapes. Some backends later consolidate them into a single naming domain. + +## Schema and IDs + +`SymbolId` and `SymbolKind` live in `reflectapi-schema/src/symbol.rs`. They are internal compiler identifiers, not part of the stable JSON contract. + +Key points: + +- `Schema.id`, `Function.id`, and member/type IDs are marked `skip_serializing` +- `ensure_symbol_ids()` in `reflectapi-schema/src/ids.rs` assigns IDs after deserialization +- the schema root now uses `SymbolKind::Schema` +- the schema root path includes the `__schema__` sentinel to avoid colliding with a user-defined type of the same name + +That means `reflectapi.json` stays wire-focused, while normalization and semantic analysis still get stable identities. + +## Type Metadata + +Every reflected type is one of: + +- `Primitive` +- `Struct` +- `Enum` + +`Primitive.fallback` lets a backend substitute a simpler representation when it does not natively model the original Rust type. Examples in the current codebase include pointer-like wrappers falling back to `T`, and ordered collections falling back to unordered equivalents or vectors. + +Language-specific metadata is carried by `LanguageSpecificTypeCodegenConfig` in `reflectapi-schema/src/codegen.rs`: + +- Rust metadata is serialized when present, for example extra derives on generated Rust types. +- Python metadata is attached in memory during schema construction and consumed by the Python backend. It is intentionally not serialized today, so CLI codegen from `reflectapi.json` still uses shared default mappings as a compatibility fallback. + +## Normalization + +Normalization lives in `reflectapi-schema/src/normalize.rs`. + +There are two parts: + +1. A mutable normalization pipeline over raw `Schema` +2. A `Normalizer` that converts the resulting schema into `SemanticSchema` + +The configurable pipeline is built with `PipelineBuilder`. The convenience constructors are: + +- `NormalizationPipeline::standard()` + Runs type consolidation, naming resolution, and circular dependency resolution. +- `NormalizationPipeline::for_codegen()` + Skips consolidation and naming, and only runs circular dependency resolution. + +After the pipeline runs, `Normalizer` performs: + +1. symbol discovery +2. type resolution +3. dependency analysis +4. semantic validation +5. semantic IR construction + +`SemanticSchema` provides resolved, deterministic views of functions and types and is defined in `reflectapi-schema/src/semantic.rs`. + +## Backend Behavior + +Backends do not all consume the schema in the same way. + +### TypeScript + +The TypeScript backend in `reflectapi/src/codegen/typescript.rs` consolidates raw schema types and renders directly from the raw schema. + +### Rust + +The Rust backend in `reflectapi/src/codegen/rust.rs` also works primarily from the raw schema after consolidation. + +### Python + +The Python backend in `reflectapi/src/codegen/python.rs` uses both representations: + +1. `schema.consolidate_types()` runs first +2. `validate_type_references()` checks raw references +3. `Normalizer::normalize_with_pipeline(...)` builds `SemanticSchema` using a pipeline that skips consolidation and naming +4. rendering uses semantic ordering and symbol information, while still consulting raw schema details where the backend needs original field/type shapes + +Python-specific type support is driven first by per-type metadata attached during reflection. When that metadata is absent, the backend falls back to shared default mappings by canonical Rust type name so serialized schemas still work. + +### OpenAPI + +The OpenAPI backend in `reflectapi/src/codegen/openapi.rs` walks the raw schema directly. + +## Runtime-Specific Types + +ReflectAPI includes special API-facing types whose semantics matter to codegen: + +- `reflectapi::Option`: three-state optional value for PATCH-like APIs +- `reflectapi::Empty`: explicit empty request/response body type +- `reflectapi::Infallible`: explicit “no error payload” type + +The Python backend treats these as runtime-provided abstractions rather than generated models. + +## Testing and Validation + +`reflectapi-demo` is the main regression suite. + +The snapshot harness in `reflectapi-demo/src/tests/assert.rs` generates five artifacts per test: + +- raw schema JSON +- TypeScript client output +- Rust client output +- OpenAPI output +- Python client output + +The workspace also contains compile-pass and compile-fail tests driven by `trybuild`. + +This architecture chapter is intended to describe the code paths that exist in the repository, not an aspirational future design. diff --git a/docs/src/clients/README.md b/docs/src/clients/README.md index 23f8be85..c5e91261 100644 --- a/docs/src/clients/README.md +++ b/docs/src/clients/README.md @@ -1,70 +1,95 @@ # Client Generation -`reflectapi` automatically generates type-safe client libraries for multiple programming languages from your Rust API server. This section covers how to generate and use clients in different languages. +`reflectapi` can generate client code from a reflected schema JSON file. -## Supported Languages +## Supported Outputs -| Language | Status | -|------------|----------| -| TypeScript | ✅ Stable | -| Rust | ✅ Stable | -| Python | ✅ Experiemental | +| Output | Status | Notes | +|--------|--------|-------| +| TypeScript | Stable | Single generated file | +| Rust | Stable | Single generated file | +| Python | Experimental | Package-style output with `__init__.py` and `generated.py` | -## Code Generation Workflow +OpenAPI generation is also supported by the CLI, but it is documented separately as an API description format rather than a client library. -See demo project setup [https://github.com/thepartly/reflectapi/tree/main/reflectapi-demo](https://github.com/thepartly/reflectapi/tree/main/reflectapi-demo) +## Workflow -1. **Define your API server** using `reflectapi` traits and builder -2. **Generate schema** as JSON from your Rust application -3. **Run the CLI** to generate client libraries -4. **Use the clients** in your applications with full type safety +1. Define your API server using `reflectapi` derives and the builder API. +2. Write the schema JSON from your Rust application. +3. Run `reflectapi codegen` for the target language. +4. Commit or consume the generated client code from your application. -```bash -# Generate schema (from your Rust app) -cargo run # Your app should save reflectapi-schema.json +The CLI defaults to `reflectapi.json` if `--schema` is omitted. The demo project uses that filename. If your application writes a different filename such as `reflectapi-schema.json`, pass that path explicitly. -# Generate clients -cargo run reflectapi codegen --language typescript --schema reflectapi-schema.json --output clients/typescript -cargo run reflectapi codegen --language python --schema reflectapi-schema.json --output clients/python -cargo run reflectapi codegen --language rust --schema reflectapi-schema.json --output clients/rust +```bash +# Create output directories first. TypeScript and Rust write a single file +# unless the output path already exists as a directory or ends with a slash. +mkdir -p clients/typescript clients/python clients/rust + +# Generate TypeScript client -> clients/typescript/generated.ts +cargo run --bin reflectapi -- codegen \ + --language typescript \ + --schema reflectapi.json \ + --output clients/typescript/ + +# Generate Python client -> clients/python/__init__.py and generated.py +cargo run --bin reflectapi -- codegen \ + --language python \ + --schema reflectapi.json \ + --output clients/python/ \ + --python-sync + +# Generate Rust client -> clients/rust/generated.rs +cargo run --bin reflectapi -- codegen \ + --language rust \ + --schema reflectapi.json \ + --output clients/rust/ ``` -## Common Features +If you installed the CLI separately, replace `cargo run --bin reflectapi --` with `reflectapi`. + +## Output Shape + +The generators do not all emit the same file layout: + +| Output | Files written by the generator | +|--------|--------------------------------| +| TypeScript | `generated.ts` | +| Rust | `generated.rs` | +| Python | `__init__.py`, `generated.py` | + +The demo repository includes extra project scaffolding around some generated clients, but that scaffolding is not produced by `reflectapi codegen` itself. + +## Language Behavior -All generated clients share these characteristics: +### TypeScript -### Type Safety -- Native type definitions for each language -- Compile-time or runtime type checking -- IDE support with autocompletion +- Uses generated TypeScript types and function wrappers. +- Uses a `fetch`-based default client implementation. +- Parses JSON responses, but does not generate runtime schema validators today. +- Supports custom client implementations via the generated client interface. -### Extensibility -- Default base client implementation is provided -- Which can be replaced or extended with features, such opentelemetry instrumentation or playwrite tracing +### Python -### Error Handling -- Structured error types -- Network error handling +- Generates Pydantic-based models and client code. +- Generates an async client by default. +- Adds a sync client only when `--python-sync` is passed. +- Uses `reflectapi_runtime` for client base classes and runtime helpers. -### Async Support -- Modern async/await patterns +### Rust -### Documentation -- Generated from your Rust documentation -- Type information in IDE tooltips +- Generates typed async client methods. +- Integrates with `reflectapi::rt::Client`. +- Supports optional tracing instrumentation through `--instrument`. +- Generates serde-compatible types and request helpers for JSON-based transport. -### HTTP Client Libraries +## Shared Characteristics -| Language | HTTP Library | Features | -|----------|--------------|----------| -| TypeScript | fetch API | Native browser/Node.js support | -| Python | httpx | Async/sync, HTTP/2, connection pooling | -| Rust | reqwest | Async, HTTP/2, TLS, middleware | +The generated clients all aim to provide: -### Serialization +- Types derived from the Rust-reflected schema +- Function wrappers with generated documentation +- Structured handling of application errors versus transport/protocol failures +- Good IDE support through generated type information -| Language | Serialization | Validation | -|----------|---------------|------------| -| TypeScript | JSON.parse/stringify | Runtime type checking | -| Python | Pydantic | Schema validation, type coercion | -| Rust | JSON or MessagePack (serde) | Compile-time, zero-cost | +They do not currently all provide the same runtime validation guarantees or the same runtime transport abstractions, so those details should be considered language-specific. diff --git a/docs/src/getting-started/installation.md b/docs/src/getting-started/installation.md index 07556595..4fc067d6 100644 --- a/docs/src/getting-started/installation.md +++ b/docs/src/getting-started/installation.md @@ -13,9 +13,11 @@ cargo add reflectapi --features builder,axum,json,chrono Install the CLI tool to generate client libraries: ```bash -cargo install reflectapi-cli --help +cargo install reflectapi-cli ``` +This installs the `reflectapi` binary. + ## Next Steps - **New users**: Follow the [Quick Start](./quick-start.md) guide diff --git a/docs/src/getting-started/quick-start.md b/docs/src/getting-started/quick-start.md index 807d8dac..5edc14c1 100644 --- a/docs/src/getting-started/quick-start.md +++ b/docs/src/getting-started/quick-start.md @@ -16,15 +16,14 @@ cd my-api ## Add Dependencies -Add `reflectapi` to your `Cargo.toml`: - -```toml -[dependencies] -reflectapi = { version = "0.15", features = ["builder", "axum"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -tokio = { version = "1.0", features = ["full"] } -axum = "0.7" +Add the dependencies used by this example: + +```bash +cargo add reflectapi --features builder,axum +cargo add serde --features derive +cargo add serde_json +cargo add tokio --features full +cargo add axum ``` ## Define Your API @@ -33,12 +32,6 @@ Replace the contents of `src/main.rs`: ```rust,ignore // This is a complete example for src/main.rs -// Add these dependencies to your Cargo.toml first: -// [dependencies] -// reflectapi = { version = "0.15", features = ["builder", "axum"] } -// serde = { version = "1.0", features = ["derive"] } -// serde_json = "1.0" -// tokio = { version = "1.0", features = ["full"] } use reflectapi::{Builder, Input, Output}; use serde::{Deserialize, Serialize}; @@ -100,9 +93,9 @@ async fn main() -> Result<(), Box> { // Save schema for client generation let schema_json = serde_json::to_string_pretty(&schema)?; - std::fs::write("reflectapi-schema.json", schema_json)?; - - println!("✅ API schema generated at reflectapi-schema.json"); + std::fs::write("reflectapi.json", schema_json)?; + + println!("✅ API schema generated at reflectapi.json"); // Start the HTTP server let app_state = (); // No state needed for this example @@ -126,7 +119,7 @@ cargo run You should see: ```text -✅ API schema generated at reflectapi-schema.json +✅ API schema generated at reflectapi.json 🚀 Server running on http://0.0.0.0:3000 📖 Ready to generate clients! ``` @@ -141,11 +134,11 @@ First, install the CLI: cargo install reflectapi-cli ``` -Then generate a TypeScript client: +This installs the `reflectapi` binary. Then generate a TypeScript client: ```bash -mkdir clients -reflectapi-cli codegen --language typescript --schema reflectapi-schema.json --output clients/typescript +mkdir -p clients/typescript +reflectapi codegen --language typescript --schema reflectapi.json --output clients/typescript/ ``` ## Use Your Generated Client @@ -153,17 +146,18 @@ reflectapi-cli codegen --language typescript --schema reflectapi-schema.json --o The generated TypeScript client will be fully typed: ```typescript -import { client } from './clients/typescript'; +import { client } from "./clients/typescript/generated"; const c = client('http://localhost:3000'); -// Create a user - fully type-safe! -const newUser = await c.users.create({ +// Create a user. Generated methods take typed input and typed headers. +const created = await c.users.create({ name: 'Bob', email: 'bob@example.com' -}); +}, {}); -// Get a user -const user = await c.users.get(1); +if (created.is_ok()) { + console.log(created.unwrap_ok()); +} ``` That's it! diff --git a/reflectapi-cli/Cargo.toml b/reflectapi-cli/Cargo.toml index 18ed0762..4db642c1 100644 --- a/reflectapi-cli/Cargo.toml +++ b/reflectapi-cli/Cargo.toml @@ -17,6 +17,7 @@ categories = ["web-programming", "development-tools", "api-bindings"] [[bin]] name = "reflectapi" path = "src/main.rs" +doc = false [lints] workspace = true diff --git a/reflectapi-cli/src/main.rs b/reflectapi-cli/src/main.rs index 46d23e19..ef9acb53 100644 --- a/reflectapi-cli/src/main.rs +++ b/reflectapi-cli/src/main.rs @@ -190,6 +190,7 @@ fn main() -> anyhow::Result<()> { generate_async: python_async, generate_sync: python_sync, generate_testing: python_testing, + format, base_url: None, }; reflectapi::codegen::python::generate_files(schema, &config)? diff --git a/reflectapi-demo/clients/python/generated.py b/reflectapi-demo/clients/python/generated.py index 4b59f5c4..8ca74b6e 100644 --- a/reflectapi-demo/clients/python/generated.py +++ b/reflectapi-demo/clients/python/generated.py @@ -9,9 +9,9 @@ # Standard library imports +import warnings from datetime import datetime from enum import Enum -import warnings from typing import Annotated, Any, Generic, Literal, Optional, TypeVar, Union # Third-party imports @@ -19,7 +19,6 @@ BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -27,8 +26,37 @@ # Runtime imports from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse -from reflectapi_runtime import ReflectapiOption from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiOption + + +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") # Type variables for generic types @@ -38,8 +66,6 @@ class MyapiHealthCheckFail(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -74,45 +100,40 @@ class MyapiModelBehavior(RootModel[MyapiModelBehaviorVariants]): @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance(data, MyapiModelBehaviorAggressiveVariant): - return data - if isinstance(data, MyapiModelBehaviorOtherVariant): - return data - - # Handle JSON data (for deserialization) - if isinstance(data, str) and data == "Calm": - return data - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Aggressive": - if isinstance(value, list): - return MyapiModelBehaviorAggressiveVariant( - field_0=value[0], field_1=value[1] + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Calm": "_unit", + "Aggressive": lambda v: ( + MyapiModelBehaviorAggressiveVariant(field_0=v[0], field_1=v[1]) + if isinstance(v, list) + else (_ for _ in ()).throw( + ValueError("Expected list for tuple variant Aggressive") ) - else: - raise ValueError("Expected list for tuple variant Aggressive") - if key == "Other": - return MyapiModelBehaviorOtherVariant(**value) - - raise ValueError(f"Unknown variant for MyapiModelBehavior: {data}") + ), + "Other": lambda v: MyapiModelBehaviorOtherVariant(**v), + }, + (MyapiModelBehaviorAggressiveVariant, MyapiModelBehaviorOtherVariant), + "MyapiModelBehavior", + ) @model_serializer - def _serialize_externally_tagged(self): - if self.root == "Calm": - return "Calm" - if isinstance(self.root, MyapiModelBehaviorAggressiveVariant): - return {"Aggressive": [self.root.field_0, self.root.field_1]} - if isinstance(self.root, MyapiModelBehaviorOtherVariant): - return {"Other": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize MyapiModelBehavior variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Calm": (lambda r: r == "Calm", lambda r: "Calm"), + "Aggressive": ( + lambda r: isinstance(r, MyapiModelBehaviorAggressiveVariant), + lambda r: {"Aggressive": [r.field_0, r.field_1]}, + ), + "Other": ( + lambda r: isinstance(r, MyapiModelBehaviorOtherVariant), + lambda r: {"Other": r.model_dump(by_alias=True)}, + ), + }, + "MyapiModelBehavior", ) @@ -150,8 +171,6 @@ class MyapiModelKind(RootModel): class MyapiModelInputPet(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) name: str = Field(description="identity") @@ -164,8 +183,6 @@ class MyapiModelInputPet(BaseModel): class MyapiModelOutputPet(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) name: str = Field(description="identity") @@ -178,24 +195,18 @@ class MyapiModelOutputPet(BaseModel): class MyapiProtoHeaders(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) authorization: str = Field(description="Authorization header") class MyapiProtoInternalError(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) message: str class MyapiProtoPaginated(BaseModel, Generic[T]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) items: list[T] = Field(description="slice of a collection") @@ -203,8 +214,6 @@ class MyapiProtoPaginated(BaseModel, Generic[T]): class MyapiProtoPetsListRequest(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) limit: int | None = None @@ -212,16 +221,12 @@ class MyapiProtoPetsListRequest(BaseModel): class MyapiProtoPetsRemoveRequest(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) name: str = Field(description="identity") class MyapiProtoPetsUpdateRequest(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) name: str = Field(description="identity") @@ -237,8 +242,6 @@ class MyapiProtoPetsUpdateRequest(BaseModel): class MyapiProtoValidationA(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) message: str @@ -265,44 +268,42 @@ class MyapiProtoPetsCreateError(RootModel[MyapiProtoPetsCreateErrorVariants]): @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance(data, MyapiProtoPetsCreateErrorInvalidIdentityVariant): - return data - - # Handle JSON data (for deserialization) - if isinstance(data, str) and data == "Conflict": - return data - if isinstance(data, str) and data == "NotAuthorized": - return data - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "InvalidIdentity": - return MyapiProtoPetsCreateErrorInvalidIdentityVariant(**value) - - raise ValueError(f"Unknown variant for MyapiProtoPetsCreateError: {data}") + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Conflict": "_unit", + "NotAuthorized": "_unit", + "InvalidIdentity": lambda v: ( + MyapiProtoPetsCreateErrorInvalidIdentityVariant(**v) + ), + }, + (MyapiProtoPetsCreateErrorInvalidIdentityVariant,), + "MyapiProtoPetsCreateError", + ) @model_serializer - def _serialize_externally_tagged(self): - if self.root == "Conflict": - return "Conflict" - if self.root == "NotAuthorized": - return "NotAuthorized" - if isinstance(self.root, MyapiProtoPetsCreateErrorInvalidIdentityVariant): - return {"InvalidIdentity": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize MyapiProtoPetsCreateError variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Conflict": (lambda r: r == "Conflict", lambda r: "Conflict"), + "NotAuthorized": ( + lambda r: r == "NotAuthorized", + lambda r: "NotAuthorized", + ), + "InvalidIdentity": ( + lambda r: isinstance( + r, MyapiProtoPetsCreateErrorInvalidIdentityVariant + ), + lambda r: {"InvalidIdentity": r.model_dump(by_alias=True)}, + ), + }, + "MyapiProtoPetsCreateError", ) class MyapiProtoPetsListErrorInvalidCursor(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) kind: Literal["InvalidCursor"] = Field( @@ -311,8 +312,6 @@ class MyapiProtoPetsListErrorInvalidCursor(BaseModel): class MyapiProtoPetsListErrorUnauthorized(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) kind: Literal["Unauthorized"] = Field( @@ -321,8 +320,6 @@ class MyapiProtoPetsListErrorUnauthorized(BaseModel): class MyapiProtoPetsListErrorInternal(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) kind: Literal["Internal"] = Field( @@ -343,8 +340,6 @@ class MyapiProtoPetsListError(RootModel): class MyapiProtoPetsRemoveError(str, Enum): - """Generated enum.""" - NOT_FOUND = "NotFound" NOT_AUTHORIZED = "NotAuthorized" @@ -370,38 +365,36 @@ class MyapiProtoPetsUpdateError(RootModel[MyapiProtoPetsUpdateErrorVariants]): @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance(data, MyapiProtoPetsUpdateErrorValidationVariant): - return data - - # Handle JSON data (for deserialization) - if isinstance(data, str) and data == "NotFound": - return data - if isinstance(data, str) and data == "NotAuthorized": - return data - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Validation": - return MyapiProtoPetsUpdateErrorValidationVariant(field_0=value) - - raise ValueError(f"Unknown variant for MyapiProtoPetsUpdateError: {data}") + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "NotFound": "_unit", + "NotAuthorized": "_unit", + "Validation": lambda v: MyapiProtoPetsUpdateErrorValidationVariant( + field_0=v + ), + }, + (MyapiProtoPetsUpdateErrorValidationVariant,), + "MyapiProtoPetsUpdateError", + ) @model_serializer - def _serialize_externally_tagged(self): - if self.root == "NotFound": - return "NotFound" - if self.root == "NotAuthorized": - return "NotAuthorized" - if isinstance(self.root, MyapiProtoPetsUpdateErrorValidationVariant): - return {"Validation": self.root.field_0} - - raise ValueError( - f"Cannot serialize MyapiProtoPetsUpdateError variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "NotFound": (lambda r: r == "NotFound", lambda r: "NotFound"), + "NotAuthorized": ( + lambda r: r == "NotAuthorized", + lambda r: "NotAuthorized", + ), + "Validation": ( + lambda r: isinstance(r, MyapiProtoPetsUpdateErrorValidationVariant), + lambda r: {"Validation": r.field_0}, + ), + }, + "MyapiProtoPetsUpdateError", ) @@ -422,30 +415,31 @@ class MyapiProtoValidationError(RootModel[MyapiProtoValidationErrorVariants]): @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance(data, MyapiProtoValidationErrorValidationAVariant): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "ValidationA": - return MyapiProtoValidationErrorValidationAVariant(field_0=value) - - raise ValueError(f"Unknown variant for MyapiProtoValidationError: {data}") + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "ValidationA": lambda v: MyapiProtoValidationErrorValidationAVariant( + field_0=v + ) + }, + (MyapiProtoValidationErrorValidationAVariant,), + "MyapiProtoValidationError", + ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance(self.root, MyapiProtoValidationErrorValidationAVariant): - return {"ValidationA": self.root.field_0} - - raise ValueError( - f"Cannot serialize MyapiProtoValidationError variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "ValidationA": ( + lambda r: isinstance( + r, MyapiProtoValidationErrorValidationAVariant + ), + lambda r: {"ValidationA": r.field_0}, + ) + }, + "MyapiProtoValidationError", ) @@ -930,24 +924,26 @@ def __init__( StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - myapi.HealthCheckFail.model_rebuild() - myapi.model.Behavior.model_rebuild() - myapi.model.Kind.model_rebuild() - myapi.model.input.Pet.model_rebuild() - myapi.model.output.Pet.model_rebuild() - myapi.proto.Headers.model_rebuild() - myapi.proto.InternalError.model_rebuild() - myapi.proto.Paginated.model_rebuild() - myapi.proto.PetsCreateError.model_rebuild() - myapi.proto.PetsListError.model_rebuild() - myapi.proto.PetsListRequest.model_rebuild() - myapi.proto.PetsRemoveError.model_rebuild() - myapi.proto.PetsRemoveRequest.model_rebuild() - myapi.proto.PetsUpdateError.model_rebuild() - myapi.proto.PetsUpdateRequest.model_rebuild() - myapi.proto.ValidationA.model_rebuild() - myapi.proto.ValidationError.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + MyapiHealthCheckFail, + MyapiModelBehavior, + MyapiModelKind, + MyapiModelInputPet, + MyapiModelOutputPet, + MyapiProtoHeaders, + MyapiProtoInternalError, + MyapiProtoPaginated, + MyapiProtoPetsCreateError, + MyapiProtoPetsListError, + MyapiProtoPetsListRequest, + MyapiProtoPetsRemoveError, + MyapiProtoPetsRemoveRequest, + MyapiProtoPetsUpdateError, + MyapiProtoPetsUpdateRequest, + MyapiProtoValidationA, + MyapiProtoValidationError, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/clients/typescript/generated.ts b/reflectapi-demo/clients/typescript/generated.ts index 48c1f537..a83c1b9e 100644 --- a/reflectapi-demo/clients/typescript/generated.ts +++ b/reflectapi-demo/clients/typescript/generated.ts @@ -250,7 +250,9 @@ class ClientInstance { } type UnionToIntersection = ( - U extends any ? (k: U) => unknown : never + U extends any + ? (k: U) => unknown + : never ) extends (k: infer I) => void ? I : never; diff --git a/reflectapi-demo/src/tests/namespace.rs b/reflectapi-demo/src/tests/namespace.rs index 71a34d79..1e8ba147 100644 --- a/reflectapi-demo/src/tests/namespace.rs +++ b/reflectapi-demo/src/tests/namespace.rs @@ -65,3 +65,80 @@ fn test_generic_and_type_conflict() { .route(get, |b| b.name("get")) .rename_types("reflectapi_demo::tests::namespace::T", "T")) } + +#[test] +fn test_python_destutter_collision_falls_back_to_original_name() { + #[derive( + Debug, serde::Serialize, serde::Deserialize, reflectapi::Input, reflectapi::Output, + )] + struct First { + first: String, + } + + #[derive( + Debug, serde::Serialize, serde::Deserialize, reflectapi::Input, reflectapi::Output, + )] + struct Second { + second: u8, + } + + async fn first_get(_s: S, _: reflectapi::Empty, _: reflectapi::Empty) -> First { + First { + first: String::new(), + } + } + + async fn second_get(_s: S, _: reflectapi::Empty, _: reflectapi::Empty) -> Second { + Second { second: 0 } + } + + let (schema, _) = reflectapi::Builder::<()>::new() + .route(first_get, |b| b.name("first.get")) + .route(second_get, |b| b.name("second.get")) + .rename_types( + "reflectapi_demo::tests::namespace::First", + "OfferRequestPartIdentity", + ) + .rename_types( + "reflectapi_demo::tests::namespace::Second", + "offer_request::OfferRequestPartIdentity", + ) + .build() + .unwrap(); + + let python = reflectapi::codegen::python::generate( + schema, + &reflectapi::codegen::python::Config::default(), + ) + .unwrap(); + + assert_eq!( + python + .matches("class OfferRequestPartIdentity(BaseModel):") + .count(), + 1 + ); + assert!(python.contains("class OfferRequestOfferRequestPartIdentity(BaseModel):")); + assert!(python.contains("OfferRequestPartIdentity = OfferRequestOfferRequestPartIdentity")); +} + +#[test] +fn test_python_init_exports_client() { + #[derive( + Debug, serde::Serialize, serde::Deserialize, reflectapi::Input, reflectapi::Output, + )] + struct TestType { + value: u8, + } + + let files = reflectapi::codegen::python::generate_files( + super::into_schema::(), + &reflectapi::codegen::python::Config::default(), + ) + .unwrap(); + let init_py = files.get("__init__.py").unwrap(); + + assert!(init_py.contains("from .generated import AsyncClient, Client")); + assert!(init_py.contains("__all__ = [\"AsyncClient\", \"Client\"]")); + assert!(!init_py.contains("SyncClient")); +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated-5.snap index 4f635d94..a7807335 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicStructWithDeprecatedField(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -142,8 +140,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.StructWithDeprecatedField.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicStructWithDeprecatedField, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_enum_documented-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_enum_documented-4.snap index b0bc3526..c3418a01 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_enum_documented-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_enum_documented-4.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + # Type variables for generic types @@ -74,46 +102,43 @@ class ReflectapiDemoTestsBasicTestEnumDocumented( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance(data, ReflectapiDemoTestsBasicTestEnumDocumentedVariant1Variant): - return data - if isinstance(data, ReflectapiDemoTestsBasicTestEnumDocumentedVariant2Variant): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ReflectapiDemoTestsBasicTestEnumDocumentedVariant1Variant( - field_0=value - ) - if key == "Variant2": - return ReflectapiDemoTestsBasicTestEnumDocumentedVariant2Variant( - **value - ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsBasicTestEnumDocumented: {data}" + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Variant1": lambda v: ( + ReflectapiDemoTestsBasicTestEnumDocumentedVariant1Variant(field_0=v) + ), + "Variant2": lambda v: ( + ReflectapiDemoTestsBasicTestEnumDocumentedVariant2Variant(**v) + ), + }, + ( + ReflectapiDemoTestsBasicTestEnumDocumentedVariant1Variant, + ReflectapiDemoTestsBasicTestEnumDocumentedVariant2Variant, + ), + "ReflectapiDemoTestsBasicTestEnumDocumented", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( - self.root, ReflectapiDemoTestsBasicTestEnumDocumentedVariant1Variant - ): - return {"Variant1": self.root.field_0} - if isinstance( - self.root, ReflectapiDemoTestsBasicTestEnumDocumentedVariant2Variant - ): - return {"Variant2": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsBasicTestEnumDocumented variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Variant1": ( + lambda r: isinstance( + r, ReflectapiDemoTestsBasicTestEnumDocumentedVariant1Variant + ), + lambda r: {"Variant1": r.field_0}, + ), + "Variant2": ( + lambda r: isinstance( + r, ReflectapiDemoTestsBasicTestEnumDocumentedVariant2Variant + ), + lambda r: {"Variant2": r.model_dump(by_alias=True)}, + ), + }, + "ReflectapiDemoTestsBasicTestEnumDocumented", ) @@ -229,8 +254,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestEnumDocumented.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestEnumDocumented, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_enum_with_skip_variant-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_enum_with_skip_variant-5.snap index 35fa07a5..f4e8f4d4 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_enum_with_skip_variant-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_enum_with_skip_variant-5.snap @@ -26,15 +26,11 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicInputTestEnumWithSkipVariant(str, Enum): - """Generated enum.""" - A = "A" I = "I" class ReflectapiDemoTestsBasicOutputTestEnumWithSkipVariant(str, Enum): - """Generated enum.""" - A = "A" O = "O" @@ -161,9 +157,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.input.TestEnumWithSkipVariant.model_rebuild() - reflectapi_demo.tests.basic.output.TestEnumWithSkipVariant.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicInputTestEnumWithSkipVariant, + ReflectapiDemoTestsBasicOutputTestEnumWithSkipVariant, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_documented-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_documented-4.snap index 7589927d..3b907222 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_documented-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_documented-4.snap @@ -140,8 +140,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructDocumented.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructDocumented, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_empty-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_empty-5.snap index f4276e04..79be466c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_empty-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_empty-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructEmpty(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -136,8 +134,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructEmpty.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructEmpty, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_newtype-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_newtype-5.snap index c8045d02..888298e5 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_newtype-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_newtype-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/basic.rs expression: "super :: into_python_code :: < TestStructNewtype > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_static_str-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_static_str-4.snap index 7aa9890a..31277687 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_static_str-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_static_str-4.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructOneBasicFieldStaticStr(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") @@ -130,8 +128,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructOneBasicFieldStaticStr.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructOneBasicFieldStaticStr, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string-4.snap index f8c17f3c..9619a3ed 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string-4.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructOneBasicFieldString(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") @@ -144,8 +142,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructOneBasicFieldString.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructOneBasicFieldString, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both-5.snap index 369fb8e1..332468f6 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructOneBasicFieldStringReflectBoth(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") @@ -148,8 +146,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructOneBasicFieldStringReflectBoth.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructOneBasicFieldStringReflectBoth, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_equally-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_equally-5.snap index 0ceb49dd..553e5473 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_equally-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_equally-5.snap @@ -27,8 +27,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructOneBasicFieldStringReflectBothEqually( BaseModel ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -150,8 +148,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructOneBasicFieldStringReflectBothEqually.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructOneBasicFieldStringReflectBothEqually, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_equally2-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_equally2-4.snap index 12082e68..1f284d3c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_equally2-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_equally2-4.snap @@ -27,8 +27,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructOneBasicFieldStringReflectBothEqually( BaseModel ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -146,8 +144,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructOneBasicFieldStringReflectBothEqually.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructOneBasicFieldStringReflectBothEqually, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_with_attributes-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_with_attributes-5.snap index fb9ae717..51cb4c1c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_with_attributes-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_string_reflectapi_both_with_attributes-5.snap @@ -27,18 +27,14 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicInputTestStructOneBasicFieldStringReflectBothDifferently( BaseModel ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") -class ReflectapiDemoTestsBasicOutputTestStructOneBasicFieldStringReflectBothDifferently( +class ReflectapiDemoTestsBasicOutputTestStructOneBasicFieldStringReflectBothD_afce1cc8( BaseModel ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -62,7 +58,7 @@ class reflectapi_demo: class output: """Namespace for output types.""" - TestStructOneBasicFieldStringReflectBothDifferently = ReflectapiDemoTestsBasicOutputTestStructOneBasicFieldStringReflectBothDifferently + TestStructOneBasicFieldStringReflectBothD_afce1cc8 = ReflectapiDemoTestsBasicOutputTestStructOneBasicFieldStringReflectBothD_afce1cc8 class AsyncInoutClient: @@ -166,9 +162,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.input.TestStructOneBasicFieldStringReflectBothDifferently.model_rebuild() - reflectapi_demo.tests.basic.output.TestStructOneBasicFieldStringReflectBothDifferently.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicInputTestStructOneBasicFieldStringReflectBothDifferently, + ReflectapiDemoTestsBasicOutputTestStructOneBasicFieldStringReflectBothD_afce1cc8, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_u32-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_u32-4.snap index c1c1c38a..0f265927 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_u32-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_one_basic_field_u32-4.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructOneBasicFieldU32(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructOneBasicFieldU32.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructOneBasicFieldU32, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_option-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_option-5.snap index c963a7ee..032abd79 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_option-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_option-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructOption(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int | None = Field(default=None, serialization_alias="_f", validation_alias="_f") @@ -139,8 +137,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructOption.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructOption, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_tuple-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_tuple-5.snap index b89f25d7..a5739afa 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_tuple-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_tuple-5.snap @@ -25,7 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructTuple(BaseModel): - """Generated data model.""" model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -140,8 +139,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructTuple.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructTuple, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_unit_type-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_unit_type-5.snap index 99904cf7..a4400307 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_unit_type-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_unit_type-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/basic.rs expression: "super :: into_python_code :: < TestStructUnitType > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_additional_derives-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_additional_derives-5.snap index b70547ad..f39691a0 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_additional_derives-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_additional_derives-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTest(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) us: list[None] @@ -36,14 +34,10 @@ class ReflectapiDemoTestsBasicTest(BaseModel): class ReflectapiDemoTestsBasicX(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) class ReflectapiDemoTestsBasicY(str, Enum): - """Generated enum.""" - Y = "Y" @@ -155,10 +149,12 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.Test.model_rebuild() - reflectapi_demo.tests.basic.X.model_rebuild() - reflectapi_demo.tests.basic.Y.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTest, + ReflectapiDemoTestsBasicX, + ReflectapiDemoTestsBasicY, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_all_primitive_type_fields-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_all_primitive_type_fields-5.snap index 39613578..01ea7b37 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_all_primitive_type_fields-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_all_primitive_type_fields-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithAllPrimitiveTypeFields(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f_u8: int = Field(serialization_alias="_f_u8", validation_alias="_f_u8") @@ -233,8 +231,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithAllPrimitiveTypeFields.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithAllPrimitiveTypeFields, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_arc-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_arc-5.snap index 309214f5..3e206167 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_arc-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_arc-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithArc(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithArc.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithArc, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_arc_pointer_only-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_arc_pointer_only-5.snap index 7baa4bac..e88c67ab 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_arc_pointer_only-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_arc_pointer_only-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithArcPointerOnly(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f_pointer_arc: int = Field( @@ -142,8 +140,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithArcPointerOnly.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithArcPointerOnly, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes-5.snap index b3c8c94b..8ca61590 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/basic.rs expression: "super :: into_python_code :: < TestStructWithAttributes > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_input_only-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_input_only-5.snap index bbe42397..0ebe4c85 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_input_only-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_input_only-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithAttributesInputOnly(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithAttributesInputOnly.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithAttributesInputOnly, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_output_only-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_output_only-5.snap index f3dafc2b..cc426f64 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_output_only-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_output_only-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithAttributesOutputOnly(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") @@ -144,8 +142,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithAttributesOutputOnly.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithAttributesOutputOnly, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_type_only-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_type_only-5.snap index 9032b21a..589994cd 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_type_only-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_attributes_type_only-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/basic.rs expression: "super :: into_python_code :: < TestStructWithAttributesTypeOnly > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_external_generic_type_fallback-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_external_generic_type_fallback-5.snap index 5675ba5f..5c7b851b 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_external_generic_type_fallback-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_external_generic_type_fallback-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithExternalGenericTypeFallback(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) data: dict[str, int] @@ -148,8 +146,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithExternalGenericTypeFallback.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithExternalGenericTypeFallback, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_fixed_size_array-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_fixed_size_array-5.snap index af3eec3a..58937597 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_fixed_size_array-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_fixed_size_array-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithFixedSizeArray(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: bytes = Field(serialization_alias="_f", validation_alias="_f") @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithFixedSizeArray.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithFixedSizeArray, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashmap-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashmap-5.snap index be526eb9..a669b578 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashmap-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashmap-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithHashMap(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: dict[int, str] = Field(serialization_alias="_f", validation_alias="_f") @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithHashMap.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithHashMap, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashset_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashset_field-5.snap index ec4c116d..27182cfe 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashset_field-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashset_field-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithHashSetField(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f_hashset: bytes = Field( @@ -142,8 +140,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithHashSetField.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithHashSetField, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashset_field_generic-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashset_field_generic-5.snap index 514bfa4e..099ded8f 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashset_field_generic-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_hashset_field_generic-5.snap @@ -31,8 +31,6 @@ G = TypeVar("G") class ReflectapiDemoTestsBasicTestStructWithHashSetFieldGeneric(BaseModel, Generic[G]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f_hashset: list[G] = Field( @@ -160,8 +158,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithHashSetFieldGeneric.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithHashSetFieldGeneric, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_nested-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_nested-5.snap index 720c3d85..ce9ccbf7 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_nested-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_nested-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") class ReflectapiDemoTestsBasicTestStructWithNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.basic.TestStructNested = Field( @@ -149,9 +145,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructNested.model_rebuild() - reflectapi_demo.tests.basic.TestStructWithNested.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructNested, + ReflectapiDemoTestsBasicTestStructWithNested, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_nested_external-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_nested_external-5.snap index 39d30505..190c7476 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_nested_external-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_nested_external-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithNestedExternal(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.test_lib.TestStructNested = Field( @@ -35,8 +33,6 @@ class ReflectapiDemoTestsBasicTestStructWithNestedExternal(BaseModel): class ReflectapiDemoTestsTestLibTestStructNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") @@ -155,9 +151,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithNestedExternal.model_rebuild() - reflectapi_demo.tests.test_lib.TestStructNested.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithNestedExternal, + ReflectapiDemoTestsTestLibTestStructNested, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_self_via_arc-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_self_via_arc-5.snap index d417ecd8..f4a8e5e4 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_self_via_arc-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_self_via_arc-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithSelfViaArc(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.basic.TestStructWithSelfViaArc = Field( @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithSelfViaArc.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithSelfViaArc, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field-5.snap index 54ac90a9..1a5acc18 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithSkipField(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -136,8 +134,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithSkipField.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithSkipField, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field_input-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field_input-5.snap index d052cf13..c8cee362 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field_input-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field_input-5.snap @@ -25,14 +25,10 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicInputTestStructWithSkipFieldInput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) class ReflectapiDemoTestsBasicOutputTestStructWithSkipFieldInput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -160,9 +156,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.input.TestStructWithSkipFieldInput.model_rebuild() - reflectapi_demo.tests.basic.output.TestStructWithSkipFieldInput.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicInputTestStructWithSkipFieldInput, + ReflectapiDemoTestsBasicOutputTestStructWithSkipFieldInput, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field_output-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field_output-5.snap index 46c77102..3dee605c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field_output-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_skip_field_output-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicInputTestStructWithSkipFieldOutput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") class ReflectapiDemoTestsBasicOutputTestStructWithSkipFieldOutput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -160,9 +156,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.input.TestStructWithSkipFieldOutput.model_rebuild() - reflectapi_demo.tests.basic.output.TestStructWithSkipFieldOutput.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicInputTestStructWithSkipFieldOutput, + ReflectapiDemoTestsBasicOutputTestStructWithSkipFieldOutput, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_array-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_array-5.snap index c3d1e56b..83c7d6e2 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_array-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_array-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithTransformArray(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: bytes = Field(serialization_alias="_f", validation_alias="_f") @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithTransformArray.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithTransformArray, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_both-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_both-5.snap index cf78b4e5..a361ba61 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_both-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_both-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithTransformBoth(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithTransformBoth.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithTransformBoth, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_fallback-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_fallback-5.snap index 9fdd95c4..a0993009 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_fallback-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_fallback-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithTransformFallback(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -144,8 +142,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithTransformFallback.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithTransformFallback, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_fallback_nested-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_fallback_nested-5.snap index 600258d9..2866e498 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_fallback_nested-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_fallback_nested-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithTransformFallbackNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -144,8 +142,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithTransformFallbackNested.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithTransformFallbackNested, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_input-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_input-5.snap index 15856e46..10bca281 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_input-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_input-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicInputTestStructWithTransformInput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") class ReflectapiDemoTestsBasicOutputTestStructWithTransformInput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -162,9 +158,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.input.TestStructWithTransformInput.model_rebuild() - reflectapi_demo.tests.basic.output.TestStructWithTransformInput.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicInputTestStructWithTransformInput, + ReflectapiDemoTestsBasicOutputTestStructWithTransformInput, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_output-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_output-5.snap index eae22a12..380c7357 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_output-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_transform_output-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicInputTestStructWithTransformOutput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") class ReflectapiDemoTestsBasicOutputTestStructWithTransformOutput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") @@ -162,9 +158,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.input.TestStructWithTransformOutput.model_rebuild() - reflectapi_demo.tests.basic.output.TestStructWithTransformOutput.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicInputTestStructWithTransformOutput, + ReflectapiDemoTestsBasicOutputTestStructWithTransformOutput, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_tuple-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_tuple-5.snap index b194eb1f..024a2cc8 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_tuple-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_tuple-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithTuple(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: std.tuple.Tuple2[int, str] = Field( @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithTuple.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithTuple, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_tuple12-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_tuple12-5.snap index 841473d7..528f25a7 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_tuple12-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_tuple12-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithTuple12(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: std.tuple.Tuple12[int, str, int, str, int, str, int, str, int, str, int, str] = ( @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithTuple12.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithTuple12, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec-5.snap index bea3b0f4..f0dc64e9 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithVec(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: bytes = Field(serialization_alias="_f", validation_alias="_f") @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithVec.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithVec, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_external-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_external-5.snap index e714c4b8..a70f7dc9 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_external-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_external-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithVecExternal(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: list[reflectapi_demo.tests.test_lib.TestStructNested] = Field( @@ -35,8 +33,6 @@ class ReflectapiDemoTestsBasicTestStructWithVecExternal(BaseModel): class ReflectapiDemoTestsTestLibTestStructNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") @@ -155,9 +151,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithVecExternal.model_rebuild() - reflectapi_demo.tests.test_lib.TestStructNested.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithVecExternal, + ReflectapiDemoTestsTestLibTestStructNested, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_nested-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_nested-5.snap index 200bcee5..d703c554 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_nested-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_nested-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithVecNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: list[list[reflectapi_demo.tests.test_lib.TestStructNested]] = Field( @@ -35,8 +33,6 @@ class ReflectapiDemoTestsBasicTestStructWithVecNested(BaseModel): class ReflectapiDemoTestsTestLibTestStructNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: str = Field(serialization_alias="_f", validation_alias="_f") @@ -153,9 +149,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithVecNested.model_rebuild() - reflectapi_demo.tests.test_lib.TestStructNested.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithVecNested, + ReflectapiDemoTestsTestLibTestStructNested, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_two-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_two-5.snap index 99d73b0b..ae911438 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_two-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_vec_two-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsBasicTestStructWithVecTwo(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: bytes = Field(serialization_alias="_f", validation_alias="_f") @@ -139,8 +137,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.basic.TestStructWithVecTwo.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsBasicTestStructWithVecTwo, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum-4.snap index dd0929e3..2febe857 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum-4.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsEnumsTestEnum(str, Enum): - """Generated enum.""" - VARIANT1 = "Variant1" VARIANT2 = "Variant2" @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnum.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnum, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_empty-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_empty-4.snap index 72b8e901..4341d2e5 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_empty-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_empty-4.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsEnumsTestEnumEmpty(str, Enum): - """Generated enum.""" - pass @@ -137,8 +135,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumEmpty.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumEmpty, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_rename_num-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_rename_num-5.snap index d1d97873..7bc97ea6 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_rename_num-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_rename_num-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsEnumsNums(str, Enum): - """Generated enum.""" - ZERO = "0" A = "A" @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.Nums.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsNums, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_basic_variant_and_fields_and_named_fields-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_basic_variant_and_fields_and_named_fields-4.snap index 20061b55..6463be51 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_basic_variant_and_fields_and_named_fields-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_basic_variant_and_fields_and_named_fields-4.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant1Variant( BaseModel ): @@ -72,61 +100,55 @@ class ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFields( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( + def _validate(cls, data): + return _parse_externally_tagged( data, - ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant1Variant, - ): - return data - if isinstance( - data, - ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant2Variant, - ): - return data - - # Handle JSON data (for deserialization) - if isinstance(data, str) and data == "Variant0": - return data - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - if isinstance(value, list): - return ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant1Variant( - field_0=value[0], field_1=value[1] + { + "Variant0": "_unit", + "Variant1": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant1Variant( + field_0=v[0], field_1=v[1] + ) + if isinstance(v, list) + else (_ for _ in ()).throw( + ValueError("Expected list for tuple variant Variant1") ) - else: - raise ValueError("Expected list for tuple variant Variant1") - if key == "Variant2": - return ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant2Variant( - **value - ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFields: {data}" + ), + "Variant2": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant2Variant( + **v + ) + ), + }, + ( + ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant1Variant, + ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant2Variant, + ), + "ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFields", ) @model_serializer - def _serialize_externally_tagged(self): - if self.root == "Variant0": - return "Variant0" - if isinstance( - self.root, - ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant1Variant, - ): - return {"Variant1": [self.root.field_0, self.root.field_1]} - if isinstance( + def _serialize(self): + return _serialize_externally_tagged( self.root, - ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant2Variant, - ): - return {"Variant2": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFields variant: {type(self.root)}" + { + "Variant0": (lambda r: r == "Variant0", lambda r: "Variant0"), + "Variant1": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant1Variant, + ), + lambda r: {"Variant1": [r.field_0, r.field_1]}, + ), + "Variant2": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFieldsVariant2Variant, + ), + lambda r: {"Variant2": r.model_dump(by_alias=True)}, + ), + }, + "ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFields", ) @@ -244,8 +266,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithBasicVariantAndFieldsAndNamedFields.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithBasicVariantAndFieldsAndNamedFields, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_ignored_input-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_ignored_input-4.snap index c2226d35..4401af5c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_ignored_input-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_ignored_input-4.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsEnumsTestEnumWithDiscriminantIgnored(str, Enum): - """Generated enum.""" - VARIANT1 = "Variant1" VARIANT2 = "Variant2" @@ -144,8 +142,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithDiscriminantIgnored.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithDiscriminantIgnored, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_ignored_output-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_ignored_output-4.snap index a0359ef3..a35160db 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_ignored_output-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_ignored_output-4.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsEnumsTestEnumWithDiscriminantIgnored(str, Enum): - """Generated enum.""" - VARIANT1 = "Variant1" VARIANT2 = "Variant2" @@ -130,8 +128,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithDiscriminantIgnored.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithDiscriminantIgnored, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_input-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_input-4.snap index 4ebe0d2f..3612a421 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_input-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_input-4.snap @@ -144,8 +144,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithDiscriminant.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithDiscriminant, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_output-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_output-4.snap index fa0609e5..87da130b 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_output-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_discriminant_output-4.snap @@ -134,8 +134,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithDiscriminant.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithDiscriminant, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_empty_variant_and_fields-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_empty_variant_and_fields-4.snap index 57319bdb..d4784580 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_empty_variant_and_fields-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_empty_variant_and_fields-4.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFieldsVariant2Variant( BaseModel ): @@ -57,44 +85,36 @@ class ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFields( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( + def _validate(cls, data): + return _parse_externally_tagged( data, - ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFieldsVariant2Variant, - ): - return data - - # Handle JSON data (for deserialization) - if isinstance(data, str) and data == "Variant1": - return data - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant2": - return ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFieldsVariant2Variant( - field_0=value - ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFields: {data}" + { + "Variant1": "_unit", + "Variant2": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFieldsVariant2Variant( + field_0=v + ) + ), + }, + (ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFieldsVariant2Variant,), + "ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFields", ) @model_serializer - def _serialize_externally_tagged(self): - if self.root == "Variant1": - return "Variant1" - if isinstance( + def _serialize(self): + return _serialize_externally_tagged( self.root, - ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFieldsVariant2Variant, - ): - return {"Variant2": self.root.field_0} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFields variant: {type(self.root)}" + { + "Variant1": (lambda r: r == "Variant1", lambda r: "Variant1"), + "Variant2": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFieldsVariant2Variant, + ), + lambda r: {"Variant2": r.field_0}, + ), + }, + "ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFields", ) @@ -213,8 +233,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithEmptyVariantAndFields.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithEmptyVariantAndFields, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_fields-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_fields-4.snap index 13c76fb5..919d5f75 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_fields-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_fields-4.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant1Variant(BaseModel): """Variant1 variant""" @@ -64,49 +92,49 @@ class ReflectapiDemoTestsEnumsTestEnumWithFields( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance(data, ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant1Variant): - return data - if isinstance(data, ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant2Variant): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant1Variant( - field_0=value - ) - if key == "Variant2": - if isinstance(value, list): - return ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant2Variant( - field_0=value[0], field_1=value[1] + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Variant1": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant1Variant(field_0=v) + ), + "Variant2": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant2Variant( + field_0=v[0], field_1=v[1] ) - else: - raise ValueError("Expected list for tuple variant Variant2") - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsEnumsTestEnumWithFields: {data}" + if isinstance(v, list) + else (_ for _ in ()).throw( + ValueError("Expected list for tuple variant Variant2") + ) + ), + }, + ( + ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant1Variant, + ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant2Variant, + ), + "ReflectapiDemoTestsEnumsTestEnumWithFields", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( - self.root, ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant1Variant - ): - return {"Variant1": self.root.field_0} - if isinstance( - self.root, ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant2Variant - ): - return {"Variant2": [self.root.field_0, self.root.field_1]} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsEnumsTestEnumWithFields variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Variant1": ( + lambda r: isinstance( + r, ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant1Variant + ), + lambda r: {"Variant1": r.field_0}, + ), + "Variant2": ( + lambda r: isinstance( + r, ReflectapiDemoTestsEnumsTestEnumWithFieldsVariant2Variant + ), + lambda r: {"Variant2": [r.field_0, r.field_1]}, + ), + }, + "ReflectapiDemoTestsEnumsTestEnumWithFields", ) @@ -222,8 +250,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithFields.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithFields, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics-4.snap index 8550df10..7a3c01d8 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics-4.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + # Type variables for generic types @@ -78,53 +106,51 @@ class ReflectapiDemoTestsEnumsTestEnumWithGenerics( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( - data, ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant1Variant - ): - return data - if isinstance( - data, ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant2Variant - ): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant1Variant( - field_0=value - ) - if key == "Variant2": - if isinstance(value, list): - return ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant2Variant( - field_0=value[0], field_1=value[1] + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Variant1": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant1Variant( + field_0=v ) - else: - raise ValueError("Expected list for tuple variant Variant2") - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsEnumsTestEnumWithGenerics: {data}" + ), + "Variant2": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant2Variant( + field_0=v[0], field_1=v[1] + ) + if isinstance(v, list) + else (_ for _ in ()).throw( + ValueError("Expected list for tuple variant Variant2") + ) + ), + }, + ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant1Variant, + ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant2Variant, + ), + "ReflectapiDemoTestsEnumsTestEnumWithGenerics", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( - self.root, ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant1Variant - ): - return {"Variant1": self.root.field_0} - if isinstance( - self.root, ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant2Variant - ): - return {"Variant2": [self.root.field_0, self.root.field_1]} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsEnumsTestEnumWithGenerics variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Variant1": ( + lambda r: isinstance( + r, ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant1Variant + ), + lambda r: {"Variant1": r.field_0}, + ), + "Variant2": ( + lambda r: isinstance( + r, ReflectapiDemoTestsEnumsTestEnumWithGenericsVariant2Variant + ), + lambda r: {"Variant2": [r.field_0, r.field_1]}, + ), + }, + "ReflectapiDemoTestsEnumsTestEnumWithGenerics", ) @@ -240,8 +266,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithGenerics.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithGenerics, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics_and_fields-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics_and_fields-4.snap index 4482a317..17bc0468 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics_and_fields-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics_and_fields-4.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + # Type variables for generic types @@ -78,55 +106,53 @@ class ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFields( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( - data, ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant1Variant - ): - return data - if isinstance( - data, ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant2Variant - ): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant1Variant( - field_0=value - ) - if key == "Variant2": - if isinstance(value, list): - return ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant2Variant( - field_0=value[0], field_1=value[1] + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Variant1": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant1Variant( + field_0=v ) - else: - raise ValueError("Expected list for tuple variant Variant2") - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFields: {data}" + ), + "Variant2": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant2Variant( + field_0=v[0], field_1=v[1] + ) + if isinstance(v, list) + else (_ for _ in ()).throw( + ValueError("Expected list for tuple variant Variant2") + ) + ), + }, + ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant1Variant, + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant2Variant, + ), + "ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFields", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( + def _serialize(self): + return _serialize_externally_tagged( self.root, - ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant1Variant, - ): - return {"Variant1": self.root.field_0} - if isinstance( - self.root, - ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant2Variant, - ): - return {"Variant2": [self.root.field_0, self.root.field_1]} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFields variant: {type(self.root)}" + { + "Variant1": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant1Variant, + ), + lambda r: {"Variant1": r.field_0}, + ), + "Variant2": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsVariant2Variant, + ), + lambda r: {"Variant2": [r.field_0, r.field_1]}, + ), + }, + "ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFields", ) @@ -248,8 +274,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithGenericsAndFields.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFields, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics_and_fields_and_named_fields-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics_and_fields_and_named_fields-4.snap index c20a1187..c03581e1 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics_and_fields_and_named_fields-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__enum_with_generics_and_fields_and_named_fields-4.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + # Type variables for generic types @@ -99,71 +127,66 @@ class ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFields( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( - data, - ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant1Variant, - ): - return data - if isinstance( - data, - ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant2Variant, - ): - return data - if isinstance( + def _validate(cls, data): + return _parse_externally_tagged( data, - ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant3Variant, - ): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant1Variant( - field_0=value - ) - if key == "Variant2": - if isinstance(value, list): - return ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant2Variant( - field_0=value[0], field_1=value[1] + { + "Variant1": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant1Variant( + field_0=v + ) + ), + "Variant2": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant2Variant( + field_0=v[0], field_1=v[1] + ) + if isinstance(v, list) + else (_ for _ in ()).throw( + ValueError("Expected list for tuple variant Variant2") + ) + ), + "Variant3": lambda v: ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant3Variant( + **v ) - else: - raise ValueError("Expected list for tuple variant Variant2") - if key == "Variant3": - return ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant3Variant( - **value - ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFields: {data}" + ), + }, + ( + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant1Variant, + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant2Variant, + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant3Variant, + ), + "ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFields", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( + def _serialize(self): + return _serialize_externally_tagged( self.root, - ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant1Variant, - ): - return {"Variant1": self.root.field_0} - if isinstance( - self.root, - ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant2Variant, - ): - return {"Variant2": [self.root.field_0, self.root.field_1]} - if isinstance( - self.root, - ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant3Variant, - ): - return {"Variant3": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFields variant: {type(self.root)}" + { + "Variant1": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant1Variant, + ), + lambda r: {"Variant1": r.field_0}, + ), + "Variant2": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant2Variant, + ), + lambda r: {"Variant2": [r.field_0, r.field_1]}, + ), + "Variant3": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFieldsVariant3Variant, + ), + lambda r: {"Variant3": r.model_dump(by_alias=True)}, + ), + }, + "ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFields", ) @@ -282,8 +305,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.TestEnumWithGenericsAndFieldsAndNamedFields.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsTestEnumWithGenericsAndFieldsAndNamedFields, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__internally_tagged_enum_with_tuple_variants-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__internally_tagged_enum_with_tuple_variants-5.snap index 051d6de2..8e4bde6d 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__internally_tagged_enum_with_tuple_variants-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__internally_tagged_enum_with_tuple_variants-5.snap @@ -26,15 +26,11 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsEnumsA(str, Enum): - """Generated enum.""" - X = "X" Y = "Y" class ReflectapiDemoTestsEnumsEA(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["a"] = Field(default="a", description="Discriminator field") @@ -153,9 +149,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.A.model_rebuild() - reflectapi_demo.tests.enums.E.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsA, + ReflectapiDemoTestsEnumsE, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__internally_tagged_enum_with_unit_variants-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__internally_tagged_enum_with_unit_variants-5.snap index 1b463415..6555c8b0 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__internally_tagged_enum_with_unit_variants-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__enums__internally_tagged_enum_with_unit_variants-5.snap @@ -26,16 +26,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsEnumsAX(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["X"] = Field(default="X", description="Discriminator field") class ReflectapiDemoTestsEnumsAY(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Y"] = Field(default="Y", description="Discriminator field") @@ -157,8 +153,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.enums.A.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsEnumsA, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference-4.snap index 075935c8..07aeab7c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference-4.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsGenericsTestStructWithCircularReference(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.generics.TestStructWithCircularReference = Field( @@ -146,8 +144,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithCircularReference.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithCircularReference, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic-4.snap index 510a7161..947f5db6 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic-4.snap @@ -33,8 +33,6 @@ T = TypeVar("T") class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGeneric( BaseModel, Generic[T] ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.generics.TestStructWithCircularReferenceGeneric[T] = Field( @@ -155,8 +153,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithCircularReferenceGeneric.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGeneric, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_parent-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_parent-4.snap index 3c2e3ab2..2a3441a0 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_parent-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_parent-4.snap @@ -33,8 +33,6 @@ T = TypeVar("T") class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGeneric( BaseModel, Generic[T] ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.generics.TestStructWithCircularReferenceGeneric[T] = Field( @@ -46,8 +44,6 @@ class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGeneric( class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericParent( BaseModel, Generic[T] ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.generics.TestStructWithCircularReferenceGeneric[ @@ -177,9 +173,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithCircularReferenceGeneric.model_rebuild() - reflectapi_demo.tests.generics.TestStructWithCircularReferenceGenericParent.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGeneric, + ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericParent, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box-4.snap index fb7a6721..e7d7eb53 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box-4.snap @@ -35,8 +35,6 @@ B = TypeVar("B") class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBox( BaseModel, Generic[A, B] ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f1: A = Field(serialization_alias="_f1", validation_alias="_f1") @@ -167,8 +165,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithCircularReferenceGenericWithoutBox.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBox, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box_parent-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box_parent-4.snap index 2b462663..b0edaa20 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box_parent-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box_parent-4.snap @@ -39,19 +39,15 @@ D = TypeVar("D") class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBox( BaseModel, Generic[A, B] ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f1: A = Field(serialization_alias="_f1", validation_alias="_f1") f2: B = Field(serialization_alias="_f2", validation_alias="_f2") -class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBoxParent( +class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithou_8e2b5cb9( BaseModel, Generic[C, D] ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.generics.TestStructWithCircularReferenceGenericWithoutBox[ @@ -70,7 +66,7 @@ class reflectapi_demo: """Namespace for generics types.""" TestStructWithCircularReferenceGenericWithoutBox = ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBox - TestStructWithCircularReferenceGenericWithoutBoxParent = ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBoxParent + TestStructWithCircularReferenceGenericWithou_8e2b5cb9 = ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithou_8e2b5cb9 class AsyncInputClient: @@ -184,9 +180,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithCircularReferenceGenericWithoutBox.model_rebuild() - reflectapi_demo.tests.generics.TestStructWithCircularReferenceGenericWithoutBoxParent.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBox, + ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithou_8e2b5cb9, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box_parent_specific-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box_parent_specific-4.snap index 5332fd41..74f9c5d2 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box_parent_specific-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_circular_reference_generic_without_box_parent_specific-4.snap @@ -35,19 +35,15 @@ B = TypeVar("B") class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBox( BaseModel, Generic[A, B] ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f1: A = Field(serialization_alias="_f1", validation_alias="_f1") f2: B = Field(serialization_alias="_f2", validation_alias="_f2") -class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBoxParentSpecific( +class ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithou_be812837( BaseModel ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.generics.TestStructWithCircularReferenceGenericWithoutBox[ @@ -71,7 +67,7 @@ class reflectapi_demo: """Namespace for generics types.""" TestStructWithCircularReferenceGenericWithoutBox = ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBox - TestStructWithCircularReferenceGenericWithoutBoxParentSpecific = ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBoxParentSpecific + TestStructWithCircularReferenceGenericWithou_be812837 = ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithou_be812837 class AsyncInputClient: @@ -171,9 +167,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithCircularReferenceGenericWithoutBox.model_rebuild() - reflectapi_demo.tests.generics.TestStructWithCircularReferenceGenericWithoutBoxParentSpecific.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithoutBox, + ReflectapiDemoTestsGenericsTestStructWithCircularReferenceGenericWithou_be812837, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_nested_generic_struct-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_nested_generic_struct-4.snap index 364f6ddf..7686bddd 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_nested_generic_struct-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_nested_generic_struct-4.snap @@ -31,8 +31,6 @@ A = TypeVar("A") class ReflectapiDemoTestsGenericsTestStructWithNestedGenericStruct(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.generics.TestStructWithSimpleGeneric[ @@ -41,8 +39,6 @@ class ReflectapiDemoTestsGenericsTestStructWithNestedGenericStruct(BaseModel): class ReflectapiDemoTestsGenericsTestStructWithSimpleGeneric(BaseModel, Generic[A]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: A = Field(serialization_alias="_f", validation_alias="_f") @@ -163,9 +159,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithNestedGenericStruct.model_rebuild() - reflectapi_demo.tests.generics.TestStructWithSimpleGeneric.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithNestedGenericStruct, + ReflectapiDemoTestsGenericsTestStructWithSimpleGeneric, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_nested_generic_struct_twice-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_nested_generic_struct_twice-4.snap index 6197e8c1..5cc7e7d6 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_nested_generic_struct_twice-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_nested_generic_struct_twice-4.snap @@ -31,8 +31,6 @@ A = TypeVar("A") class ReflectapiDemoTestsGenericsTestStructWithNestedGenericStructTwice(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: reflectapi_demo.tests.generics.TestStructWithSimpleGeneric[int] = Field( @@ -44,8 +42,6 @@ class ReflectapiDemoTestsGenericsTestStructWithNestedGenericStructTwice(BaseMode class ReflectapiDemoTestsGenericsTestStructWithSimpleGeneric(BaseModel, Generic[A]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: A = Field(serialization_alias="_f", validation_alias="_f") @@ -166,9 +162,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithNestedGenericStructTwice.model_rebuild() - reflectapi_demo.tests.generics.TestStructWithSimpleGeneric.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithNestedGenericStructTwice, + ReflectapiDemoTestsGenericsTestStructWithSimpleGeneric, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_simple_generic-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_simple_generic-4.snap index ccde25c4..884ab6f4 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_simple_generic-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_simple_generic-4.snap @@ -31,8 +31,6 @@ A = TypeVar("A") class ReflectapiDemoTestsGenericsTestStructWithSimpleGeneric(BaseModel, Generic[A]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: A = Field(serialization_alias="_f", validation_alias="_f") @@ -150,8 +148,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithSimpleGeneric.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithSimpleGeneric, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic-4.snap index 7ea8d753..4062451e 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic-4.snap @@ -31,8 +31,6 @@ T = TypeVar("T") class ReflectapiDemoTestsGenericsTestStructWithVecGeneric(BaseModel, Generic[T]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: list[T] = Field(serialization_alias="_f", validation_alias="_f") @@ -150,8 +148,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithVecGeneric.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithVecGeneric, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic_generic-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic_generic-4.snap index 98a157b6..6d4f3cd3 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic_generic-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic_generic-4.snap @@ -33,16 +33,12 @@ T = TypeVar("T") class ReflectapiDemoTestsGenericsTestStructWithSimpleGeneric(BaseModel, Generic[A]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: A = Field(serialization_alias="_f", validation_alias="_f") class ReflectapiDemoTestsGenericsTestStructWithVecGenericGeneric(BaseModel, Generic[T]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: list[reflectapi_demo.tests.generics.TestStructWithSimpleGeneric[T]] = Field( @@ -165,9 +161,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithSimpleGeneric.model_rebuild() - reflectapi_demo.tests.generics.TestStructWithVecGenericGeneric.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithSimpleGeneric, + ReflectapiDemoTestsGenericsTestStructWithVecGenericGeneric, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic_generic_generic-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic_generic_generic-4.snap index 825b08d2..a3327526 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic_generic_generic-4.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__generics__struct_with_vec_generic_generic_generic-4.snap @@ -31,8 +31,6 @@ T = TypeVar("T") class ReflectapiDemoTestsGenericsTestStructWithVecGeneric(BaseModel, Generic[T]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: list[T] = Field(serialization_alias="_f", validation_alias="_f") @@ -41,8 +39,6 @@ class ReflectapiDemoTestsGenericsTestStructWithVecGeneric(BaseModel, Generic[T]) class ReflectapiDemoTestsGenericsTestStructWithVecGenericGenericGeneric( BaseModel, Generic[T] ): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: list[reflectapi_demo.tests.generics.TestStructWithVecGeneric[T]] = Field( @@ -165,9 +161,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.generics.TestStructWithVecGeneric.model_rebuild() - reflectapi_demo.tests.generics.TestStructWithVecGenericGenericGeneric.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsGenericsTestStructWithVecGeneric, + ReflectapiDemoTestsGenericsTestStructWithVecGenericGenericGeneric, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__adj_repr_enum_with_untagged_variant-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__adj_repr_enum_with_untagged_variant-5.snap index dc4aa178..b1cdef9a 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__adj_repr_enum_with_untagged_variant-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__adj_repr_enum_with_untagged_variant-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestVariant1(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Variant1"] = Field( @@ -37,8 +35,6 @@ class ReflectapiDemoTestsSerdeTestVariant1(BaseModel): class ReflectapiDemoTestsSerdeTestVariant2(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Variant2"] = Field( @@ -164,8 +160,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Test.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTest, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__box_field_unwrapping-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__box_field_unwrapping-5.snap index 22649dcc..3162da42 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__box_field_unwrapping-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__box_field_unwrapping-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTreeNode(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) label: str @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TreeNode.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTreeNode, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__datetime-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__datetime-5.snap index d19bb870..aaf17517 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__datetime-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__datetime-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStruct(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) duration: timedelta @@ -163,8 +161,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStruct.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStruct, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_enum-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_enum-5.snap index f7dbbbf4..89cd241c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_enum-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_enum-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeNever(str, Enum): - """Generated enum.""" - pass @@ -137,8 +135,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Never.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeNever, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_adjacently_tagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_adjacently_tagged-5.snap index 28d9c6f2..4c20dcb2 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_adjacently_tagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_adjacently_tagged-5.snap @@ -14,18 +14,10 @@ from __future__ import annotations # Standard library imports from enum import Enum -from typing import Annotated, Any, Generic, Optional, TypeVar, Union +from typing import Annotated, Any, Generic, Literal, Optional, TypeVar, Union # Third-party imports -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PrivateAttr, - RootModel, - model_serializer, - model_validator, -) +from pydantic import BaseModel, ConfigDict, Field, RootModel # Runtime imports from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse @@ -33,89 +25,49 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible -class ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnitVariant( - BaseModel -): - """EmptyUnit variant""" +class ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmpty(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + t: Literal["Empty"] = Field(default="Empty", description="Discriminator field") + +class ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnit(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) + t: Literal["EmptyUnit"] = Field( + default="EmptyUnit", description="Discriminator field" + ) + c: list[Any] = Field(description="Tuple variant fields") + -class ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructVariant( +class ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructContent( BaseModel ): - """EmptyStruct variant""" + """EmptyStruct content""" model_config = ConfigDict(extra="ignore", populate_by_name=True) -# Adjacently tagged enum using RootModel -ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedVariants = Union[ - Literal["Empty"], - ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnitVariant, - ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructVariant, -] +class ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStruct(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + t: Literal["EmptyStruct"] = Field( + default="EmptyStruct", description="Discriminator field" + ) + c: ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructContent = ( + Field(description="EmptyStruct content") + ) -class ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTagged( - RootModel[ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedVariants] -): - """Adjacently tagged enum""" - - @model_validator(mode="before") - @classmethod - def _validate_adjacently_tagged(cls, data): - # Handle direct variant instances - if isinstance( - data, - ( - ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnitVariant, - ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructVariant, - ), - ): - return data - if isinstance(data, dict): - tag = data.get("t") - content = data.get("c") - if tag is None: - raise ValueError("Missing tag field 't'") - if content is None and tag not in ("Empty"): - raise ValueError("Missing content field 'c' for tag: {}".format(tag)) - # Dispatch based on tag - if tag == "Empty": - return "Empty" - if tag == "EmptyUnit": - if isinstance(content, list): - return ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnitVariant() - else: - raise ValueError("Expected list for tuple variant EmptyUnit") - if tag == "EmptyStruct": - return ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructVariant( - **content - ) - raise ValueError( - "Unknown variant for ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTagged: {}".format( - data - ) - ) - @model_serializer - def _serialize_adjacently_tagged(self): - if self.root == "Empty": - return {"t": "Empty"} - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnitVariant, - ): - return {"t": "EmptyUnit", "c": []} - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructVariant, - ): - return {"t": "EmptyStruct", "c": self.root.model_dump()} - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTagged variant: {type(self.root)}" - ) +class ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTagged(RootModel): + root: Annotated[ + Union[ + ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmpty, + ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnit, + ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStruct, + ], + Field(discriminator="t"), + ] # Namespace classes for dotted access to types @@ -128,8 +80,16 @@ class reflectapi_demo: class serde: """Namespace for serde types.""" - TestEmptyVariantsAdjacentlyTaggedEmptyUnitVariant = ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnitVariant - TestEmptyVariantsAdjacentlyTaggedEmptyStructVariant = ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructVariant + TestEmptyVariantsAdjacentlyTaggedEmpty = ( + ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmpty + ) + TestEmptyVariantsAdjacentlyTaggedEmptyUnit = ( + ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyUnit + ) + TestEmptyVariantsAdjacentlyTaggedEmptyStructContent = ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStructContent + TestEmptyVariantsAdjacentlyTaggedEmptyStruct = ( + ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTaggedEmptyStruct + ) TestEmptyVariantsAdjacentlyTagged = ( ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTagged ) @@ -232,8 +192,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEmptyVariantsAdjacentlyTagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEmptyVariantsAdjacentlyTagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_externally_tagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_externally_tagged-5.snap index 2d41c6ee..184562b6 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_externally_tagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_externally_tagged-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyUnitVariant( BaseModel ): @@ -64,59 +92,53 @@ class ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTagged( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( + def _validate(cls, data): + return _parse_externally_tagged( data, - ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyUnitVariant, - ): - return data - if isinstance( - data, - ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyStructVariant, - ): - return data - - # Handle JSON data (for deserialization) - if isinstance(data, str) and data == "Empty": - return data - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "EmptyUnit": - if isinstance(value, list): - return ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyUnitVariant() - else: - raise ValueError("Expected list for tuple variant EmptyUnit") - if key == "EmptyStruct": - return ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyStructVariant( - **value - ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTagged: {data}" + { + "Empty": "_unit", + "EmptyUnit": lambda v: ( + ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyUnitVariant() + if isinstance(v, list) + else (_ for _ in ()).throw( + ValueError("Expected list for tuple variant EmptyUnit") + ) + ), + "EmptyStruct": lambda v: ( + ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyStructVariant( + **v + ) + ), + }, + ( + ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyUnitVariant, + ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyStructVariant, + ), + "ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTagged", ) @model_serializer - def _serialize_externally_tagged(self): - if self.root == "Empty": - return "Empty" - if isinstance( + def _serialize(self): + return _serialize_externally_tagged( self.root, - ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyUnitVariant, - ): - return {"EmptyUnit": []} - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyStructVariant, - ): - return {"EmptyStruct": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTagged variant: {type(self.root)}" + { + "Empty": (lambda r: r == "Empty", lambda r: "Empty"), + "EmptyUnit": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyUnitVariant, + ), + lambda r: {"EmptyUnit": []}, + ), + "EmptyStruct": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTaggedEmptyStructVariant, + ), + lambda r: {"EmptyStruct": r.model_dump(by_alias=True)}, + ), + }, + "ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTagged", ) @@ -234,8 +256,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEmptyVariantsExternallyTagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEmptyVariantsExternallyTagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_internally_tagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_internally_tagged-5.snap index f19658fb..49e31a42 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_internally_tagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_internally_tagged-5.snap @@ -26,16 +26,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestEmptyVariantsInterallyTaggedEmpty(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Empty"] = Field(default="Empty", description="Discriminator field") class ReflectapiDemoTestsSerdeTestEmptyVariantsInterallyTaggedEmptyStruct(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["EmptyStruct"] = Field( @@ -171,8 +167,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEmptyVariantsInterallyTagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEmptyVariantsInterallyTagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_untagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_untagged-5.snap index e3bcbec8..dd7b7192 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_untagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__empty_variants_untagged-5.snap @@ -26,20 +26,14 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestEmptyVariantsUntaggedEmpty(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) class ReflectapiDemoTestsSerdeTestEmptyVariantsUntaggedEmptyUnit(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) class ReflectapiDemoTestsSerdeTestEmptyVariantsUntaggedEmptyStruct(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -167,8 +161,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEmptyVariantsUntagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEmptyVariantsUntagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_mixed_variant_types_internally_tagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_mixed_variant_types_internally_tagged-5.snap index e1208878..349e7202 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_mixed_variant_types_internally_tagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_mixed_variant_types_internally_tagged-5.snap @@ -26,16 +26,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeMixedUnit(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Unit"] = Field(default="Unit", description="Discriminator field") class ReflectapiDemoTestsSerdeMixedWrap(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Wrap"] = Field(default="Wrap", description="Discriminator field") @@ -43,8 +39,6 @@ class ReflectapiDemoTestsSerdeMixedWrap(BaseModel): class ReflectapiDemoTestsSerdeMixedFull(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Full"] = Field(default="Full", description="Discriminator field") @@ -172,8 +166,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Mixed.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeMixed, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename-5.snap index 355a5235..1c3eb4dc 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeMyEnum(str, Enum): - """Generated enum.""" - V1 = "V1" V2 = "V2" @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.MyEnum.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeMyEnum, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_all-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_all-5.snap index 98fef325..f8aa8415 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_all-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_all-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestEnumRenameAll(str, Enum): - """Generated enum.""" - FIELD_NAME = "fieldName" @@ -137,8 +135,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumRenameAll.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumRenameAll, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_all_on_variant-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_all_on_variant-5.snap index 0eeb72e5..ec258936 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_all_on_variant-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_all_on_variant-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant1Variant(BaseModel): """Variant1 variant""" @@ -65,54 +93,49 @@ class ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariant( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( - data, ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant1Variant - ): - return data - if isinstance( - data, ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant2Variant - ): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ( + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Variant1": lambda v: ( ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant1Variant( - **value + **v ) - ) - if key == "Variant2": - return ( + ), + "Variant2": lambda v: ( ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant2Variant( - field_0=value + field_0=v ) - ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariant: {data}" + ), + }, + ( + ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant1Variant, + ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant2Variant, + ), + "ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariant", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( - self.root, ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant1Variant - ): - return {"Variant1": self.root.model_dump()} - if isinstance( - self.root, ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant2Variant - ): - return {"Variant2": self.root.field_0} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariant variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Variant1": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant1Variant, + ), + lambda r: {"Variant1": r.model_dump(by_alias=True)}, + ), + "Variant2": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariantVariant2Variant, + ), + lambda r: {"Variant2": r.field_0}, + ), + }, + "ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariant", ) @@ -230,8 +253,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumRenameAllOnVariant.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumRenameAllOnVariant, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_variant_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_variant_field-5.snap index 533d4182..ae5172e6 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_variant_field-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_rename_variant_field-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsSerdeTestEnumRenameVariantFieldVariant2Variant(BaseModel): """Variant2 variant""" @@ -54,40 +82,34 @@ class ReflectapiDemoTestsSerdeTestEnumRenameVariantField( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( - data, ReflectapiDemoTestsSerdeTestEnumRenameVariantFieldVariant2Variant - ): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant2": - return ( + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Variant2": lambda v: ( ReflectapiDemoTestsSerdeTestEnumRenameVariantFieldVariant2Variant( - **value + **v ) ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsSerdeTestEnumRenameVariantField: {data}" + }, + (ReflectapiDemoTestsSerdeTestEnumRenameVariantFieldVariant2Variant,), + "ReflectapiDemoTestsSerdeTestEnumRenameVariantField", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( - self.root, ReflectapiDemoTestsSerdeTestEnumRenameVariantFieldVariant2Variant - ): - return {"Variant2": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEnumRenameVariantField variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Variant2": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestEnumRenameVariantFieldVariant2Variant, + ), + lambda r: {"Variant2": r.model_dump(by_alias=True)}, + ) + }, + "ReflectapiDemoTestsSerdeTestEnumRenameVariantField", ) @@ -202,8 +224,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumRenameVariantField.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumRenameVariantField, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag-5.snap index 233b7903..c53d4a00 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestEnumTagVariant1(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Variant1"] = Field( @@ -37,8 +35,6 @@ class ReflectapiDemoTestsSerdeTestEnumTagVariant1(BaseModel): class ReflectapiDemoTestsSerdeTestEnumTagVariant2(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Variant2"] = Field( @@ -165,8 +161,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumTag.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumTag, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag_content-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag_content-5.snap index 7aa08bfe..7893e732 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag_content-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag_content-5.snap @@ -14,18 +14,10 @@ from __future__ import annotations # Standard library imports from enum import Enum -from typing import Annotated, Any, Generic, Optional, TypeVar, Union +from typing import Annotated, Any, Generic, Literal, Optional, TypeVar, Union # Third-party imports -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PrivateAttr, - RootModel, - model_serializer, - model_validator, -) +from pydantic import BaseModel, ConfigDict, Field, RootModel # Runtime imports from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse @@ -33,83 +25,42 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible -class ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Variant(BaseModel): - """Variant1 variant""" +class ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Content(BaseModel): + """Variant1 content""" model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int -class ReflectapiDemoTestsSerdeTestEnumTagContentVariant2Variant(BaseModel): - """Variant2 variant""" +class ReflectapiDemoTestsSerdeTestEnumTagContentVariant1(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + type: Literal["Variant1"] = Field( + default="Variant1", description="Discriminator field" + ) + content: ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Content = Field( + description="Variant1 content" + ) + +class ReflectapiDemoTestsSerdeTestEnumTagContentVariant2(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) - field_0: int - - -# Adjacently tagged enum using RootModel -ReflectapiDemoTestsSerdeTestEnumTagContentVariants = Union[ - ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Variant, - ReflectapiDemoTestsSerdeTestEnumTagContentVariant2Variant, -] - - -class ReflectapiDemoTestsSerdeTestEnumTagContent( - RootModel[ReflectapiDemoTestsSerdeTestEnumTagContentVariants] -): - """Adjacently tagged enum""" - - @model_validator(mode="before") - @classmethod - def _validate_adjacently_tagged(cls, data): - # Handle direct variant instances - if isinstance( - data, - ( - ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Variant, - ReflectapiDemoTestsSerdeTestEnumTagContentVariant2Variant, - ), - ): - return data - if isinstance(data, dict): - tag = data.get("type") - content = data.get("content") - if tag is None: - raise ValueError("Missing tag field 'type'") - if content is None and tag not in (): - raise ValueError( - "Missing content field 'content' for tag: {}".format(tag) - ) - # Dispatch based on tag - if tag == "Variant1": - return ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Variant( - **content - ) - if tag == "Variant2": - return ReflectapiDemoTestsSerdeTestEnumTagContentVariant2Variant( - field_0=content - ) - raise ValueError( - "Unknown variant for ReflectapiDemoTestsSerdeTestEnumTagContent: {}".format( - data - ) - ) + type: Literal["Variant2"] = Field( + default="Variant2", description="Discriminator field" + ) + content: int - @model_serializer - def _serialize_adjacently_tagged(self): - if isinstance( - self.root, ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Variant - ): - return {"type": "Variant1", "content": self.root.model_dump()} - if isinstance( - self.root, ReflectapiDemoTestsSerdeTestEnumTagContentVariant2Variant - ): - return {"type": "Variant2", "content": self.root.field_0} - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEnumTagContent variant: {type(self.root)}" - ) + +class ReflectapiDemoTestsSerdeTestEnumTagContent(RootModel): + root: Annotated[ + Union[ + ReflectapiDemoTestsSerdeTestEnumTagContentVariant1, + ReflectapiDemoTestsSerdeTestEnumTagContentVariant2, + ], + Field(discriminator="type"), + ] # Namespace classes for dotted access to types @@ -122,11 +73,14 @@ class reflectapi_demo: class serde: """Namespace for serde types.""" - TestEnumTagContentVariant1Variant = ( - ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Variant + TestEnumTagContentVariant1Content = ( + ReflectapiDemoTestsSerdeTestEnumTagContentVariant1Content + ) + TestEnumTagContentVariant1 = ( + ReflectapiDemoTestsSerdeTestEnumTagContentVariant1 ) - TestEnumTagContentVariant2Variant = ( - ReflectapiDemoTestsSerdeTestEnumTagContentVariant2Variant + TestEnumTagContentVariant2 = ( + ReflectapiDemoTestsSerdeTestEnumTagContentVariant2 ) TestEnumTagContent = ReflectapiDemoTestsSerdeTestEnumTagContent @@ -224,8 +178,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumTagContent.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumTagContent, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag_content_rename_all-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag_content_rename_all-5.snap index 226b586d..9f5b4862 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag_content_rename_all-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_tag_content_rename_all-5.snap @@ -14,18 +14,10 @@ from __future__ import annotations # Standard library imports from enum import Enum -from typing import Annotated, Any, Generic, Optional, TypeVar, Union +from typing import Annotated, Any, Generic, Literal, Optional, TypeVar, Union # Third-party imports -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PrivateAttr, - RootModel, - model_serializer, - model_validator, -) +from pydantic import BaseModel, ConfigDict, Field, RootModel # Runtime imports from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse @@ -33,89 +25,42 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible -class ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Variant(BaseModel): - """variant1 variant""" +class ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Content(BaseModel): + """variant1 content""" model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int -class ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2Variant(BaseModel): - """variant2 variant""" +class ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + type: Literal["variant1"] = Field( + default="variant1", description="Discriminator field" + ) + content: ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Content = Field( + description="variant1 content" + ) + +class ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) - field_0: int - - -# Adjacently tagged enum using RootModel -ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariants = Union[ - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Variant, - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2Variant, -] - - -class ReflectapiDemoTestsSerdeTestEnumTagContentRenameAll( - RootModel[ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariants] -): - """Adjacently tagged enum""" - - @model_validator(mode="before") - @classmethod - def _validate_adjacently_tagged(cls, data): - # Handle direct variant instances - if isinstance( - data, - ( - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Variant, - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2Variant, - ), - ): - return data - if isinstance(data, dict): - tag = data.get("type") - content = data.get("content") - if tag is None: - raise ValueError("Missing tag field 'type'") - if content is None and tag not in (): - raise ValueError( - "Missing content field 'content' for tag: {}".format(tag) - ) - # Dispatch based on tag - if tag == "variant1": - return ( - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Variant( - **content - ) - ) - if tag == "variant2": - return ( - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2Variant( - field_0=content - ) - ) - raise ValueError( - "Unknown variant for ReflectapiDemoTestsSerdeTestEnumTagContentRenameAll: {}".format( - data - ) - ) + type: Literal["variant2"] = Field( + default="variant2", description="Discriminator field" + ) + content: int - @model_serializer - def _serialize_adjacently_tagged(self): - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Variant, - ): - return {"type": "variant1", "content": self.root.model_dump()} - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2Variant, - ): - return {"type": "variant2", "content": self.root.field_0} - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEnumTagContentRenameAll variant: {type(self.root)}" - ) + +class ReflectapiDemoTestsSerdeTestEnumTagContentRenameAll(RootModel): + root: Annotated[ + Union[ + ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1, + ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2, + ], + Field(discriminator="type"), + ] # Namespace classes for dotted access to types @@ -128,11 +73,14 @@ class reflectapi_demo: class serde: """Namespace for serde types.""" - TestEnumTagContentRenameAllVariant1Variant = ( - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Variant + TestEnumTagContentRenameAllVariant1Content = ( + ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1Content + ) + TestEnumTagContentRenameAllVariant1 = ( + ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant1 ) - TestEnumTagContentRenameAllVariant2Variant = ( - ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2Variant + TestEnumTagContentRenameAllVariant2 = ( + ReflectapiDemoTestsSerdeTestEnumTagContentRenameAllVariant2 ) TestEnumTagContentRenameAll = ( ReflectapiDemoTestsSerdeTestEnumTagContentRenameAll @@ -232,8 +180,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumTagContentRenameAll.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumTagContentRenameAll, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_untagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_untagged-5.snap index ca331871..81824f53 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_untagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_untagged-5.snap @@ -26,16 +26,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestEnumUntaggedVariant1(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) - field_0: int + value: int class ReflectapiDemoTestsSerdeTestEnumUntaggedVariant2(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int @@ -155,8 +151,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumUntagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumUntagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_field_skip-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_field_skip-5.snap index 4a820178..8e45349f 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_field_skip-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_field_skip-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsSerdeTestEnumWithFieldSkipVariant1Variant(BaseModel): """Variant1 variant""" @@ -52,38 +80,31 @@ class ReflectapiDemoTestsSerdeTestEnumWithFieldSkip( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( - data, ReflectapiDemoTestsSerdeTestEnumWithFieldSkipVariant1Variant - ): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ReflectapiDemoTestsSerdeTestEnumWithFieldSkipVariant1Variant( - **value + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Variant1": lambda v: ( + ReflectapiDemoTestsSerdeTestEnumWithFieldSkipVariant1Variant(**v) ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsSerdeTestEnumWithFieldSkip: {data}" + }, + (ReflectapiDemoTestsSerdeTestEnumWithFieldSkipVariant1Variant,), + "ReflectapiDemoTestsSerdeTestEnumWithFieldSkip", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( - self.root, ReflectapiDemoTestsSerdeTestEnumWithFieldSkipVariant1Variant - ): - return {"Variant1": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEnumWithFieldSkip variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Variant1": ( + lambda r: isinstance( + r, ReflectapiDemoTestsSerdeTestEnumWithFieldSkipVariant1Variant + ), + lambda r: {"Variant1": r.model_dump(by_alias=True)}, + ) + }, + "ReflectapiDemoTestsSerdeTestEnumWithFieldSkip", ) @@ -196,8 +217,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumWithFieldSkip.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumWithFieldSkip, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_many_variants-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_many_variants-5.snap index 7563f4d0..80a6aadd 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_many_variants-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_many_variants-5.snap @@ -26,24 +26,18 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeLargeEnumAlpha(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Alpha"] = Field(default="Alpha", description="Discriminator field") class ReflectapiDemoTestsSerdeLargeEnumBeta(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Beta"] = Field(default="Beta", description="Discriminator field") class ReflectapiDemoTestsSerdeLargeEnumGamma(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Gamma"] = Field(default="Gamma", description="Discriminator field") @@ -51,8 +45,6 @@ class ReflectapiDemoTestsSerdeLargeEnumGamma(BaseModel): class ReflectapiDemoTestsSerdeLargeEnumDelta(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Delta"] = Field(default="Delta", description="Discriminator field") @@ -60,8 +52,6 @@ class ReflectapiDemoTestsSerdeLargeEnumDelta(BaseModel): class ReflectapiDemoTestsSerdeLargeEnumEpsilon(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Epsilon"] = Field( @@ -70,8 +60,6 @@ class ReflectapiDemoTestsSerdeLargeEnumEpsilon(BaseModel): class ReflectapiDemoTestsSerdeLargeEnumZeta(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Zeta"] = Field(default="Zeta", description="Discriminator field") @@ -79,16 +67,12 @@ class ReflectapiDemoTestsSerdeLargeEnumZeta(BaseModel): class ReflectapiDemoTestsSerdeLargeEnumEta(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Eta"] = Field(default="Eta", description="Discriminator field") class ReflectapiDemoTestsSerdeLargeEnumTheta(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Theta"] = Field(default="Theta", description="Discriminator field") @@ -96,16 +80,12 @@ class ReflectapiDemoTestsSerdeLargeEnumTheta(BaseModel): class ReflectapiDemoTestsSerdeLargeEnumIota(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Iota"] = Field(default="Iota", description="Discriminator field") class ReflectapiDemoTestsSerdeLargeEnumKappa(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Kappa"] = Field(default="Kappa", description="Discriminator field") @@ -113,16 +93,12 @@ class ReflectapiDemoTestsSerdeLargeEnumKappa(BaseModel): class ReflectapiDemoTestsSerdeLargeEnumLambda(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Lambda"] = Field(default="Lambda", description="Discriminator field") class ReflectapiDemoTestsSerdeLargeEnumMu(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Mu"] = Field(default="Mu", description="Discriminator field") @@ -268,8 +244,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.LargeEnum.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeLargeEnum, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_rename_to_invalid_chars-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_rename_to_invalid_chars-5.snap index 5f7a2237..9c2fef1a 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_rename_to_invalid_chars-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_rename_to_invalid_chars-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidCharsVariant1Variant( BaseModel ): @@ -56,40 +84,34 @@ class ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidChars( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( + def _validate(cls, data): + return _parse_externally_tagged( data, - ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidCharsVariant1Variant, - ): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidCharsVariant1Variant( - **value + { + "variant-name&&": lambda v: ( + ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidCharsVariant1Variant( + **v + ) ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidChars: {data}" + }, + (ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidCharsVariant1Variant,), + "ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidChars", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( + def _serialize(self): + return _serialize_externally_tagged( self.root, - ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidCharsVariant1Variant, - ): - return {"Variant1": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidChars variant: {type(self.root)}" + { + "variant-name&&": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidCharsVariant1Variant, + ), + lambda r: {"variant-name&&": r.model_dump(by_alias=True)}, + ) + }, + "ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidChars", ) @@ -208,8 +230,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumWithRenameToInvalidChars.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumWithRenameToInvalidChars, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_serde_rename_on_variants-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_serde_rename_on_variants-5.snap index d0b2408a..81e110e8 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_serde_rename_on_variants-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_serde_rename_on_variants-5.snap @@ -14,18 +14,10 @@ from __future__ import annotations # Standard library imports from enum import Enum -from typing import Annotated, Any, Generic, Optional, TypeVar, Union +from typing import Annotated, Any, Generic, Literal, Optional, TypeVar, Union # Third-party imports -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PrivateAttr, - RootModel, - model_serializer, - model_validator, -) +from pydantic import BaseModel, ConfigDict, Field, RootModel # Runtime imports from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse @@ -33,69 +25,52 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible -class ReflectapiDemoTestsSerdeActionCreateItemVariant(BaseModel): - """create_item variant""" +class ReflectapiDemoTestsSerdeActionCreateItemContent(BaseModel): + """create_item content""" model_config = ConfigDict(extra="ignore", populate_by_name=True) name: str -class ReflectapiDemoTestsSerdeActionDeleteItemVariant(BaseModel): - """delete_item variant""" +class ReflectapiDemoTestsSerdeActionCreateItem(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + kind: Literal["create_item"] = Field( + default="create_item", description="Discriminator field" + ) + data: ReflectapiDemoTestsSerdeActionCreateItemContent = Field( + description="create_item content" + ) + + +class ReflectapiDemoTestsSerdeActionDeleteItemContent(BaseModel): + """delete_item content""" model_config = ConfigDict(extra="ignore", populate_by_name=True) id: int -# Adjacently tagged enum using RootModel -ReflectapiDemoTestsSerdeActionVariants = Union[ - ReflectapiDemoTestsSerdeActionCreateItemVariant, - ReflectapiDemoTestsSerdeActionDeleteItemVariant, -] - - -class ReflectapiDemoTestsSerdeAction(RootModel[ReflectapiDemoTestsSerdeActionVariants]): - """Adjacently tagged enum""" - - @model_validator(mode="before") - @classmethod - def _validate_adjacently_tagged(cls, data): - # Handle direct variant instances - if isinstance( - data, - ( - ReflectapiDemoTestsSerdeActionCreateItemVariant, - ReflectapiDemoTestsSerdeActionDeleteItemVariant, - ), - ): - return data - if isinstance(data, dict): - tag = data.get("kind") - content = data.get("data") - if tag is None: - raise ValueError("Missing tag field 'kind'") - if content is None and tag not in (): - raise ValueError("Missing content field 'data' for tag: {}".format(tag)) - # Dispatch based on tag - if tag == "create_item": - return ReflectapiDemoTestsSerdeActionCreateItemVariant(**content) - if tag == "delete_item": - return ReflectapiDemoTestsSerdeActionDeleteItemVariant(**content) - raise ValueError( - "Unknown variant for ReflectapiDemoTestsSerdeAction: {}".format(data) - ) +class ReflectapiDemoTestsSerdeActionDeleteItem(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) - @model_serializer - def _serialize_adjacently_tagged(self): - if isinstance(self.root, ReflectapiDemoTestsSerdeActionCreateItemVariant): - return {"kind": "create_item", "data": self.root.model_dump()} - if isinstance(self.root, ReflectapiDemoTestsSerdeActionDeleteItemVariant): - return {"kind": "delete_item", "data": self.root.model_dump()} - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeAction variant: {type(self.root)}" - ) + kind: Literal["delete_item"] = Field( + default="delete_item", description="Discriminator field" + ) + data: ReflectapiDemoTestsSerdeActionDeleteItemContent = Field( + description="delete_item content" + ) + + +class ReflectapiDemoTestsSerdeAction(RootModel): + root: Annotated[ + Union[ + ReflectapiDemoTestsSerdeActionCreateItem, + ReflectapiDemoTestsSerdeActionDeleteItem, + ], + Field(discriminator="kind"), + ] # Namespace classes for dotted access to types @@ -108,8 +83,10 @@ class reflectapi_demo: class serde: """Namespace for serde types.""" - ActionCreateItemVariant = ReflectapiDemoTestsSerdeActionCreateItemVariant - ActionDeleteItemVariant = ReflectapiDemoTestsSerdeActionDeleteItemVariant + ActionCreateItemContent = ReflectapiDemoTestsSerdeActionCreateItemContent + ActionCreateItem = ReflectapiDemoTestsSerdeActionCreateItem + ActionDeleteItemContent = ReflectapiDemoTestsSerdeActionDeleteItemContent + ActionDeleteItem = ReflectapiDemoTestsSerdeActionDeleteItem Action = ReflectapiDemoTestsSerdeAction @@ -206,8 +183,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Action.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeAction, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_other-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_other-5.snap index a9ad367b..a3373d76 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_other-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_other-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestEnumWithVariantOtherV0(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["V0"] = Field(default="V0", description="Discriminator field") @@ -41,16 +39,12 @@ class ReflectapiDemoTestsSerdeInputTestEnumWithVariantOther(RootModel): class ReflectapiDemoTestsSerdeOutputTestEnumWithVariantOtherV0(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["V0"] = Field(default="V0", description="Discriminator field") class ReflectapiDemoTestsSerdeOutputTestEnumWithVariantOtherVariant1(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Variant1"] = Field( @@ -199,9 +193,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestEnumWithVariantOther.model_rebuild() - reflectapi_demo.tests.serde.output.TestEnumWithVariantOther.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestEnumWithVariantOther, + ReflectapiDemoTestsSerdeOutputTestEnumWithVariantOther, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip-5.snap index 9ad62d20..ec43ccac 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestEnumWithVariantSkip(str, Enum): - """Generated enum.""" - pass @@ -137,8 +135,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumWithVariantSkip.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumWithVariantSkip, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip_deserialize-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip_deserialize-5.snap index 6817e991..111e73eb 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip_deserialize-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip_deserialize-5.snap @@ -26,14 +26,10 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestEnumWithVariantSkipDeserialize(str, Enum): - """Generated enum.""" - pass class ReflectapiDemoTestsSerdeOutputTestEnumWithVariantSkipDeserialize(str, Enum): - """Generated enum.""" - _VARIANT1 = "_Variant1" @@ -163,9 +159,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestEnumWithVariantSkipDeserialize.model_rebuild() - reflectapi_demo.tests.serde.output.TestEnumWithVariantSkipDeserialize.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestEnumWithVariantSkipDeserialize, + ReflectapiDemoTestsSerdeOutputTestEnumWithVariantSkipDeserialize, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip_serialize-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip_serialize-5.snap index a6522535..597a0944 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip_serialize-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_skip_serialize-5.snap @@ -26,14 +26,10 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestEnumWithVariantSkipSerialize(str, Enum): - """Generated enum.""" - VARIANT1 = "Variant1" class ReflectapiDemoTestsSerdeOutputTestEnumWithVariantSkipSerialize(str, Enum): - """Generated enum.""" - pass @@ -163,9 +159,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestEnumWithVariantSkipSerialize.model_rebuild() - reflectapi_demo.tests.serde.output.TestEnumWithVariantSkipSerialize.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestEnumWithVariantSkipSerialize, + ReflectapiDemoTestsSerdeOutputTestEnumWithVariantSkipSerialize, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_untagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_untagged-5.snap index 38cc0bf6..ffd462c0 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_untagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__enum_with_variant_untagged-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsSerdeTestEnumWithVariantUntaggedVariant1Variant(BaseModel): """Variant1 variant""" @@ -54,41 +82,34 @@ class ReflectapiDemoTestsSerdeTestEnumWithVariantUntagged( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( - data, ReflectapiDemoTestsSerdeTestEnumWithVariantUntaggedVariant1Variant - ): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Variant1": - return ( + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Variant1": lambda v: ( ReflectapiDemoTestsSerdeTestEnumWithVariantUntaggedVariant1Variant( - field_0=value + field_0=v ) ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsSerdeTestEnumWithVariantUntagged: {data}" + }, + (ReflectapiDemoTestsSerdeTestEnumWithVariantUntaggedVariant1Variant,), + "ReflectapiDemoTestsSerdeTestEnumWithVariantUntagged", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( + def _serialize(self): + return _serialize_externally_tagged( self.root, - ReflectapiDemoTestsSerdeTestEnumWithVariantUntaggedVariant1Variant, - ): - return {"Variant1": self.root.field_0} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestEnumWithVariantUntagged variant: {type(self.root)}" + { + "Variant1": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestEnumWithVariantUntaggedVariant1Variant, + ), + lambda r: {"Variant1": r.field_0}, + ) + }, + "ReflectapiDemoTestsSerdeTestEnumWithVariantUntagged", ) @@ -203,8 +224,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestEnumWithVariantUntagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestEnumWithVariantUntagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__external_impls-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__external_impls-5.snap index 170d9e2b..d9d41ed2 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__external_impls-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__external_impls-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTest(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) index_map: dict[int, int] @@ -141,8 +139,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Test.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTest, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__field_all_python_keywords-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__field_all_python_keywords-5.snap index 6e5141a2..a20ee112 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__field_all_python_keywords-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__field_all_python_keywords-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeKeywords(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type_: str = Field(serialization_alias="type", validation_alias="type") @@ -141,8 +139,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Keywords.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeKeywords, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__field_names_with_special_chars-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__field_names_with_special_chars-5.snap index 58e8f934..262b5f09 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__field_names_with_special_chars-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__field_names_with_special_chars-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeSpecialNames(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) content_type: str = Field( @@ -146,8 +144,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.SpecialNames.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeSpecialNames, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_adjacently_tagged_enum_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_adjacently_tagged_enum_field-5.snap index 2563f520..b989d83d 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_adjacently_tagged_enum_field-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_adjacently_tagged_enum_field-5.snap @@ -14,18 +14,10 @@ from __future__ import annotations # Standard library imports from enum import Enum -from typing import Annotated, Any, Generic, Optional, TypeVar, Union +from typing import Annotated, Any, Generic, Literal, Optional, TypeVar, Union # Third-party imports -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PrivateAttr, - RootModel, - model_serializer, - model_validator, -) +from pydantic import BaseModel, ConfigDict, Field, RootModel # Runtime imports from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse @@ -34,79 +26,51 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeMessage(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) id: str payload: reflectapi_demo.tests.serde.Payload -class ReflectapiDemoTestsSerdePayloadTextVariant(BaseModel): - """Text variant""" +class ReflectapiDemoTestsSerdePayloadTextContent(BaseModel): + """Text content""" model_config = ConfigDict(extra="ignore", populate_by_name=True) body: str -class ReflectapiDemoTestsSerdePayloadBinaryVariant(BaseModel): - """Binary variant""" +class ReflectapiDemoTestsSerdePayloadText(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + kind: Literal["Text"] = Field(default="Text", description="Discriminator field") + data: ReflectapiDemoTestsSerdePayloadTextContent = Field(description="Text content") + + +class ReflectapiDemoTestsSerdePayloadBinaryContent(BaseModel): + """Binary content""" model_config = ConfigDict(extra="ignore", populate_by_name=True) size: int -# Adjacently tagged enum using RootModel -ReflectapiDemoTestsSerdePayloadVariants = Union[ - ReflectapiDemoTestsSerdePayloadTextVariant, - ReflectapiDemoTestsSerdePayloadBinaryVariant, -] - - -class ReflectapiDemoTestsSerdePayload( - RootModel[ReflectapiDemoTestsSerdePayloadVariants] -): - """Adjacently tagged enum""" - - @model_validator(mode="before") - @classmethod - def _validate_adjacently_tagged(cls, data): - # Handle direct variant instances - if isinstance( - data, - ( - ReflectapiDemoTestsSerdePayloadTextVariant, - ReflectapiDemoTestsSerdePayloadBinaryVariant, - ), - ): - return data - if isinstance(data, dict): - tag = data.get("kind") - content = data.get("data") - if tag is None: - raise ValueError("Missing tag field 'kind'") - if content is None and tag not in (): - raise ValueError("Missing content field 'data' for tag: {}".format(tag)) - # Dispatch based on tag - if tag == "Text": - return ReflectapiDemoTestsSerdePayloadTextVariant(**content) - if tag == "Binary": - return ReflectapiDemoTestsSerdePayloadBinaryVariant(**content) - raise ValueError( - "Unknown variant for ReflectapiDemoTestsSerdePayload: {}".format(data) - ) +class ReflectapiDemoTestsSerdePayloadBinary(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) - @model_serializer - def _serialize_adjacently_tagged(self): - if isinstance(self.root, ReflectapiDemoTestsSerdePayloadTextVariant): - return {"kind": "Text", "data": self.root.model_dump()} - if isinstance(self.root, ReflectapiDemoTestsSerdePayloadBinaryVariant): - return {"kind": "Binary", "data": self.root.model_dump()} - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdePayload variant: {type(self.root)}" - ) + kind: Literal["Binary"] = Field(default="Binary", description="Discriminator field") + data: ReflectapiDemoTestsSerdePayloadBinaryContent = Field( + description="Binary content" + ) + + +class ReflectapiDemoTestsSerdePayload(RootModel): + root: Annotated[ + Union[ + ReflectapiDemoTestsSerdePayloadText, ReflectapiDemoTestsSerdePayloadBinary + ], + Field(discriminator="kind"), + ] # Namespace classes for dotted access to types @@ -120,8 +84,10 @@ class reflectapi_demo: """Namespace for serde types.""" Message = ReflectapiDemoTestsSerdeMessage - PayloadTextVariant = ReflectapiDemoTestsSerdePayloadTextVariant - PayloadBinaryVariant = ReflectapiDemoTestsSerdePayloadBinaryVariant + PayloadTextContent = ReflectapiDemoTestsSerdePayloadTextContent + PayloadText = ReflectapiDemoTestsSerdePayloadText + PayloadBinaryContent = ReflectapiDemoTestsSerdePayloadBinaryContent + PayloadBinary = ReflectapiDemoTestsSerdePayloadBinary Payload = ReflectapiDemoTestsSerdePayload @@ -218,9 +184,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Message.model_rebuild() - reflectapi_demo.tests.serde.Payload.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeMessage, + ReflectapiDemoTestsSerdePayload, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_enum_with_unit_variants_only-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_enum_with_unit_variants_only-5.snap index a6c0949a..8750e563 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_enum_with_unit_variants_only-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_enum_with_unit_variants_only-5.snap @@ -70,8 +70,6 @@ class ReflectapiDemoTestsSerdeItem(RootModel): class ReflectapiDemoTestsSerdeStatusActive(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) status: Literal["Active"] = Field( @@ -80,8 +78,6 @@ class ReflectapiDemoTestsSerdeStatusActive(BaseModel): class ReflectapiDemoTestsSerdeStatusInactive(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) status: Literal["Inactive"] = Field( @@ -90,8 +86,6 @@ class ReflectapiDemoTestsSerdeStatusInactive(BaseModel): class ReflectapiDemoTestsSerdeStatusPending(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) status: Literal["Pending"] = Field( @@ -223,9 +217,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Item.model_rebuild() - reflectapi_demo.tests.serde.Status.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeItem, + ReflectapiDemoTestsSerdeStatus, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_externally_tagged_enum_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_externally_tagged_enum_field-5.snap index 1e7c313d..5ae1ce78 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_externally_tagged_enum_field-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_externally_tagged_enum_field-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,9 +32,36 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible -class ReflectapiDemoTestsSerdeDrawing(BaseModel): - """Generated data model.""" +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + +class ReflectapiDemoTestsSerdeDrawing(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) name: str @@ -70,36 +96,35 @@ class ReflectapiDemoTestsSerdeShape(RootModel[ReflectapiDemoTestsSerdeShapeVaria @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance(data, ReflectapiDemoTestsSerdeShapeCircleVariant): - return data - if isinstance(data, ReflectapiDemoTestsSerdeShapeRectVariant): - return data - - # Handle JSON data (for deserialization) - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Circle": - return ReflectapiDemoTestsSerdeShapeCircleVariant(**value) - if key == "Rect": - return ReflectapiDemoTestsSerdeShapeRectVariant(**value) - - raise ValueError(f"Unknown variant for ReflectapiDemoTestsSerdeShape: {data}") + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Circle": lambda v: ReflectapiDemoTestsSerdeShapeCircleVariant(**v), + "Rect": lambda v: ReflectapiDemoTestsSerdeShapeRectVariant(**v), + }, + ( + ReflectapiDemoTestsSerdeShapeCircleVariant, + ReflectapiDemoTestsSerdeShapeRectVariant, + ), + "ReflectapiDemoTestsSerdeShape", + ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance(self.root, ReflectapiDemoTestsSerdeShapeCircleVariant): - return {"Circle": self.root.model_dump()} - if isinstance(self.root, ReflectapiDemoTestsSerdeShapeRectVariant): - return {"Rect": self.root.model_dump()} - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeShape variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Circle": ( + lambda r: isinstance(r, ReflectapiDemoTestsSerdeShapeCircleVariant), + lambda r: {"Circle": r.model_dump(by_alias=True)}, + ), + "Rect": ( + lambda r: isinstance(r, ReflectapiDemoTestsSerdeShapeRectVariant), + lambda r: {"Rect": r.model_dump(by_alias=True)}, + ), + }, + "ReflectapiDemoTestsSerdeShape", ) @@ -212,9 +237,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Drawing.model_rebuild() - reflectapi_demo.tests.serde.Shape.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeDrawing, + ReflectapiDemoTestsSerdeShape, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_internally_tagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_internally_tagged-5.snap index 95d3d2cc..7a3afc41 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_internally_tagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_internally_tagged-5.snap @@ -34,30 +34,22 @@ Payload = TypeVar("Payload") class ReflectapiDemoTestsSerdeA(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) a: int class ReflectapiDemoTestsSerdeB(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) b: int class ReflectapiDemoTestsSerdeS(BaseModel, Generic[Payload, Additional]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) class ReflectapiDemoTestsSerdeTestS(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["S"] = Field(default="S", description="Discriminator field") @@ -179,11 +171,13 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.A.model_rebuild() - reflectapi_demo.tests.serde.B.model_rebuild() - reflectapi_demo.tests.serde.S.model_rebuild() - reflectapi_demo.tests.serde.Test.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeA, + ReflectapiDemoTestsSerdeB, + ReflectapiDemoTestsSerdeS, + ReflectapiDemoTestsSerdeTest, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_internally_tagged_enum_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_internally_tagged_enum_field-5.snap index 8e87a143..07a82978 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_internally_tagged_enum_field-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_internally_tagged_enum_field-5.snap @@ -66,8 +66,6 @@ class ReflectapiDemoTestsSerdeOffer(RootModel): class ReflectapiDemoTestsSerdeOfferKindSingle(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Single"] = Field(default="Single", description="Discriminator field") @@ -75,8 +73,6 @@ class ReflectapiDemoTestsSerdeOfferKindSingle(BaseModel): class ReflectapiDemoTestsSerdeOfferKindGroup(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Group"] = Field(default="Group", description="Discriminator field") @@ -204,9 +200,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Offer.model_rebuild() - reflectapi_demo.tests.serde.OfferKind.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeOffer, + ReflectapiDemoTestsSerdeOfferKind, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_multiple_structs-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_multiple_structs-5.snap index 57a00356..3849760a 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_multiple_structs-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_multiple_structs-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeDocument(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) title: str @@ -37,8 +35,6 @@ class ReflectapiDemoTestsSerdeDocument(BaseModel): class ReflectapiDemoTestsSerdeMetadata(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) author: str @@ -46,8 +42,6 @@ class ReflectapiDemoTestsSerdeMetadata(BaseModel): class ReflectapiDemoTestsSerdeTimestamps(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) created_at: str @@ -162,10 +156,12 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Document.model_rebuild() - reflectapi_demo.tests.serde.Metadata.model_rebuild() - reflectapi_demo.tests.serde.Timestamps.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeDocument, + ReflectapiDemoTestsSerdeMetadata, + ReflectapiDemoTestsSerdeTimestamps, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_optional_internally_tagged_enum-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_optional_internally_tagged_enum-5.snap index 2897c29b..df23ac33 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_optional_internally_tagged_enum-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_optional_internally_tagged_enum-5.snap @@ -55,8 +55,6 @@ class ReflectapiDemoTestsSerdeTask(RootModel): class ReflectapiDemoTestsSerdePriorityHigh(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) kind: Literal["High"] = Field(default="High", description="Discriminator field") @@ -64,8 +62,6 @@ class ReflectapiDemoTestsSerdePriorityHigh(BaseModel): class ReflectapiDemoTestsSerdePriorityLow(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) kind: Literal["Low"] = Field(default="Low", description="Discriminator field") @@ -191,9 +187,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Priority.model_rebuild() - reflectapi_demo.tests.serde.Task.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdePriority, + ReflectapiDemoTestsSerdeTask, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_struct_and_internal_enum_combined-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_struct_and_internal_enum_combined-5.snap index daf93012..7ead5b06 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_struct_and_internal_enum_combined-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_struct_and_internal_enum_combined-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeAudit(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) modified_by: str @@ -77,8 +75,6 @@ class ReflectapiDemoTestsSerdePost(RootModel): class ReflectapiDemoTestsSerdeContentText(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Text"] = Field(default="Text", description="Discriminator field") @@ -86,8 +82,6 @@ class ReflectapiDemoTestsSerdeContentText(BaseModel): class ReflectapiDemoTestsSerdeContentImage(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["Image"] = Field(default="Image", description="Discriminator field") @@ -216,10 +210,12 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Audit.model_rebuild() - reflectapi_demo.tests.serde.Content.model_rebuild() - reflectapi_demo.tests.serde.Post.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeAudit, + ReflectapiDemoTestsSerdeContent, + ReflectapiDemoTestsSerdePost, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_struct_with_nested_flatten-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_struct_with_nested_flatten-5.snap index 0104634a..4878edd7 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_struct_with_nested_flatten-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_struct_with_nested_flatten-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInner(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) z: str class ReflectapiDemoTestsSerdeMiddle(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) y: int @@ -42,8 +38,6 @@ class ReflectapiDemoTestsSerdeMiddle(BaseModel): class ReflectapiDemoTestsSerdeOuter(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) x: str @@ -159,10 +153,12 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Inner.model_rebuild() - reflectapi_demo.tests.serde.Middle.model_rebuild() - reflectapi_demo.tests.serde.Outer.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInner, + ReflectapiDemoTestsSerdeMiddle, + ReflectapiDemoTestsSerdeOuter, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_unit-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_unit-5.snap index e33271b4..c2d908b1 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_unit-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_unit-5.snap @@ -33,16 +33,12 @@ Payload = TypeVar("Payload") class ReflectapiDemoTestsSerdeK(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) a: int class ReflectapiDemoTestsSerdeS(BaseModel, Generic[Payload, Additional]): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -165,9 +161,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.K.model_rebuild() - reflectapi_demo.tests.serde.S.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeK, + ReflectapiDemoTestsSerdeS, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_untagged_enum_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_untagged_enum_field-5.snap index 45df6567..8b7abdf3 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_untagged_enum_field-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__flatten_untagged_enum_field-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeCell(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) label: str @@ -35,16 +33,12 @@ class ReflectapiDemoTestsSerdeCell(BaseModel): class ReflectapiDemoTestsSerdeValueNum(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) value: float class ReflectapiDemoTestsSerdeValueText(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) text: str @@ -164,9 +158,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Cell.model_rebuild() - reflectapi_demo.tests.serde.Value.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeCell, + ReflectapiDemoTestsSerdeValue, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_adjacently_tagged_enum-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_adjacently_tagged_enum-5.snap index 064c4073..ed55bb87 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_adjacently_tagged_enum-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_adjacently_tagged_enum-5.snap @@ -14,18 +14,10 @@ from __future__ import annotations # Standard library imports from enum import Enum -from typing import Annotated, Any, Generic, Optional, TypeVar, Union +from typing import Annotated, Any, Generic, Literal, Optional, TypeVar, Union # Third-party imports -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PrivateAttr, - RootModel, - model_serializer, - model_validator, -) +from pydantic import BaseModel, ConfigDict, Field, RootModel # Runtime imports from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse @@ -39,59 +31,37 @@ from reflectapi_runtime import ReflectapiInfallible T = TypeVar("T") -class ReflectapiDemoTestsSerdeTaggedItemVariant(BaseModel, Generic[T]): - """Item variant""" - +class ReflectapiDemoTestsSerdeTaggedItem(BaseModel, Generic[T]): model_config = ConfigDict(extra="ignore", populate_by_name=True) - field_0: T - + t: Literal["Item"] = Field(default="Item", description="Discriminator field") + c: T -# Adjacently tagged enum using RootModel -ReflectapiDemoTestsSerdeTaggedVariants = Union[ - ReflectapiDemoTestsSerdeTaggedItemVariant, Literal["Nothing"] -] +class ReflectapiDemoTestsSerdeTaggedNothing(BaseModel, Generic[T]): + model_config = ConfigDict(extra="ignore", populate_by_name=True) -class ReflectapiDemoTestsSerdeTagged( - RootModel[ReflectapiDemoTestsSerdeTaggedVariants], Generic[T] -): - """Adjacently tagged enum""" + t: Literal["Nothing"] = Field(default="Nothing", description="Discriminator field") - @model_validator(mode="before") - @classmethod - def _validate_adjacently_tagged(cls, data): - # Handle direct variant instances - if isinstance(data, (ReflectapiDemoTestsSerdeTaggedItemVariant)): - return data - if isinstance(data, dict): - tag = data.get("t") - content = data.get("c") - if tag is None: - raise ValueError("Missing tag field 't'") - if content is None and tag not in ("Nothing"): - raise ValueError("Missing content field 'c' for tag: {}".format(tag)) - # Dispatch based on tag - if tag == "Item": - return ReflectapiDemoTestsSerdeTaggedItemVariant(field_0=content) - if tag == "Nothing": - return "Nothing" - raise ValueError( - "Unknown variant for ReflectapiDemoTestsSerdeTagged: {}".format(data) - ) - @model_serializer - def _serialize_adjacently_tagged(self): - if isinstance(self.root, ReflectapiDemoTestsSerdeTaggedItemVariant): - return {"t": "Item", "c": self.root.field_0} - if self.root == "Nothing": - return {"t": "Nothing"} - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTagged variant: {type(self.root)}" - ) +class ReflectapiDemoTestsSerdeTagged(Generic[T]): + """""" + @classmethod def __class_getitem__(cls, params): - return cls + """Enable subscripting for generic discriminated union.""" + if not isinstance(params, tuple): + params = (params,) + if len(params) != 1: + raise TypeError(f"Expected 1 type parameters, got {len(params)}") + + return Annotated[ + Union[ + ReflectapiDemoTestsSerdeTaggedItem[params[0]], + ReflectapiDemoTestsSerdeTaggedNothing[params[0]], + ], + Field(discriminator="t"), + ] # Namespace classes for dotted access to types @@ -104,7 +74,8 @@ class reflectapi_demo: class serde: """Namespace for serde types.""" - TaggedItemVariant = ReflectapiDemoTestsSerdeTaggedItemVariant + TaggedItem = ReflectapiDemoTestsSerdeTaggedItem + TaggedNothing = ReflectapiDemoTestsSerdeTaggedNothing Tagged = ReflectapiDemoTestsSerdeTagged @@ -201,8 +172,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Tagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_externally_tagged_enum-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_externally_tagged_enum-5.snap index acb3e9f9..c75bf7e2 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_externally_tagged_enum-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_externally_tagged_enum-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + # Type variables for generic types @@ -64,34 +92,33 @@ class ReflectapiDemoTestsSerdeWrapper( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance(data, ReflectapiDemoTestsSerdeWrapperValueVariant): - return data - - # Handle JSON data (for deserialization) - if isinstance(data, str) and data == "Empty": - return data - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "Value": - return ReflectapiDemoTestsSerdeWrapperValueVariant(field_0=value) - - raise ValueError(f"Unknown variant for ReflectapiDemoTestsSerdeWrapper: {data}") + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Value": lambda v: ReflectapiDemoTestsSerdeWrapperValueVariant( + field_0=v + ), + "Empty": "_unit", + }, + (ReflectapiDemoTestsSerdeWrapperValueVariant,), + "ReflectapiDemoTestsSerdeWrapper", + ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance(self.root, ReflectapiDemoTestsSerdeWrapperValueVariant): - return {"Value": self.root.field_0} - if self.root == "Empty": - return "Empty" - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeWrapper variant: {type(self.root)}" + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Value": ( + lambda r: isinstance( + r, ReflectapiDemoTestsSerdeWrapperValueVariant + ), + lambda r: {"Value": r.field_0}, + ), + "Empty": (lambda r: r == "Empty", lambda r: "Empty"), + }, + "ReflectapiDemoTestsSerdeWrapper", ) @@ -202,8 +229,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Wrapper.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeWrapper, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_struct_repr_transparent-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_struct_repr_transparent-5.snap index d47d9065..2fcb4a20 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_struct_repr_transparent-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_struct_repr_transparent-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/serde.rs expression: "super :: into_python_code :: < TestStruct > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_struct_repr_transparent_partially_generic-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_struct_repr_transparent_partially_generic-5.snap index 31b84a7e..0ce9077d 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_struct_repr_transparent_partially_generic-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_struct_repr_transparent_partially_generic-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/serde.rs expression: "super :: into_python_code :: < TestStruct > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__kebab_case-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__kebab_case-5.snap index c74060b7..df04d61a 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__kebab_case-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__kebab_case-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTest(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int = Field( @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Test.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTest, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__multiple_underscore_prefix_fields-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__multiple_underscore_prefix_fields-5.snap index 4d4f295b..8ce037ff 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__multiple_underscore_prefix_fields-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__multiple_underscore_prefix_fields-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeUnderscored(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) single: int = Field(serialization_alias="_single", validation_alias="_single") @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Underscored.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeUnderscored, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_deeply_nested_modules-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_deeply_nested_modules-5.snap index f49bcd70..89beb4f2 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_deeply_nested_modules-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_deeply_nested_modules-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeDeepNestedInnerDeepType(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) data: str @@ -147,8 +145,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.deep.nested.inner.DeepType.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeDeepNestedInnerDeepType, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_single_segment_type-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_single_segment_type-5.snap index ab97e063..49589607 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_single_segment_type-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_single_segment_type-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeSimpleTopLevel(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) value: int @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.SimpleTopLevel.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeSimpleTopLevel, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_with_numeric_start-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_with_numeric_start-5.snap index 7c9c3e4f..018fd7e3 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_with_numeric_start-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__namespace_with_numeric_start-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTypeWithNumbers(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_123: str = Field(serialization_alias="123start", validation_alias="123start") @@ -141,8 +139,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TypeWithNumbers.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTypeWithNumbers, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_generic_containers-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_generic_containers-5.snap index 53ac8c41..8f25d0f9 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_generic_containers-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_generic_containers-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeComplex(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) matrix: list[list[int]] @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Complex.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeComplex, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_internally_tagged_enums-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_internally_tagged_enums-5.snap index c4b88175..8a09f660 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_internally_tagged_enums-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_internally_tagged_enums-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestV1(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) version: Literal["v1"] = Field(default="v1", description="Discriminator field") @@ -35,8 +33,6 @@ class ReflectapiDemoTestsSerdeTestV1(BaseModel): class ReflectapiDemoTestsSerdeTestV2(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) version: Literal["v2"] = Field(default="v2", description="Discriminator field") @@ -51,8 +47,6 @@ class ReflectapiDemoTestsSerdeTest(RootModel): class ReflectapiDemoTestsSerdeV1A(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["A"] = Field(default="A", description="Discriminator field") @@ -60,8 +54,6 @@ class ReflectapiDemoTestsSerdeV1A(BaseModel): class ReflectapiDemoTestsSerdeV1B(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["B"] = Field(default="B", description="Discriminator field") @@ -76,8 +68,6 @@ class ReflectapiDemoTestsSerdeV1(RootModel): class ReflectapiDemoTestsSerdeV2C(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["C"] = Field(default="C", description="Discriminator field") @@ -85,8 +75,6 @@ class ReflectapiDemoTestsSerdeV2C(BaseModel): class ReflectapiDemoTestsSerdeV2D(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["D"] = Field(default="D", description="Discriminator field") @@ -214,10 +202,12 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Test.model_rebuild() - reflectapi_demo.tests.serde.V1.model_rebuild() - reflectapi_demo.tests.serde.V2.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTest, + ReflectapiDemoTestsSerdeV1, + ReflectapiDemoTestsSerdeV2, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_internally_tagged_enums_minimal-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_internally_tagged_enums_minimal-5.snap index 33e39df6..d530a3ef 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_internally_tagged_enums_minimal-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__nested_internally_tagged_enums_minimal-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestV1(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) version: Literal["v1"] = Field(default="v1", description="Discriminator field") @@ -41,8 +39,6 @@ class ReflectapiDemoTestsSerdeTest(RootModel): class ReflectapiDemoTestsSerdeV1A(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["A"] = Field(default="A", description="Discriminator field") @@ -162,9 +158,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Test.model_rebuild() - reflectapi_demo.tests.serde.V1.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTest, + ReflectapiDemoTestsSerdeV1, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_adjacently_tagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_adjacently_tagged-5.snap index fc347cb7..d2211315 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_adjacently_tagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_adjacently_tagged-5.snap @@ -14,18 +14,10 @@ from __future__ import annotations # Standard library imports from enum import Enum -from typing import Annotated, Any, Generic, Optional, TypeVar, Union +from typing import Annotated, Any, Generic, Literal, Optional, TypeVar, Union # Third-party imports -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PrivateAttr, - RootModel, - model_serializer, - model_validator, -) +from pydantic import BaseModel, ConfigDict, Field, RootModel # Runtime imports from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse @@ -33,109 +25,43 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible -class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedIntVariant(BaseModel): - """int variant""" - +class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedInt(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) - field_0: int - + t: Literal["int"] = Field(default="int", description="Discriminator field") + c: int -class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedStringVariant( - BaseModel -): - """string variant""" +class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedString(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) - field_0: str + t: Literal["string"] = Field(default="string", description="Discriminator field") + c: str + + +class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBool(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + t: Literal["bool"] = Field(default="bool", description="Discriminator field") + c: bool -class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBoolVariant(BaseModel): - """bool variant""" +class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedUnit(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) - field_0: bool - - -# Adjacently tagged enum using RootModel -ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedVariants = Union[ - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedIntVariant, - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedStringVariant, - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBoolVariant, - Literal["unit"], -] - - -class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTagged( - RootModel[ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedVariants] -): - """Adjacently tagged enum""" - - @model_validator(mode="before") - @classmethod - def _validate_adjacently_tagged(cls, data): - # Handle direct variant instances - if isinstance( - data, - ( - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedIntVariant, - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedStringVariant, - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBoolVariant, - ), - ): - return data - if isinstance(data, dict): - tag = data.get("t") - content = data.get("c") - if tag is None: - raise ValueError("Missing tag field 't'") - if content is None and tag not in ("unit"): - raise ValueError("Missing content field 'c' for tag: {}".format(tag)) - # Dispatch based on tag - if tag == "int": - return ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedIntVariant( - field_0=content - ) - if tag == "string": - return ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedStringVariant( - field_0=content - ) - if tag == "bool": - return ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBoolVariant( - field_0=content - ) - if tag == "unit": - return "unit" - raise ValueError( - "Unknown variant for ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTagged: {}".format( - data - ) - ) + t: Literal["unit"] = Field(default="unit", description="Discriminator field") - @model_serializer - def _serialize_adjacently_tagged(self): - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedIntVariant, - ): - return {"t": "int", "c": self.root.field_0} - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedStringVariant, - ): - return {"t": "string", "c": self.root.field_0} - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBoolVariant, - ): - return {"t": "bool", "c": self.root.field_0} - if self.root == "unit": - return {"t": "unit"} - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTagged variant: {type(self.root)}" - ) + +class ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTagged(RootModel): + root: Annotated[ + Union[ + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedInt, + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedString, + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBool, + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedUnit, + ], + Field(discriminator="t"), + ] # Namespace classes for dotted access to types @@ -148,14 +74,17 @@ class reflectapi_demo: class serde: """Namespace for serde types.""" - TestNewtypeVariantsAdjacentlyTaggedIntVariant = ( - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedIntVariant + TestNewtypeVariantsAdjacentlyTaggedInt = ( + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedInt + ) + TestNewtypeVariantsAdjacentlyTaggedString = ( + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedString ) - TestNewtypeVariantsAdjacentlyTaggedStringVariant = ( - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedStringVariant + TestNewtypeVariantsAdjacentlyTaggedBool = ( + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBool ) - TestNewtypeVariantsAdjacentlyTaggedBoolVariant = ( - ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedBoolVariant + TestNewtypeVariantsAdjacentlyTaggedUnit = ( + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTaggedUnit ) TestNewtypeVariantsAdjacentlyTagged = ( ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTagged @@ -259,8 +188,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestNewtypeVariantsAdjacentlyTagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestNewtypeVariantsAdjacentlyTagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_externally_tagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_externally_tagged-5.snap index b4c7ea68..b45b10b2 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_externally_tagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_externally_tagged-5.snap @@ -21,7 +21,6 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PrivateAttr, RootModel, model_serializer, model_validator, @@ -33,6 +32,35 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}") + + class ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedIntVariant(BaseModel): """int variant""" @@ -75,70 +103,64 @@ class ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTagged( @model_validator(mode="before") @classmethod - def _validate_externally_tagged(cls, data): - # Handle direct variant instances (for programmatic creation) - if isinstance( - data, ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedIntVariant - ): - return data - if isinstance( + def _validate(cls, data): + return _parse_externally_tagged( data, - ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedStringVariant, - ): - return data - if isinstance( - data, ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedBoolVariant - ): - return data - - # Handle JSON data (for deserialization) - if isinstance(data, str) and data == "unit": - return data - - if isinstance(data, dict): - if len(data) != 1: - raise ValueError("Externally tagged enum must have exactly one key") - - key, value = next(iter(data.items())) - if key == "int": - return ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedIntVariant( - field_0=value - ) - if key == "string": - return ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedStringVariant( - field_0=value - ) - if key == "bool": - return ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedBoolVariant( - field_0=value - ) - - raise ValueError( - f"Unknown variant for ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTagged: {data}" + { + "int": lambda v: ( + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedIntVariant( + field_0=v + ) + ), + "string": lambda v: ( + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedStringVariant( + field_0=v + ) + ), + "bool": lambda v: ( + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedBoolVariant( + field_0=v + ) + ), + "unit": "_unit", + }, + ( + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedIntVariant, + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedStringVariant, + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedBoolVariant, + ), + "ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTagged", ) @model_serializer - def _serialize_externally_tagged(self): - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedIntVariant, - ): - return {"int": self.root.field_0} - if isinstance( - self.root, - ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedStringVariant, - ): - return {"string": self.root.field_0} - if isinstance( + def _serialize(self): + return _serialize_externally_tagged( self.root, - ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedBoolVariant, - ): - return {"bool": self.root.field_0} - if self.root == "unit": - return "unit" - - raise ValueError( - f"Cannot serialize ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTagged variant: {type(self.root)}" + { + "int": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedIntVariant, + ), + lambda r: {"int": r.field_0}, + ), + "string": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedStringVariant, + ), + lambda r: {"string": r.field_0}, + ), + "bool": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTaggedBoolVariant, + ), + lambda r: {"bool": r.field_0}, + ), + "unit": (lambda r: r == "unit", lambda r: "unit"), + }, + "ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTagged", ) @@ -263,8 +285,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestNewtypeVariantsExternallyTagged.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestNewtypeVariantsExternallyTagged, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_internally_tagged-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_internally_tagged-5.snap index 4f2eb66a..0cc54e8c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_internally_tagged-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__newtype_variants_internally_tagged-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeStrukt1(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) a: int @@ -35,8 +33,6 @@ class ReflectapiDemoTestsSerdeStrukt1(BaseModel): class ReflectapiDemoTestsSerdeStrukt2(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) c: int @@ -44,8 +40,6 @@ class ReflectapiDemoTestsSerdeStrukt2(BaseModel): class ReflectapiDemoTestsSerdeEnumA(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["a"] = Field(default="a", description="Discriminator field") @@ -54,8 +48,6 @@ class ReflectapiDemoTestsSerdeEnumA(BaseModel): class ReflectapiDemoTestsSerdeEnumB(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["b"] = Field(default="b", description="Discriminator field") @@ -180,10 +172,12 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Enum.model_rebuild() - reflectapi_demo.tests.serde.Strukt1.model_rebuild() - reflectapi_demo.tests.serde.Strukt2.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeEnum, + ReflectapiDemoTestsSerdeStrukt1, + ReflectapiDemoTestsSerdeStrukt2, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__option_of_option-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__option_of_option-5.snap index d64ef1b7..d967907b 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__option_of_option-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__option_of_option-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) value: str | None | None = None @@ -139,8 +137,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Nested.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeNested, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__self_referential_struct-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__self_referential_struct-5.snap index 61120080..3369bbfc 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__self_referential_struct-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__self_referential_struct-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeCategory(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) name: str @@ -139,8 +137,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Category.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeCategory, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_from-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_from-5.snap index a09ea2e9..b3d7a133 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_from-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_from-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructFrom(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int class ReflectapiDemoTestsSerdeTestStructFromProxy(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int @@ -147,9 +143,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructFrom.model_rebuild() - reflectapi_demo.tests.serde.TestStructFromProxy.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructFrom, + ReflectapiDemoTestsSerdeTestStructFromProxy, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_into-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_into-5.snap index 662f54ac..5f16797f 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_into-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_into-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructInto(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int class ReflectapiDemoTestsSerdeTestStructIntoProxy(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int @@ -147,9 +143,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructInto.model_rebuild() - reflectapi_demo.tests.serde.TestStructIntoProxy.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructInto, + ReflectapiDemoTestsSerdeTestStructIntoProxy, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename-5.snap index cbc43008..73d2db31 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeMyStruct(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -136,8 +134,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.MyStruct.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeMyStruct, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all-5.snap index f6c0a618..f0a6cb5e 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructRenameAll(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int = Field( @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructRenameAll.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructRenameAll, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all_differently-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all_differently-5.snap index 8f0a3619..ecf40fd8 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all_differently-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all_differently-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestStructRenameAllDifferently(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int class ReflectapiDemoTestsSerdeOutputTestStructRenameAllDifferently(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int = Field( @@ -164,9 +160,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestStructRenameAllDifferently.model_rebuild() - reflectapi_demo.tests.serde.output.TestStructRenameAllDifferently.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestStructRenameAllDifferently, + ReflectapiDemoTestsSerdeOutputTestStructRenameAllDifferently, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all_pascal_case-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all_pascal_case-5.snap index a4cd3aa8..368261a3 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all_pascal_case-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_all_pascal_case-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructRenameAllPascalCase(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int = Field( @@ -146,8 +144,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructRenameAllPascalCase.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructRenameAllPascalCase, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_differently-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_differently-5.snap index 73e2d3bd..e5a40930 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_differently-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_differently-5.snap @@ -25,14 +25,10 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeMyStructInput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) class ReflectapiDemoTestsSerdeMyStructOutput(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -143,9 +139,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.MyStructInput.model_rebuild() - reflectapi_demo.tests.serde.MyStructOutput.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeMyStructInput, + ReflectapiDemoTestsSerdeMyStructOutput, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_field-5.snap index eaa742ed..2cffe776 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_field-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_rename_field-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestStructRenameField(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int = Field( @@ -35,8 +33,6 @@ class ReflectapiDemoTestsSerdeInputTestStructRenameField(BaseModel): class ReflectapiDemoTestsSerdeOutputTestStructRenameField(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int @@ -160,9 +156,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestStructRenameField.model_rebuild() - reflectapi_demo.tests.serde.output.TestStructRenameField.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestStructRenameField, + ReflectapiDemoTestsSerdeOutputTestStructRenameField, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_repr_transparent_generic_inner_type-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_repr_transparent_generic_inner_type-5.snap index e54e0b3b..62c8756b 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_repr_transparent_generic_inner_type-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_repr_transparent_generic_inner_type-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTest(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) inner: bytes @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.Test.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTest, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_try_from-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_try_from-5.snap index 91a92da7..15fe6252 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_try_from-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_try_from-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructTryFormProxy(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int class ReflectapiDemoTestsSerdeTestStructTryFrom(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int @@ -147,9 +143,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructTryFormProxy.model_rebuild() - reflectapi_demo.tests.serde.TestStructTryFrom.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructTryFormProxy, + ReflectapiDemoTestsSerdeTestStructTryFrom, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten-5.snap index 30423229..abcb02fd 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructWithFlatten(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int class ReflectapiDemoTestsSerdeTestStructWithFlattenNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int @@ -149,9 +145,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructWithFlatten.model_rebuild() - reflectapi_demo.tests.serde.TestStructWithFlattenNested.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructWithFlatten, + ReflectapiDemoTestsSerdeTestStructWithFlattenNested, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten_optional-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten_optional-5.snap index d97d1c86..ab05a5c5 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten_optional-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten_optional-5.snap @@ -26,16 +26,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructWithFlattenNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int class ReflectapiDemoTestsSerdeTestStructWithFlattenOptional(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int | None = None @@ -156,9 +152,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructWithFlattenNested.model_rebuild() - reflectapi_demo.tests.serde.TestStructWithFlattenOptional.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructWithFlattenNested, + ReflectapiDemoTestsSerdeTestStructWithFlattenOptional, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten_optional_and_required-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten_optional_and_required-5.snap index a9b72a75..8f64251e 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten_optional_and_required-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_flatten_optional_and_required-5.snap @@ -26,8 +26,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructRenameAll(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int = Field( @@ -36,16 +34,12 @@ class ReflectapiDemoTestsSerdeTestStructRenameAll(BaseModel): class ReflectapiDemoTestsSerdeTestStructWithFlattenNested(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int class ReflectapiDemoTestsSerdeTestStructWithFlattenOptionalAndRequired(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int | None = None @@ -174,10 +168,12 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructRenameAll.model_rebuild() - reflectapi_demo.tests.serde.TestStructWithFlattenNested.model_rebuild() - reflectapi_demo.tests.serde.TestStructWithFlattenOptionalAndRequired.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructRenameAll, + ReflectapiDemoTestsSerdeTestStructWithFlattenNested, + ReflectapiDemoTestsSerdeTestStructWithFlattenOptionalAndRequired, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_rename_to_invalid_chars-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_rename_to_invalid_chars-5.snap index 19ac1fd3..bf1ad103 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_rename_to_invalid_chars-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_rename_to_invalid_chars-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructWithRenameToInvalidChars(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="field-name&&", validation_alias="field-name&&") @@ -144,8 +142,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructWithRenameToInvalidChars.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructWithRenameToInvalidChars, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_rename_to_kebab_case-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_rename_to_kebab_case-5.snap index fed2460f..648cc91c 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_rename_to_kebab_case-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_rename_to_kebab_case-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeStructName(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) field_name: int = Field( @@ -140,8 +138,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.StructName.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeStructName, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_default-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_default-5.snap index 1eb93882..e8752db4 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_default-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_default-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestStructWithSerdeDefault(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int | None = None class ReflectapiDemoTestsSerdeOutputTestStructWithSerdeDefault(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int @@ -162,9 +158,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestStructWithSerdeDefault.model_rebuild() - reflectapi_demo.tests.serde.output.TestStructWithSerdeDefault.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestStructWithSerdeDefault, + ReflectapiDemoTestsSerdeOutputTestStructWithSerdeDefault, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip-5.snap index 63f230c6..1f82dbd9 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStructWithSerdeSkip(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -136,8 +134,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStructWithSerdeSkip.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStructWithSerdeSkip, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_deserialize-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_deserialize-5.snap index 52a915da..3f6517e9 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_deserialize-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_deserialize-5.snap @@ -25,14 +25,10 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestStructWithSerdeSkipDeserialize(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) class ReflectapiDemoTestsSerdeOutputTestStructWithSerdeSkipDeserialize(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int @@ -164,9 +160,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestStructWithSerdeSkipDeserialize.model_rebuild() - reflectapi_demo.tests.serde.output.TestStructWithSerdeSkipDeserialize.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestStructWithSerdeSkipDeserialize, + ReflectapiDemoTestsSerdeOutputTestStructWithSerdeSkipDeserialize, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_serialize-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_serialize-5.snap index b8965b63..1fecf25d 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_serialize-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_serialize-5.snap @@ -25,16 +25,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestStructWithSerdeSkipSerialize(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int = Field(serialization_alias="_f", validation_alias="_f") class ReflectapiDemoTestsSerdeOutputTestStructWithSerdeSkipSerialize(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -164,9 +160,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestStructWithSerdeSkipSerialize.model_rebuild() - reflectapi_demo.tests.serde.output.TestStructWithSerdeSkipSerialize.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestStructWithSerdeSkipSerialize, + ReflectapiDemoTestsSerdeOutputTestStructWithSerdeSkipSerialize, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_serialize_if-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_serialize_if-5.snap index a1a8e240..1e47ecf8 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_serialize_if-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_skip_serialize_if-5.snap @@ -26,16 +26,12 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeInputTestStructWithSerdeSkipSerializeIf(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int | None = None class ReflectapiDemoTestsSerdeOutputTestStructWithSerdeSkipSerializeIf(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) f: int | None = None @@ -167,9 +163,11 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.input.TestStructWithSerdeSkipSerializeIf.model_rebuild() - reflectapi_demo.tests.serde.output.TestStructWithSerdeSkipSerializeIf.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeInputTestStructWithSerdeSkipSerializeIf, + ReflectapiDemoTestsSerdeOutputTestStructWithSerdeSkipSerializeIf, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_transparent-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_transparent-5.snap index 38276207..c50ac3de 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_transparent-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__struct_with_serde_transparent-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/serde.rs expression: "super :: into_python_code :: < TestStructWithSerdeTransparent > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__timezone-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__timezone-5.snap index ca5f1b59..76f76738 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__timezone-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__timezone-5.snap @@ -25,8 +25,6 @@ from reflectapi_runtime import ReflectapiInfallible class ReflectapiDemoTestsSerdeTestStruct(BaseModel): - """Generated data model.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) timezone: str @@ -138,8 +136,10 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: - reflectapi_demo.tests.serde.TestStruct.model_rebuild() -except AttributeError: - # Some types may not have model_rebuild method - pass +for _model in [ + ReflectapiDemoTestsSerdeTestStruct, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__unit_struct-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__unit_struct-5.snap index 4b7497c8..5fbe1cf2 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__unit_struct-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__unit_struct-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/serde.rs expression: "super :: into_python_code :: < TestUnitStruct > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__unit_tuple_struct-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__unit_tuple_struct-5.snap index 5340e847..36b2be93 100644 --- a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__unit_tuple_struct-5.snap +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__unit_tuple_struct-5.snap @@ -2,12 +2,12 @@ source: reflectapi-demo/src/tests/serde.rs expression: "super :: into_python_code :: < TestUnitTupleStruct > ()" --- -''' +""" Generated Python client for api_client. DO NOT MODIFY THIS FILE MANUALLY. This file is automatically generated by ReflectAPI. -''' +""" from __future__ import annotations @@ -64,7 +64,6 @@ class AsyncClient(AsyncClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = AsyncInoutClient(self) @@ -108,7 +107,6 @@ class Client(ClientBase): ) -> None: super().__init__(base_url, **kwargs) - self.inout = InoutClient(self) @@ -119,7 +117,3 @@ StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references -try: -except AttributeError: - # Some types may not have model_rebuild method - pass diff --git a/reflectapi-derive/src/derive.rs b/reflectapi-derive/src/derive.rs index 9bb83825..bace5ff3 100644 --- a/reflectapi-derive/src/derive.rs +++ b/reflectapi-derive/src/derive.rs @@ -170,7 +170,6 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { codegen_config: reflectapi_schema::LanguageSpecificTypeCodegenConfig, ) -> Struct { Struct { - id: Default::default(), name: type_def_name, serde_name, description: type_def_description, @@ -385,7 +384,6 @@ fn visit_variant( }; Variant { - id: Default::default(), name: variant_def_name, serde_name, description: parse_doc_attributes(&variant.original.attrs), diff --git a/reflectapi-derive/src/tokenizable_schema.rs b/reflectapi-derive/src/tokenizable_schema.rs index c52674d5..691f1463 100644 --- a/reflectapi-derive/src/tokenizable_schema.rs +++ b/reflectapi-derive/src/tokenizable_schema.rs @@ -117,7 +117,6 @@ impl ToTokens for TokenizableField<'_> { } tokens.extend(quote::quote! { reflectapi::Field { - id: Default::default(), name: #name.into(), serde_name: #serde_name.into(), description: #description.into(), @@ -169,7 +168,6 @@ impl ToTokens for TokenizableVariant<'_> { let untagged = self.inner.untagged; tokens.extend(quote::quote! { reflectapi::Variant { - id: Default::default(), name: #name.into(), serde_name: #serde_name.into(), description: #description.into(), @@ -241,7 +239,6 @@ impl ToTokens for TokenizableEnum<'_> { TokenizableLanguageSpecificTypeCodegenConfig(&self.inner.codegen_config); tokens.extend(quote::quote! { reflectapi::Enum { - id: Default::default(), name: #name.into(), serde_name: #serde_name.into(), description: #description.into(), @@ -316,7 +313,6 @@ impl ToTokens for TokenizableStruct<'_> { TokenizableLanguageSpecificTypeCodegenConfig(&self.inner.codegen_config); tokens.extend(quote::quote! { reflectapi::Struct { - id: Default::default(), name: #name.into(), serde_name: #serde_name.into(), description: #description.into(), @@ -349,13 +345,15 @@ impl ToTokens for TokenizablePrimitive<'_> { .fallback .as_ref() .map(TokenizableTypeReference::new); + let codegen_config = + TokenizableLanguageSpecificTypeCodegenConfig(&self.inner.codegen_config); tokens.extend(quote::quote! { reflectapi::Primitive { - id: Default::default(), name: #name.into(), description: #description.into(), parameters: vec![#(#parameters),*], fallback: #fallback, + codegen_config: #codegen_config, } }); } diff --git a/reflectapi-schema/src/codegen.rs b/reflectapi-schema/src/codegen.rs index b10bb65a..dbf0dc70 100644 --- a/reflectapi-schema/src/codegen.rs +++ b/reflectapi-schema/src/codegen.rs @@ -3,10 +3,15 @@ use std::collections::BTreeSet; #[derive(Debug, Clone, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct LanguageSpecificTypeCodegenConfig { pub rust: RustTypeCodegenConfig, - // Add other languages as required } #[derive(Debug, Clone, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct RustTypeCodegenConfig { pub additional_derives: BTreeSet, } + +impl LanguageSpecificTypeCodegenConfig { + pub fn is_serialization_default(&self) -> bool { + self.rust == RustTypeCodegenConfig::default() + } +} diff --git a/reflectapi-schema/src/ids.rs b/reflectapi-schema/src/ids.rs index 79b6eba6..b2d94dd8 100644 --- a/reflectapi-schema/src/ids.rs +++ b/reflectapi-schema/src/ids.rs @@ -1,222 +1,211 @@ -/// ID assignment utilities for ensuring unique, stable SymbolIds before normalization +/// Compiler-owned ID assignment for schema symbols. /// -/// This module fixes the core issue where JSON-deserialized schemas have default -/// SymbolIds that cause conflicts during normalization discovery. +/// SymbolIds are a compiler concept used during normalization and codegen. +/// They are NOT stored on raw schema types — raw types are the interchange +/// format (JSON-serializable, derive-macro-produced). Instead, IDs are +/// assigned here and passed to the normalizer as a side table. use crate::symbol::STDLIB_TYPES; -use crate::{Enum, Schema, Struct, SymbolId, SymbolKind, Type, Typespace}; +use crate::{Schema, SymbolId, SymbolKind, Type, Typespace}; use std::collections::HashMap; -/// Ensure all symbols in the schema have unique, stable IDs based on their canonical names. +/// Side table of compiler-assigned SymbolIds, keyed by fully-qualified name. +/// +/// Built once from a raw `Schema` and consumed by the normalizer to assign +/// stable identities when constructing `SemanticSchema`. +#[derive(Debug, Clone)] +pub struct SchemaIds { + /// Schema root ID + pub schema_id: SymbolId, + /// Function name → SymbolId + pub functions: HashMap, + /// Type FQN → SymbolId (includes types from both input and output typespaces) + pub types: HashMap, + /// (parent FQN, member name) → SymbolId for fields and variants + pub members: HashMap<(String, String), SymbolId>, +} + +/// Build a `SchemaIds` side table from a raw schema. /// /// Types that share a fully-qualified name across input and output typespaces -/// receive distinct SymbolIds via the disambiguator field. This prevents -/// collisions when types with the same name have different definitions in -/// each typespace (e.g., request vs response variants of the same type). -pub fn ensure_symbol_ids(schema: &mut Schema) { - if schema.id.is_unknown() { - schema.id = SymbolId::new( - SymbolKind::Struct, +/// receive distinct SymbolIds via the disambiguator field. +pub fn build_schema_ids(schema: &Schema) -> SchemaIds { + let mut ids = SchemaIds { + schema_id: SymbolId::new( + SymbolKind::Schema, vec!["__schema__".to_string(), schema.name.clone()], - ); - } + ), + functions: HashMap::new(), + types: HashMap::new(), + members: HashMap::new(), + }; - let mut seen: HashMap = HashMap::new(); - register_stdlib_types(&mut seen); + // Pre-register well-known stdlib types + for &(name, kind) in STDLIB_TYPES { + ids.types + .entry(name.to_string()) + .or_insert_with(|| SymbolId::new(kind, split_path(name))); + } - for function in &mut schema.functions { - if function.id.is_unknown() { - function.id = SymbolId::new(SymbolKind::Endpoint, vec![function.name.clone()]); - } + // Register functions + for function in &schema.functions { + ids.functions + .entry(function.name.clone()) + .or_insert_with(|| SymbolId::new(SymbolKind::Endpoint, vec![function.name.clone()])); } - // Use separate seen maps per typespace, sharing stdlib registrations. - // This ensures types with the same FQN in different typespaces get - // distinct SymbolIds (via disambiguator) when they are different types. - let mut input_seen = seen.clone(); - let mut output_seen = seen; - assign_typespace_ids(&mut schema.input_types, &mut input_seen); - assign_typespace_ids(&mut schema.output_types, &mut output_seen); + // Register types from both typespaces + let stdlib_snapshot = ids.types.clone(); + let mut input_seen = stdlib_snapshot.clone(); + let mut output_seen = stdlib_snapshot; + + register_typespace_ids(&schema.input_types, &mut input_seen, &mut ids.members); + register_typespace_ids(&schema.output_types, &mut output_seen, &mut ids.members); // For any FQN that appears in both typespaces with different types, // disambiguate the output typespace's IDs for (fqn, input_id) in &input_seen { if let Some(output_id) = output_seen.get(fqn) { if input_id == output_id { - // Same ID means same type — check if the actual types differ let input_ty = schema.input_types.get_type(fqn); let output_ty = schema.output_types.get_type(fqn); if let (Some(input_ty), Some(output_ty)) = (input_ty, output_ty) { if input_ty != output_ty { - // Different types with same name — disambiguate output let disambiguated = SymbolId::with_disambiguator(output_id.kind, output_id.path.clone(), 1); - assign_disambiguated_id(&mut schema.output_types, fqn, &disambiguated); + // Re-register output type's members with the new parent ID + if let Some(output_ty) = schema.output_types.get_type(fqn) { + reregister_members(output_ty, &disambiguated, &mut ids.members); + } + output_seen.insert(fqn.clone(), disambiguated); } } } } } -} -/// Pre-register well-known stdlib types that might be referenced but not always defined -fn register_stdlib_types(seen: &mut HashMap) { - for &(name, kind) in STDLIB_TYPES { - seen.entry(name.to_string()) - .or_insert_with(|| SymbolId::new(kind, split_path(name))); + // Merge both typespace maps (output_seen may have disambiguated entries) + ids.types.extend(input_seen); + for (fqn, output_id) in output_seen { + // Output overwrites only if it has a disambiguated ID + if output_id.disambiguator > 0 { + // Store disambiguated IDs with a prefix to avoid overwriting input IDs + ids.types.insert(format!("__output__::{fqn}"), output_id); + } else { + ids.types.entry(fqn).or_insert(output_id); + } } -} -/// Assign IDs to all types in a typespace -fn assign_typespace_ids(typespace: &mut Typespace, seen: &mut HashMap) { - let mut new_typespace = Typespace::new(); + ids +} +/// Register types from a typespace into the ID maps +fn register_typespace_ids( + typespace: &Typespace, + seen: &mut HashMap, + members: &mut HashMap<(String, String), SymbolId>, +) { for ty in typespace.types() { - let mut updated_type = ty.clone(); - let type_name = updated_type.name().to_string(); - assign_type_id(&type_name, &mut updated_type, seen); - new_typespace.insert_type(updated_type); + let type_name = ty.name().to_string(); + let id = seen + .entry(type_name.clone()) + .or_insert_with(|| { + let kind = match ty { + Type::Primitive(_) => SymbolKind::Primitive, + Type::Struct(_) => SymbolKind::Struct, + Type::Enum(_) => SymbolKind::Enum, + }; + SymbolId::new(kind, split_path(&type_name)) + }) + .clone(); + + register_type_members(ty, &id, members); } - - *typespace = new_typespace; } -/// Assign a unique ID to a type and its nested members -fn assign_type_id(fqn: &str, ty: &mut Type, seen: &mut HashMap) { - let id = seen - .entry(fqn.to_string()) - .or_insert_with(|| { - let kind = match ty { - Type::Primitive(_) => SymbolKind::Primitive, - Type::Struct(_) => SymbolKind::Struct, - Type::Enum(_) => SymbolKind::Enum, - }; - SymbolId::new(kind, split_path(fqn)) - }) - .clone(); - +/// Register field and variant IDs for a type +fn register_type_members( + ty: &Type, + parent_id: &SymbolId, + members: &mut HashMap<(String, String), SymbolId>, +) { + let parent_fqn = parent_id.qualified_name(); match ty { - Type::Primitive(p) => { - if p.id.is_unknown() { - p.id = id; - } - } + Type::Primitive(_) => {} Type::Struct(s) => { - if s.id.is_unknown() { - s.id = id.clone(); - } - assign_struct_member_ids(s, &s.id.clone()); + register_struct_members(s, &parent_fqn, parent_id, members); } Type::Enum(e) => { - if e.id.is_unknown() { - e.id = id.clone(); - } - assign_enum_member_ids(e, &e.id.clone()); + register_enum_members(e, &parent_fqn, parent_id, members); } } } -/// Reassign the top-level ID of a type in a typespace (for disambiguation) -fn assign_disambiguated_id(typespace: &mut Typespace, fqn: &str, new_id: &SymbolId) { - let types: Vec<_> = typespace.types().cloned().collect(); - let mut new_typespace = Typespace::new(); - for mut ty in types { - if ty.name() == fqn { - match &mut ty { - Type::Primitive(p) => p.id = new_id.clone(), - Type::Struct(s) => { - s.id = new_id.clone(); - // Clear member IDs so they get re-assigned with the new parent - clear_struct_member_ids(s); - assign_struct_member_ids(s, new_id); - } - Type::Enum(e) => { - e.id = new_id.clone(); - clear_enum_member_ids(e); - assign_enum_member_ids(e, new_id); - } - } - } - new_typespace.insert_type(ty); - } - *typespace = new_typespace; -} - -fn clear_struct_member_ids(s: &mut Struct) { - match &mut s.fields { - crate::Fields::Named(fields) | crate::Fields::Unnamed(fields) => { - for field in fields { - field.id = SymbolId::default(); - } - } - crate::Fields::None => {} - } -} - -fn clear_enum_member_ids(e: &mut Enum) { - for variant in &mut e.variants { - variant.id = SymbolId::default(); - match &mut variant.fields { - crate::Fields::Named(fields) | crate::Fields::Unnamed(fields) => { - for field in fields { - field.id = SymbolId::default(); - } - } - crate::Fields::None => {} - } - } -} - -/// Assign IDs to struct fields -fn assign_struct_member_ids(s: &mut Struct, owner: &SymbolId) { - match &mut s.fields { +/// Register struct field IDs +fn register_struct_members( + s: &crate::Struct, + parent_fqn: &str, + parent_id: &SymbolId, + members: &mut HashMap<(String, String), SymbolId>, +) { + match &s.fields { crate::Fields::Named(fields) => { for field in fields { - if field.id.is_unknown() { - let mut path = owner.path.clone(); - path.push(field.name.clone()); - field.id = SymbolId::new(SymbolKind::Field, path); - } + let mut path = parent_id.path.clone(); + path.push(field.name.clone()); + members + .entry((parent_fqn.to_string(), field.name.clone())) + .or_insert_with(|| SymbolId::new(SymbolKind::Field, path)); } } crate::Fields::Unnamed(fields) => { - for (i, field) in fields.iter_mut().enumerate() { - if field.id.is_unknown() { - let mut path = owner.path.clone(); - path.push(format!("arg{i:02}")); - field.id = SymbolId::new(SymbolKind::Field, path); - } + for (i, _field) in fields.iter().enumerate() { + let arg_name = format!("arg{i:02}"); + let mut path = parent_id.path.clone(); + path.push(arg_name.clone()); + members + .entry((parent_fqn.to_string(), arg_name)) + .or_insert_with(|| SymbolId::new(SymbolKind::Field, path)); } } crate::Fields::None => {} } } -/// Assign IDs to enum variants and their fields -fn assign_enum_member_ids(e: &mut Enum, owner: &SymbolId) { - for variant in &mut e.variants { - if variant.id.is_unknown() { - let mut path = owner.path.clone(); - path.push(variant.name.clone()); - variant.id = SymbolId::new(SymbolKind::Variant, path); - } - - match &mut variant.fields { +/// Register enum variant and variant field IDs +fn register_enum_members( + e: &crate::Enum, + parent_fqn: &str, + parent_id: &SymbolId, + members: &mut HashMap<(String, String), SymbolId>, +) { + for variant in &e.variants { + let mut variant_path = parent_id.path.clone(); + variant_path.push(variant.name.clone()); + let variant_id = members + .entry((parent_fqn.to_string(), variant.name.clone())) + .or_insert_with(|| SymbolId::new(SymbolKind::Variant, variant_path.clone())) + .clone(); + + match &variant.fields { crate::Fields::Named(fields) => { for field in fields { - if field.id.is_unknown() { - let mut path = variant.id.path.clone(); - path.push(field.name.clone()); - field.id = SymbolId::new(SymbolKind::Field, path); - } + let mut field_path = variant_id.path.clone(); + field_path.push(field.name.clone()); + let variant_fqn = variant_id.qualified_name(); + members + .entry((variant_fqn, field.name.clone())) + .or_insert_with(|| SymbolId::new(SymbolKind::Field, field_path)); } } crate::Fields::Unnamed(fields) => { - for (i, field) in fields.iter_mut().enumerate() { - if field.id.is_unknown() { - let mut path = variant.id.path.clone(); - path.push(format!("arg{i:02}")); - field.id = SymbolId::new(SymbolKind::Field, path); - } + for (i, _field) in fields.iter().enumerate() { + let arg_name = format!("arg{i:02}"); + let mut field_path = variant_id.path.clone(); + field_path.push(arg_name.clone()); + let variant_fqn = variant_id.qualified_name(); + members + .entry((variant_fqn, arg_name)) + .or_insert_with(|| SymbolId::new(SymbolKind::Field, field_path)); } } crate::Fields::None => {} @@ -224,14 +213,65 @@ fn assign_enum_member_ids(e: &mut Enum, owner: &SymbolId) { } } +/// Re-register members with a new parent ID (used after disambiguation) +fn reregister_members( + ty: &Type, + new_parent_id: &SymbolId, + members: &mut HashMap<(String, String), SymbolId>, +) { + let parent_fqn = new_parent_id.qualified_name(); + match ty { + Type::Primitive(_) => {} + Type::Struct(s) => { + register_struct_members(s, &parent_fqn, new_parent_id, members); + } + Type::Enum(e) => { + register_enum_members(e, &parent_fqn, new_parent_id, members); + } + } +} + /// Split a fully-qualified name into path components fn split_path(fqn: &str) -> Vec { fqn.split("::").map(|s| s.to_string()).collect() } +impl SchemaIds { + /// Look up the ID for a type by its FQN + pub fn type_id(&self, fqn: &str) -> SymbolId { + self.types + .get(fqn) + .cloned() + .unwrap_or_else(|| SymbolId::new(infer_kind(fqn), split_path(fqn))) + } + + /// Look up the ID for a member (field or variant) by parent FQN and member name + pub fn member_id(&self, parent_fqn: &str, member_name: &str) -> SymbolId { + self.members + .get(&(parent_fqn.to_string(), member_name.to_string())) + .cloned() + .unwrap_or_else(|| { + let mut path = split_path(parent_fqn); + path.push(member_name.to_string()); + SymbolId::new(SymbolKind::Field, path) + }) + } +} + +fn infer_kind(fqn: &str) -> SymbolKind { + // Default to Struct for unknown types — the normalizer will + // assign the correct kind when it discovers the actual type + if fqn.starts_with("std::") || fqn.starts_with("chrono::") || fqn.starts_with("uuid::") { + SymbolKind::Primitive + } else { + SymbolKind::Struct + } +} + #[cfg(test)] mod tests { use super::*; + use crate::{Field, Fields, Variant}; #[test] fn test_split_path() { @@ -247,57 +287,58 @@ mod tests { } #[test] - fn test_is_unknown() { - let unknown = SymbolId::default(); - assert!(unknown.is_unknown()); - - let known = SymbolId::new(SymbolKind::Struct, vec!["MyType".to_string()]); - assert!(!known.is_unknown()); - } - - #[test] - fn test_pre_assigned_id_member_paths_consistent() { - // Regression: when a struct has a pre-assigned ID (e.g. from Struct::new), - // field IDs must use the struct's actual ID as their parent, not the - // seen-map ID which uses split_path and produces different path segments. - use crate::{Field, Fields}; - + fn test_build_schema_ids_basic() { let mut schema = Schema::new(); schema.name = "Test".to_string(); - // Struct::new("api::User") sets id.path = ["api::User"] (single unsplit element) let mut s = crate::Struct::new("api::User"); s.fields = Fields::Named(vec![Field::new("name".into(), "String".into())]); schema.input_types.insert_type(s.into()); - ensure_symbol_ids(&mut schema); + let ids = build_schema_ids(&schema); - let s = schema - .input_types - .get_type("api::User") - .unwrap() - .as_struct() - .unwrap(); + // Schema root ID should exist + assert_eq!(ids.schema_id.kind, SymbolKind::Schema); + assert!(ids.schema_id.path.contains(&"__schema__".to_string())); - // The struct's ID path should be preserved as-is - let struct_path = &s.id.path; + // Type ID should exist + let user_id = ids.type_id("api::User"); + assert_eq!(user_id.kind, SymbolKind::Struct); + assert_eq!(user_id.path, vec!["api", "User"]); - // The field's path should start with the struct's actual path - let field = s.fields().next().unwrap(); - let field_path = &field.id.path; - - assert_eq!( - &field_path[..field_path.len() - 1], - struct_path.as_slice(), - "Field path prefix {field_path:?} should match struct path {struct_path:?}" - ); + // Field ID should exist + let field_id = ids.member_id("api::User", "name"); + assert_eq!(field_id.kind, SymbolKind::Field); + assert_eq!(field_id.path, vec!["api", "User", "name"]); } #[test] - fn test_pre_assigned_id_enum_member_paths_consistent() { - // Same regression test for enums - use crate::Variant; + fn test_disambiguated_types() { + let mut schema = Schema::new(); + schema.name = "Test".to_string(); + // Input "Foo" has field "a" + let mut input_foo = crate::Struct::new("Foo"); + input_foo.fields = Fields::Named(vec![Field::new("a".into(), "u32".into())]); + schema.input_types.insert_type(input_foo.into()); + + // Output "Foo" has field "b" (different structure) + let mut output_foo = crate::Struct::new("Foo"); + output_foo.fields = Fields::Named(vec![Field::new("b".into(), "u64".into())]); + schema.output_types.insert_type(output_foo.into()); + + let ids = build_schema_ids(&schema); + + // Output Foo should be disambiguated + let output_id = ids + .types + .get("__output__::Foo") + .expect("disambiguated output type should exist"); + assert_eq!(output_id.disambiguator, 1); + } + + #[test] + fn test_enum_member_ids() { let mut schema = Schema::new(); schema.name = "Test".to_string(); @@ -305,154 +346,53 @@ mod tests { e.variants = vec![Variant::new("Active".into())]; schema.input_types.insert_type(e.into()); - ensure_symbol_ids(&mut schema); + let ids = build_schema_ids(&schema); - let e = schema - .input_types - .get_type("api::Status") - .unwrap() - .as_enum() - .unwrap(); + let enum_id = ids.type_id("api::Status"); + assert_eq!(enum_id.kind, SymbolKind::Enum); - let enum_path = &e.id.path; - let variant = &e.variants[0]; - let variant_path = &variant.id.path; - - assert_eq!( - &variant_path[..variant_path.len() - 1], - enum_path.as_slice(), - "Variant path prefix {variant_path:?} should match enum path {enum_path:?}" - ); + let variant_id = ids.member_id("api::Status", "Active"); + assert_eq!(variant_id.kind, SymbolKind::Variant); + assert_eq!(variant_id.path, vec!["api", "Status", "Active"]); } #[test] fn test_zero_padded_tuple_field_ordering() { - // Regression: unnamed (tuple) fields with >= 10 elements must use zero-padded - // indices so that lexicographic sorting preserves declaration order. - use crate::{Field, Fields}; - let mut schema = Schema::new(); schema.name = "Test".to_string(); let mut tuple_struct = crate::Struct::new("BigTuple"); - // Create 12 unnamed fields let fields: Vec = (0..12) .map(|i| Field::new(format!("{i}"), format!("u{}", 8 + i).into())) .collect(); tuple_struct.fields = Fields::Unnamed(fields); schema.input_types.insert_type(tuple_struct.into()); - ensure_symbol_ids(&mut schema); - - let s = schema - .input_types - .get_type("BigTuple") - .unwrap() - .as_struct() - .unwrap(); - - let field_ids: Vec = s - .fields() - .map(|f| f.id.path.last().unwrap().clone()) - .collect(); - - // Verify zero-padded format: arg00, arg01, ..., arg09, arg10, arg11 - assert_eq!(field_ids.len(), 12); - assert_eq!(field_ids[0], "arg00"); - assert_eq!(field_ids[1], "arg01"); - assert_eq!(field_ids[2], "arg02"); - assert_eq!(field_ids[9], "arg09"); - assert_eq!(field_ids[10], "arg10"); - assert_eq!(field_ids[11], "arg11"); - - // Verify lexicographic sort preserves declaration order: - // arg02 < arg10 (not arg10 < arg2 which would happen without padding) - let mut sorted_ids = field_ids.clone(); - sorted_ids.sort(); - assert_eq!( - field_ids, sorted_ids, - "Zero-padded field IDs should sort in declaration order" - ); - } - - #[test] - fn test_disambiguated_id_updates_member_ids() { - // Regression: when a type in the output typespace is disambiguated (because - // a type with the same name but different fields exists in the input typespace), - // the field IDs of the disambiguated type must reflect the new parent ID. - use crate::{Field, Fields}; - - let mut schema = Schema::new(); - schema.name = "Test".to_string(); - - // Input "Foo" has field "a" - let mut input_foo = crate::Struct::new("Foo"); - input_foo.fields = Fields::Named(vec![Field::new("a".into(), "u32".into())]); - schema.input_types.insert_type(input_foo.into()); - - // Output "Foo" has field "b" (different structure triggers disambiguation) - let mut output_foo = crate::Struct::new("Foo"); - output_foo.fields = Fields::Named(vec![Field::new("b".into(), "u64".into())]); - schema.output_types.insert_type(output_foo.into()); + let ids = build_schema_ids(&schema); - ensure_symbol_ids(&mut schema); + // Verify zero-padded format + let field0 = ids.member_id("BigTuple", "arg00"); + assert_eq!(field0.path.last().unwrap(), "arg00"); - // The output Foo should have disambiguator=1 - let output_struct = schema - .output_types - .get_type("Foo") - .unwrap() - .as_struct() - .unwrap(); - assert_eq!( - output_struct.id.disambiguator, 1, - "Output Foo should have disambiguator=1, got {:?}", - output_struct.id - ); - - // The field "b" should have a path consistent with the disambiguated parent - let field_b = output_struct.fields().next().unwrap(); - let field_path_prefix = &field_b.id.path[..field_b.id.path.len() - 1]; - assert_eq!( - field_path_prefix, - output_struct.id.path.as_slice(), - "Field path prefix {:?} should match disambiguated parent path {:?}", - field_b.id.path, - output_struct.id.path - ); + let field10 = ids.member_id("BigTuple", "arg10"); + assert_eq!(field10.path.last().unwrap(), "arg10"); } #[test] fn test_schema_root_id_does_not_collide_with_type() { - // Regression: a schema named "User" with a struct also named "User" - // should not produce colliding IDs. The schema root uses the "__schema__" - // sentinel in its path to avoid this. let mut schema = Schema::new(); schema.name = "User".to_string(); let user_struct = crate::Struct::new("User"); schema.input_types.insert_type(user_struct.into()); - ensure_symbol_ids(&mut schema); - - let struct_type = schema - .input_types - .get_type("User") - .unwrap() - .as_struct() - .unwrap(); + let ids = build_schema_ids(&schema); + let struct_id = ids.type_id("User"); assert_ne!( - schema.id, struct_type.id, - "Schema root ID {:?} should not collide with struct ID {:?}", - schema.id, struct_type.id - ); - - // Verify the schema root uses the __schema__ sentinel - assert!( - schema.id.path.contains(&"__schema__".to_string()), - "Schema root ID path should contain '__schema__' sentinel, got {:?}", - schema.id.path + ids.schema_id, struct_id, + "Schema root ID should not collide with struct ID" ); + assert!(ids.schema_id.path.contains(&"__schema__".to_string())); } } diff --git a/reflectapi-schema/src/lib.rs b/reflectapi-schema/src/lib.rs index 70f9f4a8..6e556b7a 100644 --- a/reflectapi-schema/src/lib.rs +++ b/reflectapi-schema/src/lib.rs @@ -9,7 +9,7 @@ mod symbol; mod visit; pub use self::codegen::*; -pub use self::ids::ensure_symbol_ids; +pub use self::ids::{build_schema_ids, SchemaIds}; pub use self::normalize::{ CircularDependencyResolutionStage, Consolidation, Naming, NamingResolutionStage, NormalizationError, NormalizationPipeline, NormalizationStage, Normalizer, PipelineBuilder, @@ -36,9 +36,6 @@ use std::{ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Schema { - #[serde(skip_serializing, default)] - pub id: SymbolId, - pub name: String, #[serde(skip_serializing_if = "String::is_empty", default)] @@ -63,7 +60,6 @@ impl Default for Schema { impl Schema { pub fn new() -> Self { Schema { - id: SymbolId::default(), name: String::new(), description: String::new(), functions: Vec::new(), @@ -102,7 +98,6 @@ impl Schema { pub fn extend(&mut self, other: Self) { let Self { - id: _, functions, input_types, output_types, @@ -421,9 +416,6 @@ impl Typespace { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Function { - #[serde(skip_serializing, default)] - pub id: SymbolId, - /// Includes entity and action, for example: users.login pub name: String, /// URL mounting path, for example: /api/v1 @@ -469,9 +461,7 @@ pub struct Function { impl Function { pub fn new(name: String) -> Self { - let id = SymbolId::endpoint_id(vec![name.clone()]); Function { - id, name, deprecation_note: Default::default(), path: Default::default(), @@ -746,11 +736,8 @@ impl Type { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] pub struct Primitive { - #[serde(skip_serializing, default)] - pub id: SymbolId, - pub name: String, #[serde(skip_serializing_if = "String::is_empty", default)] pub description: String, @@ -762,6 +749,12 @@ pub struct Primitive { /// Fallback type to use when the type is not supported by the target language #[serde(skip_serializing_if = "Option::is_none", default)] pub fallback: Option, + + #[serde( + skip_serializing_if = "LanguageSpecificTypeCodegenConfig::is_serialization_default", + default + )] + pub codegen_config: LanguageSpecificTypeCodegenConfig, } impl Primitive { @@ -771,13 +764,12 @@ impl Primitive { parameters: Vec, fallback: Option, ) -> Self { - let id = SymbolId::new(SymbolKind::Primitive, vec![name.clone()]); Primitive { - id, name, description, parameters, fallback, + codegen_config: Default::default(), } } @@ -854,35 +846,14 @@ impl Primitive { } } -impl PartialEq for Primitive { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.description == other.description - && self.parameters == other.parameters - && self.fallback == other.fallback - } -} - -impl std::hash::Hash for Primitive { - fn hash(&self, state: &mut H) { - self.name.hash(state); - self.description.hash(state); - self.parameters.hash(state); - self.fallback.hash(state); - } -} - impl From for Type { fn from(val: Primitive) -> Self { Type::Primitive(val) } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct Struct { - #[serde(skip_serializing, default)] - pub id: SymbolId, - /// Name of a struct, should be a valid Rust struct name identifier pub name: String, @@ -905,16 +876,17 @@ pub struct Struct { #[serde(skip_serializing_if = "is_false", default)] pub transparent: bool, - #[serde(skip_serializing_if = "is_default", default)] + #[serde( + skip_serializing_if = "LanguageSpecificTypeCodegenConfig::is_serialization_default", + default + )] pub codegen_config: LanguageSpecificTypeCodegenConfig, } impl Struct { pub fn new(name: impl Into) -> Self { let name = name.into(); - let id = SymbolId::struct_id(vec![name.clone()]); Struct { - id, name, serde_name: Default::default(), description: Default::default(), @@ -985,18 +957,6 @@ impl Struct { } } -impl PartialEq for Struct { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.serde_name == other.serde_name - && self.description == other.description - && self.parameters == other.parameters - && self.fields == other.fields - && self.transparent == other.transparent - && self.codegen_config == other.codegen_config - } -} - impl From for Type { fn from(val: Struct) -> Self { Type::Struct(val) @@ -1078,9 +1038,6 @@ impl IntoIterator for Fields { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq)] pub struct Field { - #[serde(skip_serializing, default)] - pub id: SymbolId, - /// Field name, should be a valid Rust field name identifier pub name: String, /// If a serialized name is not a valid Rust field name identifier @@ -1134,7 +1091,6 @@ impl PartialEq for Field { fn eq( &self, Self { - id: _, name, serde_name, description, @@ -1173,7 +1129,6 @@ impl std::hash::Hash for Field { impl Field { pub fn new(name: String, type_ref: TypeReference) -> Self { Field { - id: SymbolId::default(), name, type_ref, serde_name: Default::default(), @@ -1244,15 +1199,8 @@ fn is_false(b: &bool) -> bool { !*b } -fn is_default(t: &T) -> bool { - *t == Default::default() -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct Enum { - #[serde(skip_serializing, default)] - pub id: SymbolId, - pub name: String, #[serde(skip_serializing_if = "String::is_empty", default)] pub serde_name: String, @@ -1269,15 +1217,16 @@ pub struct Enum { #[serde(skip_serializing_if = "Vec::is_empty", default)] pub variants: Vec, - #[serde(skip_serializing_if = "is_default", default)] + #[serde( + skip_serializing_if = "LanguageSpecificTypeCodegenConfig::is_serialization_default", + default + )] pub codegen_config: LanguageSpecificTypeCodegenConfig, } impl Enum { pub fn new(name: String) -> Self { - let id = SymbolId::enum_id(vec![name.clone()]); Enum { - id, name, serde_name: Default::default(), description: Default::default(), @@ -1317,29 +1266,14 @@ impl Enum { } } -impl PartialEq for Enum { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.serde_name == other.serde_name - && self.description == other.description - && self.parameters == other.parameters - && self.representation == other.representation - && self.variants == other.variants - && self.codegen_config == other.codegen_config - } -} - impl From for Type { fn from(val: Enum) -> Self { Type::Enum(val) } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct Variant { - #[serde(skip_serializing, default)] - pub id: SymbolId, - pub name: String, #[serde(skip_serializing_if = "String::is_empty", default)] pub serde_name: String, @@ -1358,7 +1292,6 @@ pub struct Variant { impl Variant { pub fn new(name: String) -> Self { Variant { - id: SymbolId::default(), name, serde_name: String::new(), description: String::new(), @@ -1397,17 +1330,6 @@ impl Variant { } } -impl PartialEq for Variant { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.serde_name == other.serde_name - && self.description == other.description - && self.fields == other.fields - && self.discriminant == other.discriminant - && self.untagged == other.untagged - } -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash, Default)] #[serde(rename_all = "snake_case")] pub enum Representation { diff --git a/reflectapi-schema/src/normalize.rs b/reflectapi-schema/src/normalize.rs index f8ded076..58a7ae21 100644 --- a/reflectapi-schema/src/normalize.rs +++ b/reflectapi-schema/src/normalize.rs @@ -314,20 +314,10 @@ fn extract_simple_name(qualified_name: &str) -> String { } fn rename_type(ty: &mut Type, new_name: &str) { - let new_path: Vec = new_name.split("::").map(|s| s.to_string()).collect(); match ty { - Type::Struct(s) => { - s.name = new_name.to_string(); - s.id.path = new_path; - } - Type::Enum(e) => { - e.name = new_name.to_string(); - e.id.path = new_path; - } - Type::Primitive(p) => { - p.name = new_name.to_string(); - p.id.path = new_path; - } + Type::Struct(s) => s.name = new_name.to_string(), + Type::Enum(e) => e.name = new_name.to_string(), + Type::Primitive(p) => p.name = new_name.to_string(), } } @@ -521,7 +511,7 @@ pub enum ResolutionStrategy { /// Try boxing first, then forward declarations #[default] Intelligent, - /// Always use Box for self-references + /// Always use `Box` for self-references Boxing, /// Always use forward declarations ForwardDeclarations, @@ -879,11 +869,13 @@ impl std::error::Error for NormalizationError {} #[derive(Debug)] struct NormalizationContext { symbol_table: SymbolTable, - raw_types: HashMap, - raw_functions: HashMap, + raw_types: HashMap, + raw_functions: HashMap, resolution_cache: HashMap, generic_scope: BTreeSet, errors: Vec, + /// Compiler-owned ID table, populated in Phase 0 + schema_ids: Option, } impl Default for NormalizationContext { @@ -901,6 +893,7 @@ impl NormalizationContext { resolution_cache: HashMap::new(), generic_scope: BTreeSet::new(), errors: Vec::new(), + schema_ids: None, } } @@ -942,8 +935,9 @@ impl Normalizer { // Clone so that pipeline stages can mutate without affecting the caller let mut schema = schema.clone(); - // Phase 0: Ensure all symbols have unique, stable IDs - crate::ids::ensure_symbol_ids(&mut schema); + // Phase 0: Build compiler-owned ID table (side table, not on raw schema) + let schema_ids = crate::ids::build_schema_ids(&schema); + self.context.schema_ids = Some(schema_ids); // Capture original type names BEFORE the pipeline transforms them. // NamingResolution (if present in the pipeline) will strip module @@ -989,21 +983,34 @@ impl Normalizer { } fn discover_symbols(&mut self, schema: &Schema) -> Result<(), Vec> { + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set before discovery"); + let schema_info = SymbolInfo { - id: schema.id.clone(), + id: ids.schema_id.clone(), name: schema.name.clone(), - path: schema.id.path.clone(), - kind: SymbolKind::Struct, + path: ids.schema_id.path.clone(), + kind: SymbolKind::Schema, resolved: false, dependencies: BTreeSet::new(), }; self.context.symbol_table.register(schema_info); for function in &schema.functions { + let func_id = ids + .functions + .get(&function.name) + .cloned() + .unwrap_or_else(|| { + SymbolId::new(SymbolKind::Endpoint, vec![function.name.clone()]) + }); let function_info = SymbolInfo { - id: function.id.clone(), + id: func_id.clone(), name: function.name.clone(), - path: function.id.path.clone(), + path: func_id.path.clone(), kind: SymbolKind::Endpoint, resolved: false, dependencies: BTreeSet::new(), @@ -1011,7 +1018,7 @@ impl Normalizer { self.context.symbol_table.register(function_info); self.context .raw_functions - .insert(function.id.clone(), function.clone()); + .insert(function.name.clone(), function.clone()); } self.discover_types_from_typespace(&schema.input_types); @@ -1031,39 +1038,50 @@ impl Normalizer { } fn discover_type_symbols(&mut self, ty: &Type) { - let (id, name, kind) = match ty { - Type::Primitive(p) => (p.id.clone(), p.name.clone(), SymbolKind::Primitive), - Type::Struct(s) => (s.id.clone(), s.name.clone(), SymbolKind::Struct), - Type::Enum(e) => (e.id.clone(), e.name.clone(), SymbolKind::Enum), + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); + let name = ty.name().to_string(); + let kind = match ty { + Type::Primitive(_) => SymbolKind::Primitive, + Type::Struct(_) => SymbolKind::Struct, + Type::Enum(_) => SymbolKind::Enum, }; - - let path = id.path.clone(); + let id = ids.type_id(&name); let symbol_info = SymbolInfo { id: id.clone(), - name, - path, + name: name.clone(), + path: id.path.clone(), kind, resolved: false, dependencies: BTreeSet::new(), }; self.context.symbol_table.register(symbol_info); - self.context.raw_types.insert(id, ty.clone()); + self.context.raw_types.insert(name.clone(), ty.clone()); match ty { - Type::Struct(s) => self.discover_struct_symbols(s), - Type::Enum(e) => self.discover_enum_symbols(e), + Type::Struct(s) => self.discover_struct_symbols(&name, s), + Type::Enum(e) => self.discover_enum_symbols(&name, e), Type::Primitive(_) => {} } } - fn discover_struct_symbols(&mut self, strukt: &Struct) { + fn discover_struct_symbols(&mut self, parent_fqn: &str, strukt: &Struct) { + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); for field in strukt.fields() { + let field_id = ids.member_id(parent_fqn, &field.name); let field_info = SymbolInfo { - id: field.id.clone(), + id: field_id.clone(), name: field.name.clone(), - path: field.id.path.clone(), + path: field_id.path.clone(), kind: SymbolKind::Field, resolved: false, dependencies: BTreeSet::new(), @@ -1072,23 +1090,31 @@ impl Normalizer { } } - fn discover_enum_symbols(&mut self, enm: &Enum) { + fn discover_enum_symbols(&mut self, parent_fqn: &str, enm: &Enum) { + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); for variant in enm.variants() { + let variant_id = ids.member_id(parent_fqn, &variant.name); let variant_info = SymbolInfo { - id: variant.id.clone(), + id: variant_id.clone(), name: variant.name.clone(), - path: variant.id.path.clone(), + path: variant_id.path.clone(), kind: SymbolKind::Variant, resolved: false, dependencies: BTreeSet::new(), }; self.context.symbol_table.register(variant_info); + let variant_fqn = variant_id.qualified_name(); for field in variant.fields() { + let field_id = ids.member_id(&variant_fqn, &field.name); let field_info = SymbolInfo { - id: field.id.clone(), + id: field_id.clone(), name: field.name.clone(), - path: field.id.path.clone(), + path: field_id.path.clone(), kind: SymbolKind::Field, resolved: false, dependencies: BTreeSet::new(), @@ -1123,12 +1149,23 @@ impl Normalizer { self.add_stdlib_types_to_cache(); - for (function_id, function) in &self.context.raw_functions.clone() { - self.resolve_function_references(function_id, function); + // Clone the ID lookups upfront to avoid borrow conflict with &mut self + let ids = self + .context + .schema_ids + .clone() + .expect("schema_ids must be set"); + for (func_name, function) in &self.context.raw_functions.clone() { + let func_id = + ids.functions.get(func_name).cloned().unwrap_or_else(|| { + SymbolId::new(SymbolKind::Endpoint, vec![func_name.clone()]) + }); + self.resolve_function_references(&func_id, function); } - for (type_id, ty) in &self.context.raw_types.clone() { - self.resolve_type_references(type_id, ty); + for (type_name, ty) in &self.context.raw_types.clone() { + let type_id = ids.type_id(type_name); + self.resolve_type_references(&type_id, ty); } if self.context.has_errors() { @@ -1244,6 +1281,11 @@ impl Normalizer { schema: &Schema, original_names: &HashMap, ) -> Result> { + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); let mut semantic_types = BTreeMap::new(); let mut semantic_functions = BTreeMap::new(); @@ -1253,19 +1295,26 @@ impl Normalizer { }; for symbol_id in sorted_symbols { - if let Some(raw_type) = self.context.raw_types.get(&symbol_id) { - let semantic_type = self.build_semantic_type(raw_type, original_names)?; - semantic_types.insert(symbol_id, semantic_type); + // Look up the type by the symbol's name (raw_types keyed by name) + if let Some(symbol_info) = self.context.symbol_table.symbols.get(&symbol_id) { + if let Some(raw_type) = self.context.raw_types.get(&symbol_info.name) { + let semantic_type = self.build_semantic_type(raw_type, original_names)?; + semantic_types.insert(symbol_id, semantic_type); + } } } - for (function_id, raw_function) in &self.context.raw_functions { + for (func_name, raw_function) in &self.context.raw_functions { + let func_id = + ids.functions.get(func_name).cloned().unwrap_or_else(|| { + SymbolId::new(SymbolKind::Endpoint, vec![func_name.clone()]) + }); let semantic_function = self.build_semantic_function(raw_function)?; - semantic_functions.insert(function_id.clone(), semantic_function); + semantic_functions.insert(func_id, semantic_function); } Ok(SemanticSchema { - id: schema.id.clone(), + id: ids.schema_id.clone(), name: schema.name.clone(), description: schema.description.clone(), functions: semantic_functions, @@ -1307,8 +1356,13 @@ impl Normalizer { .cloned() .unwrap_or_else(|| primitive.name.clone()); + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); Ok(SemanticPrimitive { - id: primitive.id.clone(), + id: ids.type_id(&primitive.name), name: primitive.name.clone(), original_name, description: primitive.description.clone(), @@ -1323,6 +1377,7 @@ impl Normalizer { }) .collect(), fallback, + codegen_config: primitive.codegen_config.clone(), }) } @@ -1331,11 +1386,17 @@ impl Normalizer { strukt: &Struct, original_names: &HashMap, ) -> Result> { + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); let mut fields = BTreeMap::new(); for field in strukt.fields() { - let semantic_field = self.build_semantic_field(field)?; - fields.insert(field.id.clone(), semantic_field); + let field_id = ids.member_id(&strukt.name, &field.name); + let semantic_field = self.build_semantic_field(&strukt.name, field)?; + fields.insert(field_id, semantic_field); } let original_name = original_names @@ -1344,7 +1405,7 @@ impl Normalizer { .unwrap_or_else(|| strukt.name.clone()); Ok(SemanticStruct { - id: strukt.id.clone(), + id: ids.type_id(&strukt.name), name: strukt.name.clone(), original_name, serde_name: strukt.serde_name.clone(), @@ -1372,11 +1433,17 @@ impl Normalizer { enm: &Enum, original_names: &HashMap, ) -> Result> { + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); let mut variants = BTreeMap::new(); for variant in enm.variants() { - let semantic_variant = self.build_semantic_variant(variant)?; - variants.insert(variant.id.clone(), semantic_variant); + let variant_id = ids.member_id(&enm.name, &variant.name); + let semantic_variant = self.build_semantic_variant(&enm.name, variant)?; + variants.insert(variant_id, semantic_variant); } let original_name = original_names @@ -1385,7 +1452,7 @@ impl Normalizer { .unwrap_or_else(|| enm.name.clone()); Ok(SemanticEnum { - id: enm.id.clone(), + id: ids.type_id(&enm.name), name: enm.name.clone(), original_name, serde_name: enm.serde_name.clone(), @@ -1408,12 +1475,18 @@ impl Normalizer { fn build_semantic_field( &self, + parent_fqn: &str, field: &Field, ) -> Result> { + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); let resolved_type_ref = self.build_resolved_type_reference(&field.type_ref)?; Ok(SemanticField { - id: field.id.clone(), + id: ids.member_id(parent_fqn, &field.name), name: field.name.clone(), serde_name: field.serde_name.clone(), description: field.description.clone(), @@ -1427,13 +1500,22 @@ impl Normalizer { fn build_semantic_variant( &self, + enum_fqn: &str, variant: &Variant, ) -> Result> { + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); + let variant_id = ids.member_id(enum_fqn, &variant.name); + let variant_fqn = variant_id.qualified_name(); let mut fields = BTreeMap::new(); for field in variant.fields() { - let semantic_field = self.build_semantic_field(field)?; - fields.insert(field.id.clone(), semantic_field); + let field_id = ids.member_id(&variant_fqn, &field.name); + let semantic_field = self.build_semantic_field(&variant_fqn, field)?; + fields.insert(field_id, semantic_field); } let field_style = match &variant.fields { @@ -1443,7 +1525,7 @@ impl Normalizer { }; Ok(SemanticVariant { - id: variant.id.clone(), + id: variant_id, name: variant.name.clone(), serde_name: variant.serde_name.clone(), description: variant.description.clone(), @@ -1475,8 +1557,19 @@ impl Normalizer { .as_ref() .and_then(|tr| self.resolve_global_type_reference(&tr.name)); + let ids = self + .context + .schema_ids + .as_ref() + .expect("schema_ids must be set"); Ok(SemanticFunction { - id: function.id.clone(), + id: ids + .functions + .get(&function.name) + .cloned() + .unwrap_or_else(|| { + SymbolId::new(SymbolKind::Endpoint, vec![function.name.clone()]) + }), name: function.name.clone(), path: function.path.clone(), description: function.description.clone(), @@ -1707,7 +1800,7 @@ mod tests { } #[test] - fn test_ensure_symbol_ids_idempotent() { + fn test_build_schema_ids_idempotent() { let mut schema = Schema::new(); schema.name = "Test".to_string(); @@ -1715,35 +1808,20 @@ mod tests { user_struct.fields = Fields::Named(vec![Field::new("id".into(), "u64".into())]); schema.input_types.insert_type(user_struct.into()); - // Run twice - crate::ensure_symbol_ids(&mut schema); - let ids_first: Vec<_> = schema - .input_types - .types() - .map(|t| match t { - Type::Struct(s) => s.id.clone(), - _ => unreachable!(), - }) - .collect(); - - crate::ensure_symbol_ids(&mut schema); - let ids_second: Vec<_> = schema - .input_types - .types() - .map(|t| match t { - Type::Struct(s) => s.id.clone(), - _ => unreachable!(), - }) - .collect(); + // Run twice — should produce identical results + let ids_first = crate::build_schema_ids(&schema); + let ids_second = crate::build_schema_ids(&schema); + let user_id_first = ids_first.type_id("User"); + let user_id_second = ids_second.type_id("User"); assert_eq!( - ids_first, ids_second, - "ensure_symbol_ids should be idempotent" + user_id_first, user_id_second, + "build_schema_ids should be idempotent" ); } #[test] - fn test_ensure_symbol_ids_enum_variants_and_fields() { + fn test_build_schema_ids_enum_variants_and_fields() { let mut schema = Schema::new(); schema.name = "Test".to_string(); @@ -1756,40 +1834,37 @@ mod tests { enm.variants = vec![variant, Variant::new("Inactive".into())]; schema.input_types.insert_type(enm.into()); - crate::ensure_symbol_ids(&mut schema); + let ids = crate::build_schema_ids(&schema); - let enm = schema - .input_types - .get_type("Status") - .unwrap() - .as_enum() - .unwrap(); - assert!(!enm.id.is_unknown(), "Enum should have a non-unknown id"); - - for variant in &enm.variants { - assert!( - !variant.id.is_unknown(), - "Variant '{}' should have a non-unknown id", - variant.name - ); - for field in variant.fields() { - assert!( - !field.id.is_unknown(), - "Field '{}' in variant '{}' should have a non-unknown id", - field.name, - variant.name - ); - } - } + let enum_id = ids.type_id("Status"); + assert!(!enum_id.is_unknown(), "Enum should have a non-unknown id"); + + let active_id = ids.member_id("Status", "Active"); + assert!( + !active_id.is_unknown(), + "Variant 'Active' should have a non-unknown id" + ); + + let inactive_id = ids.member_id("Status", "Inactive"); + assert!( + !inactive_id.is_unknown(), + "Variant 'Inactive' should have a non-unknown id" + ); + + // Check variant field ID + let active_fqn = active_id.qualified_name(); + let since_id = ids.member_id(&active_fqn, "since"); + assert!( + !since_id.is_unknown(), + "Field 'since' should have a non-unknown id" + ); // Check paths are structured correctly - let active = &enm.variants[0]; - assert_eq!(active.id.path.last().unwrap(), "Active"); - let since_field = active.fields().next().unwrap(); + assert_eq!(active_id.path.last().unwrap(), "Active"); assert!( - since_field.id.path.contains(&"Active".to_string()), + since_id.path.contains(&"Active".to_string()), "Field path should include parent variant: {:?}", - since_field.id.path + since_id.path ); } diff --git a/reflectapi-schema/src/semantic.rs b/reflectapi-schema/src/semantic.rs index 48ffa7dc..7b194dad 100644 --- a/reflectapi-schema/src/semantic.rs +++ b/reflectapi-schema/src/semantic.rs @@ -66,6 +66,9 @@ pub struct SemanticPrimitive { /// Resolved fallback type reference pub fallback: Option, + + /// Language-specific configuration + pub codegen_config: crate::LanguageSpecificTypeCodegenConfig, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/reflectapi-schema/src/symbol.rs b/reflectapi-schema/src/symbol.rs index d7e40d19..95efb0c0 100644 --- a/reflectapi-schema/src/symbol.rs +++ b/reflectapi-schema/src/symbol.rs @@ -12,6 +12,7 @@ pub struct SymbolId { Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] pub enum SymbolKind { + Schema, Struct, Enum, TypeAlias, @@ -58,7 +59,7 @@ pub const STDLIB_TYPE_PREFIXES: &[&str] = &["std::", "chrono::", "uuid::"]; impl Default for SymbolId { fn default() -> Self { Self { - kind: SymbolKind::Struct, + kind: SymbolKind::Schema, path: vec!["unknown".to_string()], disambiguator: 0, } @@ -94,30 +95,6 @@ impl SymbolId { Self::new(SymbolKind::Struct, path) } - /// Create a symbol ID for an enum - pub fn enum_id(path: Vec) -> Self { - Self::new(SymbolKind::Enum, path) - } - - /// Create a symbol ID for an endpoint/function - pub fn endpoint_id(path: Vec) -> Self { - Self::new(SymbolKind::Endpoint, path) - } - - /// Create a symbol ID for a variant - pub fn variant_id(enum_path: Vec, variant_name: String) -> Self { - let mut path = enum_path; - path.push(variant_name); - Self::new(SymbolKind::Variant, path) - } - - /// Create a symbol ID for a field - pub fn field_id(parent_path: Vec, field_name: String) -> Self { - let mut path = parent_path; - path.push(field_name); - Self::new(SymbolKind::Field, path) - } - /// Get the simple name (last component of path) pub fn name(&self) -> Option<&str> { self.path.last().map(|s| s.as_str()) diff --git a/reflectapi/src/builder/handler.rs b/reflectapi/src/builder/handler.rs index 8a23d873..290f678e 100644 --- a/reflectapi/src/builder/handler.rs +++ b/reflectapi/src/builder/handler.rs @@ -144,7 +144,6 @@ where .unwrap_or_default(); let function_def = Function { - id: Default::default(), name: rb.name.clone(), path: rb.path.clone(), deprecation_note: rb.deprecation_note, diff --git a/reflectapi/src/codegen/openapi.rs b/reflectapi/src/codegen/openapi.rs index e682061c..612174df 100644 --- a/reflectapi/src/codegen/openapi.rs +++ b/reflectapi/src/codegen/openapi.rs @@ -601,11 +601,11 @@ impl Converter<'_> { static STRING_TYPE: OnceLock = OnceLock::new(); return STRING_TYPE.get_or_init(|| { crate::Type::Primitive(crate::Primitive { - id: Default::default(), name: "std::string::String".into(), description: "UTF-8 encoded string".into(), parameters: vec![], fallback: None, + codegen_config: Default::default(), }) }); }; @@ -812,7 +812,6 @@ impl Converter<'_> { crate::TypeReference::new(variant.name().to_owned(), type_ref.arguments.clone()); let mut strukt = crate::Struct { - id: Default::default(), name: variant.name().to_owned(), serde_name: variant.serde_name.to_owned(), description: variant.description().to_owned(), diff --git a/reflectapi/src/codegen/python.rs b/reflectapi/src/codegen/python.rs index a72f4f2b..9bedf3d1 100644 --- a/reflectapi/src/codegen/python.rs +++ b/reflectapi/src/codegen/python.rs @@ -1,4 +1,5 @@ -use std::collections::{BTreeMap, BTreeSet, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::sync::LazyLock; use crate::{Schema, TypeReference}; use reflectapi_schema::{Function, Type}; @@ -30,6 +31,9 @@ pub struct Config { pub generate_sync: bool, /// Whether to generate testing utilities pub generate_testing: bool, + /// Attempt to format the generated code with ruff. Will fall back to basic + /// formatting if ruff is not available. + pub format: bool, /// Base URL for the API (optional) pub base_url: Option, } @@ -41,53 +45,299 @@ impl Default for Config { generate_async: true, generate_sync: true, generate_testing: false, + format: true, base_url: None, } } } -/// Generate optimized imports with proper sorting and deduplication -fn generate_optimized_imports(imports: &templates::Imports) -> String { - use std::collections::BTreeSet; +#[derive(Default)] +struct PythonMetadataUsage { + stdlib_imports: BTreeSet, + runtime_imports: BTreeSet, + runtime_provided_types: BTreeSet, +} - let mut stdlib_imports = BTreeSet::new(); - let mut typing_imports = BTreeSet::new(); - let mut third_party_imports = BTreeSet::new(); - let mut runtime_imports = BTreeSet::new(); +/// Resolve Python field optionality: determines whether a field needs a default +/// value of `None` and whether `| None` should be appended to its type hint. +fn resolve_field_optionality( + type_name: &str, + field_type: String, + required: bool, +) -> (bool, Option, String) { + let is_option_type = type_name == "std::option::Option" || type_name == "reflectapi::Option"; + if !required { + if is_option_type { + (true, Some("None".to_string()), field_type) + } else { + ( + true, + Some("None".to_string()), + format!("{field_type} | None"), + ) + } + } else if is_option_type { + (true, Some("None".to_string()), field_type) + } else { + (false, None, field_type) + } +} + +struct PythonTypeMapping { + type_hint: &'static str, + imports: &'static [&'static str], + runtime_imports: &'static [&'static str], + provided_by_runtime: bool, + ignore_type_arguments: bool, +} + +impl PythonTypeMapping { + const fn simple(type_hint: &'static str) -> Self { + Self { + type_hint, + imports: &[], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: false, + } + } +} + +fn python_type_mappings() -> &'static HashMap<&'static str, PythonTypeMapping> { + static MAPPINGS: LazyLock> = LazyLock::new(|| { + let mut m = HashMap::new(); + + // Integer types + for name in [ + "i8", + "i16", + "i32", + "i64", + "u8", + "u16", + "u32", + "u64", + "isize", + "usize", + "std::num::NonZeroU8", + "std::num::NonZeroU16", + "std::num::NonZeroU32", + "std::num::NonZeroU64", + "std::num::NonZeroU128", + ] { + m.insert(name, PythonTypeMapping::simple("int")); + } + + // Float types + m.insert("f32", PythonTypeMapping::simple("float")); + m.insert("f64", PythonTypeMapping::simple("float")); + + // Bool + m.insert("bool", PythonTypeMapping::simple("bool")); + + // String types + for name in [ + "String", + "std::string::String", + "url::Url", + "rust_decimal::Decimal", + "chrono_tz::Tz", + ] { + m.insert(name, PythonTypeMapping::simple("str")); + } + + // Collections + m.insert("std::vec::Vec", PythonTypeMapping::simple("list[T]")); + for name in [ + "std::collections::HashMap", + "std::collections::BTreeMap", + "indexmap::IndexMap", + ] { + m.insert(name, PythonTypeMapping::simple("dict[K, V]")); + } + + // Option / Result + m.insert("std::option::Option", PythonTypeMapping::simple("T | None")); + m.insert("std::result::Result", PythonTypeMapping::simple("T | E")); + + // ReflectAPI runtime types + m.insert( + "reflectapi::Option", + PythonTypeMapping { + type_hint: "ReflectapiOption[T]", + imports: &[], + runtime_imports: &["ReflectapiOption"], + provided_by_runtime: true, + ignore_type_arguments: false, + }, + ); + m.insert( + "reflectapi::Empty", + PythonTypeMapping { + type_hint: "ReflectapiEmpty", + imports: &[], + runtime_imports: &["ReflectapiEmpty"], + provided_by_runtime: true, + ignore_type_arguments: false, + }, + ); + m.insert( + "reflectapi::Infallible", + PythonTypeMapping { + type_hint: "ReflectapiInfallible", + imports: &[], + runtime_imports: &["ReflectapiInfallible"], + provided_by_runtime: true, + ignore_type_arguments: false, + }, + ); + + // Date/time types + for name in ["chrono::DateTime", "chrono::NaiveDateTime"] { + m.insert( + name, + PythonTypeMapping { + type_hint: "datetime", + imports: &["from datetime import datetime"], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: true, + }, + ); + } + m.insert( + "chrono::NaiveDate", + PythonTypeMapping { + type_hint: "date", + imports: &["from datetime import date"], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: false, + }, + ); + m.insert( + "uuid::Uuid", + PythonTypeMapping { + type_hint: "UUID", + imports: &["from uuid import UUID"], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: false, + }, + ); + m.insert( + "std::time::Duration", + PythonTypeMapping { + type_hint: "timedelta", + imports: &["from datetime import timedelta"], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: false, + }, + ); + + // Special types + m.insert("std::tuple::Tuple0", PythonTypeMapping::simple("None")); + m.insert("serde_json::Value", PythonTypeMapping::simple("Any")); + + // Wrapper types (transparent) + for name in ["std::boxed::Box", "std::sync::Arc", "std::rc::Rc"] { + m.insert(name, PythonTypeMapping::simple("T")); + } + + // Path types + for name in ["std::path::PathBuf", "std::path::Path"] { + m.insert( + name, + PythonTypeMapping { + type_hint: "Path", + imports: &["from pathlib import Path"], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: false, + }, + ); + } + + // Network types + m.insert( + "std::net::IpAddr", + PythonTypeMapping { + type_hint: "IPv4Address | IPv6Address", + imports: &["from ipaddress import IPv4Address, IPv6Address"], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: false, + }, + ); + m.insert( + "std::net::Ipv4Addr", + PythonTypeMapping { + type_hint: "IPv4Address", + imports: &["from ipaddress import IPv4Address"], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: false, + }, + ); + m.insert( + "std::net::Ipv6Addr", + PythonTypeMapping { + type_hint: "IPv6Address", + imports: &["from ipaddress import IPv6Address"], + runtime_imports: &[], + provided_by_runtime: false, + ignore_type_arguments: false, + }, + ); + + m + }); + &MAPPINGS +} + +fn collect_python_metadata_usage(all_type_names: &[String]) -> PythonMetadataUsage { + let mut usage = PythonMetadataUsage::default(); + let mappings = python_type_mappings(); + + for type_name in all_type_names { + let Some(mapping) = mappings.get(type_name.as_str()) else { + continue; + }; - // Standard library - datetime - if imports.has_datetime || imports.has_date || imports.has_timedelta { - let mut datetime_parts = vec![]; - if imports.has_datetime { - datetime_parts.push("datetime"); + for import in mapping.imports { + usage.stdlib_imports.insert((*import).to_string()); } - if imports.has_date { - datetime_parts.push("date"); + for import in mapping.runtime_imports { + usage.runtime_imports.insert((*import).to_string()); } - if imports.has_timedelta { - datetime_parts.push("timedelta"); + if mapping.provided_by_runtime { + usage.runtime_provided_types.insert(type_name.clone()); } - stdlib_imports.insert(format!( - "from datetime import {}", - datetime_parts.join(", ") - )); } + usage +} + +/// Generate optimized imports with proper sorting and deduplication +fn generate_optimized_imports(imports: &templates::Imports) -> String { + let mut stdlib_imports = BTreeSet::new(); + let mut typing_imports = BTreeSet::new(); + let mut third_party_imports = BTreeSet::new(); + let mut runtime_imports: BTreeSet = BTreeSet::new(); + // Standard library - enum if imports.has_enums { stdlib_imports.insert("from enum import Enum".to_string()); } - // Standard library - uuid - if imports.has_uuid { - stdlib_imports.insert("from uuid import UUID".to_string()); - } - // Standard library - warnings if imports.has_warnings { stdlib_imports.insert("import warnings".to_string()); } + stdlib_imports.extend(imports.extra_stdlib_imports.iter().cloned()); + // Typing imports - always include base ones typing_imports.insert("Any"); typing_imports.insert("Optional"); @@ -116,38 +366,54 @@ fn generate_optimized_imports(imports: &templates::Imports) -> String { third_party_imports.insert("RootModel"); third_party_imports.insert("model_validator"); third_party_imports.insert("model_serializer"); - third_party_imports.insert("PrivateAttr"); } // Runtime imports - client bases if imports.has_async && imports.has_sync { - runtime_imports.insert("AsyncClientBase, ClientBase, ApiResponse"); + runtime_imports.insert("AsyncClientBase, ClientBase, ApiResponse".to_string()); } else if imports.has_async { - runtime_imports.insert("AsyncClientBase, ApiResponse"); + runtime_imports.insert("AsyncClientBase, ApiResponse".to_string()); } else if imports.has_sync { - runtime_imports.insert("ClientBase, ApiResponse"); + runtime_imports.insert("ClientBase, ApiResponse".to_string()); } - // Runtime imports - special types - let mut special_types = vec![]; - if imports.has_reflectapi_option { - special_types.push("ReflectapiOption"); - } - if imports.has_reflectapi_empty { - special_types.push("ReflectapiEmpty"); - } - if imports.has_reflectapi_infallible { - special_types.push("ReflectapiInfallible"); - } + runtime_imports.extend(imports.extra_runtime_imports.iter().cloned()); // Build the final import string let mut result = Vec::new(); // Add header result.push("# Standard library imports".to_string()); + let mut merged_from_imports: BTreeMap> = BTreeMap::new(); + let mut plain_stdlib_imports = Vec::new(); for import in stdlib_imports { + if let Some(rest) = import.strip_prefix("from ") { + if let Some((module, names)) = rest.split_once(" import ") { + let entry = merged_from_imports.entry(module.to_string()).or_default(); + for name in names.split(", ") { + entry.insert(name.to_string()); + } + continue; + } + } + plain_stdlib_imports.push(import); + } + for import in plain_stdlib_imports { result.push(import); } + for (module, names) in merged_from_imports { + let mut names = names.into_iter().collect::>(); + if module == "datetime" { + let rank = |name: &str| match name { + "datetime" => 0, + "date" => 1, + "timedelta" => 2, + _ => 99, + }; + names.sort_by_key(|name| (rank(name), name.clone())); + } + result.push(format!("from {module} import {}", names.join(", "))); + } // Add typing imports if !typing_imports.is_empty() { @@ -172,11 +438,6 @@ fn generate_optimized_imports(imports: &templates::Imports) -> String { result.push(format!("from reflectapi_runtime import {import}")); } - // Add special types as separate imports for clarity - for special_type in special_types { - result.push(format!("from reflectapi_runtime import {special_type}")); - } - // Add testing imports if imports.has_testing { result.push( @@ -320,9 +581,10 @@ fn render_struct_with_flatten( struct_def: &reflectapi_schema::Struct, schema: &Schema, implemented_types: &BTreeMap, + class_names: &BTreeMap, used_type_vars: &mut BTreeSet, ) -> anyhow::Result { - let struct_name = improve_class_name(&struct_def.name); + let struct_name = python_class_name(&struct_def.name, class_names); let active_generics: Vec = struct_def .parameters .iter() @@ -379,7 +641,7 @@ fn render_struct_with_flatten( } } -/// Resolve the target type name for a flattened field, unwrapping Option +/// Resolve the target type name for a flattened field, unwrapping `Option` fn resolve_flattened_type_name(type_ref: &TypeReference) -> &str { if (type_ref.name == "std::option::Option" || type_ref.name == "reflectapi::Option") && !type_ref.arguments.is_empty() @@ -523,23 +785,8 @@ fn render_struct_with_flattened_internal_enum( active_generics, used_type_vars, )?; - let is_option = vf.type_ref.name == "std::option::Option" - || vf.type_ref.name == "reflectapi::Option"; - let (optional, default_value, final_type) = if !vf.required { - if is_option { - (true, Some("None".to_string()), field_type) - } else { - ( - true, - Some("None".to_string()), - format!("{field_type} | None"), - ) - } - } else if is_option { - (true, Some("None".to_string()), field_type) - } else { - (false, None, field_type) - }; + let (optional, default_value, final_type) = + resolve_field_optionality(&vf.type_ref.name, field_type, vf.required); let (sanitized, alias) = sanitize_field_name_with_alias(vf.name(), vf.serde_name()); fields.push(templates::Field { @@ -956,7 +1203,7 @@ fn generate_init_py(config: &Config) -> String { let mut imports = vec!["AsyncClient"]; if config.generate_sync { - imports.push("SyncClient"); + imports.push("Client"); } let imports_list = format!("{imports:?}"); @@ -1015,11 +1262,10 @@ fn modules_from_rendered_types( } pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { - let implemented_types = build_implemented_types(); - // Consolidate input/output types FIRST so both the SemanticSchema and // the raw Schema share the same unified type names. let all_type_names = schema.consolidate_types(); + let implemented_types = build_implemented_types(); validate_type_references(&schema)?; // Build the semantic IR with a codegen-specific pipeline that skips @@ -1066,12 +1312,14 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { for sem_type in semantic.types() { if let reflectapi_schema::SemanticType::Enum(sem_enum) = sem_type { match &sem_enum.representation { - reflectapi_schema::Representation::Internal { .. } => { + reflectapi_schema::Representation::Internal { .. } + | reflectapi_schema::Representation::Adjacent { .. } => { + // Both internally and adjacently tagged enums now use + // Literal discriminator fields + Pydantic discriminated unions has_literal = true; has_discriminated_unions = true; } - reflectapi_schema::Representation::External - | reflectapi_schema::Representation::Adjacent { .. } => { + reflectapi_schema::Representation::External => { let has_complex_variants = sem_enum .variants .values() @@ -1091,24 +1339,9 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { ) }; - // Check if we need ReflectapiOption import - let has_reflectapi_option = schema_uses_reflectapi_option(&schema, &all_type_names); - - // Check if we need ReflectapiEmpty import - let has_reflectapi_empty = schema_uses_type(&schema, &all_type_names, "reflectapi::Empty"); - - // Check if we need ReflectapiInfallible import - let has_reflectapi_infallible = - schema_uses_type(&schema, &all_type_names, "reflectapi::Infallible"); - // Use semantic IR for function introspection let has_warnings = semantic.functions().any(|f| f.deprecation_note.is_some()); - - // Check if we need datetime imports (for chrono and time types) - let has_datetime = check_datetime_usage(&schema, &all_type_names); - let has_uuid = check_uuid_usage(&schema, &all_type_names); - let has_timedelta = check_timedelta_usage(&schema, &all_type_names); - let has_date = check_date_usage(&schema, &all_type_names); + let python_metadata = collect_python_metadata_usage(&all_type_names); // Generate imports let imports = templates::Imports { @@ -1116,14 +1349,9 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { has_sync: config.generate_sync, has_testing: config.generate_testing, has_enums, - has_reflectapi_option, - has_reflectapi_empty, - has_reflectapi_infallible, has_warnings, - has_datetime, - has_uuid, - has_timedelta, - has_date, + extra_stdlib_imports: python_metadata.stdlib_imports.iter().cloned().collect(), + extra_runtime_imports: python_metadata.runtime_imports.iter().cloned().collect(), has_generics: true, has_annotated: true, // Always include for external type fallbacks has_literal, @@ -1134,22 +1362,55 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { // Use optimized import generation instead of template generated_code.push(generate_optimized_imports(&imports)); + // Emit reusable helper functions for externally tagged enums (once, not per-enum) + if has_externally_tagged_enums { + generated_code.push( + r#" +# Helper functions for externally tagged enum serialization/deserialization +def _parse_externally_tagged(data, variants: dict, types: tuple, enum_name: str): + """Parse an externally tagged enum from {key: value} format.""" + if types and isinstance(data, types): + return data + if isinstance(data, str) and data in variants: + handler = variants[data] + if handler == "_unit": + return data + if isinstance(data, dict): + if len(data) != 1: + raise ValueError("Externally tagged enum must have exactly one key") + key, value = next(iter(data.items())) + if key in variants: + handler = variants[key] + if handler == "_unit": + return key + return handler(value) + raise ValueError(f"Unknown variant for {enum_name}: {data}") + + +def _serialize_externally_tagged(root, serializers: dict, enum_name: str): + """Serialize an externally tagged enum to {key: value} format.""" + for variant_name, (check, serialize) in serializers.items(): + if check(root): + return serialize(root) + raise ValueError(f"Cannot serialize {enum_name} variant: {type(root)}")"# + .to_string(), + ); + } + // Types that are provided by the runtime library and should not be generated - let runtime_provided_types = [ - "reflectapi::Option", - "reflectapi::Empty", - "reflectapi::Infallible", - "std::option::Option", - ]; + let mut non_rendered_types = python_metadata.runtime_provided_types.clone(); + non_rendered_types.insert("std::option::Option".to_string()); + + let class_names = build_python_class_name_map( + semantic + .types() + .filter(|st| !non_rendered_types.contains(st.name())) + .map(|st| st.name()), + ); // Build the set of Python class names that will be emitted, so we can // detect TypeVar-vs-class name collisions below. - let emitted_class_names: HashSet = semantic - .types() - .filter(|st| !runtime_provided_types.contains(&st.name())) - .filter_map(|st| schema.get_type(st.name())) - .map(|t| improve_class_name(t.name())) - .collect(); + let emitted_class_names: HashSet = class_names.values().cloned().collect(); // Collect TypeVars used across all types, using semantic IR ordering. // Since the codegen pipeline skips NamingResolution, sem_type.name() @@ -1157,7 +1418,7 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { let mut used_type_vars: BTreeSet = BTreeSet::new(); for sem_type in semantic.types() { let type_name = sem_type.name(); - if runtime_provided_types.contains(&type_name) { + if non_rendered_types.contains(type_name) { continue; } let type_def = match schema.get_type(type_name) { @@ -1221,7 +1482,7 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { for sem_type in semantic.types() { let type_name = sem_type.name().to_string(); - if runtime_provided_types.contains(&type_name.as_str()) { + if non_rendered_types.contains(&type_name) { continue; } @@ -1234,7 +1495,13 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { // TypeVars have already been collected, use empty set for rendering let mut dummy_type_vars = BTreeSet::new(); - let rendered = render_type(type_def, &schema, &implemented_types, &mut dummy_type_vars)?; + let rendered = render_type( + type_def, + &schema, + &implemented_types, + &class_names, + &mut dummy_type_vars, + )?; // Only store non-empty renders (excludes unwrapped tuple structs) if !rendered.trim().is_empty() { @@ -1325,19 +1592,21 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { "StdNumNonZeroI64 = Annotated[int, \"Rust NonZero i64 type\"]".to_string(), "".to_string(), "# Rebuild models to resolve forward references".to_string(), - "try:".to_string(), ]; - let sorted_type_names: Vec<&String> = rendered_type_keys.iter().collect(); - for original_name in &sorted_type_names { - if !original_name.starts_with("std::") && !original_name.starts_with("reflectapi::") { - let dotted = type_name_to_python_ref(original_name); - external_types_and_rebuilds.push(format!(" {dotted}.model_rebuild()")); - } + // Rebuild models in a loop — per-type try/except so one failure + // doesn't skip all rebuilds. Use flat class names, not namespace-dotted + // refs, since these are module-level definitions. + let rebuild_models: Vec = rendered_type_keys + .iter() + .filter(|n| !n.starts_with("std::") && !n.starts_with("reflectapi::")) + .map(|n| python_class_name(n, &class_names)) + .collect(); + if !rebuild_models.is_empty() { + external_types_and_rebuilds.push(format!( + "for _model in [\n {},\n]:\n try:\n _model.model_rebuild()\n except Exception:\n pass", + rebuild_models.join(",\n ") + )); } - external_types_and_rebuilds.push("except AttributeError:".to_string()); - external_types_and_rebuilds - .push(" # Some types may not have model_rebuild method".to_string()); - external_types_and_rebuilds.push(" pass".to_string()); external_types_and_rebuilds.push("".to_string()); generated_code.push(external_types_and_rebuilds.join("\n")); @@ -1360,8 +1629,11 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { let result = generated_code.join("\n\n"); - // Format with black if available - format_python_code(&result) + if config.format { + format_python_code(&result) + } else { + Ok(basic_python_format(&result)) + } } /// Rename type parameters in the schema to avoid TypeVar/class name collisions. @@ -1582,11 +1854,12 @@ fn render_type( type_def: &Type, schema: &Schema, implemented_types: &BTreeMap, + class_names: &BTreeMap, used_type_vars: &mut BTreeSet, ) -> anyhow::Result { match type_def { - Type::Struct(s) => render_struct(s, schema, implemented_types, used_type_vars), - Type::Enum(e) => render_enum(e, schema, implemented_types, used_type_vars), + Type::Struct(s) => render_struct(s, schema, implemented_types, class_names, used_type_vars), + Type::Enum(e) => render_enum(e, schema, implemented_types, class_names, used_type_vars), Type::Primitive(_p) => { // Primitive types are handled by implemented_types mapping Ok(String::new()) // This shouldn't be reached normally @@ -1710,24 +1983,11 @@ fn make_flattened_field( used_type_vars, )?; - let is_option_type = - field.type_ref.name == "std::option::Option" || field.type_ref.name == "reflectapi::Option"; - - let (optional, default_value, final_field_type) = if !field.required || !parent_required { - if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - ( - true, - Some("None".to_string()), - format!("{field_type} | None"), - ) - } - } else if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - (false, None, field_type) - }; + let (optional, default_value, final_field_type) = resolve_field_optionality( + &field.type_ref.name, + field_type, + field.required && parent_required, + ); let (sanitized, alias) = sanitize_field_name_with_alias(field.name(), field.serde_name()); Ok(templates::Field { @@ -1815,6 +2075,7 @@ fn render_struct( struct_def: &reflectapi_schema::Struct, schema: &Schema, implemented_types: &BTreeMap, + class_names: &BTreeMap, used_type_vars: &mut BTreeSet, ) -> anyhow::Result { // Check if this struct has any flattened fields @@ -1822,7 +2083,13 @@ fn render_struct( if has_flattened { // Use runtime flatten support - return render_struct_with_flatten(struct_def, schema, implemented_types, used_type_vars); + return render_struct_with_flatten( + struct_def, + schema, + implemented_types, + class_names, + used_type_vars, + ); } // Check if this is a single-field tuple struct that should be unwrapped @@ -1854,29 +2121,8 @@ fn render_struct( used_type_vars, )?; - // Check if field type is Option or ReflectapiOption (which handle nullability themselves) - let is_option_type = field.type_ref.name == "std::option::Option" - || field.type_ref.name == "reflectapi::Option"; - - // Fix default value handling for optional fields - let (optional, default_value, field_type) = if !field.required { - // Field is not required - add | None if not already an Option type - if is_option_type { - (true, Some("None".to_string()), base_field_type) // Option types handle nullability themselves - } else { - ( - true, - Some("None".to_string()), - format!("{base_field_type} | None"), - ) - } - } else if is_option_type { - // Field is required but Option type - still needs default None - (true, Some("None".to_string()), base_field_type) // Option types handle nullability themselves - } else { - // Required non-Option field - (false, None, base_field_type) - }; + let (optional, default_value, field_type) = + resolve_field_optionality(&field.type_ref.name, base_field_type, field.required); let (sanitized, alias) = sanitize_field_name_with_alias(field.name(), field.serde_name()); @@ -1898,7 +2144,7 @@ fn render_struct( // Check if this is a generic struct (has type parameters) let has_generics = !struct_def.parameters.is_empty(); - let class_name = improve_class_name(&struct_def.name); + let class_name = python_class_name(&struct_def.name, class_names); let class_template = templates::DataClass { name: class_name, @@ -1916,6 +2162,7 @@ fn render_enum( enum_def: &reflectapi_schema::Enum, schema: &Schema, implemented_types: &BTreeMap, + class_names: &BTreeMap, used_type_vars: &mut BTreeSet, ) -> anyhow::Result { use reflectapi_schema::{Fields, Representation}; @@ -1928,6 +2175,7 @@ fn render_enum( tag, schema, implemented_types, + class_names, used_type_vars, )?; @@ -1941,6 +2189,7 @@ fn render_enum( content, schema, implemented_types, + class_names, used_type_vars, )?; @@ -1948,14 +2197,20 @@ fn render_enum( } Representation::None => { // Untagged enums - render_untagged_enum(enum_def, schema, implemented_types, used_type_vars) + render_untagged_enum( + enum_def, + schema, + implemented_types, + class_names, + used_type_vars, + ) } _ => { // Check if this is a primitive-represented enum (has discriminant values) let has_discriminants = enum_def.variants.iter().any(|v| v.discriminant.is_some()); if has_discriminants { - render_primitive_enum(enum_def) + render_primitive_enum(enum_def, class_names) } else { // Check if this has complex variants (tuple or struct variants) let has_complex_variants = enum_def.variants.iter().any(|v| { @@ -1972,6 +2227,7 @@ fn render_enum( enum_def, schema, implemented_types, + class_names, used_type_vars, ) } else { @@ -1987,7 +2243,7 @@ fn render_enum( .collect(); let enum_template = templates::EnumClass { - name: improve_class_name(&enum_def.name), + name: python_class_name(&enum_def.name, class_names), description: Some(enum_def.description().to_string()), variants, }; @@ -2005,14 +2261,15 @@ fn render_adjacently_tagged_enum( content: &str, schema: &Schema, implemented_types: &BTreeMap, + class_names: &BTreeMap, used_type_vars: &mut BTreeSet, ) -> anyhow::Result<(String, Vec)> { use reflectapi_schema::Fields; - let enum_name = improve_class_name(&enum_def.name); + let enum_name = python_class_name(&enum_def.name, class_names); - let mut variant_models = Vec::new(); - let mut union_variants = Vec::new(); + let mut variant_class_definitions: Vec = Vec::new(); + let mut union_variant_names: Vec = Vec::new(); let generic_params: Vec = infer_enum_generic_params(enum_def, schema); // Track used generic type variables @@ -2020,48 +2277,85 @@ fn render_adjacently_tagged_enum( used_type_vars.insert(generic.clone()); } + let is_generic = !generic_params.is_empty(); + + // Generate individual variant classes, each with the tag field as Literal discriminator for variant in &enum_def.variants { + let variant_class_name = format!("{}{}", enum_name, to_pascal_case(variant.name())); + + // For generic enums, add type parameters to union variant names + if is_generic { + let params_str = generic_params.join(", "); + union_variant_names.push(format!("{variant_class_name}[{params_str}]")); + } else { + union_variant_names.push(variant_class_name.clone()); + } + + // Start with the tag discriminator field + let discriminator_default_value = Some(format!("\"{}\"", variant.serde_name())); + let mut fields = vec![templates::Field { + name: tag.to_string(), + type_annotation: format!("Literal['{}']", variant.serde_name()), + description: Some("Discriminator field".to_string()), + deprecation_note: None, + optional: false, + default_value: discriminator_default_value, + alias: None, + }]; + + // Add the content field based on variant type match &variant.fields { Fields::None => { - union_variants.push(format!("Literal[\"{}\"]", variant.name())); + // Unit variant: only the tag field, no content field needed. + // The content field is optional and absent on the wire. } Fields::Unnamed(unnamed_fields) => { - let variant_class_name = - format!("{}{}Variant", enum_name, to_pascal_case(variant.name())); - let mut fields = Vec::new(); - for (i, field) in unnamed_fields.iter().enumerate() { + // Tuple variant: add content field with the appropriate type + if unnamed_fields.len() == 1 { let field_type = type_ref_to_python_type( - &field.type_ref, + &unnamed_fields[0].type_ref, schema, implemented_types, &generic_params, used_type_vars, )?; fields.push(templates::Field { - name: format!("field_{i}"), + name: content.to_string(), type_annotation: field_type, - description: Some(field.description().to_string()), - deprecation_note: field.deprecation_note.clone(), + description: Some(unnamed_fields[0].description().to_string()), + deprecation_note: unnamed_fields[0].deprecation_note.clone(), + optional: false, + default_value: None, + alias: None, + }); + } else { + // Multi-field tuple: content is a list + // Resolve types for side-effects (populates used_type_vars) + for f in unnamed_fields { + type_ref_to_python_type( + &f.type_ref, + schema, + implemented_types, + &generic_params, + used_type_vars, + )?; + } + fields.push(templates::Field { + name: content.to_string(), + type_annotation: "list[Any]".to_string(), + description: Some("Tuple variant fields".to_string()), + deprecation_note: None, optional: false, default_value: None, alias: None, }); } - let variant_model = templates::DataClass { - name: variant_class_name.clone(), - description: Some(format!("{} variant", variant.name())), - fields, - is_tuple: !unnamed_fields.is_empty(), - is_generic: !generic_params.is_empty(), - generic_params: generic_params.clone(), - }; - variant_models.push(variant_model.render()); - union_variants.push(variant_class_name); } Fields::Named(named_fields) => { - let variant_class_name = - format!("{}{}Variant", enum_name, to_pascal_case(variant.name())); - let mut fields = Vec::new(); + // Struct variant: we need a nested content model, then reference it + // First generate the content model + let content_class_name = format!("{variant_class_name}Content"); + let mut content_fields = Vec::new(); for field in named_fields { let field_type = type_ref_to_python_type( &field.type_ref, @@ -2070,26 +2364,11 @@ fn render_adjacently_tagged_enum( &generic_params, used_type_vars, )?; - let is_option_type = field.type_ref.name == "std::option::Option" - || field.type_ref.name == "reflectapi::Option"; - let (optional, default_value, final_field_type) = if !field.required { - if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - ( - true, - Some("None".to_string()), - format!("{field_type} | None"), - ) - } - } else if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - (false, None, field_type) - }; + let (optional, default_value, final_field_type) = + resolve_field_optionality(&field.type_ref.name, field_type, field.required); let (sanitized, alias) = sanitize_field_name_with_alias(field.name(), field.serde_name()); - fields.push(templates::Field { + content_fields.push(templates::Field { name: sanitized, type_annotation: final_field_type, description: Some(field.description().to_string()), @@ -2099,165 +2378,109 @@ fn render_adjacently_tagged_enum( alias, }); } - let variant_model = templates::DataClass { - name: variant_class_name.clone(), - description: Some(format!("{} variant", variant.name())), - fields, + let content_model = templates::DataClass { + name: content_class_name.clone(), + description: Some(format!("{} content", variant.name())), + fields: content_fields, is_tuple: false, - is_generic: !generic_params.is_empty(), + is_generic, generic_params: generic_params.clone(), }; - variant_models.push(variant_model.render()); - union_variants.push(variant_class_name); + variant_class_definitions.push(content_model.render()); + + // Reference the content model in the variant class + let content_type = if is_generic { + format!("{}[{}]", content_class_name, generic_params.join(", ")) + } else { + content_class_name + }; + fields.push(templates::Field { + name: content.to_string(), + type_annotation: content_type, + description: Some(format!("{} content", variant.name())), + deprecation_note: None, + optional: false, + default_value: None, + alias: None, + }); } } - } - // Build RootModel with before validator and serializer following {tag, content} - let mut code = String::new(); - if !variant_models.is_empty() { - code.push_str(&variant_models.join("\n\n")); - code.push_str("\n\n"); - } - let generic_inherits = if !generic_params.is_empty() { - format!(", Generic[{}]", generic_params.join(", ")) - } else { - String::new() - }; - code.push_str(&format!( - "# Adjacently tagged enum using RootModel\n{enum_name}Variants = Union[{union}]\n\nclass {enum_name}(RootModel[{enum_name}Variants]{generic_inherits}):\n \"\"\"Adjacently tagged enum\"\"\"\n\n @model_validator(mode='before')\n @classmethod\n def _validate_adjacently_tagged(cls, data):\n # Handle direct variant instances\n if isinstance(data, ({direct_types})):\n return data\n if isinstance(data, dict):\n tag = data.get('{tag}')\n content = data.get('{content}')\n if tag is None:\n raise ValueError(\"Missing tag field '{tag}'\")\n if content is None and tag not in ({unit_variants_tuple}):\n raise ValueError(\"Missing content field '{content}' for tag: {{}}\".format(tag))\n # Dispatch based on tag\n{dispatch_cases}\n raise ValueError(\"Unknown variant for {enum_name}: {{}}\".format(data))\n\n @model_serializer\n def _serialize_adjacently_tagged(self):\n{serialize_cases}\n raise ValueError(f\"Cannot serialize {enum_name} variant: {{type(self.root)}}\")\n", - enum_name = enum_name, - union = union_variants.join(", "), - generic_inherits = generic_inherits, - direct_types = union_variants - .iter() - .filter(|s| !s.starts_with("Literal[")) - .cloned() - .collect::>() - .join(", "), - tag = tag, - content = content, - unit_variants_tuple = enum_def - .variants - .iter() - .filter(|v| matches!(v.fields, Fields::None)) - .map(|v| format!("'{}'", v.name())) - .collect::>() - .join(", "), - dispatch_cases = generate_adjacent_dispatch_cases(enum_def, &enum_name)?, - serialize_cases = generate_adjacent_serialize_cases(enum_def, &enum_name, tag, content)?, - )); - if !generic_params.is_empty() { - code.push_str("\n def __class_getitem__(cls, params):\n return cls\n"); + // Generate the variant class + let variant_template = templates::DataClass { + name: variant_class_name, + description: Some(variant.description().to_string()), + fields, + is_tuple: false, + is_generic, + generic_params: generic_params.clone(), + }; + + variant_class_definitions.push(variant_template.render()); } - Ok((code, union_variants)) -} + // Generate the discriminated union using the same pattern as internally tagged enums + let union_variants_for_template: Vec = enum_def + .variants + .iter() + .enumerate() + .map(|(i, variant)| { + let variant_class_name = format!("{}{}", enum_name, to_pascal_case(variant.name())); + let full_type_annotation = &union_variant_names[i]; -fn generate_adjacent_dispatch_cases( - enum_def: &reflectapi_schema::Enum, - enum_name: &str, -) -> anyhow::Result { - use reflectapi_schema::Fields; - let mut cases = Vec::new(); - for variant in &enum_def.variants { - let wire_name = variant.serde_name(); - let rust_name = variant.name(); - match &variant.fields { - Fields::None => { - cases.push(format!( - " if tag == \"{wire_name}\":\n return \"{wire_name}\"" - )); + templates::UnionVariant { + name: variant.name().to_string(), + type_annotation: full_type_annotation.clone(), + base_name: variant_class_name, + description: Some(variant.description().to_string()), } - Fields::Unnamed(unnamed_fields) => { - let class_name = format!("{enum_name}{}Variant", to_pascal_case(rust_name)); - if unnamed_fields.len() == 1 { - cases.push(format!( - " if tag == \"{wire_name}\":\n return {class_name}(field_0=content)" - )); - } else { - let assigns = (0..unnamed_fields.len()) - .map(|i| format!("field_{i}=content[{i}]")) - .collect::>() - .join(", "); - cases.push(format!( - " if tag == \"{wire_name}\":\n if isinstance(content, list):\n return {class_name}({assigns})\n else:\n raise ValueError(\"Expected list for tuple variant {wire_name}\")" - )); - } - } - Fields::Named(_named_fields) => { - let class_name = format!("{enum_name}{}Variant", to_pascal_case(rust_name)); - cases.push(format!( - " if tag == \"{wire_name}\":\n return {class_name}(**content)" - )); - } - } - } - Ok(cases.join("\n")) -} + }) + .collect(); -fn generate_adjacent_serialize_cases( - enum_def: &reflectapi_schema::Enum, - enum_name: &str, - tag: &str, - content: &str, -) -> anyhow::Result { - use reflectapi_schema::Fields; - let mut cases = Vec::new(); - for variant in &enum_def.variants { - let wire_name = variant.serde_name(); - let rust_name = variant.name(); - match &variant.fields { - Fields::None => { - cases.push(format!( - " if self.root == \"{wire_name}\":\n return {{\"{tag}\": \"{wire_name}\"}}" - )); - } - Fields::Unnamed(unnamed_fields) => { - let class_name = format!("{enum_name}{}Variant", to_pascal_case(rust_name)); - // For tuple variants in adjacently tagged enums, serialize the content properly - if unnamed_fields.len() == 1 { - // Single field tuple: serialize the field value directly - cases.push(format!( - " if isinstance(self.root, {class_name}):\n return {{\"{tag}\": \"{wire_name}\", \"{content}\": self.root.field_0}}" - )); - } else { - // Multiple field tuple: serialize as array - let field_accesses: Vec = (0..unnamed_fields.len()) - .map(|i| format!("self.root.field_{i}")) - .collect(); - cases.push(format!( - " if isinstance(self.root, {class_name}):\n return {{\"{tag}\": \"{wire_name}\", \"{content}\": [{}]}}", - field_accesses.join(", ") - )); - } - } - Fields::Named(_named_fields) => { - let class_name = format!("{enum_name}{}Variant", to_pascal_case(rust_name)); - cases.push(format!( - " if isinstance(self.root, {class_name}):\n return {{\"{tag}\": \"{wire_name}\", \"{content}\": self.root.model_dump()}}" - )); - } - } + let union_template = templates::UnionClass { + name: enum_name.clone(), + description: Some(enum_def.description().to_string()), + variants: union_variants_for_template, + discriminator_field: tag.to_string(), + is_generic, + generic_params: generic_params.clone(), + }; + + let union_definition = union_template.render(); + + // Combine all parts + let mut result = String::new(); + + // Add variant class definitions (content models + variant models) + result.push_str(&variant_class_definitions.join("\n\n")); + if !result.is_empty() { + result.push_str("\n\n"); } - Ok(cases.join("\n")) + + // Add union definition + result.push_str(&union_definition); + result.push_str("\n\n"); + + Ok((result, union_variant_names)) } fn render_externally_tagged_enum( enum_def: &reflectapi_schema::Enum, schema: &Schema, implemented_types: &BTreeMap, + class_names: &BTreeMap, used_type_vars: &mut BTreeSet, ) -> anyhow::Result { use reflectapi_schema::Fields; - let enum_name = improve_class_name(&enum_def.name); + let enum_name = python_class_name(&enum_def.name, class_names); let mut variant_models = Vec::new(); let mut union_variants = Vec::new(); - let mut instance_validator_cases = Vec::new(); - let mut validator_cases = Vec::new(); - let mut dict_validator_cases = Vec::new(); - let mut serializer_cases = Vec::new(); + + // Collect per-variant data for the compact helper-based approach + let mut variant_entries = Vec::new(); // (wire_name, handler_expr) + let mut serializer_entries = Vec::new(); // (wire_name, check_expr, serialize_expr) + let mut variant_class_names = Vec::new(); // class names for isinstance checks // Collect active generic parameter names for this enum let generic_params: Vec = infer_enum_generic_params(enum_def, schema); @@ -2270,24 +2493,23 @@ fn render_externally_tagged_enum( // Generate variant models and build validation/serialization logic for variant in &enum_def.variants { let variant_name = variant.name(); + let wire_name = variant.serde_name(); match &variant.fields { Fields::None => { // Unit variant: represented as string literal union_variants.push(format!("Literal[{variant_name:?}]")); - validator_cases.push(format!( - " if isinstance(data, str) and data == \"{variant_name}\":\n return data" - )); - - serializer_cases.push(format!( - " if self.root == \"{variant_name}\":\n return \"{variant_name}\"" + variant_entries.push(format!("\"{wire_name}\": \"_unit\"")); + serializer_entries.push(format!( + "\"{wire_name}\": (lambda r: r == \"{wire_name}\", lambda r: \"{wire_name}\")" )); } Fields::Unnamed(unnamed_fields) => { // Tuple variant: create a model class let variant_class_name = format!("{}{}Variant", enum_name, to_pascal_case(variant_name)); + variant_class_names.push(variant_class_name.clone()); let mut fields = Vec::new(); let mut field_names = Vec::new(); @@ -2331,32 +2553,31 @@ fn render_externally_tagged_enum( }; union_variants.push(union_member); - // Handle direct instance validation - instance_validator_cases.push(format!( - " if isinstance(data, {variant_class_name}):\n return data" - )); - // For tuple variants, the JSON value can be a single value or array if field_names.len() == 1 { - dict_validator_cases.push(format!( - " if key == \"{variant_name}\":\n return {variant_class_name}(field_0=value)" + variant_entries.push(format!( + "\"{wire_name}\": lambda v: {variant_class_name}(field_0=v)" )); - - serializer_cases.push(format!( - " if isinstance(self.root, {variant_class_name}):\n return {{\"{variant_name}\": self.root.field_0}}" + serializer_entries.push(format!( + "\"{wire_name}\": (lambda r: isinstance(r, {variant_class_name}), lambda r: {{\"{wire_name}\": r.field_0}})" )); } else { - dict_validator_cases.push(format!( - " if key == \"{}\":\n if isinstance(value, list):\n return {}({})\n else:\n raise ValueError(\"Expected list for tuple variant {}\")", - variant_name, variant_class_name, - field_names.iter().enumerate().map(|(i, name)| format!("{name}=value[{i}]")).collect::>().join(", "), - variant_name + let assigns = field_names + .iter() + .enumerate() + .map(|(i, name)| format!("{name}=v[{i}]")) + .collect::>() + .join(", "); + variant_entries.push(format!( + "\"{wire_name}\": lambda v: {variant_class_name}({assigns}) if isinstance(v, list) else (_ for _ in ()).throw(ValueError(\"Expected list for tuple variant {wire_name}\"))" )); - - serializer_cases.push(format!( - " if isinstance(self.root, {}):\n return {{\"{}\": [{}]}}", - variant_class_name, variant_name, - field_names.iter().map(|name| format!("self.root.{name}")).collect::>().join(", ") + let field_accesses = field_names + .iter() + .map(|name| format!("r.{name}")) + .collect::>() + .join(", "); + serializer_entries.push(format!( + "\"{wire_name}\": (lambda r: isinstance(r, {variant_class_name}), lambda r: {{\"{wire_name}\": [{field_accesses}]}})" )); } } @@ -2364,6 +2585,7 @@ fn render_externally_tagged_enum( // Struct variant: create a model class let variant_class_name = format!("{}{}Variant", enum_name, to_pascal_case(variant_name)); + variant_class_names.push(variant_class_name.clone()); let mut fields = Vec::new(); for field in named_fields { @@ -2375,15 +2597,8 @@ fn render_externally_tagged_enum( used_type_vars, )?; - let (optional, default_value, final_field_type) = if !field.required { - ( - true, - Some("None".to_string()), - format!("{field_type} | None"), - ) - } else { - (false, None, field_type) - }; + let (optional, default_value, final_field_type) = + resolve_field_optionality(&field.type_ref.name, field_type, field.required); let (sanitized, alias) = sanitize_field_name_with_alias(field.name(), field.serde_name()); @@ -2415,17 +2630,11 @@ fn render_externally_tagged_enum( }; union_variants.push(union_member); - // Handle direct instance validation - instance_validator_cases.push(format!( - " if isinstance(data, {variant_class_name}):\n return data" - )); - - dict_validator_cases.push(format!( - " if key == \"{variant_name}\":\n return {variant_class_name}(**value)" + variant_entries.push(format!( + "\"{wire_name}\": lambda v: {variant_class_name}(**v)" )); - - serializer_cases.push(format!( - " if isinstance(self.root, {variant_class_name}):\n return {{\"{variant_name}\": self.root.model_dump()}}" + serializer_entries.push(format!( + "\"{wire_name}\": (lambda r: isinstance(r, {variant_class_name}), lambda r: {{\"{wire_name}\": r.model_dump(by_alias=True)}})" )); } } @@ -2434,8 +2643,8 @@ fn render_externally_tagged_enum( // Check if this enum is generic let is_generic = !generic_params.is_empty(); - // Always use RootModel approach; add Generic[...] when needed - let template = templates::ExternallyTaggedEnumRootModel { + // Render using compact helper-based template + let template = templates::ExternallyTaggedEnumCompact { name: enum_name.clone(), description: if enum_def.description().is_empty() { None @@ -2445,23 +2654,21 @@ fn render_externally_tagged_enum( variant_models, union_variants: union_variants.join(", "), is_single_variant: union_variants.len() == 1, - instance_validator_cases: instance_validator_cases.join("\n"), - validator_cases: validator_cases.join("\n"), - dict_validator_cases: dict_validator_cases.join("\n"), - serializer_cases: serializer_cases.join("\n"), + variant_entries, + serializer_entries, + variant_class_names, is_generic, generic_params: generic_params.clone(), }; let enum_code = template.render(); - // TypeVar definitions are emitted once at the top of the file; - // inline declarations are suppressed to avoid collisions with class names. - let result = enum_code; - - Ok(result) + Ok(enum_code) } -fn render_primitive_enum(enum_def: &reflectapi_schema::Enum) -> anyhow::Result { +fn render_primitive_enum( + enum_def: &reflectapi_schema::Enum, + class_names: &BTreeMap, +) -> anyhow::Result { // Determine if this is an integer or float enum let is_float_enum = false; let mut enum_variants = Vec::new(); @@ -2486,7 +2693,7 @@ fn render_primitive_enum(enum_def: &reflectapi_schema::Enum) -> anyhow::Result, - used_type_vars: &mut BTreeSet, -) -> anyhow::Result<(String, Vec)> { - let (rendered, union_variant_names) = render_internally_tagged_enum_core( - enum_def, - tag, - schema, - implemented_types, - used_type_vars, - )?; - Ok((rendered, union_variant_names)) -} - -fn render_internally_tagged_enum_core( - enum_def: &reflectapi_schema::Enum, - tag: &str, - schema: &Schema, - implemented_types: &BTreeMap, + class_names: &BTreeMap, used_type_vars: &mut BTreeSet, ) -> anyhow::Result<(String, Vec)> { use reflectapi_schema::{Fields, Type}; - let enum_name = improve_class_name(&enum_def.name); + let enum_name = python_class_name(&enum_def.name, class_names); let mut variant_class_definitions: Vec = Vec::new(); let mut union_variant_names: Vec = Vec::new(); @@ -2618,26 +2809,12 @@ fn render_internally_tagged_enum_core( used_type_vars, )?; - // Handle field optionality - let is_option_type = struct_field.type_ref.name - == "std::option::Option" - || struct_field.type_ref.name == "reflectapi::Option"; let (optional, default_value, final_field_type) = - if !struct_field.required { - if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - ( - true, - Some("None".to_string()), - format!("{field_type} | None"), - ) - } - } else if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - (false, None, field_type) - }; + resolve_field_optionality( + &struct_field.type_ref.name, + field_type, + struct_field.required, + ); let (sanitized, alias) = sanitize_field_name_with_alias( struct_field.name(), @@ -2688,23 +2865,8 @@ fn render_internally_tagged_enum_core( used_type_vars, )?; - let is_option_type = field.type_ref.name == "std::option::Option" - || field.type_ref.name == "reflectapi::Option"; - let (optional, default_value, final_field_type) = if !field.required { - if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - ( - true, - Some("None".to_string()), - format!("{field_type} | None"), - ) - } - } else if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - (false, None, field_type) - }; + let (optional, default_value, final_field_type) = + resolve_field_optionality(&field.type_ref.name, field_type, field.required); let (sanitized, alias) = sanitize_field_name_with_alias(field.name(), field.serde_name()); @@ -2812,11 +2974,12 @@ fn render_untagged_enum( enum_def: &reflectapi_schema::Enum, schema: &Schema, implemented_types: &BTreeMap, + class_names: &BTreeMap, used_type_vars: &mut BTreeSet, ) -> anyhow::Result { use reflectapi_schema::Fields; - let enum_name = improve_class_name(&enum_def.name); + let enum_name = python_class_name(&enum_def.name, class_names); let mut variant_classes = Vec::new(); let mut union_variants = Vec::new(); @@ -2844,25 +3007,8 @@ fn render_untagged_enum( &generic_params, used_type_vars, )?; - // Check if field type is Option or ReflectapiOption (which handle nullability themselves) - let is_option_type = field.type_ref.name == "std::option::Option" - || field.type_ref.name == "reflectapi::Option"; - // Handle optionality - let (optional, default_value, final_field_type) = if !field.required { - if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - ( - true, - Some("None".to_string()), - format!("{field_type} | None"), - ) - } - } else if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - (false, None, field_type) - }; + let (optional, default_value, final_field_type) = + resolve_field_optionality(&field.type_ref.name, field_type, field.required); let (sanitized, alias) = sanitize_field_name_with_alias(field.name(), field.serde_name()); @@ -2887,26 +3033,15 @@ fn render_untagged_enum( &generic_params, used_type_vars, )?; - let is_option_type = field.type_ref.name == "std::option::Option" - || field.type_ref.name == "reflectapi::Option"; - let (optional, default_value, final_field_type) = if !field.required { - if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - ( - true, - Some("None".to_string()), - format!("{field_type} | None"), - ) - } - } else if is_option_type { - (true, Some("None".to_string()), field_type) - } else { - (false, None, field_type) - }; + let (optional, default_value, final_field_type) = + resolve_field_optionality(&field.type_ref.name, field_type, field.required); fields.push(templates::Field { - name: format!("field_{i}"), + name: if unnamed_fields.len() == 1 { + "value".to_string() + } else { + format!("field_{i}") + }, type_annotation: final_field_type, description: Some(field.description().to_string()), deprecation_note: field.deprecation_note.clone(), @@ -3295,25 +3430,107 @@ fn to_pascal_case(s: &str) -> String { /// /// For type *references* (in annotations), use [`type_name_to_python_ref`] /// instead, which produces a dotted path like `reflectapi_demo.tests.serde.Offer`. -fn improve_class_name(original_name: &str) -> String { - // Handle Rust module paths with :: - if original_name.contains("::") { - let parts: Vec<&str> = original_name.split("::").collect(); - return parts - .iter() - .map(|part| to_pascal_case(part)) - .collect::>() - .join(""); +/// Maximum class name length before truncation with hash suffix. +const MAX_CLASS_NAME_LEN: usize = 80; + +fn maybe_destutter_pascal_parts(pascal_parts: &[String]) -> String { + let mut result_parts: Vec<&str> = Vec::new(); + + if pascal_parts.len() >= 2 { + let leaf = pascal_parts.last().unwrap(); + for (i, part) in pascal_parts.iter().enumerate() { + if i + 1 == pascal_parts.len() { + result_parts.push(part); + } else if i + 1 == pascal_parts.len() - 1 && leaf.starts_with(part.as_str()) { + continue; + } else { + result_parts.push(part); + } + } + } else { + result_parts = pascal_parts.iter().map(|s| s.as_str()).collect(); } - // Handle dotted namespaces (e.g., "myapi.model.Pet" -> "Pet") - if original_name.contains('.') { - if let Some(pos) = original_name.rfind('.') { - return improve_class_name_part(&original_name[pos + 1..]); + result_parts.join("") +} + +fn finalize_class_name(original_name: &str, raw: String) -> String { + if raw.len() > MAX_CLASS_NAME_LEN { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + original_name.hash(&mut hasher); + let hash_full = format!("{:016x}", hasher.finish()); + let hash = &hash_full[..8]; + let truncated = &raw[..MAX_CLASS_NAME_LEN - 9]; + format!("{truncated}_{hash}") + } else { + raw + } +} + +fn build_flat_python_class_name(original_name: &str, remove_stutter: bool) -> String { + let raw = if original_name.contains("::") { + let parts: Vec<&str> = original_name.split("::").collect(); + let pascal_parts: Vec = parts.iter().map(|part| to_pascal_case(part)).collect(); + + if remove_stutter { + maybe_destutter_pascal_parts(&pascal_parts) + } else { + pascal_parts.join("") } + } else if original_name.contains('.') { + let pos = original_name.rfind('.').unwrap(); + improve_class_name_part(&original_name[pos + 1..]) + } else { + improve_class_name_part(original_name) + }; + + finalize_class_name(original_name, raw) +} + +fn build_python_class_name_map<'a>( + type_names: impl IntoIterator, +) -> BTreeMap { + let candidates: Vec<(String, String, String)> = type_names + .into_iter() + .map(|type_name| { + ( + type_name.to_string(), + build_flat_python_class_name(type_name, false), + build_flat_python_class_name(type_name, true), + ) + }) + .collect(); + + let mut preferred_counts: BTreeMap = BTreeMap::new(); + for (_, _, preferred_name) in &candidates { + *preferred_counts.entry(preferred_name.clone()).or_default() += 1; } - improve_class_name_part(original_name) + candidates + .into_iter() + .map(|(type_name, fallback_name, preferred_name)| { + let chosen_name = if preferred_counts.get(&preferred_name) == Some(&1) { + preferred_name + } else { + fallback_name + }; + (type_name, chosen_name) + }) + .collect() +} + +fn python_class_name(type_name: &str, class_names: &BTreeMap) -> String { + class_names + .get(type_name) + .cloned() + .unwrap_or_else(|| build_flat_python_class_name(type_name, true)) +} + +fn improve_class_name(original_name: &str) -> String { + build_flat_python_class_name(original_name, true) } /// Convert a fully-qualified Rust type name to a dotted Python reference. @@ -3437,55 +3654,10 @@ fn sanitize_field_name_with_alias(name: &str, serde_name: &str) -> (String, Opti /// Map external/undefined types to sensible Python equivalents /// Uses typing.Annotated to provide rich metadata like TypeScript comments +/// Last-resort fallback for types not in the static mapping and not in the schema. +/// Wraps unknown types in Annotated[Any, ...] so the generated code is still valid Python. fn map_external_type_to_python(type_name: &str) -> String { - match type_name { - // Rust std library numeric types - name if name.contains("NonZero") - && (name.contains("U32") - || name.contains("U64") - || name.contains("I32") - || name.contains("I64")) => - { - format!("Annotated[int, \"Rust NonZero integer type: {name}\"]") - } - - // Decimal types (often used for financial data) - JSON serialized as strings - name if name.contains("Decimal") => { - format!("Annotated[str, \"Rust Decimal type (JSON string): {name}\"]") - } - - // Common domain-specific types that are typically strings - name if name.contains("Uuid") || name.contains("UUID") => { - format!("Annotated[str, \"UUID type: {name}\"]") - } - name if name.contains("Id") || name.ends_with("ID") => { - format!("Annotated[str, \"ID type: {name}\"]") - } - - // Duration and time types - properly mapped to Python equivalents - name if name.contains("Duration") => { - format!("Annotated[timedelta, \"Rust Duration type: {name}\"]") - } - name if name.contains("Instant") => { - format!("Annotated[datetime, \"Rust Instant type: {name}\"]") - } - - // Path types - mapped to pathlib.Path - name if name.contains("PathBuf") || name.contains("Path") => { - format!("Annotated[Path, \"Rust Path type: {name}\"]") - } - - // IP address types - mapped to ipaddress module - name if name.contains("IpAddr") - || name.contains("Ipv4Addr") - || name.contains("Ipv6Addr") => - { - format!("Annotated[IPv4Address | IPv6Address, \"Rust IP address type: {name}\"]") - } - - // For completely unmapped types, use Annotated[Any, ...] with metadata - _ => format!("Annotated[Any, \"External type: {type_name}\"]"), - } + format!("Annotated[Any, \"External type: {type_name}\"]") } // Type substitution function - handles TypeReference to Python type conversion @@ -3516,9 +3688,11 @@ fn type_ref_to_python_type( } } - // Special case: chrono::DateTime and similar types - ignore generic arguments - if type_ref.name.starts_with("chrono::") { - // chrono types map directly to Python datetime types regardless of timezone parameter + if python_type_mappings() + .get(type_ref.name.as_str()) + .map(|m| m.ignore_type_arguments) + .unwrap_or(false) + { return Ok(python_type.clone()); } @@ -3625,635 +3799,100 @@ fn type_ref_to_python_type( // Never do string replacement in the type name itself return Ok(format!("{}[{}]", base_type, arg_types.join(", "))); } - - return Ok(base_type); - } - - // 4. FINAL: Final fallback for undefined external types - try to map to sensible Python types - let fallback_type = map_external_type_to_python(&type_ref.name); - Ok(fallback_type) -} - -// Wrapper for backwards compatibility - doesn't track TypeVars -fn type_ref_to_python_type_simple( - type_ref: &TypeReference, - schema: &Schema, - implemented_types: &BTreeMap, - active_generics: &[String], -) -> anyhow::Result { - let mut unused_type_vars = BTreeSet::new(); - type_ref_to_python_type( - type_ref, - schema, - implemented_types, - active_generics, - &mut unused_type_vars, - ) -} - -// Get type parameter names for a given type -fn get_type_parameters(type_name: &str) -> Vec<&'static str> { - match type_name { - "std::boxed::Box" => vec!["T"], - "std::sync::Arc" => vec!["T"], - "std::rc::Rc" => vec!["T"], - "std::vec::Vec" => vec!["T"], - "std::option::Option" => vec!["T"], - "reflectapi::Option" => vec!["T"], - "std::collections::HashMap" => vec!["K", "V"], - "std::collections::BTreeMap" => vec!["K", "V"], - "std::result::Result" => vec!["T", "E"], - _ => vec![], - } -} - -/// Safely replace generic parameter in a type string using word boundaries. -/// This prevents replacing partial matches like "T" in "DateTime". -fn safe_replace_generic_param(type_str: &str, param: &str, replacement: &str) -> String { - let mut result = String::new(); - let chars: Vec = type_str.chars().collect(); - let param_chars: Vec = param.chars().collect(); - let mut i = 0; - - while i < chars.len() { - // Check if we can match the parameter at this position - if i + param_chars.len() <= chars.len() { - let slice: Vec = chars[i..i + param_chars.len()].to_vec(); - if slice == param_chars { - // Check word boundaries: ensure it's not part of a larger identifier - let prev_is_boundary = - i == 0 || !chars[i - 1].is_alphanumeric() && chars[i - 1] != '_'; - let next_is_boundary = i + param_chars.len() == chars.len() - || (!chars[i + param_chars.len()].is_alphanumeric() - && chars[i + param_chars.len()] != '_'); - - if prev_is_boundary && next_is_boundary { - // Safe to replace - this is a whole word match - result.push_str(replacement); - i += param_chars.len(); - continue; - } - } - } - - // No match or not a word boundary - copy the character - result.push(chars[i]); - i += 1; - } - - result -} - -/// Check if the schema uses datetime types (chrono) -fn check_datetime_usage(schema: &Schema, all_type_names: &[String]) -> bool { - // Check all types for datetime usage - for type_name in all_type_names { - if let Some(type_def) = schema.get_type(type_name) { - if type_uses_datetime(type_def) { - return true; - } - } - } - - // Check function parameters and return types - for function in schema.functions() { - if let Some(input_type) = &function.input_type { - if let Some(type_def) = schema.get_type(&input_type.name) { - if type_uses_datetime(type_def) { - return true; - } - } - } - - if let Some(output_type) = &function.output_type { - if let Some(type_def) = schema.get_type(&output_type.name) { - if type_uses_datetime(type_def) { - return true; - } - } - } - } - - false -} - -/// Check if the schema uses UUID types -fn check_uuid_usage(schema: &Schema, all_type_names: &[String]) -> bool { - // Check all types for UUID usage - for type_name in all_type_names { - if let Some(type_def) = schema.get_type(type_name) { - if type_uses_uuid(type_def) { - return true; - } - } - } - - // Check function parameters and return types - for function in schema.functions() { - if let Some(input_type) = &function.input_type { - if let Some(type_def) = schema.get_type(&input_type.name) { - if type_uses_uuid(type_def) { - return true; - } - } - } - - if let Some(output_type) = &function.output_type { - if let Some(type_def) = schema.get_type(&output_type.name) { - if type_uses_uuid(type_def) { - return true; - } - } - } - } - - false -} - -/// Check if the schema uses timedelta types (std::time::Duration) -fn check_timedelta_usage(schema: &Schema, all_type_names: &[String]) -> bool { - // Check all types for timedelta usage - for type_name in all_type_names { - if let Some(type_def) = schema.get_type(type_name) { - if type_uses_timedelta(type_def) { - return true; - } - } - } - - // Check function parameters and return types - for function in schema.functions() { - if let Some(input_type) = &function.input_type { - if let Some(type_def) = schema.get_type(&input_type.name) { - if type_uses_timedelta(type_def) { - return true; - } - } - } - - if let Some(output_type) = &function.output_type { - if let Some(type_def) = schema.get_type(&output_type.name) { - if type_uses_timedelta(type_def) { - return true; - } - } - } - } - - false -} - -/// Collect all unique generic parameter names from the schema -/// Check if the schema uses date types (chrono::NaiveDate) -fn check_date_usage(schema: &Schema, all_type_names: &[String]) -> bool { - // Check all types for date usage - for type_name in all_type_names { - if let Some(type_def) = schema.get_type(type_name) { - if type_uses_date(type_def) { - return true; - } - } - } - - // Check function parameters and return types - for function in schema.functions() { - if let Some(input_type) = &function.input_type { - if let Some(type_def) = schema.get_type(&input_type.name) { - if type_uses_date(type_def) { - return true; - } - } - } - - if let Some(output_type) = &function.output_type { - if let Some(type_def) = schema.get_type(&output_type.name) { - if type_uses_date(type_def) { - return true; - } - } - } - } - - false -} - -/// Check if a specific type definition uses datetime -fn type_uses_datetime(type_def: &reflectapi_schema::Type) -> bool { - match type_def { - reflectapi_schema::Type::Struct(struct_def) => { - for field in struct_def.fields.iter() { - if type_ref_uses_datetime(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Type::Enum(enum_def) => { - for variant in &enum_def.variants { - match &variant.fields { - reflectapi_schema::Fields::Named(fields) => { - for field in fields { - if type_ref_uses_datetime(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Fields::Unnamed(fields) => { - for field in fields { - if type_ref_uses_datetime(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Fields::None => {} - } - } - } - reflectapi_schema::Type::Primitive(_) => { - // Primitive types don't contain datetime fields - return false; - } - } - false -} - -/// Check if a type reference uses datetime -fn type_ref_uses_datetime(type_ref: &TypeReference) -> bool { - // Check for chrono types that map to datetime - if type_ref.name == "chrono::DateTime" || type_ref.name == "chrono::NaiveDateTime" { - return true; - } - - // Check generic arguments recursively - for param in &type_ref.arguments { - if type_ref_uses_datetime(param) { - return true; - } - } - - false -} - -/// Check if a specific type definition uses UUID -fn type_uses_uuid(type_def: &reflectapi_schema::Type) -> bool { - match type_def { - reflectapi_schema::Type::Struct(struct_def) => { - for field in struct_def.fields.iter() { - if type_ref_uses_uuid(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Type::Enum(enum_def) => { - for variant in &enum_def.variants { - match &variant.fields { - reflectapi_schema::Fields::Named(fields) => { - for field in fields { - if type_ref_uses_uuid(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Fields::Unnamed(fields) => { - for field in fields { - if type_ref_uses_uuid(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Fields::None => {} - } - } - } - reflectapi_schema::Type::Primitive(_) => { - return false; - } - } - false -} - -fn type_ref_uses_uuid(type_ref: &TypeReference) -> bool { - // Check for UUID types - if type_ref.name == "uuid::Uuid" { - return true; - } - - // Check generic arguments recursively - for param in &type_ref.arguments { - if type_ref_uses_uuid(param) { - return true; - } - } - - false -} - -/// Check if a specific type definition uses timedelta -fn type_uses_timedelta(type_def: &reflectapi_schema::Type) -> bool { - match type_def { - reflectapi_schema::Type::Struct(struct_def) => { - for field in struct_def.fields.iter() { - if type_ref_uses_timedelta(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Type::Enum(enum_def) => { - for variant in &enum_def.variants { - match &variant.fields { - reflectapi_schema::Fields::Named(fields) => { - for field in fields { - if type_ref_uses_timedelta(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Fields::Unnamed(fields) => { - for field in fields { - if type_ref_uses_timedelta(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Fields::None => {} - } - } - } - reflectapi_schema::Type::Primitive(_) => { - return false; - } - } - false -} - -fn type_ref_uses_timedelta(type_ref: &TypeReference) -> bool { - // Check for Duration types that map to timedelta - if type_ref.name == "std::time::Duration" { - return true; - } - - // Check generic arguments recursively - for param in &type_ref.arguments { - if type_ref_uses_timedelta(param) { - return true; - } - } - - false -} - -/// Check if a specific type definition uses date -fn type_uses_date(type_def: &reflectapi_schema::Type) -> bool { - match type_def { - reflectapi_schema::Type::Struct(struct_def) => { - for field in struct_def.fields.iter() { - if type_ref_uses_date(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Type::Enum(enum_def) => { - for variant in &enum_def.variants { - match &variant.fields { - reflectapi_schema::Fields::Named(fields) => { - for field in fields { - if type_ref_uses_date(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Fields::Unnamed(fields) => { - for field in fields { - if type_ref_uses_date(&field.type_ref) { - return true; - } - } - } - reflectapi_schema::Fields::None => {} - } - } - } - reflectapi_schema::Type::Primitive(_) => { - return false; - } - } - false -} - -fn type_ref_uses_date(type_ref: &TypeReference) -> bool { - // Check for date types - if type_ref.name == "chrono::NaiveDate" { - return true; - } - - // Check generic arguments recursively - for param in &type_ref.arguments { - if type_ref_uses_date(param) { - return true; - } - } - - false -} - -/// Check if the schema uses reflectapi::Option anywhere -fn schema_uses_reflectapi_option(schema: &Schema, all_type_names: &Vec) -> bool { - // Check all types in the schema for reflectapi::Option usage - for type_name in all_type_names { - if let Some(type_def) = schema.get_type(type_name) { - if type_uses_reflectapi_option(type_def) { - return true; - } - } - } - false -} - -/// Check if the schema uses a specific type anywhere -fn schema_uses_type(schema: &Schema, all_type_names: &Vec, target_type: &str) -> bool { - // Check if the type is directly mentioned in the schema - if all_type_names.contains(&target_type.to_string()) { - return true; - } - - // Check all types in the schema for usage of the target type - for type_name in all_type_names { - if let Some(type_def) = schema.get_type(type_name) { - if type_references_type(type_def, target_type) { - return true; - } - } - } - false -} - -/// Check if a type definition references a specific type -fn type_references_type(type_def: &reflectapi_schema::Type, target_type: &str) -> bool { - match type_def { - reflectapi_schema::Type::Struct(s) => { - // Check all fields in the struct - for field in s.fields.iter() { - if type_reference_references_type(&field.type_ref, target_type) { - return true; - } - } - } - reflectapi_schema::Type::Enum(e) => { - // Check all variant fields in the enum - for variant in &e.variants { - match &variant.fields { - reflectapi_schema::Fields::Named(fields) => { - for field in fields { - if type_reference_references_type(&field.type_ref, target_type) { - return true; - } - } - } - reflectapi_schema::Fields::Unnamed(fields) => { - for field in fields { - if type_reference_references_type(&field.type_ref, target_type) { - return true; - } - } - } - reflectapi_schema::Fields::None => {} - } - } - } - reflectapi_schema::Type::Primitive(_) => { - // Primitives don't reference other types - } + + return Ok(base_type); } - false + + // 4. FINAL: Final fallback for undefined external types - try to map to sensible Python types + let fallback_type = map_external_type_to_python(&type_ref.name); + Ok(fallback_type) } -/// Check if a type reference references a specific type -fn type_reference_references_type(type_ref: &TypeReference, target_type: &str) -> bool { - // Check if this is directly the target type - if type_ref.name == target_type { - return true; - } +// Wrapper for backwards compatibility - doesn't track TypeVars +fn type_ref_to_python_type_simple( + type_ref: &TypeReference, + schema: &Schema, + implemented_types: &BTreeMap, + active_generics: &[String], +) -> anyhow::Result { + let mut unused_type_vars = BTreeSet::new(); + type_ref_to_python_type( + type_ref, + schema, + implemented_types, + active_generics, + &mut unused_type_vars, + ) +} - // Check recursively in type arguments - type_ref - .arguments - .iter() - .any(|arg| type_reference_references_type(arg, target_type)) +// Get type parameter names for a given type +fn get_type_parameters(type_name: &str) -> Vec<&'static str> { + match type_name { + "std::boxed::Box" => vec!["T"], + "std::sync::Arc" => vec!["T"], + "std::rc::Rc" => vec!["T"], + "std::vec::Vec" => vec!["T"], + "std::option::Option" => vec!["T"], + "reflectapi::Option" => vec!["T"], + "std::collections::HashMap" => vec!["K", "V"], + "std::collections::BTreeMap" => vec!["K", "V"], + "indexmap::IndexMap" => vec!["K", "V"], + "std::result::Result" => vec!["T", "E"], + _ => vec![], + } } -/// Recursively check if a type uses reflectapi::Option -fn type_uses_reflectapi_option(type_def: &reflectapi_schema::Type) -> bool { - match type_def { - reflectapi_schema::Type::Struct(s) => { - // Check all fields in the struct - for field in s.fields.iter() { - if type_reference_uses_reflectapi_option(&field.type_ref) { - return true; +/// Safely replace generic parameter in a type string using word boundaries. +/// This prevents replacing partial matches like "T" in "DateTime". +fn safe_replace_generic_param(type_str: &str, param: &str, replacement: &str) -> String { + let mut result = String::new(); + let chars: Vec = type_str.chars().collect(); + let param_chars: Vec = param.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Check if we can match the parameter at this position + if i + param_chars.len() <= chars.len() { + let slice: Vec = chars[i..i + param_chars.len()].to_vec(); + if slice == param_chars { + // Check word boundaries: ensure it's not part of a larger identifier + let prev_is_boundary = + i == 0 || !chars[i - 1].is_alphanumeric() && chars[i - 1] != '_'; + let next_is_boundary = i + param_chars.len() == chars.len() + || (!chars[i + param_chars.len()].is_alphanumeric() + && chars[i + param_chars.len()] != '_'); + + if prev_is_boundary && next_is_boundary { + // Safe to replace - this is a whole word match + result.push_str(replacement); + i += param_chars.len(); + continue; } } } - reflectapi_schema::Type::Enum(_) => { - // Enums don't contain reflectapi::Option - } - reflectapi_schema::Type::Primitive(_) => { - // Primitives don't contain reflectapi::Option - } - } - false -} -/// Check if a type reference uses reflectapi::Option -fn type_reference_uses_reflectapi_option(type_ref: &TypeReference) -> bool { - // Check if this is directly a reflectapi::Option - if type_ref.name == "reflectapi::Option" { - return true; + // No match or not a word boundary - copy the character + result.push(chars[i]); + i += 1; } - // Recursively check type arguments - type_ref - .arguments - .iter() - .any(type_reference_uses_reflectapi_option) + result } fn build_implemented_types() -> BTreeMap { let mut types = BTreeMap::new(); - // Primitive types - types.insert("i8".to_string(), "int".to_string()); - types.insert("i16".to_string(), "int".to_string()); - types.insert("i32".to_string(), "int".to_string()); - types.insert("i64".to_string(), "int".to_string()); - types.insert("u8".to_string(), "int".to_string()); - types.insert("u16".to_string(), "int".to_string()); - types.insert("u32".to_string(), "int".to_string()); - types.insert("u64".to_string(), "int".to_string()); - types.insert("f32".to_string(), "float".to_string()); - types.insert("f64".to_string(), "float".to_string()); - types.insert("bool".to_string(), "bool".to_string()); - types.insert("String".to_string(), "str".to_string()); - types.insert("std::string::String".to_string(), "str".to_string()); - - // Collections with full Rust type names (using modern lowercase hints for Python 3.9+) - types.insert("std::vec::Vec".to_string(), "list[T]".to_string()); - types.insert( - "std::collections::HashMap".to_string(), - "dict[K, V]".to_string(), - ); - types.insert( - "std::collections::BTreeMap".to_string(), - "dict[K, V]".to_string(), - ); - types.insert("std::option::Option".to_string(), "T | None".to_string()); - types.insert("std::result::Result".to_string(), "T | E".to_string()); - - // ReflectAPI specific types - types.insert( - "reflectapi::Option".to_string(), - "ReflectapiOption[T]".to_string(), - ); - types.insert( - "reflectapi::Empty".to_string(), - "ReflectapiEmpty".to_string(), - ); - types.insert( - "reflectapi::Infallible".to_string(), - "ReflectapiInfallible".to_string(), - ); - - // Date/time types - types.insert("chrono::DateTime".to_string(), "datetime".to_string()); - types.insert("chrono::NaiveDateTime".to_string(), "datetime".to_string()); - types.insert("chrono::NaiveDate".to_string(), "date".to_string()); - types.insert("uuid::Uuid".to_string(), "UUID".to_string()); - - // Rust std library types that need proper Python equivalents - types.insert("std::time::Duration".to_string(), "timedelta".to_string()); - types.insert("std::path::PathBuf".to_string(), "Path".to_string()); - types.insert("std::path::Path".to_string(), "Path".to_string()); - types.insert( - "std::net::IpAddr".to_string(), - "IPv4Address | IPv6Address".to_string(), - ); - types.insert("std::net::Ipv4Addr".to_string(), "IPv4Address".to_string()); - types.insert("std::net::Ipv6Addr".to_string(), "IPv6Address".to_string()); - - // Special tuple/unit types - types.insert("std::tuple::Tuple0".to_string(), "None".to_string()); - - // Common serde types - types.insert("serde_json::Value".to_string(), "Any".to_string()); - - // Smart pointer types (transparent - map to their inner type) - types.insert("std::boxed::Box".to_string(), "T".to_string()); - types.insert("std::sync::Arc".to_string(), "T".to_string()); - types.insert("std::rc::Rc".to_string(), "T".to_string()); + for (&name, mapping) in python_type_mappings() { + types.insert(name.to_string(), mapping.type_hint.to_string()); + } - // External Rust types commonly found in ReflectAPI schemas + // Compatibility fallback for legacy/unannotated schemas that use + // mangled Python class names instead of qualified Rust type names. types.insert("StdNumNonZeroU32".to_string(), "int".to_string()); types.insert("StdNumNonZeroU64".to_string(), "int".to_string()); types.insert("StdNumNonZeroI32".to_string(), "int".to_string()); types.insert("StdNumNonZeroI64".to_string(), "int".to_string()); - types.insert("RustDecimalDecimal".to_string(), "str".to_string()); // JSON representation + types.insert("RustDecimalDecimal".to_string(), "str".to_string()); types } @@ -4559,14 +4198,9 @@ pub mod templates { pub has_sync: bool, pub has_testing: bool, pub has_enums: bool, - pub has_reflectapi_option: bool, - pub has_reflectapi_empty: bool, - pub has_reflectapi_infallible: bool, pub has_warnings: bool, - pub has_datetime: bool, - pub has_uuid: bool, - pub has_timedelta: bool, - pub has_date: bool, + pub extra_stdlib_imports: Vec, + pub extra_runtime_imports: Vec, pub has_generics: bool, pub has_annotated: bool, pub has_literal: bool, @@ -4597,11 +4231,7 @@ pub mod templates { let desc = super::sanitize_for_docstring(desc); if !desc.is_empty() { writeln!(s, " \"\"\"{desc}\"\"\"").unwrap(); - } else { - writeln!(s, " \"\"\"Generated data model.\"\"\"").unwrap(); } - } else { - writeln!(s, " \"\"\"Generated data model.\"\"\"").unwrap(); } writeln!(s).unwrap(); writeln!( @@ -4667,11 +4297,7 @@ pub mod templates { let desc = super::sanitize_for_docstring(desc); if !desc.is_empty() { writeln!(s, " \"\"\"{desc}\"\"\"").unwrap(); - } else { - writeln!(s, " \"\"\"Generated enum.\"\"\"").unwrap(); } - } else { - writeln!(s, " \"\"\"Generated enum.\"\"\"").unwrap(); } writeln!(s).unwrap(); if self.variants.is_empty() { @@ -5601,21 +5227,21 @@ pub mod templates { } } - pub struct ExternallyTaggedEnumRootModel { + pub struct ExternallyTaggedEnumCompact { pub name: String, pub description: Option, pub variant_models: Vec, pub union_variants: String, pub is_single_variant: bool, - pub instance_validator_cases: String, - pub validator_cases: String, - pub dict_validator_cases: String, - pub serializer_cases: String, + pub variant_entries: Vec, + pub serializer_entries: Vec, + /// Class names of non-unit variant types (for isinstance checks on direct instances) + pub variant_class_names: Vec, pub is_generic: bool, pub generic_params: Vec, } - impl ExternallyTaggedEnumRootModel { + impl ExternallyTaggedEnumCompact { pub fn render(&self) -> String { let mut s = String::new(); for variant_model in &self.variant_models { @@ -5654,260 +5280,82 @@ pub mod templates { writeln!(s, " return cls").unwrap(); writeln!(s).unwrap(); } + + // Emit compact model_validator using helper writeln!(s, " @model_validator(mode='before')").unwrap(); writeln!(s, " @classmethod").unwrap(); - writeln!(s, " def _validate_externally_tagged(cls, data):").unwrap(); - writeln!( - s, - " # Handle direct variant instances (for programmatic creation)" - ) - .unwrap(); - writeln!(s, "{}", self.instance_validator_cases).unwrap(); - writeln!(s).unwrap(); - writeln!(s, " # Handle JSON data (for deserialization)").unwrap(); - writeln!(s, "{}", self.validator_cases).unwrap(); - writeln!(s).unwrap(); - writeln!(s, " if isinstance(data, dict):").unwrap(); - writeln!(s, " if len(data) != 1:").unwrap(); - writeln!( - s, - " raise ValueError(\"Externally tagged enum must have exactly one key\")" - ) - .unwrap(); - writeln!(s).unwrap(); - writeln!(s, " key, value = next(iter(data.items()))").unwrap(); - writeln!(s, "{}", self.dict_validator_cases).unwrap(); - writeln!(s).unwrap(); - writeln!( - s, - " raise ValueError(f\"Unknown variant for {}: {{data}}\")", - self.name - ) - .unwrap(); + writeln!(s, " def _validate(cls, data):").unwrap(); + write!(s, " return _parse_externally_tagged(data, {{").unwrap(); + for (i, entry) in self.variant_entries.iter().enumerate() { + if i > 0 { + write!(s, ", ").unwrap(); + } + write!(s, "{entry}").unwrap(); + } + // Build the types tuple for isinstance checks on direct variant instances + let types_tuple = if self.variant_class_names.is_empty() { + "()".to_string() + } else if self.variant_class_names.len() == 1 { + format!("({},)", self.variant_class_names[0]) + } else { + format!("({})", self.variant_class_names.join(", ")) + }; + writeln!(s, "}}, {types_tuple}, \"{name}\")", name = self.name).unwrap(); writeln!(s).unwrap(); + + // Emit compact model_serializer using helper writeln!(s, " @model_serializer").unwrap(); - writeln!(s, " def _serialize_externally_tagged(self):").unwrap(); - writeln!(s, "{}", self.serializer_cases).unwrap(); - writeln!(s).unwrap(); - writeln!( + writeln!(s, " def _serialize(self):").unwrap(); + write!( s, - " raise ValueError(f\"Cannot serialize {} variant: {{type(self.root)}}\")", - self.name + " return _serialize_externally_tagged(self.root, {{" ) .unwrap(); - s - } - } - - pub struct GenericExternallyTaggedEnumApproachB { - pub name: String, - pub description: Option, - pub variant_models: Vec, - pub union_variants: String, - pub is_single_variant: bool, - pub instance_validator_cases: String, - pub validator_cases: String, - pub dict_validator_cases: String, - pub serializer_cases: String, - pub is_generic: bool, - pub generic_params: Vec, - pub variant_info_list: Vec, - } - - impl GenericExternallyTaggedEnumApproachB { - pub fn render(&self) -> String { - let mut s = String::new(); - if self.is_generic { - writeln!(s).unwrap(); - writeln!( - s, - "# Generic externally tagged enum using Approach B: Generic Variant Models" - ) - .unwrap(); - // TypeVar definitions are emitted once at the top of the file; - // inline declarations are suppressed to avoid collisions with class names. - writeln!(s).unwrap(); - writeln!(s, "# Common non-generic base class with discriminator").unwrap(); - writeln!(s, "class {}Base(BaseModel):", self.name).unwrap(); - writeln!( - s, - " \"\"\"Base class for {} variants with shared discriminator.\"\"\"", - self.name - ) - .unwrap(); - writeln!(s, " _kind: str = PrivateAttr()").unwrap(); - writeln!(s).unwrap(); - for variant_model in &self.variant_models { - writeln!(s, "{variant_model}").unwrap(); - writeln!(s).unwrap(); - } - writeln!(s).unwrap(); - let params = self.generic_params.join(", "); - writeln!( - s, - "# Type alias for parameterized union - users can create specific unions as needed" - ) - .unwrap(); - writeln!( - s, - "# Example: {}[SomeType, AnotherType] = Union[{}Variant1[SomeType], {}Variant2[AnotherType]]", - self.name, self.name, self.name - ) - .unwrap(); - writeln!(s, "class {}(Generic[{}]):", self.name, params).unwrap(); - let desc = super::sanitize_for_docstring( - self.description - .as_deref() - .unwrap_or("Generic externally tagged enum using Approach B"), - ); - writeln!(s, " \"\"\"{desc}").unwrap(); - writeln!(s).unwrap(); - writeln!( - s, - " This is a generic enum where each variant is a separate generic class." - ) - .unwrap(); - writeln!( - s, - " To create a specific instance, use the variant classes directly." - ) - .unwrap(); - writeln!( - s, - " To create a union type, use Union[VariantClass[Type1], OtherVariant[Type2]]." - ) - .unwrap(); - writeln!(s, " \"\"\"").unwrap(); - writeln!(s).unwrap(); - writeln!(s, " @classmethod").unwrap(); - writeln!(s, " def __class_getitem__(cls, params):").unwrap(); - writeln!( - s, - " \"\"\"Create documentation about parameterized types.\"\"\"" - ) - .unwrap(); - writeln!(s, " if not isinstance(params, tuple):").unwrap(); - writeln!(s, " params = (params,)").unwrap(); - writeln!( - s, - " if len(params) != {}:", - self.generic_params.len() - ) - .unwrap(); - writeln!( - s, - " raise TypeError(f\"Expected {} type parameters, got {{len(params)}}\")", - self.generic_params.len() - ) - .unwrap(); - writeln!(s).unwrap(); - writeln!( - s, - " # For Approach B, users should create unions directly using variant classes" - ) - .unwrap(); - writeln!(s, " # This method serves as documentation").unwrap(); - writeln!(s, " variant_examples = [").unwrap(); - for (i, variant_info) in self.variant_info_list.iter().enumerate() { - let generic_params_str = self.generic_params.join(", "); - if i < self.variant_info_list.len() - 1 { - writeln!( - s, - " \"{}[{}]\",", - variant_info.class_name, generic_params_str - ) - .unwrap(); - } else { - writeln!( - s, - " \"{}[{}]\"", - variant_info.class_name, generic_params_str - ) - .unwrap(); - } - } - writeln!(s, " ]").unwrap(); - writeln!(s).unwrap(); - writeln!( - s, - " # Return a helpful hint rather than NotImplementedError" - ) - .unwrap(); - writeln!( - s, - " return f\"Union[{{', '.join(variant_examples)}}] # Use this pattern to create specific unions\"" - ) - .unwrap(); - } else { - writeln!(s).unwrap(); - writeln!( - s, - "# Non-generic externally tagged enum - use existing RootModel approach" - ) - .unwrap(); - if self.is_single_variant { - writeln!(s, "{}Variants = {}", self.name, self.union_variants).unwrap(); - } else { - writeln!(s, "{}Variants = Union[{}]", self.name, self.union_variants).unwrap(); + for (i, entry) in self.serializer_entries.iter().enumerate() { + if i > 0 { + write!(s, ", ").unwrap(); } - writeln!(s, "class {}(RootModel[{}Variants]):", self.name, self.name).unwrap(); - let desc = super::sanitize_for_docstring( - self.description - .as_deref() - .unwrap_or("Externally tagged enum"), - ); - writeln!(s, " \"\"\"{desc}\"\"\"").unwrap(); - writeln!(s).unwrap(); - writeln!(s, " @model_validator(mode='before')").unwrap(); - writeln!(s, " @classmethod").unwrap(); - writeln!(s, " def _validate_externally_tagged(cls, data):").unwrap(); - writeln!( - s, - " # Handle direct variant instances (for programmatic creation)" - ) - .unwrap(); - writeln!(s, "{}", self.instance_validator_cases).unwrap(); - writeln!(s).unwrap(); - writeln!(s, " # Handle JSON data (for deserialization)").unwrap(); - writeln!(s, "{}", self.validator_cases).unwrap(); - writeln!(s).unwrap(); - writeln!(s, " if isinstance(data, dict):").unwrap(); - writeln!(s, " if len(data) != 1:").unwrap(); - writeln!( - s, - " raise ValueError(\"Externally tagged enum must have exactly one key\")" - ) - .unwrap(); - writeln!(s).unwrap(); - writeln!(s, " key, value = next(iter(data.items()))").unwrap(); - writeln!(s, "{}", self.dict_validator_cases).unwrap(); - writeln!(s).unwrap(); - writeln!( - s, - " raise ValueError(f\"Unknown variant for {}: {{data}}\")", - self.name - ) - .unwrap(); - writeln!(s).unwrap(); - writeln!(s, " @model_serializer").unwrap(); - writeln!(s, " def _serialize_externally_tagged(self):").unwrap(); - writeln!(s, "{}", self.serializer_cases).unwrap(); - writeln!(s).unwrap(); - writeln!( - s, - " raise ValueError(f\"Cannot serialize {} variant: {{type(self.root)}}\")", - self.name - ) - .unwrap(); + write!(s, "{entry}").unwrap(); } - writeln!(s).unwrap(); + writeln!(s, "}}, \"{name}\")", name = self.name).unwrap(); s } } +} - #[derive(Clone)] - pub struct VariantInfo { - pub class_name: String, - pub variant_name: String, +#[cfg(test)] +mod tests { + use super::{build_python_class_name_map, generate_init_py, Config}; + + #[test] + fn python_init_exports_client() { + let init_py = generate_init_py(&Config::default()); + assert!(init_py.contains("from .generated import AsyncClient, Client")); + assert!(init_py.contains("__all__ = [\"AsyncClient\", \"Client\"]")); + assert!(!init_py.contains("SyncClient")); + } + + #[test] + fn destutter_falls_back_on_collision() { + let class_names = build_python_class_name_map([ + "OfferRequestPartIdentity", + "offer_request::OfferRequestPartIdentity", + "system::SystemVersionInfo", + ]); + + assert_eq!( + class_names.get("OfferRequestPartIdentity").unwrap(), + "OfferRequestPartIdentity" + ); + assert_eq!( + class_names + .get("offer_request::OfferRequestPartIdentity") + .unwrap(), + "OfferRequestOfferRequestPartIdentity" + ); + assert_eq!( + class_names.get("system::SystemVersionInfo").unwrap(), + "SystemVersionInfo" + ); } } diff --git a/reflectapi/src/traits.rs b/reflectapi/src/traits.rs index e495ed7e..6ab001fa 100644 --- a/reflectapi/src/traits.rs +++ b/reflectapi/src/traits.rs @@ -701,7 +701,6 @@ fn reflectapi_duration(schema: &mut crate::Typespace) -> crate::TypeReference { let type_name = "std::time::Duration"; if schema.reserve_type(type_name) { let type_def = crate::Struct { - id: Default::default(), name: type_name.into(), description: "Time duration type".into(), fields: crate::Fields::Named(vec![