diff --git a/CHANGELOG.md b/CHANGELOG.md index a15715db..03d8ccf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,18 @@ # Changelog -## Unreleased +## 0.24.0-alpha.1 -### Fixed +Generated clients keep decoding responses after the server removes a deprecated field, and Python clients reject a `None` the server can't read before sending it. TypeScript and Rust code that reads a deprecated response field must now handle its absence; see Breaking. -- A generated Python model rejects an explicit `None` for an omittable field whose type serde can't read from `null`. Since 0.21 the client sends an explicit `None` as `null`, which the server refuses, so `Model(page=None)` for a `#[serde(default)] page: Page` failed with a 400. It now raises a `ValidationError` where the model is built. Leaving the field out is unchanged. Whether a value may be `null` is now part of the shared field contract and is decided from the schema, not from Python's rendered annotation. Before, only primitives and containers were checked. Now these reject `null` too: structs with fields, tuple structs, tagged enums, newtypes and wrappers such as `Box` of those, and primitives such as `chrono::DateTime`, `uuid::Uuid` and `url::Url`. Unit structs, untagged enums with a variant that accepts `null`, newtypes of an `Option`, generic parameters and fields with a custom codec on their side (`deserialize_with` in a request, `serialize_with` in a response), including a newtype's inner field, still accept `None`. -- A generated Python module with an omittable field of a unit type, `()` or a unit struct, imports again. The field was annotated `None | None`, which Python can't evaluate, so importing the module raised `TypeError`. It is now `None`. -- A generated Rust client compiles when an optional field's type is a generated struct or enum without `Default`, such as a `#[serde(default)] memo: Memo` whose `Memo` implements `Default` only on the server. The field got `#[serde(default)]`, which needs `Default` on the generated type, and generated types derive it only when configured to. Such a field is now `Option`: `None` leaves the key out and the server applies its default. Fields whose generated type derives `Default`, through `base_derives` or the type's `additional_derives`, are unchanged. +### Breaking -### Changed +- **Deprecated fields of response-only types are optional in generated clients.** Generated clients decode responses after the server removes a deprecated field. Every generator now treats a deprecated field of a response-only type as optional: Python reads it as `None` and types a non-`Option` field `T | None`, TypeScript marks it `?:`, the Rust client types it `Option`, unless it already is one, and OpenAPI leaves it out of `required`. Before, a client generated while the field was deprecated failed to decode every response once the server dropped it, although the client never read it. TypeScript and Rust code that reads such a field must now handle its absence. Types also used in requests keep deprecated fields required, because the server may still require them until it removes them, and so do types the client decodes by trying them in turn: untagged enums and untagged variants, the type behind a flattened `Option`, and every type these reach. There, an optional field could change what a payload decodes as. ([#218](https://github.com/thepartly/reflectapi/pull/218)) + +### Fixed -- Generated clients decode responses after the server removes a deprecated field. Every generator now treats a deprecated field of a response-only type as optional: Python reads it as `None` and types a non-`Option` field `T | None`, TypeScript marks it `?:`, the Rust client types it `Option`, unless it already is one, and OpenAPI leaves it out of `required`. Before, a client generated while the field was deprecated failed to decode every response once the server dropped it, although the client never read it. TypeScript and Rust code that reads such a field must now handle its absence. Types also used in requests keep deprecated fields required, because the server may still require them until it removes them, and so do types the client decodes by trying them in turn: untagged enums and untagged variants, the type behind a flattened `Option`, and every type these reach. There, an optional field could change what a payload decodes as. +- A generated Python model rejects an explicit `None` for an omittable field whose type serde can't read from `null`. Since 0.21 the client sends an explicit `None` as `null`, which the server refuses, so `Model(page=None)` for a `#[serde(default)] page: Page` failed with a 400. It now raises a `ValidationError` where the model is built. Leaving the field out is unchanged. Whether a value may be `null` is now part of the shared field contract and is decided from the schema, not from Python's rendered annotation. Before, only primitives and containers were checked. Now these reject `null` too: structs with fields, tuple structs, tagged enums, newtypes and wrappers such as `Box` of those, and primitives such as `chrono::DateTime`, `uuid::Uuid` and `url::Url`. Unit structs, untagged enums with a variant that accepts `null`, newtypes of an `Option`, generic parameters and fields with a custom codec on their side (`deserialize_with` in a request, `serialize_with` in a response), including a newtype's inner field, still accept `None`. ([#219](https://github.com/thepartly/reflectapi/pull/219)) +- A generated Python module with an omittable field of a unit type, `()` or a unit struct, imports again. The field was annotated `None | None`, which Python can't evaluate, so importing the module raised `TypeError`. It is now `None`. ([#220](https://github.com/thepartly/reflectapi/pull/220)) +- A generated Rust client compiles when an optional field's type is a generated struct or enum without `Default`, such as a `#[serde(default)] memo: Memo` whose `Memo` implements `Default` only on the server. The field got `#[serde(default)]`, which needs `Default` on the generated type, and generated types derive it only when configured to. Such a field is now `Option`: `None` leaves the key out and the server applies its default. Fields whose generated type derives `Default`, through `base_derives` or the type's `additional_derives`, are unchanged. ([#221](https://github.com/thepartly/reflectapi/pull/221)) ## 0.23.0 diff --git a/Cargo.lock b/Cargo.lock index 26af147f..2f5abcf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1653,7 +1653,7 @@ dependencies = [ [[package]] name = "reflectapi" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "anyhow", "axum", @@ -1682,7 +1682,7 @@ dependencies = [ [[package]] name = "reflectapi-cli" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "anyhow", "clap", @@ -1746,7 +1746,7 @@ dependencies = [ [[package]] name = "reflectapi-derive" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "proc-macro-error", "proc-macro2", @@ -1758,7 +1758,7 @@ dependencies = [ [[package]] name = "reflectapi-schema" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "glob", "serde", diff --git a/reflectapi-cli/Cargo.toml b/reflectapi-cli/Cargo.toml index 9c66696b..d4242673 100644 --- a/reflectapi-cli/Cargo.toml +++ b/reflectapi-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflectapi-cli" -version = "0.23.0" +version = "0.24.0-alpha.1" edition = "2021" default-run = "reflectapi" @@ -23,7 +23,7 @@ doc = false workspace = true [dependencies] -reflectapi = { path = "../reflectapi", version = "0.23.0", features = ["codegen"] } +reflectapi = { path = "../reflectapi", version = "0.24.0-alpha.1", features = ["codegen"] } rouille = "3" clap = { version = "4.5.3", features = ["derive"] } diff --git a/reflectapi-demo/clients/python/uv.lock b/reflectapi-demo/clients/python/uv.lock index 76cace80..b824cc75 100644 --- a/reflectapi-demo/clients/python/uv.lock +++ b/reflectapi-demo/clients/python/uv.lock @@ -346,7 +346,7 @@ dev = [ [[package]] name = "reflectapi-runtime" -version = "0.23.0" +version = "0.24.0a1" source = { editable = "../../../reflectapi-python-runtime" } dependencies = [ { name = "anyio" }, diff --git a/reflectapi-demo/tests/rust/enum_compatibility/Cargo.lock b/reflectapi-demo/tests/rust/enum_compatibility/Cargo.lock index 4af172c1..34a2516a 100644 --- a/reflectapi-demo/tests/rust/enum_compatibility/Cargo.lock +++ b/reflectapi-demo/tests/rust/enum_compatibility/Cargo.lock @@ -348,7 +348,7 @@ dependencies = [ [[package]] name = "reflectapi" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "anyhow", "bytes", @@ -366,7 +366,7 @@ dependencies = [ [[package]] name = "reflectapi-derive" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "proc-macro-error", "proc-macro2", @@ -387,7 +387,7 @@ dependencies = [ [[package]] name = "reflectapi-schema" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "glob", "serde", diff --git a/reflectapi-demo/tests/rust/field_presence/Cargo.lock b/reflectapi-demo/tests/rust/field_presence/Cargo.lock index cd9d9f9b..90ec38b4 100644 --- a/reflectapi-demo/tests/rust/field_presence/Cargo.lock +++ b/reflectapi-demo/tests/rust/field_presence/Cargo.lock @@ -312,7 +312,7 @@ dependencies = [ [[package]] name = "reflectapi" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "bytes", "futures-util", @@ -327,7 +327,7 @@ dependencies = [ [[package]] name = "reflectapi-derive" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "proc-macro-error", "proc-macro2", @@ -348,7 +348,7 @@ dependencies = [ [[package]] name = "reflectapi-schema" -version = "0.23.0" +version = "0.24.0-alpha.1" dependencies = [ "glob", "serde", diff --git a/reflectapi-derive/Cargo.toml b/reflectapi-derive/Cargo.toml index 683f4579..5a161718 100644 --- a/reflectapi-derive/Cargo.toml +++ b/reflectapi-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflectapi-derive" -version = "0.23.0" +version = "0.24.0-alpha.1" edition = "2021" license = "Apache-2.0" @@ -22,7 +22,7 @@ workspace = true proc-macro = true [dependencies] -reflectapi-schema = { path = '../reflectapi-schema', version = "0.23.0" } +reflectapi-schema = { path = '../reflectapi-schema', version = "0.24.0-alpha.1" } proc-macro2 = "1.0" quote = "1.0" diff --git a/reflectapi-python-runtime/pyproject.toml b/reflectapi-python-runtime/pyproject.toml index d54c4ea5..6b3cde4a 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.23.0" +version = "0.24.0a1" 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 b0f0c9d7..1c9a46cf 100644 --- a/reflectapi-python-runtime/src/reflectapi_runtime/__init__.py +++ b/reflectapi-python-runtime/src/reflectapi_runtime/__init__.py @@ -58,7 +58,7 @@ ) from .types import BatchResult, ReflectapiEmpty, ReflectapiInfallible -__version__ = "0.23.0" +__version__ = "0.24.0a1" __all__ = [ # Authentication diff --git a/reflectapi-schema/Cargo.toml b/reflectapi-schema/Cargo.toml index f3d1447e..b9dd2ef4 100644 --- a/reflectapi-schema/Cargo.toml +++ b/reflectapi-schema/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflectapi-schema" -version = "0.23.0" +version = "0.24.0-alpha.1" edition = "2021" license = "Apache-2.0" diff --git a/reflectapi/Cargo.toml b/reflectapi/Cargo.toml index 9a5f31f1..618c63f8 100644 --- a/reflectapi/Cargo.toml +++ b/reflectapi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reflectapi" -version = "0.23.0" +version = "0.24.0-alpha.1" edition = "2021" license = "Apache-2.0" @@ -20,8 +20,8 @@ workspace = true [dependencies] # workspace dependencies -reflectapi-derive = { path = "../reflectapi-derive", version = "0.23.0" } -reflectapi-schema = { path = "../reflectapi-schema", version = "0.23.0", default-features = false } +reflectapi-derive = { path = "../reflectapi-derive", version = "0.24.0-alpha.1" } +reflectapi-schema = { path = "../reflectapi-schema", version = "0.24.0-alpha.1", default-features = false } # mandatory 3rd party dependencies serde = { version = "1.0.197", features = ["derive"] }