diff --git a/reflectapi-demo/src/tests/basic.rs b/reflectapi-demo/src/tests/basic.rs index 1aab06f5..dfb4b82d 100644 --- a/reflectapi-demo/src/tests/basic.rs +++ b/reflectapi-demo/src/tests/basic.rs @@ -538,6 +538,19 @@ fn test_reflectapi_struct_with_skip_field_output() { assert_snapshot!(TestStructWithSkipFieldOutput); } +#[derive(reflectapi::Input, reflectapi::Output, serde::Deserialize, serde::Serialize)] +struct TestStructWithRequiredField { + #[serde(default)] + #[reflectapi(required)] + required_with_default: u8, + #[serde(default)] + required_without_default: u8, +} +#[test] +fn test_reflectapi_struct_with_required_field() { + assert_snapshot!(TestStructWithRequiredField); +} + #[test] fn test_reflectapi_struct_with_additional_derives() { #[derive( diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-2.snap new file mode 100644 index 00000000..763a81a8 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-2.snap @@ -0,0 +1,74 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_typescript_code :: < TestStructWithRequiredField > ()" +--- +// 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.basic.input.TestStructWithRequiredField, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.basic.output.TestStructWithRequiredField, + {} + >; + } +} +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 basic { + export namespace input { + export interface TestStructWithRequiredField { + required_with_default: number /* u8 */; + required_without_default?: number /* u8 */; + } + } + + export namespace output { + export interface TestStructWithRequiredField { + required_with_default: number /* u8 */; + required_without_default: number /* u8 */; + } + } + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.basic.input.TestStructWithRequiredField, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.basic.input.TestStructWithRequiredField, + {}, + reflectapi_demo.tests.basic.output.TestStructWithRequiredField, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-3.snap new file mode 100644 index 00000000..55c3eba2 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-3.snap @@ -0,0 +1,81 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_rust_code :: < TestStructWithRequiredField > ()" +--- +// 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, + base_url: reflectapi::rt::Url, + } + + impl Interface { + pub fn try_new( + client: C, + base_url: reflectapi::rt::Url, + ) -> std::result::Result { + if base_url.cannot_be_a_base() { + return Err(reflectapi::rt::UrlParseError::RelativeUrlWithCannotBeABaseBase); + } + + Ok(Self { client, base_url }) + } + pub async fn inout_test( + &self, + input: super::types::reflectapi_demo::tests::basic::input::TestStructWithRequiredField, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::basic::output::TestStructWithRequiredField, + reflectapi::rt::Error, + > { + reflectapi::rt::__request_impl( + &self.client, + self.base_url + .join("/inout_test") + .expect("checked base_url already and path is valid"), + input, + headers, + ) + .await + } + } +} +pub mod types { + + pub mod reflectapi_demo { + pub mod tests { + pub mod basic { + pub mod input { + + #[derive(Debug, serde::Serialize)] + pub struct TestStructWithRequiredField { + pub required_with_default: u8, + #[serde(default = "Default::default")] + pub required_without_default: u8, + } + } + pub mod output { + + #[derive(Debug, serde::Deserialize)] + pub struct TestStructWithRequiredField { + pub required_with_default: u8, + pub required_without_default: u8, + } + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-4.snap new file mode 100644 index 00000000..eb306005 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-4.snap @@ -0,0 +1,81 @@ +--- +source: reflectapi-demo/src/tests/basic.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.basic.input.TestStructWithRequiredField" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/reflectapi_demo.tests.basic.output.TestStructWithRequiredField" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.basic.input.TestStructWithRequiredField": { + "type": "object", + "title": "reflectapi_demo.tests.basic.input.TestStructWithRequiredField", + "required": [ + "required_with_default" + ], + "properties": { + "required_with_default": { + "$ref": "#/components/schemas/u8" + }, + "required_without_default": { + "$ref": "#/components/schemas/u8" + } + } + }, + "reflectapi_demo.tests.basic.output.TestStructWithRequiredField": { + "type": "object", + "title": "reflectapi_demo.tests.basic.output.TestStructWithRequiredField", + "required": [ + "required_with_default", + "required_without_default" + ], + "properties": { + "required_with_default": { + "$ref": "#/components/schemas/u8" + }, + "required_without_default": { + "$ref": "#/components/schemas/u8" + } + } + }, + "u8": { + "description": "8-bit unsigned integer", + "type": "integer" + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-5.snap new file mode 100644 index 00000000..a2bf347e --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field-5.snap @@ -0,0 +1,165 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_python_code :: < TestStructWithRequiredField > ()" +--- +''' +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 + +# Runtime imports +from reflectapi_runtime import AsyncClientBase, ClientBase, ApiResponse +from reflectapi_runtime import ReflectapiEmpty +from reflectapi_runtime import ReflectapiInfallible +from reflectapi_runtime.testing import MockClient, create_api_response + + +class ReflectapiDemoTestsBasicInputTestStructWithRequiredField(BaseModel): + """Generated data model.""" + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + required_with_default: int + required_without_default: int | None = None + + +class ReflectapiDemoTestsBasicOutputTestStructWithRequiredField(BaseModel): + """Generated data model.""" + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + required_with_default: int + required_without_default: int + + +class AsyncInoutClient: + """Async client for inout operations.""" + + def __init__(self, client: AsyncClientBase) -> None: + self._client = client + + + async def test( + self, + data: Optional[ReflectapiDemoTestsBasicInputTestStructWithRequiredField] = None, + ) -> ApiResponse[ReflectapiDemoTestsBasicOutputTestStructWithRequiredField]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[ReflectapiDemoTestsBasicOutputTestStructWithRequiredField]: Response containing ReflectapiDemoTestsBasicOutputTestStructWithRequiredField data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return await self._client._make_request( + "POST", + path, + params=params if params else None, + json_model=data, + response_model=ReflectapiDemoTestsBasicOutputTestStructWithRequiredField, +) + + +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[ReflectapiDemoTestsBasicInputTestStructWithRequiredField] = None, + ) -> ApiResponse[ReflectapiDemoTestsBasicOutputTestStructWithRequiredField]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[ReflectapiDemoTestsBasicOutputTestStructWithRequiredField]: Response containing ReflectapiDemoTestsBasicOutputTestStructWithRequiredField data + """ + path = "/inout_test" + + params: dict[str, Any] = {} + return self._client._make_request( + "POST", + path, + params=params if params else None, + json_model=data, + response_model=ReflectapiDemoTestsBasicOutputTestStructWithRequiredField, +) + + +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 +try: + ReflectapiDemoTestsBasicInputTestStructWithRequiredField.model_rebuild() + ReflectapiDemoTestsBasicOutputTestStructWithRequiredField.model_rebuild() +except AttributeError: + # Some types may not have model_rebuild method + pass + +# Factory classes (generated after model rebuild to avoid forward references) + +# Testing utilities + + +def create_reflectapidemotestsbasicinputteststructwithrequiredfield_response(value: ReflectapiDemoTestsBasicInputTestStructWithRequiredField) -> ApiResponse[ReflectapiDemoTestsBasicInputTestStructWithRequiredField]: + """Create a mock ApiResponse for ReflectapiDemoTestsBasicInputTestStructWithRequiredField.""" + return create_api_response(value) + + +def create_reflectapidemotestsbasicoutputteststructwithrequiredfield_response(value: ReflectapiDemoTestsBasicOutputTestStructWithRequiredField) -> ApiResponse[ReflectapiDemoTestsBasicOutputTestStructWithRequiredField]: + """Create a mock ApiResponse for ReflectapiDemoTestsBasicOutputTestStructWithRequiredField.""" + return create_api_response(value) + + +def create_mock_client() -> MockClient: + """Create a mock client for testing.""" + return MockClient() diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field.snap new file mode 100644 index 00000000..e406e7b6 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_struct_with_required_field.snap @@ -0,0 +1,96 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::basic::input::TestStructWithRequiredField" + }, + "output_type": { + "name": "reflectapi_demo::tests::basic::output::TestStructWithRequiredField" + }, + "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::basic::input::TestStructWithRequiredField", + "fields": { + "named": [ + { + "name": "required_with_default", + "type": { + "name": "u8" + }, + "required": true + }, + { + "name": "required_without_default", + "type": { + "name": "u8" + } + } + ] + } + }, + { + "kind": "primitive", + "name": "u8", + "description": "8-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::basic::output::TestStructWithRequiredField", + "fields": { + "named": [ + { + "name": "required_with_default", + "type": { + "name": "u8" + }, + "required": true + }, + { + "name": "required_without_default", + "type": { + "name": "u8" + }, + "required": true + } + ] + } + }, + { + "kind": "primitive", + "name": "u8", + "description": "8-bit unsigned integer" + } + ] + } +} diff --git a/reflectapi-derive/src/derive.rs b/reflectapi-derive/src/derive.rs index bace5ff3..30c912d6 100644 --- a/reflectapi-derive/src/derive.rs +++ b/reflectapi-derive/src/derive.rs @@ -419,7 +419,7 @@ fn visit_field(cx: &Context, field: &ast::Field<'_>) -> Option field.attrs.default().is_none(), + ReflectType::Input => attrs.required || field.attrs.default().is_none(), ReflectType::Output => field.attrs.skip_serializing_if().is_none(), }; field_def.flattened = field.attrs.flatten(); diff --git a/reflectapi-derive/src/parser.rs b/reflectapi-derive/src/parser.rs index de89219c..4a113d7b 100644 --- a/reflectapi-derive/src/parser.rs +++ b/reflectapi-derive/src/parser.rs @@ -131,6 +131,7 @@ pub(crate) struct ParsedFieldAttributes { pub output_transform: String, pub input_skip: bool, pub output_skip: bool, + pub required: bool, } #[derive(Debug, Default)] @@ -386,6 +387,9 @@ pub(crate) fn parse_field_attributes( // #[reflectapi(skip)] result.input_skip = true; result.output_skip = true; + } else if meta.path == REQUIRED { + // #[reflectapi(required)] + result.required = true; } else { let path = meta.path.to_token_stream().to_string(); return Err(meta.error(format_args!("unknown reflect field attribute `{path}`"))); diff --git a/reflectapi-derive/src/symbol.rs b/reflectapi-derive/src/symbol.rs index 5c6600aa..62278923 100644 --- a/reflectapi-derive/src/symbol.rs +++ b/reflectapi-derive/src/symbol.rs @@ -22,6 +22,8 @@ pub const OUTPUT_SKIP: Symbol = Symbol("output_skip"); pub const DERIVE: Symbol = Symbol("derive"); +pub const REQUIRED: Symbol = Symbol("required"); + pub const DISCRIMINANT: Symbol = Symbol("discriminant"); impl PartialEq for Ident {