diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c9555391 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,100 @@ +# Changelog + +## 0.17.2 — transport-shape refactor + +The Rust, TypeScript, and Python clients now share a single `Request` DTO shape: `{ path, headers, body }`. The base URL lives on the transport, not on the generated `Interface`. + +### Rust + +The trait gained a `base_url` method and `Request::url` was renamed to `Request::path`: + +```rust +pub trait Client { + type Error; + fn base_url(&self) -> &Url; + fn request( + &self, + request: Request, + ) -> impl Future, Self::Error>>; +} + +pub struct Request { + pub path: String, // was: url: Url + pub headers: HeaderMap, + pub body: Bytes, +} +``` + +`reqwest::Client` no longer implements `Client` directly — it doesn't carry a base URL. Pick the path that matches your setup: + +**Plain `reqwest::Client`, generated crate has a `reqwest` feature.** The generated `Interface::try_new(reqwest::Client, base_url)` convenience constructor keeps working unchanged: + +```rust +let api = MyClient::try_new(reqwest::Client::new(), base_url)?; +``` + +The constructor is gated on the generated crate enabling its own `reqwest` feature (which should re-export `reflectapi/reqwest`). If you don't see `try_new`, this is why. + +**`reqwest_middleware::ClientWithMiddleware` (the common path with otel/retry/etc.).** Compose the transport explicitly — there's no convenience constructor for this case because of method-resolution ambiguity: + +```rust +let transport = reflectapi::rt::ReqwestMiddlewareClient::try_new( + mw_client, + base_url, +)?; +let api = MyClient::new(transport); +``` + +`ReqwestMiddlewareClient` is a type alias for `ReqwestClient`. + +**Custom transport.** Wrap with `ReqwestClient::try_new(...)` (or implement the `Client` trait yourself) and pass the result to `MyClient::new`: + +```rust +let api = MyClient::new( + reflectapi::rt::ReqwestClient::try_new(reqwest::Client::new(), base_url)? +); +``` + +`ReqwestClient::try_new` returns `Result` (a re-export of `url::ParseError`). It rejects URLs that can't have a base (`data:`, `mailto:`, etc.) via `Url::cannot_be_a_base`. There is no infallible `ReqwestClient::new` constructor. + +#### `reqwest-middleware` feature implies `reqwest` (alpha.4) + +In 0.17.2-alpha.3 enabling only `reqwest-middleware` failed to compile because the wrapper struct lives behind the `reqwest` feature. Fixed in 0.17.2-alpha.4: `reqwest-middleware` now implies `reqwest`. Consumers that already enable both don't need to change anything. + +### TypeScript (alpha.4) + +Codegen now emits two files: `generated.ts` (the API surface) and `generated.transport.ts` (the transport contract). Most consumers only need `generated.ts`. Custom transports import from the transport submodule: + +```ts +import type { Client, Request, Response } from './generated.transport'; +``` + +The bare `Request` / `Response` / `Headers` names live behind the `./generated.transport` import path, so they no longer shadow the DOM globals of the same name when imported from `generated.ts`. See #143 for the rationale. + +If your build tooling assumed a single generated file, update it to include the new sibling. Typical pnpm/npm workflows pick it up automatically (the file sits in the same directory). + +#### Library API: `typescript::generate` returns multiple files + +For consumers calling the codegen library directly rather than through the `reflectapi` CLI, the signature changed: + +```rust +// before +pub fn generate(schema: Schema, config: &Config) -> Result; + +// after +pub fn generate(schema: Schema, config: &Config) -> Result>; +``` + +The map is keyed by filename (`"generated.ts"`, `"generated.transport.ts"`). The CLI handles `--output .ts` paths by writing the matching file at the requested path and the sibling in the parent directory; downstream callers wrapping the library should do the same. + +### Python + +End-user generated clients are unchanged. Authors of custom middleware or transports should target the transport-agnostic types in `reflectapi_runtime.transport` (`Request`, `Response`, `Client`, `AsyncClient`) rather than reaching for `httpx` types directly. They are also re-exported from the top-level `reflectapi_runtime` package. + +#### Generic flatten correctness (alpha.4) + +Previously, a Rust struct that used `serde(flatten)` over a generic parameter (e.g. `IdentityData`, `UpdateOrElse`, `InsertManyOrElse`) generated a Pydantic model that silently dropped the inner type's wire fields, because the Python codegen couldn't resolve a TypeVar to a concrete struct at class-definition time. With `extra="ignore"` on the model, those fields were also discarded on parse — silent data loss for any endpoint using these patterns. + +Fix: the Python codegen now monomorphizes — for each concrete `(struct, args)` instantiation it emits a specialized class with the flatten resolved against the concrete type. The mangled name is `OriginalStruct_Arg1_Arg2…`, e.g. `UpdateOrElse_Pet_Conflict`. Method signatures, namespace classes, and `model_rebuild` lists all use the mangled name consistently. Rust and TypeScript clients are unaffected — they handled this case correctly already (serde at runtime; intersection types at compile time). + +The codegen also now hard-fails (rather than silently dropping fields) if it encounters an unresolved flatten target — a defence-in-depth check so any future regression is loud. diff --git a/Cargo.lock b/Cargo.lock index a846c0fa..80923ebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1692,7 +1692,7 @@ dependencies = [ [[package]] name = "reflectapi" -version = "0.17.2-alpha.3" +version = "0.17.2-alpha.4" dependencies = [ "anyhow", "axum", @@ -1721,7 +1721,7 @@ dependencies = [ [[package]] name = "reflectapi-cli" -version = "0.17.2-alpha.3" +version = "0.17.2-alpha.4" dependencies = [ "anyhow", "clap", @@ -1729,6 +1729,7 @@ dependencies = [ "reflectapi", "rouille", "serde_json", + "tempfile", ] [[package]] @@ -1751,6 +1752,7 @@ dependencies = [ "rmp-serde", "serde", "serde_json", + "tempfile", "tokio", "tower 0.4.13", "tower-http", @@ -1783,7 +1785,7 @@ dependencies = [ [[package]] name = "reflectapi-derive" -version = "0.17.2-alpha.3" +version = "0.17.2-alpha.4" dependencies = [ "proc-macro-error", "proc-macro2", @@ -1795,7 +1797,7 @@ dependencies = [ [[package]] name = "reflectapi-schema" -version = "0.17.2-alpha.3" +version = "0.17.2-alpha.4" dependencies = [ "glob", "serde", diff --git a/docs/src/clients/README.md b/docs/src/clients/README.md index bb581d9d..e3efbb28 100644 --- a/docs/src/clients/README.md +++ b/docs/src/clients/README.md @@ -6,7 +6,7 @@ | Output | Status | Notes | |--------|--------|-------| -| TypeScript | Stable | Single generated file | +| TypeScript | Stable | Two generated files: API surface + transport contract | | Rust | Stable | Single generated file | | Python | Experimental | Package-style output with `__init__.py` and `generated.py` | @@ -27,6 +27,7 @@ The CLI defaults to `reflectapi.json` if `--schema` is omitted. The demo project mkdir -p clients/typescript clients/python clients/rust # Generate TypeScript client -> clients/typescript/generated.ts +# and clients/typescript/generated.transport.ts cargo run --bin reflectapi -- codegen \ --language typescript \ --schema reflectapi.json \ @@ -54,7 +55,7 @@ The generators do not all emit the same file layout: | Output | Files written by the generator | |--------|--------------------------------| -| TypeScript | `generated.ts` | +| TypeScript | `generated.ts`, `generated.transport.ts` | | Rust | `generated.rs` | | Python | `__init__.py`, `generated.py` | @@ -64,6 +65,13 @@ The demo repository includes extra project scaffolding around some generated cli ### TypeScript +- Emits two files alongside each other: `generated.ts` (the API + surface — types, functions, the `client(base)` factory) and + `generated.transport.ts` (the transport contract — `Request`, + `Response`, `Headers`, `Client`, `RequestOptions`, `ClientInstance`). + The split keeps the bare DTO names from shadowing the DOM globals of + the same name when imported from `generated.ts`. Custom transports + import from `./generated.transport`. - 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. diff --git a/reflectapi-cli/Cargo.toml b/reflectapi-cli/Cargo.toml index e231d6f4..c950bcb1 100644 --- a/reflectapi-cli/Cargo.toml +++ b/reflectapi-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflectapi-cli" -version = "0.17.2-alpha.3" +version = "0.17.2-alpha.4" edition = "2021" default-run = "reflectapi" @@ -23,7 +23,7 @@ doc = false workspace = true [dependencies] -reflectapi = { path = "../reflectapi", version = "0.17.2-alpha.3", features = ["codegen"] } +reflectapi = { path = "../reflectapi", version = "0.17.2-alpha.4", features = ["codegen"] } rouille = "3" clap = { version = "4.5.3", features = ["derive"] } @@ -31,3 +31,6 @@ clap_derive = "4.5.3" anyhow = "1.0.81" serde_json = "1.0.114" + +[dev-dependencies] +tempfile = "3" diff --git a/reflectapi-cli/src/main.rs b/reflectapi-cli/src/main.rs index ef9acb53..253cd0fe 100644 --- a/reflectapi-cli/src/main.rs +++ b/reflectapi-cli/src/main.rs @@ -93,7 +93,7 @@ enum DocSubcommand { }, } -#[derive(ValueEnum, Clone, PartialEq)] +#[derive(ValueEnum, Clone, Debug, PartialEq)] enum Language { Typescript, Rust, @@ -154,19 +154,14 @@ fn main() -> anyhow::Result<()> { .context("Failed to parse schema file as JSON into reflectapi::Schema object")?; let files: std::collections::BTreeMap = match language { - Language::Typescript => { - let content = reflectapi::codegen::typescript::generate( - schema, - reflectapi::codegen::typescript::Config::default() - .format(format) - .typecheck(typecheck) - .include_tags(include_tags) - .exclude_tags(exclude_tags), - )?; - let mut files = std::collections::BTreeMap::new(); - files.insert("generated.ts".to_string(), content); - files - } + Language::Typescript => reflectapi::codegen::typescript::generate( + schema, + reflectapi::codegen::typescript::Config::default() + .format(format) + .typecheck(typecheck) + .include_tags(include_tags) + .exclude_tags(exclude_tags), + )?, Language::Rust => { let content = reflectapi::codegen::rust::generate( schema, @@ -208,9 +203,24 @@ fn main() -> anyhow::Result<()> { } }; + // The "main" emitted file per language. Used both for + // stdout selection (--output -) and for matching a + // file-shaped --output path against the codegen output. + let primary_filename = match language { + Language::Typescript => "generated.ts", + Language::Rust => "generated.rs", + Language::Python => "generated.py", + Language::Openapi => "openapi.json", + }; + if output == Some(std::path::PathBuf::from("-")) { - // For stdout, output the first/main file - if let Some(content) = files.values().next() { + // Print the language's primary file, not the + // alphabetically-first one — for TS that would be + // generated.transport.ts (sibling), for Python it + // would be __init__.py. + if let Some(content) = files.get(primary_filename) { + println!("{content}"); + } else if let Some(content) = files.values().next() { println!("{content}"); } return Ok(()); @@ -218,36 +228,62 @@ fn main() -> anyhow::Result<()> { let output_path = output.unwrap_or_else(|| std::path::PathBuf::from("./")); - // For single-file languages, write directly to the specified path if it's a file, - // or to default filename in the directory if it's a directory - // For multi-file languages (like Python), create directory and write multiple files - if files.len() == 1 { - let (filename, content) = files.iter().next().unwrap(); - let final_path = - if output_path.is_dir() || output_path.to_string_lossy().ends_with('/') { - output_path.join(filename) - } else { - output_path - }; - let mut file = std::fs::File::create(&final_path) - .context(format!("Failed to create file: {final_path:?}"))?; - file.write_all(content.as_bytes()) - .context(format!("Failed to write to file: {final_path:?}"))?; - } else { - // Multi-file: create directory and write all files + // Decide whether `output_path` names a single file or a + // directory. "File" means the path's filename matches one + // of the codegen-emitted filenames AND the path doesn't + // already exist as a directory; everything else is a + // directory (whether it exists yet or not). + // + // This matters for two cases: + // --output ./clients/python/ → directory, write all files inside + // --output ./generated.ts → file path: write generated.ts there + // and place siblings (e.g. + // generated.transport.ts) next to it + // --output ./brand-new-dir → fresh directory, create + write + let primary_name_in_path = output_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default(); + let looks_like_file = files.contains_key(primary_name_in_path) + && !output_path.is_dir() + && !output_path.to_string_lossy().ends_with('/'); + + if !looks_like_file { std::fs::create_dir_all(&output_path).context(format!( "Failed to create output directory: {output_path:?}" ))?; - - for (filename, content) in files { - let file_path = output_path.join(&filename); - let mut file = std::fs::File::create(&file_path) - .context(format!("Failed to create file: {file_path:?}"))?; - file.write_all(content.as_bytes()) - .context(format!("Failed to write to file: {file_path:?}"))?; + for (filename, content) in &files { + write_file(&output_path.join(filename), content)?; + } + } else { + let parent = parent_or_dot(&output_path); + std::fs::create_dir_all(&parent) + .context(format!("Failed to create output directory: {parent:?}"))?; + for (filename, content) in &files { + let dest = if filename == primary_name_in_path { + output_path.clone() + } else { + parent.join(filename) + }; + write_file(&dest, content)?; } } Ok(()) } } } + +fn parent_or_dot(path: &std::path::Path) -> std::path::PathBuf { + path.parent() + .filter(|p| !p.as_os_str().is_empty()) + .map(std::path::Path::to_path_buf) + .unwrap_or_else(|| std::path::PathBuf::from(".")) +} + +fn write_file(path: &std::path::Path, content: &str) -> anyhow::Result<()> { + let mut file = + std::fs::File::create(path).context(format!("Failed to create file: {path:?}"))?; + file.write_all(content.as_bytes()) + .context(format!("Failed to write to file: {path:?}"))?; + Ok(()) +} diff --git a/reflectapi-cli/tests/output_paths.rs b/reflectapi-cli/tests/output_paths.rs new file mode 100644 index 00000000..ccc662c0 --- /dev/null +++ b/reflectapi-cli/tests/output_paths.rs @@ -0,0 +1,153 @@ +//! Integration tests for `reflectapi codegen --output …` path handling. +//! +//! Multi-file codegen (TS, Python) needs to handle: +//! - existing directories +//! - fresh directories (path doesn't exist yet) +//! - file-shaped paths whose filename matches one of the emitted files +//! (siblings land in the parent directory) +//! - stdout via `--output -`, which must print the language's *primary* +//! file rather than the alphabetically-first one. + +use std::process::Command; + +fn cargo_bin() -> std::path::PathBuf { + let bin = env!("CARGO_BIN_EXE_reflectapi"); + std::path::PathBuf::from(bin) +} + +fn demo_schema() -> std::path::PathBuf { + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("reflectapi-demo") + .join("reflectapi.json") +} + +fn run(args: &[&str]) -> std::process::Output { + Command::new(cargo_bin()) + .args(args) + .output() + .expect("spawn reflectapi") +} + +#[test] +fn ts_output_into_fresh_directory() { + let tmp = tempfile::tempdir().unwrap(); + let target = tmp.path().join("brand-new-dir"); + let schema = demo_schema(); + let out = run(&[ + "codegen", + "--language", + "typescript", + "--schema", + schema.to_str().unwrap(), + "--output", + target.to_str().unwrap(), + ]); + assert!( + out.status.success(), + "exit={:?}\nstderr:\n{}", + out.status.code(), + String::from_utf8_lossy(&out.stderr) + ); + assert!(target.is_dir(), "expected fresh dir to be created"); + assert!(target.join("generated.ts").is_file()); + assert!(target.join("generated.transport.ts").is_file()); +} + +#[test] +fn python_output_into_fresh_directory() { + let tmp = tempfile::tempdir().unwrap(); + let target = tmp.path().join("python-client"); + let schema = demo_schema(); + let out = run(&[ + "codegen", + "--language", + "python", + "--schema", + schema.to_str().unwrap(), + "--output", + target.to_str().unwrap(), + ]); + assert!( + out.status.success(), + "exit={:?}\nstderr:\n{}", + out.status.code(), + String::from_utf8_lossy(&out.stderr) + ); + assert!(target.is_dir()); + assert!(target.join("generated.py").is_file()); + assert!(target.join("__init__.py").is_file()); +} + +#[test] +fn ts_output_to_file_path_writes_siblings() { + // --output …/generated.ts should still work; transport file lands + // alongside it in the parent directory. + let tmp = tempfile::tempdir().unwrap(); + let target = tmp.path().join("generated.ts"); + let schema = demo_schema(); + let out = run(&[ + "codegen", + "--language", + "typescript", + "--schema", + schema.to_str().unwrap(), + "--output", + target.to_str().unwrap(), + ]); + assert!( + out.status.success(), + "stderr:\n{}", + String::from_utf8_lossy(&out.stderr) + ); + assert!(target.is_file(), "primary file at requested path"); + assert!(tmp.path().join("generated.transport.ts").is_file()); +} + +#[test] +fn ts_stdout_emits_primary_file_not_transport() { + // --output - should print generated.ts (the API surface), not + // generated.transport.ts (the helper). BTreeMap ordering would + // pick the latter alphabetically. + let schema = demo_schema(); + let out = run(&[ + "codegen", + "--language", + "typescript", + "--schema", + schema.to_str().unwrap(), + "--output", + "-", + ]); + assert!(out.status.success()); + let stdout = String::from_utf8_lossy(&out.stdout); + // generated.ts contains the public `client` factory; the + // transport file does not. + assert!( + stdout.contains("export function client(") || stdout.contains("export function client "), + "expected stdout to be generated.ts (with the `client` factory). got:\n{stdout}", + ); +} + +#[test] +fn python_stdout_emits_generated_not_init() { + let schema = demo_schema(); + let out = run(&[ + "codegen", + "--language", + "python", + "--schema", + schema.to_str().unwrap(), + "--output", + "-", + ]); + assert!(out.status.success()); + let stdout = String::from_utf8_lossy(&out.stdout); + // __init__.py is a 5-line shim; generated.py contains the actual + // pydantic models / client classes. + assert!( + stdout.contains("AsyncClientBase") || stdout.contains("class "), + "expected stdout to be generated.py. got:\n{stdout}", + ); +} diff --git a/reflectapi-demo/Cargo.toml b/reflectapi-demo/Cargo.toml index f810ec89..4531e0a2 100644 --- a/reflectapi-demo/Cargo.toml +++ b/reflectapi-demo/Cargo.toml @@ -47,6 +47,7 @@ tower = "0.4.0" http-rest-file = "0.5.1" datatest-stable = "0.2.9" rmp-serde = "1.3.0" +tempfile = "3" [[test]] name = "run" diff --git a/reflectapi-demo/clients/typescript/generated.transport.ts b/reflectapi-demo/clients/typescript/generated.transport.ts new file mode 100644 index 00000000..2b8587da --- /dev/null +++ b/reflectapi-demo/clients/typescript/generated.transport.ts @@ -0,0 +1,49 @@ +// Transport contract. Lives in its own module so the bare names +// `Request` / `Response` / `Headers` don't shadow the DOM globals of +// the same name inside the main generated module — consumers import +// them only when they need to write a custom transport. +// +// Method is intentionally absent: every reflectapi endpoint is POST by +// design, so transports hardcode it; if that ever changes it's a +// wire-protocol break and clients regenerate. + +export interface RequestOptions { + signal?: AbortSignal; +} + +export interface Request { + path: string; + headers: Record; + body: Uint8Array; + signal?: AbortSignal; +} + +export interface Headers { + get(name: string): string | null; +} + +export interface Response { + status: number; + headers: Headers; + body: ReadableStream | null; +} + +export interface Client { + request(request: Request): Promise; +} + +export class ClientInstance { + constructor(private base: string) {} + + public request(request: Request): Promise { + return fetch(`${this.base}${request.path}`, { + method: "POST", + headers: request.headers, + // BodyInit accepts BufferSource but TS 5's generic ArrayBufferLike + // typing on Uint8Array trips a structural check; the runtime + // behaviour is correct. + body: request.body as unknown as BodyInit, + signal: request.signal, + }); + } +} diff --git a/reflectapi-demo/clients/typescript/generated.ts b/reflectapi-demo/clients/typescript/generated.ts index f1fda355..ba2c265a 100644 --- a/reflectapi-demo/clients/typescript/generated.ts +++ b/reflectapi-demo/clients/typescript/generated.ts @@ -8,33 +8,19 @@ export function client(base: string | Client): __definition.Interface { return __implementation.__client(base); } /* <----- */ -export interface RequestOptions { - signal?: AbortSignal; -} - -// Transport DTOs. Method is intentionally absent: every reflectapi -// endpoint is POST by design, so transports hardcode it; if that ever -// changes it's a wire-protocol break and clients regenerate. -export interface Request { - path: string; - headers: Record; - body: Uint8Array; - signal?: AbortSignal; -} - -export interface Headers { - get(name: string): string | null; -} - -export interface Response { - status: number; - headers: Headers; - body: ReadableStream | null; -} - -export interface Client { - request(request: Request): Promise; -} +// Transport contract lives in `./generated.transport` so the bare +// names Request/Response/Headers don't shadow the DOM globals here +// or in any consumer module that imports from this file. We pull +// them in under aliases so lib.ts itself can keep using DOM types. +import type { + Client, + RequestOptions, + Response as ClientResponse, +} from "./generated.transport"; +import { ClientInstance } from "./generated.transport"; + +export { ClientInstance }; +export type { Client, RequestOptions }; type IsAny = 0 extends 1 & T ? true : false; export type NullToEmptyObject = @@ -316,7 +302,7 @@ export async function __stream_request( } async function* __sse_to_async_iterable( - response: Response, + response: ClientResponse, options?: RequestOptions, ): AsyncIterable { const body = response.body; @@ -354,24 +340,11 @@ async function* __sse_to_async_iterable( } } -async function __read_response_body(response: Response): Promise { +async function __read_response_body(response: ClientResponse): Promise { if (!response.body) return ""; - // Hand decoding to the platform; new (global) Response wraps any + // Hand decoding to the platform; the global Response wraps any // ReadableStream and exposes the well-tested .text() path. - return await new (globalThis as any).Response(response.body).text(); -} - -class ClientInstance { - constructor(private base: string) {} - - public request(request: Request): Promise { - return (globalThis as any).fetch(`${this.base}${request.path}`, { - method: "POST", - headers: request.headers, - body: request.body, - signal: request.signal, - }); - } + return await new Response(response.body).text(); } type UnionToIntersection = ( diff --git a/reflectapi-demo/src/tests/assert.rs b/reflectapi-demo/src/tests/assert.rs index 0256bf8f..b135702b 100644 --- a/reflectapi-demo/src/tests/assert.rs +++ b/reflectapi-demo/src/tests/assert.rs @@ -60,15 +60,15 @@ fn codegen_rust(schema: reflectapi::Schema) -> String { } fn codegen_typescript(schema: reflectapi::Schema) -> String { - reflectapi::codegen::strip_boilerplate( - &reflectapi::codegen::typescript::generate( - schema, - reflectapi::codegen::typescript::Config::default() - .format(true) - .typecheck(std::env::var("CI").is_ok()), - ) - .unwrap(), + let files = reflectapi::codegen::typescript::generate( + schema, + reflectapi::codegen::typescript::Config::default() + .format(true) + .typecheck(std::env::var("CI").is_ok()), ) + .unwrap(); + // Snapshot the main file only; the transport file is schema-invariant. + reflectapi::codegen::strip_boilerplate(&files["generated.ts"]) } fn codegen_python(schema: reflectapi::Schema) -> String { @@ -204,15 +204,16 @@ macro_rules! assert_builder_snapshot { ) .unwrap(), ); - let typescript = reflectapi::codegen::strip_boilerplate( - &reflectapi::codegen::typescript::generate( + let typescript = { + let files = reflectapi::codegen::typescript::generate( schema.clone(), &reflectapi::codegen::typescript::Config::default() .format(true) .typecheck(true), ) - .unwrap(), - ); + .unwrap(); + reflectapi::codegen::strip_boilerplate(&files["generated.ts"]) + }; insta::assert_json_snapshot!(schema); insta::assert_snapshot!(typescript); insta::assert_snapshot!(rust); diff --git a/reflectapi-demo/src/tests/mod.rs b/reflectapi-demo/src/tests/mod.rs index 3cbb3b5a..bcf68ab2 100644 --- a/reflectapi-demo/src/tests/mod.rs +++ b/reflectapi-demo/src/tests/mod.rs @@ -62,7 +62,7 @@ fn write_rust_client() { #[test] fn write_typescript_client() { let (schema, _) = crate::builder().build().unwrap(); - let src = reflectapi::codegen::typescript::generate( + let files = reflectapi::codegen::typescript::generate( schema, reflectapi::codegen::typescript::Config::default() .format(true) @@ -70,14 +70,10 @@ fn write_typescript_client() { ) .unwrap(); - std::fs::write( - format!( - "{}/clients/typescript/generated.ts", - env!("CARGO_MANIFEST_DIR"), - ), - src, - ) - .unwrap(); + let dir = format!("{}/clients/typescript", env!("CARGO_MANIFEST_DIR")); + for (filename, content) in files { + std::fs::write(format!("{dir}/{filename}"), content).unwrap(); + } } #[test] diff --git a/reflectapi-demo/src/tests/serde.rs b/reflectapi-demo/src/tests/serde.rs index c0cbd3e5..7e7ba4dc 100644 --- a/reflectapi-demo/src/tests/serde.rs +++ b/reflectapi-demo/src/tests/serde.rs @@ -1194,3 +1194,375 @@ fn test_empty_enum() { enum Never {} assert_snapshot!(Never); } + +// Reproducer: a generic wrapper that flattens its generic parameter. +// Pattern used in real consumer code (e.g. UpdateOrElse). +// +// Wire format with serde(flatten) on `inner: T` is: T's fields ⊕ if_field +// at the top level. Python codegen previously dropped T's fields entirely +// because schema.get_type("T") returns None for a TypeVar. +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +struct TestFlattenInner { + inner_a: u32, + inner_b: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +struct TestFlattenInnerAlt { + alt_x: bool, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +struct TestFlattenIfElse { + code: u16, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +#[serde(bound( + serialize = "T: serde::Serialize, C: serde::Serialize", + deserialize = "T: serde::de::DeserializeOwned, C: serde::de::DeserializeOwned", +))] +struct TestUpdateOrElse { + #[serde(flatten)] + inner: T, + if_else: Option, +} + +// Two endpoints sharing the same generic wrapper but with different +// instantiations. Each must produce its own monomorphized class. +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +struct TestTwoInstantiations { + a: TestUpdateOrElse, + b: TestUpdateOrElse, +} + +// Optional-wrapped flatten of a generic param: serde unwraps Option in +// flatten position, so wire shape = T's fields ⊕ if_else (but T may +// be missing entirely). Verify codegen still expands T's fields. +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +#[serde(bound( + serialize = "T: serde::Serialize", + deserialize = "T: serde::de::DeserializeOwned", +))] +struct TestOptionalFlatten { + #[serde(flatten, default = "Option::default")] + inner: Option, + code: u16, +} + +#[test] +fn test_generic_flatten_drops_inner_fields() { + assert_snapshot!(TestUpdateOrElse); +} + +#[test] +fn test_generic_flatten_two_instantiations() { + assert_snapshot!(TestTwoInstantiations); +} + +#[test] +fn test_generic_flatten_optional() { + assert_snapshot!(TestOptionalFlatten); +} + +// Real-world pattern: an outer generic wrapper flattens an *inner* +// generic wrapper. UpdateOrElse, C> where +// IdentityData itself is a marked struct (flattens its own generic +// I and D). Codegen must monomorphize bottom-up. +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +struct TestFlattenIdent { + job_id: u64, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +struct TestFlattenIdentData { + payload: String, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +#[serde(bound( + serialize = "I: serde::Serialize, D: serde::Serialize", + deserialize = "I: serde::de::DeserializeOwned, D: serde::de::DeserializeOwned", +))] +struct TestIdentityData { + #[serde(flatten)] + identity: I, + #[serde(flatten)] + data: D, +} + +#[test] +fn test_generic_flatten_nested() { + assert_snapshot!( + TestUpdateOrElse< + TestIdentityData, + TestFlattenIfElse, + > + ); +} + +/// End-to-end Pydantic round-trip: confirms the generated class +/// actually parses serde's wire format. Skipped if `uv` (or a Python +/// with pydantic) isn't available locally — CI uses uv. +#[test] +fn test_generic_flatten_pydantic_roundtrip() { + use std::io::Write; + + let py_source = + super::into_python_code::>(); + + // Wire format that serde would actually produce / accept: T's + // fields ⊕ if_else, all at top level. + let wire_payload = + serde_json::to_string(&TestUpdateOrElse:: { + inner: TestFlattenInner { + inner_a: 7, + inner_b: "hello".into(), + }, + if_else: Some(TestFlattenIfElse { code: 409 }), + }) + .unwrap(); + + let tmp = tempfile::tempdir().unwrap(); + let module_path = tmp.path().join("generated.py"); + std::fs::write(&module_path, py_source).unwrap(); + + // The mangled class name depends on namespace and length-budget + // hashing, so the test discovers it by walking the generated + // namespace and matching on the expected field set. + let driver = format!( + r#" +import importlib.util, json +from pydantic import BaseModel +spec = importlib.util.spec_from_file_location("gen", r"{module}") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +ns = m.reflectapi_demo.tests.serde +expected = {{"inner_a", "inner_b", "if_else"}} +candidates = [ + getattr(ns, attr) + for attr in dir(ns) + if isinstance(getattr(ns, attr, None), type) + and issubclass(getattr(ns, attr), BaseModel) + and set(getattr(ns, attr).model_fields.keys()) == expected +] +assert len(candidates) == 1, ( + "expected exactly one class with fields {{inner_a, inner_b, if_else}}; " + "got " + repr([c.__name__ for c in candidates]) +) +cls = candidates[0] +parsed = cls.model_validate(json.loads(r'''{payload}''')) +assert parsed.inner_a == 7, ("inner_a", parsed.inner_a) +assert parsed.inner_b == "hello", ("inner_b", parsed.inner_b) +assert parsed.if_else is not None and parsed.if_else.code == 409, ("if_else", parsed.if_else) +# Round-trip back through json: model_dump returns a dict; serialize and +# verify the wire format still has the flatten shape (T's fields at top). +out = parsed.model_dump(by_alias=True, exclude_none=False) +assert out["inner_a"] == 7 +assert out["inner_b"] == "hello" +assert out["if_else"]["code"] == 409 +print("OK") +"#, + module = module_path.display(), + payload = wire_payload, + ); + + // Prefer `uv run python` from the python-runtime workspace (which + // declares pydantic as a dep). Fall back to bare `python3` if it + // already has pydantic on its path. Skip the test only if neither + // works — CI installs uv so it'll always exercise this. + let runtime_dir = format!( + "{}/../reflectapi-python-runtime", + env!("CARGO_MANIFEST_DIR") + ); + + let mut attempts: Vec<(String, std::process::Command)> = Vec::new(); + { + let mut c = std::process::Command::new("uv"); + c.args(["run", "--directory", &runtime_dir, "python", "-"]); + attempts.push(("uv run python".to_string(), c)); + } + if std::process::Command::new("python3") + .args(["-c", "import pydantic"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + { + let mut c = std::process::Command::new("python3"); + c.args(["-"]); + attempts.push(("system python3+pydantic".to_string(), c)); + } + + for (label, mut cmd) in attempts { + let spawn = cmd + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn(); + let mut child = match spawn { + Ok(c) => c, + Err(e) => { + eprintln!("skip via {label}: spawn failed ({e})"); + continue; + } + }; + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(driver.as_bytes()).unwrap(); + } + let output = child.wait_with_output().expect("python wait"); + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("OK"), + "{label}: missing OK marker.\nstdout:\n{stdout}\nstderr:\n{}", + String::from_utf8_lossy(&output.stderr), + ); + eprintln!("{label}: pydantic roundtrip OK"); + return; + } + // Distinguish "no pydantic" (skip) from "pydantic is there but + // assertions failed" (fail). We marshal the assertion error + // through the script's exit; pydantic missing is an + // ImportError before our prints fire. + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + if stderr.contains("ModuleNotFoundError") && stderr.contains("pydantic") { + eprintln!("skip via {label}: pydantic not installed"); + continue; + } + panic!( + "{label}: roundtrip failed (exit={:?})\nstdout:\n{}\nstderr:\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + stderr, + ); + } + eprintln!("test_generic_flatten_pydantic_roundtrip skipped: no working python+pydantic found"); +} + +// Two args that share a *leaf* name but live in different modules +// must NOT collide when mangled. Earlier mangling used only the leaf +// name; both ./module_a::Sample and ./module_b::Sample produced +// identical keys, fusing two distinct UpdateOrElse instantiations +// into one and losing one type's wire fields. +mod module_a { + #[derive( + serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output, + )] + pub struct Sample { + pub a_field: u32, + } +} +mod module_b { + #[derive( + serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output, + )] + pub struct Sample { + pub b_field: bool, + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +struct TestLeafCollisionPair { + a: TestUpdateOrElse, + b: TestUpdateOrElse, +} + +#[test] +fn test_generic_flatten_leaf_collision() { + assert_snapshot!(TestLeafCollisionPair); +} + +// Wrapper that USES a marked struct in generic position with its own +// TypeVars as args. Reproduces the case the previous monomorphizer +// tripped over: walking `IdentityData` inside a generic context +// where `I` and `D` are TypeVars (not real types). Should not +// register a monomorphization for that — only the concrete +// instantiation `WithMarkedInner` +// triggers monomorphization, and its substituted IdentityData ref is +// the one that gets a concrete monomorph. +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +#[serde(bound( + serialize = "I: serde::Serialize, D: serde::Serialize", + deserialize = "I: serde::de::DeserializeOwned, D: serde::de::DeserializeOwned", +))] +struct TestWithMarkedInner { + body: TestIdentityData, + extra: bool, +} + +#[test] +fn test_generic_flatten_typevar_in_generic_context() { + assert_snapshot!(TestWithMarkedInner); +} + +// Generic enum whose variants reference a marked struct using the +// enum's own TypeVars. Reproduces the regression where transitive +// marking only considered structs — generic enums would survive the +// pass while the marked structs they referenced got removed, +// dangling the enum's variant fields. +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +#[serde(bound( + serialize = "I: serde::Serialize, D: serde::Serialize", + deserialize = "I: serde::de::DeserializeOwned, D: serde::de::DeserializeOwned", +))] +enum TestIngestRelation { + Insert(TestIdentityData), + Remove(TestIdentityData), + Empty, +} + +#[test] +fn test_generic_flatten_enum_variant_typevar() { + assert_snapshot!(TestIngestRelation); +} + +// Note: a Rust-derive recursive marked struct (e.g. +// `struct Tree { #[flatten] value: T, children: Vec> }`) +// overflows the reflectapi derive macro during schema construction, +// before any codegen runs — that's a derive-side limitation, not a +// codegen one. The codegen pipeline itself handles a recursive +// marked struct fed in via JSON; that case is exercised by +// `recursive_marked_struct_terminates_and_renders` in +// reflectapi/src/codegen/python.rs. + +// Boundary case: a generic struct whose flatten target is a +// *concrete* type, not a TypeVar. Should NOT be marked, NOT +// monomorphized — the existing flatten-of-concrete rendering path +// handles it. Confirms the marked-detection predicate doesn't +// over-trigger on any-flatten-on-a-generic-struct. +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +#[serde(bound( + serialize = "T: serde::Serialize", + deserialize = "T: serde::de::DeserializeOwned", +))] +struct TestGenericWithConcreteFlatten { + #[serde(flatten)] + extra: TestFlattenInner, + other: T, +} + +#[test] +fn test_generic_with_concrete_flatten_not_marked() { + assert_snapshot!(TestGenericWithConcreteFlatten); +} + +// Marked struct used with a generic parameter wrapped inside another +// generic (`Marked>`). Earlier transitive marking only +// looked at the IMMEDIATE arg, so the wrapper wasn't marked, the +// inner Marked got removed, and the wrapper's ref dangled. +#[derive(serde::Serialize, serde::Deserialize, Debug, reflectapi::Input, reflectapi::Output)] +#[serde(bound( + serialize = "I: serde::Serialize", + deserialize = "I: serde::de::DeserializeOwned", +))] +struct TestWrapperWithNestedTypevarArg { + body: TestUpdateOrElse, TestFlattenIfElse>, + extra: u32, +} + +#[test] +fn test_generic_flatten_typevar_nested_in_generic_arg() { + assert_snapshot!(TestWrapperWithNestedTypevarArg); +} 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 135c9343..f2cda23e 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 @@ -25,14 +25,6 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible -# Type variables for generic types - - -Additional = TypeVar("Additional") - -Payload = TypeVar("Payload") - - class ReflectapiDemoTestsSerdeA(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -45,16 +37,21 @@ class ReflectapiDemoTestsSerdeB(BaseModel): b: int -class ReflectapiDemoTestsSerdeS(BaseModel, Generic[Payload, Additional]): +class ReflectapiDemoTestsSerdeSReflectapiDemoTestsSerdeAReflectapiDemoTestsSerdeB( + BaseModel +): model_config = ConfigDict(extra="ignore", populate_by_name=True) + a: int + b: int + class ReflectapiDemoTestsSerdeTestS(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) type: Literal["S"] = Field(default="S", description="Discriminator field") - payload: Annotated[Any, "External type: Payload"] - additional: Annotated[Any, "External type: Additional"] + a: int + b: int class ReflectapiDemoTestsSerdeTest(RootModel): @@ -73,7 +70,7 @@ class reflectapi_demo: A = ReflectapiDemoTestsSerdeA B = ReflectapiDemoTestsSerdeB - S = ReflectapiDemoTestsSerdeS + SReflectapiDemoTestsSerdeAReflectapiDemoTestsSerdeB = ReflectapiDemoTestsSerdeSReflectapiDemoTestsSerdeAReflectapiDemoTestsSerdeB TestS = ReflectapiDemoTestsSerdeTestS Test = ReflectapiDemoTestsSerdeTest @@ -172,7 +169,7 @@ StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] for _model in [ ReflectapiDemoTestsSerdeA, ReflectapiDemoTestsSerdeB, - ReflectapiDemoTestsSerdeS, + ReflectapiDemoTestsSerdeSReflectapiDemoTestsSerdeAReflectapiDemoTestsSerdeB, ReflectapiDemoTestsSerdeTest, ]: try: 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 72940cbf..3bdaeda1 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 @@ -24,23 +24,17 @@ from reflectapi_runtime import ReflectapiEmpty from reflectapi_runtime import ReflectapiInfallible -# Type variables for generic types - - -Additional = TypeVar("Additional") - -Payload = TypeVar("Payload") - - class ReflectapiDemoTestsSerdeK(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) a: int -class ReflectapiDemoTestsSerdeS(BaseModel, Generic[Payload, Additional]): +class ReflectapiDemoTestsSerdeSReflectapiDemoTestsSerdeKStdTupleTuple0(BaseModel): model_config = ConfigDict(extra="ignore", populate_by_name=True) + a: int + # Namespace classes for dotted access to types class reflectapi_demo: @@ -53,7 +47,9 @@ class reflectapi_demo: """Namespace for serde types.""" K = ReflectapiDemoTestsSerdeK - S = ReflectapiDemoTestsSerdeS + SReflectapiDemoTestsSerdeKStdTupleTuple0 = ( + ReflectapiDemoTestsSerdeSReflectapiDemoTestsSerdeKStdTupleTuple0 + ) class AsyncInoutClient: @@ -65,10 +61,10 @@ class AsyncInoutClient: async def test( self, data: Optional[ - reflectapi_demo.tests.serde.S[reflectapi_demo.tests.serde.K, None] + reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0 ] = None, ) -> ApiResponse[ - reflectapi_demo.tests.serde.S[reflectapi_demo.tests.serde.K, None] + reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0 ]: """ @@ -76,7 +72,7 @@ class AsyncInoutClient: data: Request data for the test operation. Returns: - ApiResponse[reflectapi_demo.tests.serde.S[reflectapi_demo.tests.serde.K, None]]: Response containing reflectapi_demo.tests.serde.S[reflectapi_demo.tests.serde.K, None] data + ApiResponse[reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0]: Response containing reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0 data """ path = "/inout_test" @@ -85,9 +81,7 @@ class AsyncInoutClient: path, params=params if params else None, json_model=data, - response_model=reflectapi_demo.tests.serde.S[ - reflectapi_demo.tests.serde.K, None - ], + response_model=reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0, ) @@ -113,10 +107,10 @@ class InoutClient: def test( self, data: Optional[ - reflectapi_demo.tests.serde.S[reflectapi_demo.tests.serde.K, None] + reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0 ] = None, ) -> ApiResponse[ - reflectapi_demo.tests.serde.S[reflectapi_demo.tests.serde.K, None] + reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0 ]: """ @@ -124,7 +118,7 @@ class InoutClient: data: Request data for the test operation. Returns: - ApiResponse[reflectapi_demo.tests.serde.S[reflectapi_demo.tests.serde.K, None]]: Response containing reflectapi_demo.tests.serde.S[reflectapi_demo.tests.serde.K, None] data + ApiResponse[reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0]: Response containing reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0 data """ path = "/inout_test" @@ -133,9 +127,7 @@ class InoutClient: path, params=params if params else None, json_model=data, - response_model=reflectapi_demo.tests.serde.S[ - reflectapi_demo.tests.serde.K, None - ], + response_model=reflectapi_demo.tests.serde.SReflectapiDemoTestsSerdeKStdTupleTuple0, ) @@ -161,7 +153,7 @@ StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] # Rebuild models to resolve forward references for _model in [ ReflectapiDemoTestsSerdeK, - ReflectapiDemoTestsSerdeS, + ReflectapiDemoTestsSerdeSReflectapiDemoTestsSerdeKStdTupleTuple0, ]: try: _model.model_rebuild() diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-2.snap new file mode 100644 index 00000000..582befd2 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-2.snap @@ -0,0 +1,88 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: <\nTestUpdateOrElse > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestFlattenInner, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestFlattenInner, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + {} + >; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenIfElse { + code: number /* u16 */; + } + + export interface TestFlattenInner { + inner_a: number /* u32 */; + inner_b: string; + } + + export type TestUpdateOrElse = { + if_else: C | null; + } & NullToEmptyObject; + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestFlattenInner, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestFlattenInner, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + {}, + reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestFlattenInner, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-3.snap new file mode 100644 index 00000000..2fbbe17c --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-3.snap @@ -0,0 +1,87 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: <\nTestUpdateOrElse > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::TestUpdateOrElse< + super::types::reflectapi_demo::tests::serde::TestFlattenInner, + super::types::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::TestUpdateOrElse< + super::types::reflectapi_demo::tests::serde::TestFlattenInner, + super::types::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIfElse { + pub code: u16, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenInner { + pub inner_a: u32, + pub inner_b: std::string::String, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestUpdateOrElse { + #[serde(flatten)] + pub inner: T, + pub if_else: std::option::Option, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-4.snap new file mode 100644 index 00000000..9647c78d --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-4.snap @@ -0,0 +1,134 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.serde.TestFlattenIfElse": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIfElse", + "required": [ + "code" + ], + "properties": { + "code": { + "$ref": "#/components/schemas/u16" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenInner": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenInner", + "required": [ + "inner_a", + "inner_b" + ], + "properties": { + "inner_a": { + "$ref": "#/components/schemas/u32" + }, + "inner_b": { + "$ref": "#/components/schemas/std.string.String" + } + } + }, + "std.string.String": { + "description": "UTF-8 encoded string", + "type": "string" + }, + "u16": { + "description": "16-bit unsigned integer", + "type": "integer" + }, + "u32": { + "description": "32-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-5.snap new file mode 100644 index 00000000..6323b731 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields-5.snap @@ -0,0 +1,173 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: <\nTestUpdateOrElse > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from enum import Enum +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import BaseModel, ConfigDict, Field + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible + + +class ReflectapiDemoTestsSerdeTestFlattenIfElse(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + code: int + + +class ReflectapiDemoTestsSerdeTestFlattenInner(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + inner_a: int + inner_b: str + + +class ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + if_else: reflectapi_demo.tests.serde.TestFlattenIfElse | None + inner_a: int + inner_b: str + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenIfElse = ReflectapiDemoTestsSerdeTestFlattenIfElse + TestFlattenInner = ReflectapiDemoTestsSerdeTestFlattenInner + TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 = ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69]: Response containing reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69, + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69]: Response containing reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69, + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenIfElse, + ReflectapiDemoTestsSerdeTestFlattenInner, + ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields.snap new file mode 100644 index 00000000..0fa742ec --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_drops_inner_fields.snap @@ -0,0 +1,300 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-2.snap new file mode 100644 index 00000000..6f6dac2d --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-2.snap @@ -0,0 +1,95 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: <\nTestIngestRelation > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.TestIngestRelation< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.serde.TestIngestRelation< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + {} + >; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenIdent { + job_id: number /* u64 */; + } + + export interface TestFlattenIdentData { + payload: string; + } + + export type TestIdentityData = {} & NullToEmptyObject & + NullToEmptyObject; + + export type TestIngestRelation = + | { + Insert: reflectapi_demo.tests.serde.TestIdentityData; + } + | { + Remove: reflectapi_demo.tests.serde.TestIdentityData; + } + | "Empty"; + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.TestIngestRelation< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.TestIngestRelation< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + {}, + reflectapi_demo.tests.serde.TestIngestRelation< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-3.snap new file mode 100644 index 00000000..5ef1fd32 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-3.snap @@ -0,0 +1,98 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: <\nTestIngestRelation > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::TestIngestRelation< + super::types::reflectapi_demo::tests::serde::TestFlattenIdent, + super::types::reflectapi_demo::tests::serde::TestFlattenIdentData, + >, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::TestIngestRelation< + super::types::reflectapi_demo::tests::serde::TestFlattenIdent, + super::types::reflectapi_demo::tests::serde::TestFlattenIdentData, + >, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIdent { + pub job_id: u64, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIdentData { + pub payload: std::string::String, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestIdentityData { + #[serde(flatten)] + pub identity: I, + #[serde(flatten)] + pub data: D, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub enum TestIngestRelation { + Insert( + super::super::super::reflectapi_demo::tests::serde::TestIdentityData, + ), + Remove( + super::super::super::reflectapi_demo::tests::serde::TestIdentityData, + ), + Empty, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-4.snap new file mode 100644 index 00000000..397aaf95 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-4.snap @@ -0,0 +1,182 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "title": "Insert", + "required": [ + "Insert" + ], + "properties": { + "Insert": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdent" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdentData" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestIdentityData", + "properties": {} + } + ] + } + } + }, + { + "type": "object", + "title": "Remove", + "required": [ + "Remove" + ], + "properties": { + "Remove": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdent" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdentData" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestIdentityData", + "properties": {} + } + ] + } + } + }, + { + "const": "Empty" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "title": "Insert", + "required": [ + "Insert" + ], + "properties": { + "Insert": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdent" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdentData" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestIdentityData", + "properties": {} + } + ] + } + } + }, + { + "type": "object", + "title": "Remove", + "required": [ + "Remove" + ], + "properties": { + "Remove": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdent" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdentData" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestIdentityData", + "properties": {} + } + ] + } + } + }, + { + "const": "Empty" + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.serde.TestFlattenIdent": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIdent", + "required": [ + "job_id" + ], + "properties": { + "job_id": { + "$ref": "#/components/schemas/u64" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenIdentData": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIdentData", + "required": [ + "payload" + ], + "properties": { + "payload": { + "$ref": "#/components/schemas/std.string.String" + } + } + }, + "std.string.String": { + "description": "UTF-8 encoded string", + "type": "string" + }, + "u64": { + "description": "64-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-5.snap new file mode 100644 index 00000000..9d637516 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar-5.snap @@ -0,0 +1,296 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: <\nTestIngestRelation > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from enum import Enum +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import ( + BaseModel, + ConfigDict, + Field, + RootModel, + model_serializer, + model_validator, +) + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +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 ReflectapiDemoTestsSerdeTestFlattenIdent(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + job_id: int + + +class ReflectapiDemoTestsSerdeTestFlattenIdentData(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + payload: str + + +class ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + job_id: int + payload: str + + +class ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451InsertVariant( + BaseModel +): + """Insert variant""" + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + field_0: reflectapi_demo.tests.serde.TestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 + + +class ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451RemoveVariant( + BaseModel +): + """Remove variant""" + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + field_0: reflectapi_demo.tests.serde.TestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 + + +# Externally tagged enum using RootModel +ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451Variants = Union[ + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451InsertVariant, + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451RemoveVariant, + Literal["Empty"], +] + + +class ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451( + RootModel[ + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451Variants + ] +): + """Externally tagged enum""" + + @model_validator(mode="before") + @classmethod + def _validate(cls, data): + return _parse_externally_tagged( + data, + { + "Insert": lambda v: ( + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451InsertVariant( + field_0=v + ) + ), + "Remove": lambda v: ( + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451RemoveVariant( + field_0=v + ) + ), + "Empty": "_unit", + }, + ( + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451InsertVariant, + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451RemoveVariant, + ), + "ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451", + ) + + @model_serializer + def _serialize(self): + return _serialize_externally_tagged( + self.root, + { + "Insert": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451InsertVariant, + ), + lambda r: {"Insert": r.field_0}, + ), + "Remove": ( + lambda r: isinstance( + r, + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451RemoveVariant, + ), + lambda r: {"Remove": r.field_0}, + ), + "Empty": (lambda r: r == "Empty", lambda r: "Empty"), + }, + "ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451", + ) + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenIdent = ReflectapiDemoTestsSerdeTestFlattenIdent + TestFlattenIdentData = ReflectapiDemoTestsSerdeTestFlattenIdentData + TestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 = ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 + TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451InsertVariant = ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451InsertVariant + TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451RemoveVariant = ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451RemoveVariant + TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451 = ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451 + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451]: Response containing reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451, + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451]: Response containing reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451, + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenIdent, + ReflectapiDemoTestsSerdeTestFlattenIdentData, + ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4, + ReflectapiDemoTestsSerdeTestIngestRelationReflectapiDemoTestsSerdeTestFl0a834451, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar.snap new file mode 100644 index 00000000..df2ad080 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_enum_variant_typevar.snap @@ -0,0 +1,328 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::TestIngestRelation", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdent" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData" + } + ] + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::TestIngestRelation", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdent" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData" + } + ] + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdent", + "fields": { + "named": [ + { + "name": "job_id", + "type": { + "name": "u64" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData", + "fields": { + "named": [ + { + "name": "payload", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "fields": { + "named": [ + { + "name": "identity", + "type": { + "name": "I" + }, + "required": true, + "flattened": true + }, + { + "name": "data", + "type": { + "name": "D" + }, + "required": true, + "flattened": true + } + ] + } + }, + { + "kind": "enum", + "name": "reflectapi_demo::tests::serde::TestIngestRelation", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "variants": [ + { + "name": "Insert", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "arguments": [ + { + "name": "I" + }, + { + "name": "D" + } + ] + }, + "required": true + } + ] + } + }, + { + "name": "Remove", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "arguments": [ + { + "name": "I" + }, + { + "name": "D" + } + ] + }, + "required": true + } + ] + } + }, + { + "name": "Empty", + "fields": "none" + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u64", + "description": "64-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdent", + "fields": { + "named": [ + { + "name": "job_id", + "type": { + "name": "u64" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData", + "fields": { + "named": [ + { + "name": "payload", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "fields": { + "named": [ + { + "name": "identity", + "type": { + "name": "I" + }, + "required": true, + "flattened": true + }, + { + "name": "data", + "type": { + "name": "D" + }, + "required": true, + "flattened": true + } + ] + } + }, + { + "kind": "enum", + "name": "reflectapi_demo::tests::serde::TestIngestRelation", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "variants": [ + { + "name": "Insert", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "arguments": [ + { + "name": "I" + }, + { + "name": "D" + } + ] + }, + "required": true + } + ] + } + }, + { + "name": "Remove", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "arguments": [ + { + "name": "I" + }, + { + "name": "D" + } + ] + }, + "required": true + } + ] + } + }, + { + "name": "Empty", + "fields": "none" + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u64", + "description": "64-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-2.snap new file mode 100644 index 00000000..13e7db56 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-2.snap @@ -0,0 +1,88 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: < TestLeafCollisionPair > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.TestLeafCollisionPair, + headers: {}, + options?: RequestOptions, + ) => AsyncResult; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenIfElse { + code: number /* u16 */; + } + + export interface TestLeafCollisionPair { + a: reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.module_a.Sample, + reflectapi_demo.tests.serde.TestFlattenIfElse + >; + b: reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.module_b.Sample, + reflectapi_demo.tests.serde.TestFlattenIfElse + >; + } + + export type TestUpdateOrElse = { + if_else: C | null; + } & NullToEmptyObject; + + export namespace module_a { + export interface Sample { + a_field: number /* u32 */; + } + } + + export namespace module_b { + export interface Sample { + b_field: boolean; + } + } + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.TestLeafCollisionPair, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.TestLeafCollisionPair, + {}, + reflectapi_demo.tests.serde.TestLeafCollisionPair, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-3.snap new file mode 100644 index 00000000..3a71e8b5 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-3.snap @@ -0,0 +1,102 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: < TestLeafCollisionPair > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::TestLeafCollisionPair, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::TestLeafCollisionPair, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIfElse { + pub code: u16, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestLeafCollisionPair { + pub a: super::super::super::reflectapi_demo::tests::serde::TestUpdateOrElse< + super::super::super::reflectapi_demo::tests::serde::module_a::Sample, + super::super::super::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + pub b: super::super::super::reflectapi_demo::tests::serde::TestUpdateOrElse< + super::super::super::reflectapi_demo::tests::serde::module_b::Sample, + super::super::super::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestUpdateOrElse { + #[serde(flatten)] + pub inner: T, + pub if_else: std::option::Option, + } + + pub mod module_a { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct Sample { + pub a_field: u32, + } + } + pub mod module_b { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct Sample { + pub b_field: bool, + } + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-4.snap new file mode 100644 index 00000000..7a7f8d47 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-4.snap @@ -0,0 +1,158 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestLeafCollisionPair" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestLeafCollisionPair" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "bool": { + "description": "Boolean value", + "type": "boolean" + }, + "reflectapi_demo.tests.serde.TestFlattenIfElse": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIfElse", + "required": [ + "code" + ], + "properties": { + "code": { + "$ref": "#/components/schemas/u16" + } + } + }, + "reflectapi_demo.tests.serde.TestLeafCollisionPair": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestLeafCollisionPair", + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.module_a.Sample" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + }, + "b": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.module_b.Sample" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + } + } + }, + "reflectapi_demo.tests.serde.module_a.Sample": { + "type": "object", + "title": "reflectapi_demo.tests.serde.module_a.Sample", + "required": [ + "a_field" + ], + "properties": { + "a_field": { + "$ref": "#/components/schemas/u32" + } + } + }, + "reflectapi_demo.tests.serde.module_b.Sample": { + "type": "object", + "title": "reflectapi_demo.tests.serde.module_b.Sample", + "required": [ + "b_field" + ], + "properties": { + "b_field": { + "$ref": "#/components/schemas/bool" + } + } + }, + "u16": { + "description": "16-bit unsigned integer", + "type": "integer" + }, + "u32": { + "description": "32-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-5.snap new file mode 100644 index 00000000..d0ecbc09 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision-5.snap @@ -0,0 +1,199 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: < TestLeafCollisionPair > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from enum import Enum +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import BaseModel, ConfigDict, Field + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible + + +class ReflectapiDemoTestsSerdeTestFlattenIfElse(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + code: int + + +class ReflectapiDemoTestsSerdeTestLeafCollisionPair(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + a: reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeModuleASC2dd36fb + b: reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeModuleBS8b56ffdc + + +class ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeModuleASC2dd36fb( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + if_else: reflectapi_demo.tests.serde.TestFlattenIfElse | None + a_field: int + + +class ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeModuleBS8b56ffdc( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + if_else: reflectapi_demo.tests.serde.TestFlattenIfElse | None + b_field: bool + + +class ReflectapiDemoTestsSerdeModuleASample(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + a_field: int + + +class ReflectapiDemoTestsSerdeModuleBSample(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + b_field: bool + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenIfElse = ReflectapiDemoTestsSerdeTestFlattenIfElse + TestLeafCollisionPair = ReflectapiDemoTestsSerdeTestLeafCollisionPair + TestUpdateOrElseReflectapiDemoTestsSerdeModuleASC2dd36fb = ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeModuleASC2dd36fb + TestUpdateOrElseReflectapiDemoTestsSerdeModuleBS8b56ffdc = ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeModuleBS8b56ffdc + + class module_a: + """Namespace for module_a types.""" + + Sample = ReflectapiDemoTestsSerdeModuleASample + + class module_b: + """Namespace for module_b types.""" + + Sample = ReflectapiDemoTestsSerdeModuleBSample + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[reflectapi_demo.tests.serde.TestLeafCollisionPair] = None, + ) -> ApiResponse[reflectapi_demo.tests.serde.TestLeafCollisionPair]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestLeafCollisionPair]: Response containing reflectapi_demo.tests.serde.TestLeafCollisionPair data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestLeafCollisionPair, + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[reflectapi_demo.tests.serde.TestLeafCollisionPair] = None, + ) -> ApiResponse[reflectapi_demo.tests.serde.TestLeafCollisionPair]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestLeafCollisionPair]: Response containing reflectapi_demo.tests.serde.TestLeafCollisionPair data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestLeafCollisionPair, + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenIfElse, + ReflectapiDemoTestsSerdeTestLeafCollisionPair, + ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeModuleASC2dd36fb, + ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeModuleBS8b56ffdc, + ReflectapiDemoTestsSerdeModuleASample, + ReflectapiDemoTestsSerdeModuleBSample, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision.snap new file mode 100644 index 00000000..042630a1 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_leaf_collision.snap @@ -0,0 +1,376 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::TestLeafCollisionPair" + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::TestLeafCollisionPair" + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "primitive", + "name": "bool", + "description": "Boolean value" + }, + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestLeafCollisionPair", + "fields": { + "named": [ + { + "name": "a", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::module_a::Sample" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + }, + { + "name": "b", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::module_b::Sample" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::module_a::Sample", + "fields": { + "named": [ + { + "name": "a_field", + "type": { + "name": "u32" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::module_b::Sample", + "fields": { + "named": [ + { + "name": "b_field", + "type": { + "name": "bool" + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "primitive", + "name": "bool", + "description": "Boolean value" + }, + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestLeafCollisionPair", + "fields": { + "named": [ + { + "name": "a", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::module_a::Sample" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + }, + { + "name": "b", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::module_b::Sample" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::module_a::Sample", + "fields": { + "named": [ + { + "name": "a_field", + "type": { + "name": "u32" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::module_b::Sample", + "fields": { + "named": [ + { + "name": "b_field", + "type": { + "name": "bool" + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-2.snap new file mode 100644 index 00000000..6cfa430e --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-2.snap @@ -0,0 +1,109 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: <\nTestUpdateOrElse< TestIdentityData,\nTestFlattenIfElse, > > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestIdentityData< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestIdentityData< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + {} + >; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenIdent { + job_id: number /* u64 */; + } + + export interface TestFlattenIdentData { + payload: string; + } + + export interface TestFlattenIfElse { + code: number /* u16 */; + } + + export type TestIdentityData = {} & NullToEmptyObject & + NullToEmptyObject; + + export type TestUpdateOrElse = { + if_else: C | null; + } & NullToEmptyObject; + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestIdentityData< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestIdentityData< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + {}, + reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestIdentityData< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + reflectapi_demo.tests.serde.TestFlattenIfElse + >, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-3.snap new file mode 100644 index 00000000..acc72805 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-3.snap @@ -0,0 +1,105 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: <\nTestUpdateOrElse< TestIdentityData,\nTestFlattenIfElse, > > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::TestUpdateOrElse< + super::types::reflectapi_demo::tests::serde::TestIdentityData< + super::types::reflectapi_demo::tests::serde::TestFlattenIdent, + super::types::reflectapi_demo::tests::serde::TestFlattenIdentData, + >, + super::types::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::TestUpdateOrElse< + super::types::reflectapi_demo::tests::serde::TestIdentityData< + super::types::reflectapi_demo::tests::serde::TestFlattenIdent, + super::types::reflectapi_demo::tests::serde::TestFlattenIdentData, + >, + super::types::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIdent { + pub job_id: u64, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIdentData { + pub payload: std::string::String, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIfElse { + pub code: u16, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestIdentityData { + #[serde(flatten)] + pub identity: I, + #[serde(flatten)] + pub data: D, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestUpdateOrElse { + #[serde(flatten)] + pub inner: T, + pub if_else: std::option::Option, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-4.snap new file mode 100644 index 00000000..2d135877 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-4.snap @@ -0,0 +1,166 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdent" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdentData" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestIdentityData", + "properties": {} + } + ] + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse, reflectapi_demo.tests.serde.TestFlattenIfElse>", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdent" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdentData" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestIdentityData", + "properties": {} + } + ] + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse, reflectapi_demo.tests.serde.TestFlattenIfElse>", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.serde.TestFlattenIdent": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIdent", + "required": [ + "job_id" + ], + "properties": { + "job_id": { + "$ref": "#/components/schemas/u64" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenIdentData": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIdentData", + "required": [ + "payload" + ], + "properties": { + "payload": { + "$ref": "#/components/schemas/std.string.String" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenIfElse": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIfElse", + "required": [ + "code" + ], + "properties": { + "code": { + "$ref": "#/components/schemas/u16" + } + } + }, + "std.string.String": { + "description": "UTF-8 encoded string", + "type": "string" + }, + "u16": { + "description": "16-bit unsigned integer", + "type": "integer" + }, + "u64": { + "description": "64-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-5.snap new file mode 100644 index 00000000..81916c39 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested-5.snap @@ -0,0 +1,191 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: <\nTestUpdateOrElse< TestIdentityData,\nTestFlattenIfElse, > > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from enum import Enum +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import BaseModel, ConfigDict, Field + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible + + +class ReflectapiDemoTestsSerdeTestFlattenIdent(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + job_id: int + + +class ReflectapiDemoTestsSerdeTestFlattenIdentData(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + payload: str + + +class ReflectapiDemoTestsSerdeTestFlattenIfElse(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + code: int + + +class ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + job_id: int + payload: str + + +class ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + if_else: reflectapi_demo.tests.serde.TestFlattenIfElse | None + job_id: int + payload: str + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenIdent = ReflectapiDemoTestsSerdeTestFlattenIdent + TestFlattenIdentData = ReflectapiDemoTestsSerdeTestFlattenIdentData + TestFlattenIfElse = ReflectapiDemoTestsSerdeTestFlattenIfElse + TestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 = ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 + TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1 = ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1 + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1]: Response containing reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1, + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1]: Response containing reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1, + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenIdent, + ReflectapiDemoTestsSerdeTestFlattenIdentData, + ReflectapiDemoTestsSerdeTestFlattenIfElse, + ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4, + ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestIden9793afe1, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested.snap new file mode 100644 index 00000000..976f2c92 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_nested.snap @@ -0,0 +1,396 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdent" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData" + } + ] + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdent" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData" + } + ] + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdent", + "fields": { + "named": [ + { + "name": "job_id", + "type": { + "name": "u64" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData", + "fields": { + "named": [ + { + "name": "payload", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "fields": { + "named": [ + { + "name": "identity", + "type": { + "name": "I" + }, + "required": true, + "flattened": true + }, + { + "name": "data", + "type": { + "name": "D" + }, + "required": true, + "flattened": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u64", + "description": "64-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdent", + "fields": { + "named": [ + { + "name": "job_id", + "type": { + "name": "u64" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData", + "fields": { + "named": [ + { + "name": "payload", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "fields": { + "named": [ + { + "name": "identity", + "type": { + "name": "I" + }, + "required": true, + "flattened": true + }, + { + "name": "data", + "type": { + "name": "D" + }, + "required": true, + "flattened": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u64", + "description": "64-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-2.snap new file mode 100644 index 00000000..f6cf5157 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-2.snap @@ -0,0 +1,77 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: < TestOptionalFlatten > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.input.TestOptionalFlatten, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.serde.output.TestOptionalFlatten, + {} + >; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenInner { + inner_a: number /* u32 */; + inner_b: string; + } + + export namespace input { + export type TestOptionalFlatten = { + code: number /* u16 */; + } & Partial; + } + + export namespace output { + export type TestOptionalFlatten = { + code: number /* u16 */; + } & NullToEmptyObject; + } + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.input.TestOptionalFlatten, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.input.TestOptionalFlatten, + {}, + reflectapi_demo.tests.serde.output.TestOptionalFlatten, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-3.snap new file mode 100644 index 00000000..3f06f7f1 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-3.snap @@ -0,0 +1,96 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: < TestOptionalFlatten > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::input::TestOptionalFlatten< + super::types::reflectapi_demo::tests::serde::TestFlattenInner, + >, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::output::TestOptionalFlatten< + super::types::reflectapi_demo::tests::serde::TestFlattenInner, + >, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenInner { + pub inner_a: u32, + pub inner_b: std::string::String, + } + + pub mod input { + + #[derive(Debug, serde::Serialize)] + pub struct TestOptionalFlatten { + #[serde( + default = "Default::default", + skip_serializing_if = "std::option::Option::is_none", + flatten + )] + pub inner: std::option::Option, + pub code: u16, + } + } + pub mod output { + + #[derive(Debug, serde::Deserialize)] + pub struct TestOptionalFlatten { + #[serde(flatten)] + pub inner: std::option::Option, + pub code: u16, + } + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-4.snap new file mode 100644 index 00000000..b66a9eb3 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-4.snap @@ -0,0 +1,122 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + } + ] + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.input.TestOptionalFlatten", + "required": [ + "code" + ], + "properties": { + "code": { + "$ref": "#/components/schemas/u16" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + } + ] + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.output.TestOptionalFlatten", + "required": [ + "code" + ], + "properties": { + "code": { + "$ref": "#/components/schemas/u16" + } + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.serde.TestFlattenInner": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenInner", + "required": [ + "inner_a", + "inner_b" + ], + "properties": { + "inner_a": { + "$ref": "#/components/schemas/u32" + }, + "inner_b": { + "$ref": "#/components/schemas/std.string.String" + } + } + }, + "std.string.String": { + "description": "UTF-8 encoded string", + "type": "string" + }, + "u16": { + "description": "16-bit unsigned integer", + "type": "integer" + }, + "u32": { + "description": "32-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-5.snap new file mode 100644 index 00000000..7b80c5c3 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional-5.snap @@ -0,0 +1,185 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: < TestOptionalFlatten > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from enum import Enum +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import BaseModel, ConfigDict, Field + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible + + +class ReflectapiDemoTestsSerdeTestFlattenInner(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + inner_a: int + inner_b: str + + +class ReflectapiDemoTestsSerdeInputTestOptionalFlattenReflectapiDemoTestsSerde356d19e7( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + code: int + inner_a: int | None = None + inner_b: str | None = None + + +class ReflectapiDemoTestsSerdeOutputTestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + code: int + inner_a: int + inner_b: str + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenInner = ReflectapiDemoTestsSerdeTestFlattenInner + + class input: + """Namespace for input types.""" + + TestOptionalFlattenReflectapiDemoTestsSerde356d19e7 = ReflectapiDemoTestsSerdeInputTestOptionalFlattenReflectapiDemoTestsSerde356d19e7 + + class output: + """Namespace for output types.""" + + TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30 = ReflectapiDemoTestsSerdeOutputTestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30 + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.input.TestOptionalFlattenReflectapiDemoTestsSerde356d19e7 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.output.TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.output.TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30]: Response containing reflectapi_demo.tests.serde.output.TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.output.TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30, + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.input.TestOptionalFlattenReflectapiDemoTestsSerde356d19e7 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.output.TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.output.TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30]: Response containing reflectapi_demo.tests.serde.output.TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.output.TestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30, + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenInner, + ReflectapiDemoTestsSerdeInputTestOptionalFlattenReflectapiDemoTestsSerde356d19e7, + ReflectapiDemoTestsSerdeOutputTestOptionalFlattenReflectapiDemoTestsSerd6d4b6d30, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional.snap new file mode 100644 index 00000000..ae5c4745 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_optional.snap @@ -0,0 +1,257 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::input::TestOptionalFlatten", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + } + ] + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::output::TestOptionalFlatten", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + } + ] + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::input::TestOptionalFlatten", + "parameters": [ + { + "name": "T" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "T" + } + ] + }, + "flattened": true + }, + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::output::TestOptionalFlatten", + "parameters": [ + { + "name": "T" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "T" + } + ] + }, + "required": true, + "flattened": true + }, + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-2.snap new file mode 100644 index 00000000..146fea9f --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-2.snap @@ -0,0 +1,85 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: < TestTwoInstantiations > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.TestTwoInstantiations, + headers: {}, + options?: RequestOptions, + ) => AsyncResult; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenIfElse { + code: number /* u16 */; + } + + export interface TestFlattenInner { + inner_a: number /* u32 */; + inner_b: string; + } + + export interface TestFlattenInnerAlt { + alt_x: boolean; + } + + export interface TestTwoInstantiations { + a: reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestFlattenInner, + reflectapi_demo.tests.serde.TestFlattenIfElse + >; + b: reflectapi_demo.tests.serde.TestUpdateOrElse< + reflectapi_demo.tests.serde.TestFlattenInnerAlt, + reflectapi_demo.tests.serde.TestFlattenIfElse + >; + } + + export type TestUpdateOrElse = { + if_else: C | null; + } & NullToEmptyObject; + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.TestTwoInstantiations, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.TestTwoInstantiations, + {}, + reflectapi_demo.tests.serde.TestTwoInstantiations, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-3.snap new file mode 100644 index 00000000..be0b0eb5 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-3.snap @@ -0,0 +1,98 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: < TestTwoInstantiations > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::TestTwoInstantiations, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::TestTwoInstantiations, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIfElse { + pub code: u16, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenInner { + pub inner_a: u32, + pub inner_b: std::string::String, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenInnerAlt { + pub alt_x: bool, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestTwoInstantiations { + pub a: super::super::super::reflectapi_demo::tests::serde::TestUpdateOrElse< + super::super::super::reflectapi_demo::tests::serde::TestFlattenInner, + super::super::super::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + pub b: super::super::super::reflectapi_demo::tests::serde::TestUpdateOrElse< + super::super::super::reflectapi_demo::tests::serde::TestFlattenInnerAlt, + super::super::super::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestUpdateOrElse { + #[serde(flatten)] + pub inner: T, + pub if_else: std::option::Option, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-4.snap new file mode 100644 index 00000000..83f10672 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-4.snap @@ -0,0 +1,166 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestTwoInstantiations" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestTwoInstantiations" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "bool": { + "description": "Boolean value", + "type": "boolean" + }, + "reflectapi_demo.tests.serde.TestFlattenIfElse": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIfElse", + "required": [ + "code" + ], + "properties": { + "code": { + "$ref": "#/components/schemas/u16" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenInner": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenInner", + "required": [ + "inner_a", + "inner_b" + ], + "properties": { + "inner_a": { + "$ref": "#/components/schemas/u32" + }, + "inner_b": { + "$ref": "#/components/schemas/std.string.String" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenInnerAlt": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenInnerAlt", + "required": [ + "alt_x" + ], + "properties": { + "alt_x": { + "$ref": "#/components/schemas/bool" + } + } + }, + "reflectapi_demo.tests.serde.TestTwoInstantiations": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestTwoInstantiations", + "required": [ + "a", + "b" + ], + "properties": { + "a": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + }, + "b": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInnerAlt" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + } + } + }, + "std.string.String": { + "description": "UTF-8 encoded string", + "type": "string" + }, + "u16": { + "description": "16-bit unsigned integer", + "type": "integer" + }, + "u32": { + "description": "32-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-5.snap new file mode 100644 index 00000000..b14e9899 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations-5.snap @@ -0,0 +1,193 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: < TestTwoInstantiations > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from enum import Enum +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import BaseModel, ConfigDict, Field + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible + + +class ReflectapiDemoTestsSerdeTestFlattenIfElse(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + code: int + + +class ReflectapiDemoTestsSerdeTestFlattenInner(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + inner_a: int + inner_b: str + + +class ReflectapiDemoTestsSerdeTestFlattenInnerAlt(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + alt_x: bool + + +class ReflectapiDemoTestsSerdeTestTwoInstantiations(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + a: reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 + b: reflectapi_demo.tests.serde.TestUpdateOrElseReflectapiDemoTestsSerdeTestFlatCd6baf20 + + +class ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + if_else: reflectapi_demo.tests.serde.TestFlattenIfElse | None + inner_a: int + inner_b: str + + +class ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlatCd6baf20( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + if_else: reflectapi_demo.tests.serde.TestFlattenIfElse | None + alt_x: bool + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenIfElse = ReflectapiDemoTestsSerdeTestFlattenIfElse + TestFlattenInner = ReflectapiDemoTestsSerdeTestFlattenInner + TestFlattenInnerAlt = ReflectapiDemoTestsSerdeTestFlattenInnerAlt + TestTwoInstantiations = ReflectapiDemoTestsSerdeTestTwoInstantiations + TestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 = ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69 + TestUpdateOrElseReflectapiDemoTestsSerdeTestFlatCd6baf20 = ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlatCd6baf20 + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[reflectapi_demo.tests.serde.TestTwoInstantiations] = None, + ) -> ApiResponse[reflectapi_demo.tests.serde.TestTwoInstantiations]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestTwoInstantiations]: Response containing reflectapi_demo.tests.serde.TestTwoInstantiations data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestTwoInstantiations, + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[reflectapi_demo.tests.serde.TestTwoInstantiations] = None, + ) -> ApiResponse[reflectapi_demo.tests.serde.TestTwoInstantiations]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestTwoInstantiations]: Response containing reflectapi_demo.tests.serde.TestTwoInstantiations data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestTwoInstantiations, + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenIfElse, + ReflectapiDemoTestsSerdeTestFlattenInner, + ReflectapiDemoTestsSerdeTestFlattenInnerAlt, + ReflectapiDemoTestsSerdeTestTwoInstantiations, + ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlat3ae7cb69, + ReflectapiDemoTestsSerdeTestUpdateOrElseReflectapiDemoTestsSerdeTestFlatCd6baf20, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations.snap new file mode 100644 index 00000000..b0cf68e5 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_two_instantiations.snap @@ -0,0 +1,400 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::TestTwoInstantiations" + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::TestTwoInstantiations" + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "primitive", + "name": "bool", + "description": "Boolean value" + }, + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInnerAlt", + "fields": { + "named": [ + { + "name": "alt_x", + "type": { + "name": "bool" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestTwoInstantiations", + "fields": { + "named": [ + { + "name": "a", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + }, + { + "name": "b", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInnerAlt" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "primitive", + "name": "bool", + "description": "Boolean value" + }, + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInnerAlt", + "fields": { + "named": [ + { + "name": "alt_x", + "type": { + "name": "bool" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestTwoInstantiations", + "fields": { + "named": [ + { + "name": "a", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + }, + { + "name": "b", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInnerAlt" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-2.snap new file mode 100644 index 00000000..adef3203 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-2.snap @@ -0,0 +1,91 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: <\nTestWithMarkedInner > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.TestWithMarkedInner< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.serde.TestWithMarkedInner< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + {} + >; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenIdent { + job_id: number /* u64 */; + } + + export interface TestFlattenIdentData { + payload: string; + } + + export type TestIdentityData = {} & NullToEmptyObject & + NullToEmptyObject; + + export interface TestWithMarkedInner { + body: reflectapi_demo.tests.serde.TestIdentityData; + extra: boolean; + } + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.TestWithMarkedInner< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.TestWithMarkedInner< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + {}, + reflectapi_demo.tests.serde.TestWithMarkedInner< + reflectapi_demo.tests.serde.TestFlattenIdent, + reflectapi_demo.tests.serde.TestFlattenIdentData + >, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-3.snap new file mode 100644 index 00000000..fc6d5fbd --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-3.snap @@ -0,0 +1,94 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: <\nTestWithMarkedInner > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::TestWithMarkedInner< + super::types::reflectapi_demo::tests::serde::TestFlattenIdent, + super::types::reflectapi_demo::tests::serde::TestFlattenIdentData, + >, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::TestWithMarkedInner< + super::types::reflectapi_demo::tests::serde::TestFlattenIdent, + super::types::reflectapi_demo::tests::serde::TestFlattenIdentData, + >, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIdent { + pub job_id: u64, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIdentData { + pub payload: std::string::String, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestIdentityData { + #[serde(flatten)] + pub identity: I, + #[serde(flatten)] + pub data: D, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestWithMarkedInner { + pub body: + super::super::super::reflectapi_demo::tests::serde::TestIdentityData, + pub extra: bool, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-4.snap new file mode 100644 index 00000000..a34bbe7d --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-4.snap @@ -0,0 +1,132 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestWithMarkedInner", + "required": [ + "body", + "extra" + ], + "properties": { + "body": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdent" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdentData" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestIdentityData", + "properties": {} + } + ] + }, + "extra": { + "$ref": "#/components/schemas/bool" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestWithMarkedInner", + "required": [ + "body", + "extra" + ], + "properties": { + "body": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdent" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIdentData" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestIdentityData", + "properties": {} + } + ] + }, + "extra": { + "$ref": "#/components/schemas/bool" + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "bool": { + "description": "Boolean value", + "type": "boolean" + }, + "reflectapi_demo.tests.serde.TestFlattenIdent": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIdent", + "required": [ + "job_id" + ], + "properties": { + "job_id": { + "$ref": "#/components/schemas/u64" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenIdentData": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIdentData", + "required": [ + "payload" + ], + "properties": { + "payload": { + "$ref": "#/components/schemas/std.string.String" + } + } + }, + "std.string.String": { + "description": "UTF-8 encoded string", + "type": "string" + }, + "u64": { + "description": "64-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-5.snap new file mode 100644 index 00000000..da6e813c --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context-5.snap @@ -0,0 +1,181 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: <\nTestWithMarkedInner > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import BaseModel, ConfigDict, Field + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible + + +class ReflectapiDemoTestsSerdeTestFlattenIdent(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + job_id: int + + +class ReflectapiDemoTestsSerdeTestFlattenIdentData(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + payload: str + + +class ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + job_id: int + payload: str + + +class ReflectapiDemoTestsSerdeTestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + body: reflectapi_demo.tests.serde.TestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 + extra: bool + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenIdent = ReflectapiDemoTestsSerdeTestFlattenIdent + TestFlattenIdentData = ReflectapiDemoTestsSerdeTestFlattenIdentData + TestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 = ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4 + TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66 = ReflectapiDemoTestsSerdeTestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66 + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66]: Response containing reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66, + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66 + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66 + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66]: Response containing reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66 data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66, + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenIdent, + ReflectapiDemoTestsSerdeTestFlattenIdentData, + ReflectapiDemoTestsSerdeTestIdentityDataReflectapiDemoTestsSerdeTestFlatF60133e4, + ReflectapiDemoTestsSerdeTestWithMarkedInnerReflectapiDemoTestsSerdeTestF02f28d66, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context.snap new file mode 100644 index 00000000..9e5c1f23 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_in_generic_context.snap @@ -0,0 +1,290 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::TestWithMarkedInner", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdent" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData" + } + ] + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::TestWithMarkedInner", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdent" + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData" + } + ] + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "primitive", + "name": "bool", + "description": "Boolean value" + }, + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdent", + "fields": { + "named": [ + { + "name": "job_id", + "type": { + "name": "u64" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData", + "fields": { + "named": [ + { + "name": "payload", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "fields": { + "named": [ + { + "name": "identity", + "type": { + "name": "I" + }, + "required": true, + "flattened": true + }, + { + "name": "data", + "type": { + "name": "D" + }, + "required": true, + "flattened": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestWithMarkedInner", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "fields": { + "named": [ + { + "name": "body", + "type": { + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "arguments": [ + { + "name": "I" + }, + { + "name": "D" + } + ] + }, + "required": true + }, + { + "name": "extra", + "type": { + "name": "bool" + }, + "required": true + } + ] + } + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u64", + "description": "64-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "primitive", + "name": "bool", + "description": "Boolean value" + }, + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdent", + "fields": { + "named": [ + { + "name": "job_id", + "type": { + "name": "u64" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIdentData", + "fields": { + "named": [ + { + "name": "payload", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "fields": { + "named": [ + { + "name": "identity", + "type": { + "name": "I" + }, + "required": true, + "flattened": true + }, + { + "name": "data", + "type": { + "name": "D" + }, + "required": true, + "flattened": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestWithMarkedInner", + "parameters": [ + { + "name": "I" + }, + { + "name": "D" + } + ], + "fields": { + "named": [ + { + "name": "body", + "type": { + "name": "reflectapi_demo::tests::serde::TestIdentityData", + "arguments": [ + { + "name": "I" + }, + { + "name": "D" + } + ] + }, + "required": true + }, + { + "name": "extra", + "type": { + "name": "bool" + }, + "required": true + } + ] + } + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u64", + "description": "64-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-2.snap new file mode 100644 index 00000000..24cfe75b --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-2.snap @@ -0,0 +1,81 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: <\nTestWrapperWithNestedTypevarArg > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArg, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArg, + {} + >; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenIfElse { + code: number /* u16 */; + } + + export interface TestFlattenInner { + inner_a: number /* u32 */; + inner_b: string; + } + + export type TestUpdateOrElse = { + if_else: C | null; + } & NullToEmptyObject; + + export interface TestWrapperWithNestedTypevarArg { + body: reflectapi_demo.tests.serde.TestUpdateOrElse< + I | null, + reflectapi_demo.tests.serde.TestFlattenIfElse + >; + extra: number /* u32 */; + } + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArg, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArg, + {}, + reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArg, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-3.snap new file mode 100644 index 00000000..31fdf3ea --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-3.snap @@ -0,0 +1,94 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: < TestWrapperWithNestedTypevarArg\n> ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::TestWrapperWithNestedTypevarArg< + super::types::reflectapi_demo::tests::serde::TestFlattenInner, + >, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::TestWrapperWithNestedTypevarArg< + super::types::reflectapi_demo::tests::serde::TestFlattenInner, + >, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIfElse { + pub code: u16, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenInner { + pub inner_a: u32, + pub inner_b: std::string::String, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestUpdateOrElse { + #[serde(flatten)] + pub inner: T, + pub if_else: std::option::Option, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestWrapperWithNestedTypevarArg { + pub body: super::super::super::reflectapi_demo::tests::serde::TestUpdateOrElse< + std::option::Option, + super::super::super::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + pub extra: u32, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-4.snap new file mode 100644 index 00000000..ab92ea21 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-4.snap @@ -0,0 +1,176 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArg", + "required": [ + "body", + "extra" + ], + "properties": { + "body": { + "allOf": [ + { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + } + ] + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse, reflectapi_demo.tests.serde.TestFlattenIfElse>", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + }, + "extra": { + "$ref": "#/components/schemas/u32" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArg", + "required": [ + "body", + "extra" + ], + "properties": { + "body": { + "allOf": [ + { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + } + ] + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestUpdateOrElse, reflectapi_demo.tests.serde.TestFlattenIfElse>", + "required": [ + "if_else" + ], + "properties": { + "if_else": { + "oneOf": [ + { + "description": "Null", + "type": "null" + }, + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + ] + } + } + } + ] + }, + "extra": { + "$ref": "#/components/schemas/u32" + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.serde.TestFlattenIfElse": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIfElse", + "required": [ + "code" + ], + "properties": { + "code": { + "$ref": "#/components/schemas/u16" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenInner": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenInner", + "required": [ + "inner_a", + "inner_b" + ], + "properties": { + "inner_a": { + "$ref": "#/components/schemas/u32" + }, + "inner_b": { + "$ref": "#/components/schemas/std.string.String" + } + } + }, + "std.string.String": { + "description": "UTF-8 encoded string", + "type": "string" + }, + "u16": { + "description": "16-bit unsigned integer", + "type": "integer" + }, + "u32": { + "description": "32-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-5.snap new file mode 100644 index 00000000..a66eb782 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg-5.snap @@ -0,0 +1,184 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: <\nTestWrapperWithNestedTypevarArg > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from enum import Enum +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import BaseModel, ConfigDict, Field + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible + + +class ReflectapiDemoTestsSerdeTestFlattenIfElse(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + code: int + + +class ReflectapiDemoTestsSerdeTestFlattenInner(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + inner_a: int + inner_b: str + + +class ReflectapiDemoTestsSerdeTestUpdateOrElseStdOptionOptionReflectapiDemoTesB9dceb2e( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + if_else: reflectapi_demo.tests.serde.TestFlattenIfElse | None + inner_a: int + inner_b: str + + +class ReflectapiDemoTestsSerdeTestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b( + BaseModel +): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + body: reflectapi_demo.tests.serde.TestUpdateOrElseStdOptionOptionReflectapiDemoTesB9dceb2e + extra: int + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenIfElse = ReflectapiDemoTestsSerdeTestFlattenIfElse + TestFlattenInner = ReflectapiDemoTestsSerdeTestFlattenInner + TestUpdateOrElseStdOptionOptionReflectapiDemoTesB9dceb2e = ReflectapiDemoTestsSerdeTestUpdateOrElseStdOptionOptionReflectapiDemoTesB9dceb2e + TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b = ReflectapiDemoTestsSerdeTestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b]: Response containing reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b, + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b]: Response containing reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b, + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenIfElse, + ReflectapiDemoTestsSerdeTestFlattenInner, + ReflectapiDemoTestsSerdeTestUpdateOrElseStdOptionOptionReflectapiDemoTesB9dceb2e, + ReflectapiDemoTestsSerdeTestWrapperWithNestedTypevarArgReflectapiDemoTes59f9972b, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg.snap new file mode 100644 index 00000000..3b1c51d8 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_flatten_typevar_nested_in_generic_arg.snap @@ -0,0 +1,374 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::TestWrapperWithNestedTypevarArg", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + } + ] + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::TestWrapperWithNestedTypevarArg", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + } + ] + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestWrapperWithNestedTypevarArg", + "parameters": [ + { + "name": "I" + } + ], + "fields": { + "named": [ + { + "name": "body", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "std::option::Option", + "arguments": [ + { + "name": "I" + } + ] + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + }, + { + "name": "extra", + "type": { + "name": "u32" + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "parameters": [ + { + "name": "T" + }, + { + "name": "C" + } + ], + "fields": { + "named": [ + { + "name": "inner", + "type": { + "name": "T" + }, + "required": true, + "flattened": true + }, + { + "name": "if_else", + "type": { + "name": "std::option::Option", + "arguments": [ + { + "name": "C" + } + ] + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestWrapperWithNestedTypevarArg", + "parameters": [ + { + "name": "I" + } + ], + "fields": { + "named": [ + { + "name": "body", + "type": { + "name": "reflectapi_demo::tests::serde::TestUpdateOrElse", + "arguments": [ + { + "name": "std::option::Option", + "arguments": [ + { + "name": "I" + } + ] + }, + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "required": true + }, + { + "name": "extra", + "type": { + "name": "u32" + }, + "required": true + } + ] + } + }, + { + "kind": "enum", + "name": "std::option::Option", + "description": "Optional nullable type", + "parameters": [ + { + "name": "T" + } + ], + "representation": "none", + "variants": [ + { + "name": "None", + "description": "The value is not provided, i.e. null", + "fields": "none" + }, + { + "name": "Some", + "description": "The value is provided and set to some value", + "fields": { + "unnamed": [ + { + "name": "0", + "type": { + "name": "T" + } + } + ] + } + } + ] + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-2.snap new file mode 100644 index 00000000..a5b0f974 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-2.snap @@ -0,0 +1,73 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_typescript_code :: <\nTestGenericWithConcreteFlatten > ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +export function client(base: string | Client): __definition.Interface { + return __implementation.__client(base); +} + +export namespace __definition { + export interface Interface { + inout_test: ( + input: reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten, + {} + >; + } +} +export namespace reflectapi { + /** + * Struct object with no fields + */ + export interface Empty {} + + /** + * Error object which is expected to be never returned + */ + export interface Infallible {} +} + +export namespace reflectapi_demo { + export namespace tests { + export namespace serde { + export interface TestFlattenIfElse { + code: number /* u16 */; + } + + export interface TestFlattenInner { + inner_a: number /* u32 */; + inner_b: string; + } + + export type TestGenericWithConcreteFlatten = { + other: T; + } & NullToEmptyObject; + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten, + {}, + reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-3.snap new file mode 100644 index 00000000..adea7e45 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-3.snap @@ -0,0 +1,85 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_rust_code :: < TestGenericWithConcreteFlatten\n> ()" +--- +// DO NOT MODIFY THIS FILE MANUALLY +// This file was generated by reflectapi-cli +// +// Schema name: +// + +#![allow(non_camel_case_types)] +#![allow(dead_code)] + +pub use interface::Interface; +pub use reflectapi::rt::*; + +pub mod interface { + + #[derive(Debug)] + pub struct Interface { + client: C, + } + + impl Interface { + pub fn new(client: C) -> Self { + Self { client } + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::serde::TestGenericWithConcreteFlatten< + super::types::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::serde::TestGenericWithConcreteFlatten< + super::types::reflectapi_demo::tests::serde::TestFlattenIfElse, + >, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl(&self.client, "/inout_test", input, headers).await + } + } + + #[cfg(feature = "reqwest")] + impl Interface> { + /// Convenience: build the client backed by a bare `reqwest::Client` + /// and the given base URL. Hides the + /// [`reflectapi::rt::ReqwestClient`] adapter so callers don't need + /// to name it. + pub fn try_new( + client: reqwest::Client, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + Ok(Self::new(reflectapi::rt::ReqwestClient::try_new( + client, base_url, + )?)) + } + } +} +pub mod types { + pub mod reflectapi_demo { + pub mod tests { + pub mod serde { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenIfElse { + pub code: u16, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestFlattenInner { + pub inner_a: u32, + pub inner_b: std::string::String, + } + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + pub struct TestGenericWithConcreteFlatten { + #[serde(flatten)] + pub extra: super::super::super::reflectapi_demo::tests::serde::TestFlattenInner, + pub other: T, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-4.snap new file mode 100644 index 00000000..d1e15fbd --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-4.snap @@ -0,0 +1,118 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "reflectapi :: codegen :: openapi :: Spec :: from(& schema)" +--- +{ + "openapi": "3.1.0", + "info": { + "title": "", + "description": "", + "version": "1.0.0" + }, + "paths": { + "/inout_test": { + "description": "", + "post": { + "operationId": "inout_test", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten", + "required": [ + "other" + ], + "properties": { + "other": { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenInner" + }, + { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten", + "required": [ + "other" + ], + "properties": { + "other": { + "$ref": "#/components/schemas/reflectapi_demo.tests.serde.TestFlattenIfElse" + } + } + } + ] + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.serde.TestFlattenIfElse": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenIfElse", + "required": [ + "code" + ], + "properties": { + "code": { + "$ref": "#/components/schemas/u16" + } + } + }, + "reflectapi_demo.tests.serde.TestFlattenInner": { + "type": "object", + "title": "reflectapi_demo.tests.serde.TestFlattenInner", + "required": [ + "inner_a", + "inner_b" + ], + "properties": { + "inner_a": { + "$ref": "#/components/schemas/u32" + }, + "inner_b": { + "$ref": "#/components/schemas/std.string.String" + } + } + }, + "std.string.String": { + "description": "UTF-8 encoded string", + "type": "string" + }, + "u16": { + "description": "16-bit unsigned integer", + "type": "integer" + }, + "u32": { + "description": "32-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-5.snap new file mode 100644 index 00000000..9dbd8cc8 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked-5.snap @@ -0,0 +1,190 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: "super :: into_python_code :: <\nTestGenericWithConcreteFlatten > ()" +--- +""" +Generated Python client for api_client. + +DO NOT MODIFY THIS FILE MANUALLY. +This file is automatically generated by ReflectAPI. +""" + +from __future__ import annotations + + +# Standard library imports +from typing import Annotated, Any, Generic, Optional, TypeVar, Union + +# Third-party imports +from pydantic import BaseModel, ConfigDict, Field + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible + + +# Type variables for generic types + + +T = TypeVar("T") + + +class ReflectapiDemoTestsSerdeTestFlattenIfElse(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + code: int + + +class ReflectapiDemoTestsSerdeTestFlattenInner(BaseModel): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + inner_a: int + inner_b: str + + +class ReflectapiDemoTestsSerdeTestGenericWithConcreteFlatten(BaseModel, Generic[T]): + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + other: T + inner_a: int + inner_b: str + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class serde: + """Namespace for serde types.""" + + TestFlattenIfElse = ReflectapiDemoTestsSerdeTestFlattenIfElse + TestFlattenInner = ReflectapiDemoTestsSerdeTestFlattenInner + TestGenericWithConcreteFlatten = ( + ReflectapiDemoTestsSerdeTestGenericWithConcreteFlatten + ) + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + async def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[ + reflectapi_demo.tests.serde.TestFlattenIfElse + ] + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[ + reflectapi_demo.tests.serde.TestFlattenIfElse + ] + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[reflectapi_demo.tests.serde.TestFlattenIfElse]]: Response containing reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[reflectapi_demo.tests.serde.TestFlattenIfElse] data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[ + reflectapi_demo.tests.serde.TestFlattenIfElse + ], + ) + + +class AsyncClient(AsyncClientBase): + """Async client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = AsyncInoutClient(self) + + +class InoutClient: + """Synchronous client for inout operations.""" + + def __init__(self, client: ClientBase) -> None: + self._client = client + + def test( + self, + data: Optional[ + reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[ + reflectapi_demo.tests.serde.TestFlattenIfElse + ] + ] = None, + ) -> ApiResponse[ + reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[ + reflectapi_demo.tests.serde.TestFlattenIfElse + ] + ]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[reflectapi_demo.tests.serde.TestFlattenIfElse]]: Response containing reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[reflectapi_demo.tests.serde.TestFlattenIfElse] data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + path, + params=params if params else None, + json_model=data, + response_model=reflectapi_demo.tests.serde.TestGenericWithConcreteFlatten[ + reflectapi_demo.tests.serde.TestFlattenIfElse + ], + ) + + +class Client(ClientBase): + """Synchronous client for the API.""" + + def __init__( + self, + base_url: str, + **kwargs: Any, + ) -> None: + super().__init__(base_url, **kwargs) + + self.inout = InoutClient(self) + + +# External type definitions +StdNumNonZeroU32 = Annotated[int, "Rust NonZero u32 type"] +StdNumNonZeroU64 = Annotated[int, "Rust NonZero u64 type"] +StdNumNonZeroI32 = Annotated[int, "Rust NonZero i32 type"] +StdNumNonZeroI64 = Annotated[int, "Rust NonZero i64 type"] + +# Rebuild models to resolve forward references +for _model in [ + ReflectapiDemoTestsSerdeTestFlattenIfElse, + ReflectapiDemoTestsSerdeTestFlattenInner, + ReflectapiDemoTestsSerdeTestGenericWithConcreteFlatten, +]: + try: + _model.model_rebuild() + except Exception: + pass diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked.snap new file mode 100644 index 00000000..a561d76a --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__serde__generic_with_concrete_flatten_not_marked.snap @@ -0,0 +1,214 @@ +--- +source: reflectapi-demo/src/tests/serde.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::serde::TestGenericWithConcreteFlatten", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::serde::TestGenericWithConcreteFlatten", + "arguments": [ + { + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse" + } + ] + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestGenericWithConcreteFlatten", + "parameters": [ + { + "name": "T" + } + ], + "fields": { + "named": [ + { + "name": "extra", + "type": { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + }, + "required": true, + "flattened": true + }, + { + "name": "other", + "type": { + "name": "T" + }, + "required": true + } + ] + } + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + }, + "output_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenIfElse", + "fields": { + "named": [ + { + "name": "code", + "type": { + "name": "u16" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestFlattenInner", + "fields": { + "named": [ + { + "name": "inner_a", + "type": { + "name": "u32" + }, + "required": true + }, + { + "name": "inner_b", + "type": { + "name": "std::string::String" + }, + "required": true + } + ] + } + }, + { + "kind": "struct", + "name": "reflectapi_demo::tests::serde::TestGenericWithConcreteFlatten", + "parameters": [ + { + "name": "T" + } + ], + "fields": { + "named": [ + { + "name": "extra", + "type": { + "name": "reflectapi_demo::tests::serde::TestFlattenInner" + }, + "required": true, + "flattened": true + }, + { + "name": "other", + "type": { + "name": "T" + }, + "required": true + } + ] + } + }, + { + "kind": "primitive", + "name": "std::string::String", + "description": "UTF-8 encoded string" + }, + { + "kind": "primitive", + "name": "u16", + "description": "16-bit unsigned integer" + }, + { + "kind": "primitive", + "name": "u32", + "description": "32-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-derive/Cargo.toml b/reflectapi-derive/Cargo.toml index 03135bab..fe9faaed 100644 --- a/reflectapi-derive/Cargo.toml +++ b/reflectapi-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflectapi-derive" -version = "0.17.2-alpha.3" +version = "0.17.2-alpha.4" edition = "2021" license = "Apache-2.0" @@ -22,7 +22,7 @@ workspace = true proc-macro = true [dependencies] -reflectapi-schema = { path = '../reflectapi-schema', version = "0.17.2-alpha.3" } +reflectapi-schema = { path = '../reflectapi-schema', version = "0.17.2-alpha.4" } proc-macro2 = "1.0" quote = "1.0" diff --git a/reflectapi-python-runtime/pyproject.toml b/reflectapi-python-runtime/pyproject.toml index 97ea6496..04821f8c 100644 --- a/reflectapi-python-runtime/pyproject.toml +++ b/reflectapi-python-runtime/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "reflectapi-runtime" -version = "0.17.2a3" +version = "0.17.2a4" description = "Runtime library for ReflectAPI Python clients" readme = "README.md" requires-python = ">=3.12" diff --git a/reflectapi-python-runtime/src/reflectapi_runtime/__init__.py b/reflectapi-python-runtime/src/reflectapi_runtime/__init__.py index f7f65d7a..debd2a93 100644 --- a/reflectapi-python-runtime/src/reflectapi_runtime/__init__.py +++ b/reflectapi-python-runtime/src/reflectapi_runtime/__init__.py @@ -56,7 +56,7 @@ ) from .types import BatchResult, ReflectapiEmpty, ReflectapiInfallible -__version__ = "0.17.2a3" +__version__ = "0.17.2a4" __all__ = [ # Authentication diff --git a/reflectapi-schema/Cargo.toml b/reflectapi-schema/Cargo.toml index 76dc16f3..bd5b6395 100644 --- a/reflectapi-schema/Cargo.toml +++ b/reflectapi-schema/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflectapi-schema" -version = "0.17.2-alpha.3" +version = "0.17.2-alpha.4" edition = "2021" license = "Apache-2.0" diff --git a/reflectapi-schema/src/lib.rs b/reflectapi-schema/src/lib.rs index 08edb560..9961da71 100644 --- a/reflectapi-schema/src/lib.rs +++ b/reflectapi-schema/src/lib.rs @@ -359,8 +359,14 @@ impl Typespace { return None; } - self.types_map.borrow_mut().remove(ty); - Some(self.types.remove(index)) + let removed = self.types.remove(index); + // `Vec::remove` shifts every later element down by one, so all + // map entries that pointed past `index` are now stale. Drop + // the whole map; the next `ensure_types_map` rebuilds it. + // (Removing the single key for `ty` and leaving the rest + // alone — the previous behaviour — was the bug.) + self.invalidate_types_map(); + Some(removed) } pub fn sort_types(&mut self) { @@ -1395,3 +1401,62 @@ impl Representation { matches!(self, Representation::None) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn primitive(name: &str) -> Type { + Type::Primitive(Primitive { + name: name.to_string(), + description: String::new(), + parameters: vec![], + fallback: None, + codegen_config: LanguageSpecificTypeCodegenConfig::default(), + }) + } + + /// Regression: `remove_type` used to drop only the removed key + /// from `types_map` while `Vec::remove` shifted every later + /// element's index down by one. The next call would either + /// panic on an out-of-bounds slot or silently return the wrong + /// type. The fix is to invalidate the whole map after removal + /// so the next access rebuilds it. + #[test] + fn remove_type_keeps_map_consistent_across_multiple_removals() { + let mut ts = Typespace::default(); + ts.insert_type(primitive("a")); + ts.insert_type(primitive("b")); + ts.insert_type(primitive("c")); + ts.insert_type(primitive("d")); + + // Remove two — the second one used to land on a stale index. + let _ = ts.remove_type("b"); + let _ = ts.remove_type("c"); + + assert!(ts.has_type("a"), "untouched type 'a' should still resolve"); + assert!(ts.has_type("d"), "untouched type 'd' should still resolve"); + assert!(!ts.has_type("b")); + assert!(!ts.has_type("c")); + + // Each surviving lookup should return the right Type, not + // some other slot that the stale index pointed at. + assert_eq!(ts.get_type("a").map(|t| t.name()), Some("a")); + assert_eq!(ts.get_type("d").map(|t| t.name()), Some("d")); + } + + #[test] + fn remove_type_returns_the_value_we_asked_for() { + let mut ts = Typespace::default(); + ts.insert_type(primitive("first")); + ts.insert_type(primitive("second")); + ts.insert_type(primitive("third")); + + let removed = ts.remove_type("second").expect("should find 'second'"); + assert_eq!(removed.name(), "second"); + + // The other two must still be there at the right names. + assert_eq!(ts.get_type("first").map(|t| t.name()), Some("first")); + assert_eq!(ts.get_type("third").map(|t| t.name()), Some("third")); + } +} diff --git a/reflectapi/Cargo.toml b/reflectapi/Cargo.toml index ca148c5e..af000907 100644 --- a/reflectapi/Cargo.toml +++ b/reflectapi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflectapi" -version = "0.17.2-alpha.3" +version = "0.17.2-alpha.4" edition = "2021" license = "Apache-2.0" @@ -20,8 +20,8 @@ workspace = true [dependencies] # workspace dependencies -reflectapi-derive = { path = "../reflectapi-derive", version = "0.17.2-alpha.3" } -reflectapi-schema = { path = "../reflectapi-schema", version = "0.17.2-alpha.3" } +reflectapi-derive = { path = "../reflectapi-derive", version = "0.17.2-alpha.4" } +reflectapi-schema = { path = "../reflectapi-schema", version = "0.17.2-alpha.4" } # mandatory 3rd party dependencies serde = { version = "1.0.197", features = ["derive"] } @@ -91,5 +91,5 @@ glob = ["reflectapi-schema/glob"] json = ["dep:serde_json"] indexmap = ["dep:indexmap"] reqwest = ["dep:reqwest"] -reqwest-middleware = ["dep:reqwest-middleware"] +reqwest-middleware = ["dep:reqwest-middleware", "reqwest"] rt-sse = ["dep:sseer", "rt"] diff --git a/reflectapi/src/codegen/lib.ts b/reflectapi/src/codegen/lib.ts index a564381e..862e3375 100644 --- a/reflectapi/src/codegen/lib.ts +++ b/reflectapi/src/codegen/lib.ts @@ -1,30 +1,16 @@ -export interface RequestOptions { - signal?: AbortSignal; -} - -// Transport DTOs. Method is intentionally absent: every reflectapi -// endpoint is POST by design, so transports hardcode it; if that ever -// changes it's a wire-protocol break and clients regenerate. -export interface Request { - path: string; - headers: Record; - body: Uint8Array; - signal?: AbortSignal; -} - -export interface Headers { - get(name: string): string | null; -} - -export interface Response { - status: number; - headers: Headers; - body: ReadableStream | null; -} - -export interface Client { - request(request: Request): Promise; -} +// Transport contract lives in `./generated.transport` so the bare +// names Request/Response/Headers don't shadow the DOM globals here +// or in any consumer module that imports from this file. We pull +// them in under aliases so lib.ts itself can keep using DOM types. +import type { + Client, + RequestOptions, + Response as ClientResponse, +} from "./generated.transport"; +import { ClientInstance } from "./generated.transport"; + +export { ClientInstance }; +export type { Client, RequestOptions }; type IsAny = 0 extends (1 & T) ? true : false; export type NullToEmptyObject = IsAny extends true @@ -305,7 +291,7 @@ export async function __stream_request( } async function* __sse_to_async_iterable( - response: Response, + response: ClientResponse, options?: RequestOptions, ): AsyncIterable { const body = response.body; @@ -343,24 +329,11 @@ async function* __sse_to_async_iterable( } } -async function __read_response_body(response: Response): Promise { +async function __read_response_body(response: ClientResponse): Promise { if (!response.body) return ""; - // Hand decoding to the platform; new (global) Response wraps any + // Hand decoding to the platform; the global Response wraps any // ReadableStream and exposes the well-tested .text() path. - return await new (globalThis as any).Response(response.body).text(); -} - -class ClientInstance { - constructor(private base: string) { } - - public request(request: Request): Promise { - return (globalThis as any).fetch(`${this.base}${request.path}`, { - method: "POST", - headers: request.headers, - body: request.body, - signal: request.signal, - }); - } + return await new Response(response.body).text(); } type UnionToIntersection = (U extends any ? (k: U) => unknown : never) extends ( diff --git a/reflectapi/src/codegen/lib_transport.ts b/reflectapi/src/codegen/lib_transport.ts new file mode 100644 index 00000000..16160001 --- /dev/null +++ b/reflectapi/src/codegen/lib_transport.ts @@ -0,0 +1,49 @@ +// Transport contract. Lives in its own module so the bare names +// `Request` / `Response` / `Headers` don't shadow the DOM globals of +// the same name inside the main generated module — consumers import +// them only when they need to write a custom transport. +// +// Method is intentionally absent: every reflectapi endpoint is POST by +// design, so transports hardcode it; if that ever changes it's a +// wire-protocol break and clients regenerate. + +export interface RequestOptions { + signal?: AbortSignal; +} + +export interface Request { + path: string; + headers: Record; + body: Uint8Array; + signal?: AbortSignal; +} + +export interface Headers { + get(name: string): string | null; +} + +export interface Response { + status: number; + headers: Headers; + body: ReadableStream | null; +} + +export interface Client { + request(request: Request): Promise; +} + +export class ClientInstance { + constructor(private base: string) { } + + public request(request: Request): Promise { + return fetch(`${this.base}${request.path}`, { + method: "POST", + headers: request.headers, + // BodyInit accepts BufferSource but TS 5's generic ArrayBufferLike + // typing on Uint8Array trips a structural check; the runtime + // behaviour is correct. + body: request.body as unknown as BodyInit, + signal: request.signal, + }); + } +} diff --git a/reflectapi/src/codegen/python.rs b/reflectapi/src/codegen/python.rs index 777662d4..bb62f1a4 100644 --- a/reflectapi/src/codegen/python.rs +++ b/reflectapi/src/codegen/python.rs @@ -832,6 +832,23 @@ fn render_struct_with_flattened_internal_enum( schema.get_type(inner_name) { for sf in inner_struct.fields.iter() { + if sf.flattened() { + // The inner struct has its own flatten — expand + // those fields inline so the wire shape is + // preserved across the tuple-variant boundary. + let nested = collect_flattened_fields( + &sf.type_ref, + schema, + implemented_types, + active_generics, + sf.required, + 0, + used_type_vars, + Some(sf.name()), + )?; + fields.extend(nested); + continue; + } let field_type = type_ref_to_python_type( &sf.type_ref, schema, @@ -1280,6 +1297,11 @@ pub fn generate(mut schema: Schema, config: &Config) -> anyhow::Result { // 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(); + // Pydantic cannot represent `serde(flatten)` over a generic parameter + // at runtime — the inner T's wire-level fields would silently be + // dropped. Monomorphize: emit one concrete class per (struct, args) + // pair so the flatten resolves against a concrete type. + monomorphize_flatten_generics(&mut schema)?; let implemented_types = build_implemented_types(); validate_type_references(&schema)?; @@ -1973,11 +1995,31 @@ fn collect_flattened_fields( field_name, )?; } - Some(reflectapi_schema::Type::Primitive(_)) | None => { - // Primitives (including unit types) and unresolved types cannot - // be meaningfully flattened — skip them, matching prior behavior. - // This handles cases like flattened generic parameters that resolve - // to () (std::tuple::Tuple0). + Some(reflectapi_schema::Type::Primitive(p)) => { + // Unit-like primitives (e.g. std::tuple::Tuple0) flatten to + // nothing — that's legitimate. Other primitives in a + // flatten position are a schema error: you can't flatten an + // i32 etc. + if p.name != "std::tuple::Tuple0" { + anyhow::bail!( + "python codegen: cannot flatten primitive type '{}' \ + (only structs, internally-tagged enums, and unit types \ + are valid flatten targets)", + p.name, + ); + } + } + None => { + // We resolved a flatten target by name and the type wasn't + // in the schema. After monomorphization, this should be + // impossible — generic parameters are substituted away + // before rendering. Bail rather than silently drop the + // inner type's fields (the bug fixed by monomorphization). + anyhow::bail!( + "python codegen: cannot resolve flatten target '{target_type_name}'. \ + If this is a generic parameter, monomorphization should \ + have replaced it; this is a codegen bug.", + ); } } @@ -2820,6 +2862,24 @@ fn render_internally_tagged_enum( }; for struct_field in struct_fields { + if struct_field.flattened() { + // Recurse into the flattened type and lift + // its fields into this variant; otherwise + // the wire shape gets a wrapper layer that + // serde never produced. + let nested = collect_flattened_fields( + &struct_field.type_ref, + schema, + implemented_types, + &generic_params, + struct_field.required, + 0, + used_type_vars, + Some(struct_field.name()), + )?; + fields.extend(nested); + continue; + } let field_type = type_ref_to_python_type( &struct_field.type_ref, schema, @@ -5384,9 +5444,638 @@ pub mod templates { } } +/// Pydantic equivalent of monomorphization: for any struct that uses +/// `serde(flatten)` over a generic parameter, rewrite every concrete +/// instantiation as its own non-generic struct in the schema. The +/// existing flatten-of-concrete-type rendering then handles them +/// correctly. +/// +/// Rust and TypeScript handle this case dynamically (serde flatten at +/// runtime; intersection types at the type level). Pydantic has no +/// equivalent — `Generic[T]` can't be told to lift T's fields into the +/// outer model when T is bound. So we resolve the polymorphism here. +fn monomorphize_flatten_generics(schema: &mut Schema) -> anyhow::Result<()> { + use reflectapi_schema::{Instantiate as _, Visitor}; + use std::ops::ControlFlow; + + // 1. Mark structs whose fields flatten a generic parameter + // (directly, or wrapped in Option). + fn flattened_target_is_typevar(type_ref: &TypeReference, params: &BTreeSet<&str>) -> bool { + if params.contains(type_ref.name.as_str()) { + return true; + } + // serde unwraps Option in flatten position, so a flattened + // `Option` still requires monomorphization. + if (type_ref.name == "std::option::Option" || type_ref.name == "reflectapi::Option") + && !type_ref.arguments.is_empty() + { + return flattened_target_is_typevar(&type_ref.arguments[0], params); + } + false + } + + let mut marked: BTreeSet = { + let mut out = BTreeSet::new(); + for ts in [&schema.input_types, &schema.output_types] { + for typ in ts.types() { + if let Type::Struct(s) = typ { + if s.parameters.is_empty() { + continue; + } + let params: BTreeSet<&str> = + s.parameters.iter().map(|p| p.name.as_str()).collect(); + if s.fields + .iter() + .any(|f| f.flattened() && flattened_target_is_typevar(&f.type_ref, ¶ms)) + { + out.insert(s.name.clone()); + } + } + } + } + out + }; + if marked.is_empty() { + return Ok(()); + } + + // Transitive marking: a generic struct G that doesn't itself have + // flatten-over-typevar must still be monomorphized if any of its + // fields references a (transitively-)marked struct using G's + // TypeVars as args. Otherwise, when concrete usages of G are + // resolved to monomorphized forms, the inner marked-struct ref + // would be left dangling at G's TypeVar — and removing the + // original marked struct would leave G's leftover reference + // pointing at nothing. + // + // Iterate to fixed point so chains like A → B → MarkedC + // all get marked. + /// True if `tr`'s tree mentions any of the enclosing TypeVars at + /// any depth. A TypeVar leaf is a zero-arg ref whose name is one + /// of the enclosing parameters. + fn ref_tree_contains_typevar(tr: &TypeReference, typevars: &BTreeSet<&str>) -> bool { + if tr.arguments.is_empty() && typevars.contains(tr.name.as_str()) { + return true; + } + tr.arguments + .iter() + .any(|a| ref_tree_contains_typevar(a, typevars)) + } + fn ref_uses_marked_with_typevar( + type_ref: &TypeReference, + marked: &BTreeSet, + typevars: &BTreeSet<&str>, + ) -> bool { + // Marked struct used with at least one arg that mentions an + // enclosing TypeVar somewhere in its tree (not just the + // immediate arg). `Marked>`, `Marked>>`, + // and `Marked` all qualify — without this, the wrapper + // doesn't get transitively marked, the inner marked struct + // gets removed in step 6, and the wrapper's ref dangles. + if marked.contains(&type_ref.name) + && type_ref + .arguments + .iter() + .any(|a| ref_tree_contains_typevar(a, typevars)) + { + return true; + } + type_ref + .arguments + .iter() + .any(|a| ref_uses_marked_with_typevar(a, marked, typevars)) + } + loop { + let mut new_marks: BTreeSet = BTreeSet::new(); + for ts in [&schema.input_types, &schema.output_types] { + for typ in ts.types() { + match typ { + Type::Struct(s) => { + if s.parameters.is_empty() || marked.contains(&s.name) { + continue; + } + let typevars: BTreeSet<&str> = + s.parameters.iter().map(|p| p.name.as_str()).collect(); + if s.fields + .iter() + .any(|f| ref_uses_marked_with_typevar(&f.type_ref, &marked, &typevars)) + { + new_marks.insert(s.name.clone()); + } + } + Type::Enum(e) => { + if e.parameters.is_empty() || marked.contains(&e.name) { + continue; + } + let typevars: BTreeSet<&str> = + e.parameters.iter().map(|p| p.name.as_str()).collect(); + let any = e.variants.iter().any(|v| { + v.fields.iter().any(|f| { + ref_uses_marked_with_typevar(&f.type_ref, &marked, &typevars) + }) + }); + if any { + new_marks.insert(e.name.clone()); + } + } + Type::Primitive(_) => {} + } + } + } + if new_marks.is_empty() { + break; + } + marked.extend(new_marks); + } + + // 2. Walk every type reference reachable from functions and types, + // rewriting marked refs in place (bottom-up) to their mangled + // monomorphized names. Each new (struct, normalized_args) pair + // is registered in `concrete`. Nested patterns like + // `OuterMarked, C>` resolve correctly because + // the inner ref is normalized first, so the outer's key is + // `(OuterMarked, [InnerMarked_I_D, C])` — concrete on both + // sides. + let mut concrete: BTreeMap<(String, Vec), String> = BTreeMap::new(); + let mut registered: BTreeSet = BTreeSet::new(); + + /// A type-ref is "concrete" if it resolves to a real schema type, + /// to one of our own monomorphized names (which will be inserted + /// in step 4), or is built up from concrete refs. A bare ref like + /// `T` with no arguments, no schema entry, and not yet registered + /// as a mono'd name is a TypeVar from some enclosing generic + /// context — substituting it now would produce a struct body + /// that still references a TypeVar. + fn is_concrete( + type_ref: &TypeReference, + schema: &Schema, + registered: &BTreeSet, + ) -> bool { + if type_ref.arguments.is_empty() + && schema.get_type(&type_ref.name).is_none() + && !registered.contains(&type_ref.name) + { + return false; + } + type_ref + .arguments + .iter() + .all(|a| is_concrete(a, schema, registered)) + } + + fn normalize_marked_refs( + type_ref: &mut TypeReference, + marked: &BTreeSet, + concrete: &mut BTreeMap<(String, Vec), String>, + registered: &mut BTreeSet, + schema: &Schema, + ) -> anyhow::Result<()> { + // Bottom-up: arguments first, so this ref's args are already + // resolved by the time we form the lookup key. + for a in type_ref.arguments.iter_mut() { + normalize_marked_refs(a, marked, concrete, registered, schema)?; + } + if !marked.contains(&type_ref.name) || type_ref.arguments.is_empty() { + return Ok(()); + } + // If any arg is a TypeVar from an enclosing generic context + // (e.g. `Inner` inside `struct Outer { ... }`), this + // ref isn't a concrete instantiation — it's a generic-position + // use of a marked struct. Don't register: when the enclosing + // generic resolves to a concrete instantiation, the + // substituted args will be concrete and we'll register then. + if !type_ref + .arguments + .iter() + .all(|a| is_concrete(a, schema, registered)) + { + return Ok(()); + } + let key = (type_ref.name.clone(), type_ref.arguments.clone()); + let mangled = if let Some(existing) = concrete.get(&key) { + existing.clone() + } else { + let typ = schema + .input_types + .get_type(&type_ref.name) + .or_else(|| schema.output_types.get_type(&type_ref.name)) + .ok_or_else(|| { + anyhow::anyhow!( + "monomorphization: marked type '{}' missing from schema", + type_ref.name + ) + })?; + // Register before recursing so cycles can't loop forever + // (a marked type that transitively contains a ref to itself + // with the same args would otherwise spin). + let mangled = mangle_monomorphized_name(&type_ref.name, &type_ref.arguments); + concrete.insert(key, mangled.clone()); + registered.insert(mangled.clone()); + + // Substitute with the (already-normalized) args and recurse + // into the resulting variants/fields so any further marked + // refs there get registered too. Both Struct and Enum + // implement Instantiate. + match typ { + Type::Struct(s) => { + let s = s.clone(); + if s.parameters.len() != type_ref.arguments.len() { + anyhow::bail!( + "monomorphization: arity mismatch for struct '{}': expected {}, got {}", + type_ref.name, + s.parameters.len(), + type_ref.arguments.len(), + ); + } + let mono = s.instantiate(&type_ref.arguments); + for f in mono.fields.iter() { + let mut tr = f.type_ref.clone(); + normalize_marked_refs(&mut tr, marked, concrete, registered, schema)?; + } + } + Type::Enum(e) => { + let e = e.clone(); + if e.parameters.len() != type_ref.arguments.len() { + anyhow::bail!( + "monomorphization: arity mismatch for enum '{}': expected {}, got {}", + type_ref.name, + e.parameters.len(), + type_ref.arguments.len(), + ); + } + let mono = e.instantiate(&type_ref.arguments); + for v in mono.variants.iter() { + for f in v.fields.iter() { + let mut tr = f.type_ref.clone(); + normalize_marked_refs(&mut tr, marked, concrete, registered, schema)?; + } + } + } + Type::Primitive(_) => { + anyhow::bail!( + "monomorphization: marked '{}' resolved to a primitive — only structs and enums can be monomorphized", + type_ref.name, + ); + } + } + mangled + }; + type_ref.name = mangled; + type_ref.arguments.clear(); + Ok(()) + } + + // We mutate clones to drive registration; the schema itself is + // rewritten in step 5 by the Visitor. Doing it here too would + // double-edit refs we've already normalized. + let mut seeds: Vec = Vec::new(); + for f in &schema.functions { + if let Some(t) = &f.input_type { + seeds.push(t.clone()); + } + if let Some(t) = &f.input_headers { + seeds.push(t.clone()); + } + match &f.output_type { + OutputType::Complete { output_type } => { + if let Some(t) = output_type { + seeds.push(t.clone()); + } + } + OutputType::Stream { item_type } => { + seeds.push(item_type.clone()); + } + } + if let Some(t) = &f.error_type { + seeds.push(t.clone()); + } + } + for ts in [&schema.input_types, &schema.output_types] { + for typ in ts.types() { + match typ { + Type::Struct(s) => { + for field in s.fields.iter() { + seeds.push(field.type_ref.clone()); + } + } + Type::Enum(e) => { + for v in &e.variants { + for field in v.fields.iter() { + seeds.push(field.type_ref.clone()); + } + } + } + Type::Primitive(_) => {} + } + } + } + for mut seed in seeds { + normalize_marked_refs(&mut seed, &marked, &mut concrete, &mut registered, schema)?; + } + + // 4a. Sanity-check the mangling table for collisions before + // inserting. If two distinct (struct, args) keys resolved to + // the same mangled name, `insert_type` would silently drop + // the second insert and one schema's body would survive + // under both keys' lookups — exactly the silent-data-loss + // class of bug we're trying to prevent. + { + let mut seen: BTreeMap<&String, &(String, Vec)> = BTreeMap::new(); + for (key, mangled) in concrete.iter() { + if let Some(prev) = seen.insert(mangled, key) { + anyhow::bail!( + "monomorphization: mangled-name collision: \ + '{mangled}' generated by both {key:?} and {prev:?}. \ + This is a codegen bug — please file an issue with \ + the offending schema.", + ); + } + } + } + + // 4. Insert monomorphized types into both typespaces (mirroring the + // location of the original generic). Both struct and enum + // instantiations are supported — a generic enum used as a + // variant container for marked structs needs its own concrete + // monomorphizations or the variant's field references would + // dangle after step 6 removes the originals. + for ((name, args), mangled) in concrete.iter() { + for ts_is_input in [true, false] { + let ts = if ts_is_input { + &schema.input_types + } else { + &schema.output_types + }; + let typ = match ts.get_type(name) { + Some(t) => t.clone(), + None => continue, + }; + let mono = match typ { + Type::Struct(s) => { + let mut mono = s.instantiate(args); + mono.name = mangled.clone(); + Type::Struct(mono) + } + Type::Enum(e) => { + let mut mono = e.instantiate(args); + mono.name = mangled.clone(); + Type::Enum(mono) + } + Type::Primitive(_) => continue, + }; + let target = if ts_is_input { + &mut schema.input_types + } else { + &mut schema.output_types + }; + target.insert_type(mono); + } + } + + // 5. Rewrite every type reference of the form `Marked` to the + // mangled name with no args. + struct Rewriter<'a> { + table: &'a BTreeMap<(String, Vec), String>, + } + impl<'a> Visitor for Rewriter<'a> { + type Output = (); + fn visit_type_ref( + &mut self, + type_ref: &mut TypeReference, + ) -> ControlFlow { + // Recurse into args first so inner refs resolve before outer + // lookup. Cleared args after the outer match means leaf-first + // is fine here too. + for a in type_ref.arguments.iter_mut() { + let _ = self.visit_type_ref(a); + } + let key = (type_ref.name.clone(), type_ref.arguments.clone()); + if let Some(mangled) = self.table.get(&key) { + type_ref.name = mangled.clone(); + type_ref.arguments.clear(); + } + ControlFlow::Continue(()) + } + } + let mut r = Rewriter { table: &concrete }; + let _ = r.visit_schema_inputs(schema); + let _ = r.visit_schema_outputs(schema); + + // 6. Drop the original generic marked types — they have no live + // references after rewriting, and emitting them would produce a + // bare `Generic[T]` class with the flatten field missing (the + // bug we're fixing). `remove_type` invalidates the typespace's + // internal index map, so subsequent lookups rebuild it. + for name in &marked { + let _ = schema.input_types.remove_type(name); + let _ = schema.output_types.remove_type(name); + } + + // 7. Post-pass invariant check (debug builds). Catches two + // failure classes: + // - dangling refs to a removed marked type (anywhere) + // - TypeVar leak in a body we *just monomorphized* (only + // on types we created — original schema bodies can + // legitimately reference external names like + // chrono::Utc as type-arg phantoms or const-generic + // scalars that aren't standalone schema entries) + // + // Run in debug builds only. Errors here mean the pass has + // a logic bug; that's a developer-visible problem. + debug_assert_monomorphization_invariants(schema, &marked, ®istered); + + Ok(()) +} + +#[cfg(debug_assertions)] +// `monomorphized` holds the names of types we just created (the +// values of the `concrete` table). Only those bodies need the +// TypeVar-leak check — everything else was in the schema before +// this pass and isn't our concern here. +fn debug_assert_monomorphization_invariants( + schema: &Schema, + marked: &BTreeSet, + monomorphized: &BTreeSet, +) { + // Walk every type ref reachable from functions and types. + fn walk_ref(tr: &TypeReference, f: &mut F) { + f(tr); + for a in &tr.arguments { + walk_ref(a, f); + } + } + + let mut violations: Vec = Vec::new(); + + // Check 1: no dangling refs to removed marked types, anywhere + // they could appear. Function-level refs first. + for fn_def in &schema.functions { + for site in [ + fn_def.input_type.as_ref(), + fn_def.input_headers.as_ref(), + match &fn_def.output_type { + OutputType::Complete { output_type } => output_type.as_ref(), + OutputType::Stream { item_type } => Some(item_type), + }, + fn_def.error_type.as_ref(), + ] + .into_iter() + .flatten() + { + walk_ref(site, &mut |tr| { + if marked.contains(&tr.name) { + violations.push(format!( + "function {:?} still references removed marked type '{}' \ + (post-pass dangling ref)", + fn_def.name, tr.name, + )); + } + }); + } + } + + // Then in every type's body. Plus a narrower TypeVar-leak + // check that only fires inside bodies of types we just + // monomorphized. + for ts in [&schema.input_types, &schema.output_types] { + for typ in ts.types() { + let (type_name, fields_iter): (&str, Box>) = + match typ { + Type::Struct(s) => ( + s.name.as_str(), + Box::new(s.fields.iter().map(|f| (f.name(), &f.type_ref))), + ), + Type::Enum(e) => ( + e.name.as_str(), + Box::new( + e.variants + .iter() + .flat_map(|v| v.fields.iter().map(|f| (f.name(), &f.type_ref))), + ), + ), + Type::Primitive(_) => continue, + }; + let we_monomorphized_this = monomorphized.contains(type_name); + for (field_name, type_ref) in fields_iter { + walk_ref(type_ref, &mut |tr| { + if marked.contains(&tr.name) { + violations.push(format!( + "{type_name:?}, field {field_name:?} references removed marked type '{}'", + tr.name, + )); + } + if we_monomorphized_this + && tr.arguments.is_empty() + && schema.get_type(&tr.name).is_none() + { + violations.push(format!( + "{type_name:?}, field {field_name:?} references unknown type '{}' \ + (TypeVar leak in monomorphized body)", + tr.name, + )); + } + }); + } + } + } + if !violations.is_empty() { + panic!( + "monomorphize_flatten_generics: post-pass invariants violated:\n {}", + violations.join("\n "), + ); + } +} + +#[cfg(not(debug_assertions))] +#[allow(dead_code)] +fn debug_assert_monomorphization_invariants( + _schema: &Schema, + _marked: &BTreeSet, + _monomorphized: &BTreeSet, +) { +} + +/// Mangle a `(struct, args)` instantiation into a fresh schema type +/// name. The struct's full namespace-qualified name is preserved so +/// downstream namespace mangling stays sensible; the suffix encodes +/// each argument's *full* path (`::` flattened to `_`) so two args +/// that share a leaf name but live in different modules don't fuse +/// into one mangled name (which would silently lose one type's +/// fields). +/// +/// If the resulting suffix is long enough that downstream class-name +/// handling would hash-truncate (and produce a mismatch between the +/// truncated class definition and the un-truncated dotted-path +/// reference), hash the suffix here. Same hash on both sides → all +/// references stay consistent. +fn mangle_monomorphized_name(struct_name: &str, args: &[TypeReference]) -> String { + fn arg_suffix(arg: &TypeReference) -> String { + let base = arg.name.replace("::", "_"); + if arg.arguments.is_empty() { + base + } else { + let inner = arg + .arguments + .iter() + .map(arg_suffix) + .collect::>() + .join("_"); + format!("{base}_{inner}") + } + } + let suffix = args.iter().map(arg_suffix).collect::>().join("_"); + + // Length budget: the FLAT PascalCase class name (namespace + + // struct + suffix, with `_` collapsed) must stay under 80 chars + // or downstream `finalize_class_name` will hash-truncate, while + // `type_name_to_python_ref` would still emit the un-truncated + // form — yielding two inconsistent names. Predict the + // post-PascalCase length from the struct's original name + the + // suffix, hash if it would overflow. + let candidate = format!("{struct_name}_{suffix}"); + let pascal_len = pascal_case_len(&candidate); + const FLAT_NAME_BUDGET: usize = 80; + if pascal_len <= FLAT_NAME_BUDGET { + return candidate; + } + // Trim the suffix to fit. We keep as much human-readable prefix as + // we can; the remaining bytes are a stable 8-hex-char hash. + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut h = DefaultHasher::new(); + struct_name.hash(&mut h); + suffix.hash(&mut h); + let hash = format!("{:08x}", h.finish() & 0xFFFF_FFFF); + // Pascal-collapsed budget for the suffix: + let head_pascal = pascal_case_len(&format!("{struct_name}_")); + let hash_pascal = pascal_case_len(&format!("_{hash}")); + let suffix_budget = FLAT_NAME_BUDGET.saturating_sub(head_pascal + hash_pascal); + // Walk the suffix one byte at a time tracking PascalCase length. + let mut keep = 0usize; + let mut pascal = 0usize; + let bytes = suffix.as_bytes(); + while keep < bytes.len() && pascal < suffix_budget { + if bytes[keep] != b'_' { + pascal += 1; + } + keep += 1; + } + let trimmed = std::str::from_utf8(&bytes[..keep]).unwrap_or(""); + format!("{struct_name}_{trimmed}_{hash}") +} + +/// Count the chars that a `_`-separated identifier would have once +/// PascalCased (to_pascal_case) — `_` becomes a word boundary that +/// disappears, so the post-Pascal length is the number of non-`_` +/// chars. +fn pascal_case_len(s: &str) -> usize { + s.bytes().filter(|&b| b != b'_' && b != b':').count() +} + #[cfg(test)] mod tests { - use super::{build_python_class_name_map, generate_init_py, Config}; + use super::{build_python_class_name_map, generate_init_py, mangle_monomorphized_name, Config}; + use crate::TypeReference; #[test] fn python_init_exports_client() { @@ -5419,4 +6108,214 @@ mod tests { "SystemVersionInfo" ); } + + fn tref(name: &str) -> TypeReference { + TypeReference { + name: name.to_string(), + arguments: vec![], + } + } + + fn tref_args(name: &str, args: Vec) -> TypeReference { + TypeReference { + name: name.to_string(), + arguments: args, + } + } + + #[test] + fn mangler_distinguishes_namespaces_with_same_leaf() { + // The pre-fix mangler used only the leaf, so a::Sample and + // b::Sample collided and one struct's fields were silently + // dropped (cf. test_generic_flatten_leaf_collision). + let m1 = mangle_monomorphized_name("Wrap", &[tref("a::Sample")]); + let m2 = mangle_monomorphized_name("Wrap", &[tref("b::Sample")]); + assert_ne!( + m1, m2, + "same-leaf args from distinct namespaces must mangle distinctly" + ); + } + + #[test] + fn mangler_distinguishes_arity_and_order() { + let foo_bar = mangle_monomorphized_name("Wrap", &[tref("Foo"), tref("Bar")]); + let bar_foo = mangle_monomorphized_name("Wrap", &[tref("Bar"), tref("Foo")]); + let foo_only = mangle_monomorphized_name("Wrap", &[tref("Foo")]); + assert_ne!(foo_bar, bar_foo, "arg order must affect mangling"); + assert_ne!(foo_bar, foo_only, "arity must affect mangling"); + } + + #[test] + fn mangler_distinguishes_nested_args() { + // Foo> vs Foo — the latter is a single concrete + // arg, the former wraps. The mangler must not collapse them. + let plain = mangle_monomorphized_name("Wrap", &[tref("Bar")]); + let nested = + mangle_monomorphized_name("Wrap", &[tref_args("std::vec::Vec", vec![tref("Bar")])]); + assert_ne!(plain, nested); + } + + #[test] + fn mangler_long_input_stays_under_class_name_budget() { + // Realistic deep-nesting case. The post-PascalCase length + // must not exceed 80 — otherwise downstream + // `finalize_class_name` would hash-truncate while + // `type_name_to_python_ref` would emit the un-truncated form + // and the two refs would disagree. + let deep = mangle_monomorphized_name( + "very::long::module::path::OuterWrapper", + &[ + tref_args( + "another::very::long::module::path::InnerWrapper", + vec![tref("nested::module::ConcreteTypeName")], + ), + tref("yet::another::module::path::SecondArg"), + ], + ); + let pascal_len = super::pascal_case_len(&deep); + assert!( + pascal_len <= 80, + "post-Pascal mangled name '{deep}' exceeds 80 chars (got {pascal_len})", + ); + } + + /// Regression: the post-pass invariant check used to flag + /// chrono::Utc as a "TypeVar leak" because it appears as an arg + /// of chrono::DateTime but isn't a standalone schema type. + /// Const-generic scalars (`[T; 2]`'s 2) had the same issue. + /// Both are external/structural names that legitimately don't + /// have schema entries — the check should only fire on bodies + /// of types we actually monomorphized. + #[test] + fn invariant_check_ignores_phantom_type_args_in_unmodified_bodies() { + // Marked struct + a struct holding a chrono::DateTime + // field. After monomorphization, the marked struct goes + // away and the holder remains, with its DateTime field + // intact. `chrono::Utc` is registered nowhere — the + // earlier validator would panic; the narrowed one must not. + let schema_json = r#"{ + "name": "test", + "functions": [{ + "name": "f", "path": "", + "input_type": { "name": "Holder" }, + "output_kind": "complete", + "output_type": { "name": "Wrapper", "arguments": [{ "name": "Holder" }] }, + "serialization": ["json"] + }], + "input_types": { "types": [ + { "kind": "primitive", "name": "chrono::DateTime", "description": "ts", + "parameters": [{ "name": "Tz" }], + "fallback": { "name": "std::string::String" } }, + { "kind": "primitive", "name": "std::string::String", "description": "s" }, + { "kind": "struct", "name": "Holder", + "fields": { "named": [ + { "name": "ts", + "type": { "name": "chrono::DateTime", + "arguments": [{ "name": "chrono::Utc" }] }, + "required": true } + ] } }, + { "kind": "struct", "name": "Wrapper", + "parameters": [{ "name": "T" }], + "fields": { "named": [ + { "name": "inner", "type": { "name": "T" }, "required": true, "flattened": true } + ] } } + ] }, + "output_types": { "types": [ + { "kind": "primitive", "name": "chrono::DateTime", "description": "ts", + "parameters": [{ "name": "Tz" }], + "fallback": { "name": "std::string::String" } }, + { "kind": "primitive", "name": "std::string::String", "description": "s" }, + { "kind": "struct", "name": "Holder", + "fields": { "named": [ + { "name": "ts", + "type": { "name": "chrono::DateTime", + "arguments": [{ "name": "chrono::Utc" }] }, + "required": true } + ] } }, + { "kind": "struct", "name": "Wrapper", + "parameters": [{ "name": "T" }], + "fields": { "named": [ + { "name": "inner", "type": { "name": "T" }, "required": true, "flattened": true } + ] } } + ] } + }"#; + let schema: crate::Schema = serde_json::from_str(schema_json).unwrap(); + // No panic: the validator must not flag chrono::Utc. + let py = super::generate(schema, &Config::default()).expect("codegen must run cleanly"); + // Sanity: the chrono mapping made it to the output. + assert!(py.contains("datetime") || py.contains("Holder")); + } + + /// End-to-end: a marked struct that flattens its generic param + /// AND references itself recursively (`Vec>`). The + /// reflectapi Rust derive macros can't construct such a type + /// (they overflow during schema building), but a hand-built or + /// JSON-loaded schema can — and the codegen must terminate + /// thanks to the register-before-recurse guard in + /// `normalize_marked_refs`. Without that guard, the second + /// recursive lookup would re-register and spin. + #[test] + fn recursive_marked_struct_terminates_and_renders() { + let schema_json = r#"{ + "name": "test", + "functions": [{ + "name": "f", + "path": "", + "input_type": { "name": "FlatTree", "arguments": [{ "name": "Item" }] }, + "output_kind": "complete", + "output_type": { "name": "FlatTree", "arguments": [{ "name": "Item" }] }, + "serialization": ["json"] + }], + "input_types": { "types": [ + { "kind": "primitive", "name": "std::string::String", "description": "UTF-8" }, + { "kind": "primitive", "name": "u32", "description": "u32" }, + { "kind": "primitive", "name": "std::vec::Vec", "description": "Vec", + "parameters": [{ "name": "T" }] }, + { "kind": "struct", "name": "Item", + "fields": { "named": [ + { "name": "id", "type": { "name": "u32" }, "required": true }, + { "name": "label", "type": { "name": "std::string::String" }, "required": true } + ] } }, + { "kind": "struct", "name": "FlatTree", + "parameters": [{ "name": "T" }], + "fields": { "named": [ + { "name": "value", "type": { "name": "T" }, "required": true, "flattened": true }, + { "name": "children", + "type": { "name": "std::vec::Vec", + "arguments": [{ "name": "FlatTree", "arguments": [{ "name": "T" }] }] }, + "required": true } + ] } } + ] }, + "output_types": { "types": [ + { "kind": "primitive", "name": "std::string::String", "description": "UTF-8" }, + { "kind": "primitive", "name": "u32", "description": "u32" }, + { "kind": "primitive", "name": "std::vec::Vec", "description": "Vec", + "parameters": [{ "name": "T" }] }, + { "kind": "struct", "name": "Item", + "fields": { "named": [ + { "name": "id", "type": { "name": "u32" }, "required": true }, + { "name": "label", "type": { "name": "std::string::String" }, "required": true } + ] } }, + { "kind": "struct", "name": "FlatTree", + "parameters": [{ "name": "T" }], + "fields": { "named": [ + { "name": "value", "type": { "name": "T" }, "required": true, "flattened": true }, + { "name": "children", + "type": { "name": "std::vec::Vec", + "arguments": [{ "name": "FlatTree", "arguments": [{ "name": "T" }] }] }, + "required": true } + ] } } + ] } + }"#; + let schema: crate::Schema = serde_json::from_str(schema_json).unwrap(); + let py = super::generate(schema, &Config::default()).expect("codegen must terminate"); + // The monomorphized class is named with both struct path and + // arg leaf (no namespace prefix on either here, both are + // top-level). The flatten pulled Item's id+label up; the + // recursive children list refers back to the same class. + assert!(py.contains("class FlatTreeItem")); + assert!(py.contains("children: list[FlatTreeItem]")); + assert!(py.contains(" id: int")); + assert!(py.contains(" label: str")); + } } diff --git a/reflectapi/src/codegen/typescript.rs b/reflectapi/src/codegen/typescript.rs index f18ce3c1..679bf00b 100644 --- a/reflectapi/src/codegen/typescript.rs +++ b/reflectapi/src/codegen/typescript.rs @@ -1,6 +1,6 @@ use std::{ borrow::Cow, - collections::{BTreeSet, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, process::{Command, Stdio}, }; @@ -43,7 +43,10 @@ impl Config { } } -pub fn generate(mut schema: crate::Schema, config: &Config) -> anyhow::Result { +pub fn generate( + mut schema: crate::Schema, + config: &Config, +) -> anyhow::Result> { let implemented_types = build_implemented_types(); let mut rendered_types = HashMap::new(); @@ -121,39 +124,54 @@ pub fn generate(mut schema: crate::Schema, config: &Config) -> anyhow::Result anyhow::Result<()> { - let path = super::tmp_path(src).with_extension("ts"); - std::fs::write(&path, src)?; +fn format_ts(src: String) -> anyhow::Result { + // NOTE: When updating the biome version, also update .github/workflows/ci.yaml + let biome_package = "@biomejs/biome@1.8.3"; + Ok(format_with( + // In descending order of speed. The output should be the same. + [ + Command::new("biome").args(["format", "--stdin-file-path", "dummy.ts"]), + Command::new("npx").arg("-y").arg(biome_package).args([ + "format", + "--stdin-file-path", + "dummy.ts", + ]), + Command::new("prettier").args(["--parser", "typescript"]), + Command::new("npx") + .arg("-y") + .arg("prettier") + .args(["--parser", "typescript"]), + ], + src, + )?) +} + +fn typecheck(main: &str, transport: &str) -> anyhow::Result<()> { + // tsc resolves `import ... from './generated.transport'` against + // the filesystem, so both files must live in the same directory. + let dir = super::tmp_path(main).with_extension("dir"); + std::fs::create_dir_all(&dir)?; + let main_path = dir.join("generated.ts"); + let transport_path = dir.join("generated.transport.ts"); + std::fs::write(&main_path, main)?; + std::fs::write(&transport_path, transport)?; for cmd in [ &mut Command::new("tsc"), @@ -168,7 +186,8 @@ fn typecheck(src: &str) -> anyhow::Result<()> { .arg("--skipLibCheck") .arg("--strict") .args(["--lib", "esnext,DOM"]) - .arg(&path) + .arg(&main_path) + .arg(&transport_path) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() @@ -190,8 +209,8 @@ fn typecheck(src: &str) -> anyhow::Result<()> { )); } - // Remove only after success check to keep file around for debugging - std::fs::remove_file(&path)?; + // Remove only after success check to keep files around for debugging + let _ = std::fs::remove_dir_all(&dir); return Ok(()); }