-
Notifications
You must be signed in to change notification settings - Fork 3
advance transform attribute to access full field metadata #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,9 +22,61 @@ ReflectAPI provides `#[reflectapi(...)]` attributes that control how Rust types | |
|
|
||
| | Attribute | Description | | ||
| |-----------|-------------| | ||
| | `#[reflectapi(transform = "path::to::fn")]` | Apply a type transformation callback for both schemas. | | ||
| | `#[reflectapi(input_transform = "path::to::fn")]` | Apply a type transformation callback for input only. | | ||
| | `#[reflectapi(output_transform = "path::to::fn")]` | Apply a type transformation callback for output only. | | ||
| | `#[reflectapi(transform = "path::to::fn")]` | Apply a field transformation callback for both schemas. Signature: `fn(&mut Field, &Typespace)`. | | ||
| | `#[reflectapi(input_transform = "path::to::fn")]` | Apply a field transformation callback for input only. | | ||
| | `#[reflectapi(output_transform = "path::to::fn")]` | Apply a field transformation callback for output only. | | ||
|
|
||
| The transform callback receives `&mut Field` and can modify any field metadata — type, `required`, `hidden`, etc. It runs after type references are resolved. | ||
|
|
||
| ReflectAPI ships with built-in transforms in the `reflectapi::transforms` module: | ||
| - `reflectapi::transforms::fallback_recursively` — unwraps transparent wrappers (e.g. `Arc<T>` → `T`) | ||
| - `reflectapi::transforms::make_required` — makes a field required: `Option<T>` → required `T`, `reflectapi::Option<T>` → required `Option<T>` (nullable but not omittable). For non-Option types, marks the field as required without changing the type. | ||
| - `reflectapi::transforms::make_nonnullable` — makes a field non-nullable: `Option<T>` → `T`, `reflectapi::Option<T>` → `T` (keeps optionality, removes nullability). No-op for non-Option types. | ||
| - `reflectapi::transforms::make_required_and_nonnullable` — makes a field both required and non-nullable: `Option<T>` → required `T`, `reflectapi::Option<T>` → required `T`. For non-Option types, marks the field as required without changing the type. | ||
|
|
||
| > **Note:** In reflectapi, "required" and "non-nullable" are distinct concepts. A field can be: | ||
| > - Required + non-nullable (`T`, required) — must be present, cannot be null | ||
| > - Required + nullable (`Option<T>`, required) — must be present, can be null | ||
| > - Optional + nullable (`reflectapi::Option<T>`) — can be omitted or null | ||
| > - Optional + non-nullable (`T`, not required) — can be omitted, but if present cannot be null | ||
| > | ||
| > `make_required` removes the "can be omitted" (undefined) state. `make_nonnullable` removes the "can be null" state. `make_required_and_nonnullable` removes both. | ||
|
|
||
| **Example: Controlling requiredness and nullability in the schema** | ||
|
|
||
| A field may use an Option wrapper at the Rust level (e.g., because a middleware populates it before your handler runs) but you want generated clients to see a different contract. | ||
|
|
||
| ```rust,ignore | ||
| #[derive(serde::Deserialize, reflectapi::Input)] | ||
| struct MyRequest { | ||
| /// In Rust this is `Option<String>` (populated by middleware), | ||
| /// but clients see it as a required `String` field. | ||
| #[serde(default)] | ||
| #[reflectapi(input_transform = "reflectapi::transforms::make_required")] | ||
| pub tenant_id: Option<String>, | ||
| } | ||
|
|
||
| #[derive(serde::Deserialize, reflectapi::Input)] | ||
| struct PatchRequest { | ||
| /// In Rust this is `reflectapi::Option<String>` (populated by middleware), | ||
| /// but clients see it as a required `Option<String>` (nullable, not omittable). | ||
|
Comment on lines
+61
to
+62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Subtle difference based on
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. making std option required means removing Undefined and None variants for the non-nullable field. making reflectapi required means removing Undefined variant, as None is expected possible null value. the difference is on purpose as the semantics of both types required. however, the client can provide their own transform functions.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I use a function called
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to rename something here to make this clearer
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you know that from reflectapi (and our API standard) required is not equal to not-nullable? These are two distinct things. It is possible to have non-required but nullable and required but nullable and 2 other combos. So the transform function does exactly that: it removes the declaration for the field to be required, which is for non nullable field means no None of the standard Option and for nullable field it means no Undefined of the reflectapi Option.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can add make_nonnullable, and super set of both make_required_and_nonnullable
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be happy with this outcome, will mark as approved. |
||
| #[serde(default)] | ||
| #[reflectapi(input_transform = "reflectapi::transforms::make_required")] | ||
| pub correlation_id: reflectapi::Option<String>, | ||
|
|
||
| /// In Rust this is `reflectapi::Option<String>`, | ||
| /// but clients see it as an optional `String` (can be omitted, but not null). | ||
| #[serde(default)] | ||
| #[reflectapi(input_transform = "reflectapi::transforms::make_nonnullable")] | ||
| pub display_name: reflectapi::Option<String>, | ||
|
|
||
| /// In Rust this is `reflectapi::Option<String>`, | ||
| /// but clients see it as a required `String` (must be present, cannot be null). | ||
| #[serde(default)] | ||
| #[reflectapi(input_transform = "reflectapi::transforms::make_required_and_nonnullable")] | ||
| pub user_id: reflectapi::Option<String>, | ||
| } | ||
| ``` | ||
|
|
||
| ### Visibility | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| use crate::{Field, TypeReference, Typespace}; | ||
|
|
||
| /// Unwraps transparent wrappers (e.g. `Arc<T>` → `T`) on the field's type reference. | ||
| pub fn fallback_recursively(field: &mut Field, schema: &Typespace) { | ||
| field.type_ref.fallback_recursively(schema); | ||
| } | ||
|
|
||
| /// Makes a field required in the schema. | ||
| /// | ||
| /// - `std::option::Option<T>` → required `T` | ||
| /// - `reflectapi::Option<T>` → required `std::option::Option<T>` | ||
| /// (removes the "undefined" state, keeps nullable) | ||
| /// - Other types → marks the field as required without changing the type | ||
| pub fn make_required(field: &mut Field, _schema: &Typespace) { | ||
| if field.type_ref.name() == "std::option::Option" { | ||
| if let Some(inner) = field.type_ref.arguments().next().cloned() { | ||
| field.type_ref = inner; | ||
| } | ||
| } else if field.type_ref.name() == "reflectapi::Option" { | ||
| let arguments = field.type_ref.arguments().cloned().collect::<Vec<_>>(); | ||
| field.type_ref = TypeReference::new("std::option::Option", arguments); | ||
| } | ||
| field.required = true; | ||
| } | ||
|
|
||
| /// Makes a field non-nullable in the schema. | ||
| /// | ||
| /// - `std::option::Option<T>` → `T` (required unchanged) | ||
| /// - `reflectapi::Option<T>` → `T` (required unchanged) | ||
| /// (removes the "null" state, keeps optionality) | ||
| /// - Other types → no-op (already non-nullable) | ||
| pub fn make_nonnullable(field: &mut Field, _schema: &Typespace) { | ||
| if field.type_ref.name() == "std::option::Option" | ||
| || field.type_ref.name() == "reflectapi::Option" | ||
| { | ||
| if let Some(inner) = field.type_ref.arguments().next().cloned() { | ||
| field.type_ref = inner; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Makes a field both required and non-nullable in the schema. | ||
| /// | ||
| /// - `std::option::Option<T>` → required `T` | ||
| /// - `reflectapi::Option<T>` → required `T` | ||
| /// (removes both "undefined" and "null" states) | ||
| /// - Other types → marks the field as required without changing the type | ||
| pub fn make_required_and_nonnullable(field: &mut Field, schema: &Typespace) { | ||
| make_nonnullable(field, schema); | ||
| make_required(field, schema); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.