diff --git a/reflectapi-demo/src/tests/basic.rs b/reflectapi-demo/src/tests/basic.rs index 6ed34247..b5829228 100644 --- a/reflectapi-demo/src/tests/basic.rs +++ b/reflectapi-demo/src/tests/basic.rs @@ -636,6 +636,28 @@ fn test_reflectapi_deprecated() { assert_snapshot!(StructWithDeprecatedField); } +#[test] +#[allow(deprecated)] +fn test_reflectapi_deprecated_type_and_variants() { + #[derive(serde::Serialize, reflectapi::Input, serde::Deserialize, reflectapi::Output)] + #[deprecated = "this struct is deprecated"] + struct DeprecatedStruct { + _f: u8, + } + assert_snapshot!(DeprecatedStruct); + + #[derive(serde::Serialize, reflectapi::Input, serde::Deserialize, reflectapi::Output)] + #[deprecated] + enum DeprecatedEnumWithVariants { + Plain, + #[deprecated] + DeprecatedVariant, + #[deprecated = "use Plain instead"] + DeprecatedVariantWithNote, + } + assert_snapshot!(DeprecatedEnumWithVariants); +} + #[derive(reflectapi::Input, reflectapi::Output, serde::Deserialize, serde::Serialize)] struct TestStructWithExternalGenericTypeFallback { #[reflectapi(transform = "reflectapi::TypeReference::fallback_recursively")] diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-10.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-10.snap new file mode 100644 index 00000000..27cfed23 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-10.snap @@ -0,0 +1,156 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_python_code :: < DeprecatedEnumWithVariants > ()" +--- +""" +DO NOT MODIFY THIS FILE MANUALLY +This file was generated by reflectapi-cli + +Schema name: +""" + +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 ReflectapiDemoTestsBasicDeprecatedEnumWithVariants(str, Enum): + PLAIN = "Plain" + DEPRECATED_VARIANT = "DeprecatedVariant" + DEPRECATED_VARIANT_WITH_NOTE = "DeprecatedVariantWithNote" + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class basic: + """Namespace for basic types.""" + + DeprecatedEnumWithVariants = ( + ReflectapiDemoTestsBasicDeprecatedEnumWithVariants + ) + + +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.basic.DeprecatedEnumWithVariants] = None, + ) -> ApiResponse[reflectapi_demo.tests.basic.DeprecatedEnumWithVariants]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.basic.DeprecatedEnumWithVariants]: Response containing reflectapi_demo.tests.basic.DeprecatedEnumWithVariants 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.basic.DeprecatedEnumWithVariants, + ) + + +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.basic.DeprecatedEnumWithVariants] = None, + ) -> ApiResponse[reflectapi_demo.tests.basic.DeprecatedEnumWithVariants]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.basic.DeprecatedEnumWithVariants]: Response containing reflectapi_demo.tests.basic.DeprecatedEnumWithVariants 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.basic.DeprecatedEnumWithVariants, + ) + + +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 +_rebuild_errors: list[str] = [] +for _model in [ + ReflectapiDemoTestsBasicDeprecatedEnumWithVariants, +]: + if not hasattr(_model, "model_rebuild"): + continue + try: + _model.model_rebuild() + except Exception as _exc: + _rebuild_errors.append(f" - {_model.__name__}: {type(_exc).__name__}: {_exc}") +if _rebuild_errors: + raise RuntimeError( + "reflectapi: failed to rebuild " + + str(len(_rebuild_errors)) + + " generated model(s). This usually means the codegen emitted an annotation pointing at a symbol that was never defined (a dangling type reference). Fix the codegen rather than catching this error.\n" + + "\n".join(_rebuild_errors) + ) diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-2.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-2.snap new file mode 100644 index 00000000..49903800 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-2.snap @@ -0,0 +1,60 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_typescript_code :: < DeprecatedStruct > ()" +--- +// 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.DeprecatedStruct, + 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 basic { + export interface DeprecatedStruct { + _f: number /* u8 */; + } + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.basic.DeprecatedStruct, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.basic.DeprecatedStruct, + {}, + reflectapi_demo.tests.basic.DeprecatedStruct, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-3.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-3.snap new file mode 100644 index 00000000..5dbeb12f --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-3.snap @@ -0,0 +1,68 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_rust_code :: < DeprecatedStruct > ()" +--- +// 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::basic::DeprecatedStruct, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::basic::DeprecatedStruct, + 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 basic { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + #[deprecated(note = "this struct is deprecated")] + pub struct DeprecatedStruct { + pub _f: u8, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-4.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-4.snap new file mode 100644 index 00000000..ae7c42a4 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-4.snap @@ -0,0 +1,62 @@ +--- +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.DeprecatedStruct" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/reflectapi_demo.tests.basic.DeprecatedStruct" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.basic.DeprecatedStruct": { + "type": "object", + "title": "reflectapi_demo.tests.basic.DeprecatedStruct", + "required": [ + "_f" + ], + "properties": { + "_f": { + "$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_deprecated_type_and_variants-5.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-5.snap new file mode 100644 index 00000000..94b8e23e --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-5.snap @@ -0,0 +1,155 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_python_code :: < DeprecatedStruct > ()" +--- +""" +DO NOT MODIFY THIS FILE MANUALLY +This file was generated by reflectapi-cli + +Schema name: +""" + +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 ReflectapiDemoTestsBasicDeprecatedStruct(BaseModel): + model_config = ConfigDict( + extra="ignore", populate_by_name=True, protected_namespaces=(), defer_build=True + ) + + f: int = Field(serialization_alias="_f", validation_alias="_f") + + +# Namespace classes for dotted access to types +class reflectapi_demo: + """Namespace for reflectapi_demo types.""" + + class tests: + """Namespace for tests types.""" + + class basic: + """Namespace for basic types.""" + + DeprecatedStruct = ReflectapiDemoTestsBasicDeprecatedStruct + + +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.basic.DeprecatedStruct] = None, + ) -> ApiResponse[reflectapi_demo.tests.basic.DeprecatedStruct]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.basic.DeprecatedStruct]: Response containing reflectapi_demo.tests.basic.DeprecatedStruct 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.basic.DeprecatedStruct, + ) + + +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.basic.DeprecatedStruct] = None, + ) -> ApiResponse[reflectapi_demo.tests.basic.DeprecatedStruct]: + """ + + Args: + data: Request data for the test operation. + + Returns: + ApiResponse[reflectapi_demo.tests.basic.DeprecatedStruct]: Response containing reflectapi_demo.tests.basic.DeprecatedStruct 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.basic.DeprecatedStruct, + ) + + +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 +_rebuild_errors: list[str] = [] +for _model in [ + ReflectapiDemoTestsBasicDeprecatedStruct, +]: + if not hasattr(_model, "model_rebuild"): + continue + try: + _model.model_rebuild() + except Exception as _exc: + _rebuild_errors.append(f" - {_model.__name__}: {type(_exc).__name__}: {_exc}") +if _rebuild_errors: + raise RuntimeError( + "reflectapi: failed to rebuild " + + str(len(_rebuild_errors)) + + " generated model(s). This usually means the codegen emitted an annotation pointing at a symbol that was never defined (a dangling type reference). Fix the codegen rather than catching this error.\n" + + "\n".join(_rebuild_errors) + ) diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-6.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-6.snap new file mode 100644 index 00000000..04f09c99 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-6.snap @@ -0,0 +1,86 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::basic::DeprecatedEnumWithVariants" + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::basic::DeprecatedEnumWithVariants" + }, + "serialization": [ + "json", + "msgpack" + ] + } + ], + "input_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Empty", + "description": "Struct object with no fields", + "fields": "none" + }, + { + "kind": "enum", + "name": "reflectapi_demo::tests::basic::DeprecatedEnumWithVariants", + "deprecation_note": "", + "variants": [ + { + "name": "Plain", + "fields": "none" + }, + { + "name": "DeprecatedVariant", + "deprecation_note": "", + "fields": "none" + }, + { + "name": "DeprecatedVariantWithNote", + "deprecation_note": "use Plain instead", + "fields": "none" + } + ] + } + ] + }, + "output_types": { + "types": [ + { + "kind": "struct", + "name": "reflectapi::Infallible", + "description": "Error object which is expected to be never returned", + "fields": "none" + }, + { + "kind": "enum", + "name": "reflectapi_demo::tests::basic::DeprecatedEnumWithVariants", + "deprecation_note": "", + "variants": [ + { + "name": "Plain", + "fields": "none" + }, + { + "name": "DeprecatedVariant", + "deprecation_note": "", + "fields": "none" + }, + { + "name": "DeprecatedVariantWithNote", + "deprecation_note": "use Plain instead", + "fields": "none" + } + ] + } + ] + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-7.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-7.snap new file mode 100644 index 00000000..cdbd18c7 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-7.snap @@ -0,0 +1,64 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_typescript_code :: < DeprecatedEnumWithVariants > ()" +--- +// 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.DeprecatedEnumWithVariants, + headers: {}, + options?: RequestOptions, + ) => AsyncResult< + reflectapi_demo.tests.basic.DeprecatedEnumWithVariants, + {} + >; + } +} +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 type DeprecatedEnumWithVariants = + | "Plain" + | "DeprecatedVariant" + | "DeprecatedVariantWithNote"; + } + } +} + +namespace __implementation { + + function inout_test(client: Client) { + return ( + input: reflectapi_demo.tests.basic.DeprecatedEnumWithVariants, + headers: {}, + options?: RequestOptions, + ) => + __request< + reflectapi_demo.tests.basic.DeprecatedEnumWithVariants, + {}, + reflectapi_demo.tests.basic.DeprecatedEnumWithVariants, + {} + >(client, "/inout_test", input, headers, options); + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-8.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-8.snap new file mode 100644 index 00000000..cf3f5dc4 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-8.snap @@ -0,0 +1,72 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: "super :: into_rust_code :: < DeprecatedEnumWithVariants > ()" +--- +// 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::basic::DeprecatedEnumWithVariants, + headers: reflectapi::Empty, + ) -> Result< + super::types::reflectapi_demo::tests::basic::DeprecatedEnumWithVariants, + 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 basic { + + #[derive(Debug, serde::Deserialize, serde::Serialize)] + #[deprecated] + pub enum DeprecatedEnumWithVariants { + Plain, + #[deprecated] + DeprecatedVariant, + #[deprecated(note = "use Plain instead")] + DeprecatedVariantWithNote, + } + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-9.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-9.snap new file mode 100644 index 00000000..7a444481 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants-9.snap @@ -0,0 +1,59 @@ +--- +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.DeprecatedEnumWithVariants" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/reflectapi_demo.tests.basic.DeprecatedEnumWithVariants" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "reflectapi_demo.tests.basic.DeprecatedEnumWithVariants": { + "oneOf": [ + { + "const": "Plain" + }, + { + "const": "DeprecatedVariant" + }, + { + "const": "DeprecatedVariantWithNote" + } + ] + } + } + } +} diff --git a/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants.snap b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants.snap new file mode 100644 index 00000000..dbeac3d0 --- /dev/null +++ b/reflectapi-demo/src/tests/snapshots/reflectapi_demo__tests__basic__reflectapi_deprecated_type_and_variants.snap @@ -0,0 +1,86 @@ +--- +source: reflectapi-demo/src/tests/basic.rs +expression: schema +--- +{ + "name": "", + "functions": [ + { + "name": "inout_test", + "path": "", + "input_type": { + "name": "reflectapi_demo::tests::basic::DeprecatedStruct" + }, + "output_kind": "complete", + "output_type": { + "name": "reflectapi_demo::tests::basic::DeprecatedStruct" + }, + "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::DeprecatedStruct", + "deprecation_note": "this struct is deprecated", + "fields": { + "named": [ + { + "name": "_f", + "type": { + "name": "u8" + }, + "required": true + } + ] + } + }, + { + "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::DeprecatedStruct", + "deprecation_note": "this struct is deprecated", + "fields": { + "named": [ + { + "name": "_f", + "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 1aef78fe..f267361a 100644 --- a/reflectapi-derive/src/derive.rs +++ b/reflectapi-derive/src/derive.rs @@ -165,6 +165,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { fn make_alias_type( type_def_name: String, type_def_description: String, + deprecation_note: Option, serde_name: String, type_ref: reflectapi_schema::TypeReference, codegen_config: reflectapi_schema::LanguageSpecificTypeCodegenConfig, @@ -173,6 +174,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { name: type_def_name, serde_name, description: type_def_description, + deprecation_note, parameters: Vec::new(), fields: Fields::Unnamed(vec![Field::new("0".into(), type_ref)]), transparent: true, @@ -186,6 +188,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { return make_alias_type( type_def_name, type_def_description, + attrs.deprecation_note.clone(), serde_name, visit_field_type(cx, &input_type_attribute), codegen_config, @@ -196,6 +199,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { return make_alias_type( type_def_name, type_def_description, + attrs.deprecation_note.clone(), serde_name, visit_field_type(cx, a), codegen_config, @@ -206,6 +210,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { return make_alias_type( type_def_name, type_def_description, + attrs.deprecation_note.clone(), serde_name, visit_field_type(cx, a), codegen_config, @@ -218,6 +223,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { return make_alias_type( type_def_name, type_def_description, + attrs.deprecation_note.clone(), serde_name, visit_field_type(cx, &output_type_attribute), codegen_config, @@ -228,6 +234,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { return make_alias_type( type_def_name, type_def_description, + attrs.deprecation_note.clone(), serde_name, visit_field_type(cx, a), codegen_config, @@ -241,6 +248,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { ast::Data::Enum(variants) => { let mut result = Enum::new(type_def_name); result.description = type_def_description; + result.deprecation_note = attrs.deprecation_note.clone(); result.serde_name = serde_name; result.codegen_config = codegen_config; match container.attrs.tag() { @@ -274,9 +282,12 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { variant.attrs.skip_serializing() || variant_attrs.output_skip } } { - result - .variants - .push(visit_variant(cx, variant, attrs.discriminant)); + result.variants.push(visit_variant( + cx, + variant, + attrs.discriminant, + variant_attrs.deprecation_note, + )); } } visit_generic_parameters(cx, container.generics, &mut result.parameters); @@ -288,6 +299,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { let mut result = make_alias_type( type_def_name, type_def_description, + attrs.deprecation_note.clone(), serde_name, visit_field_type(cx, &unit_type), codegen_config, @@ -298,6 +310,7 @@ fn visit_type(cx: &Context, container: &ast::Container<'_>) -> Type { let mut result = Struct::new(type_def_name); result.codegen_config = codegen_config; result.description = type_def_description; + result.deprecation_note = attrs.deprecation_note.clone(); let fields = fields .iter() .filter_map(|field| visit_field(cx, field)) @@ -357,6 +370,7 @@ fn visit_variant( cx: &Context, variant: &ast::Variant<'_>, use_discriminant: bool, + deprecation_note: Option, ) -> reflectapi_schema::Variant { let (variant_def_name, serde_name) = visit_name(cx, variant.attrs.name(), Some(&variant.original.ident)); @@ -387,6 +401,7 @@ fn visit_variant( name: variant_def_name, serde_name, description: parse_doc_attributes(&variant.original.attrs), + deprecation_note, fields, discriminant, untagged: variant.attrs.untagged(), diff --git a/reflectapi-derive/src/parser.rs b/reflectapi-derive/src/parser.rs index a0a7f7d5..5e3f1f86 100644 --- a/reflectapi-derive/src/parser.rs +++ b/reflectapi-derive/src/parser.rs @@ -116,6 +116,7 @@ mod tests { #[derive(Default)] pub(crate) struct ParsedTypeAttributes { + pub deprecation_note: Option, pub input_type: Option, pub output_type: Option, pub discriminant: bool, @@ -136,6 +137,7 @@ pub(crate) struct ParsedFieldAttributes { #[derive(Debug, Default)] pub(crate) struct ParsedVariantAttributes { + pub deprecation_note: Option, pub input_skip: bool, pub output_skip: bool, } @@ -174,6 +176,13 @@ pub(crate) fn parse_type_attributes( let mut result = ParsedTypeAttributes::default(); for attr in attributes.iter() { + if attr.path() == DEPRECATED { + match parse_deprecated_attr(attr) { + Ok(note) => result.deprecation_note = Some(note), + Err(err) => cx.syn_error(err), + } + } + if attr.path() != REFLECT { continue; } @@ -265,10 +274,18 @@ pub(crate) fn parse_variant_attributes( cx: &Context, attributes: &[syn::Attribute], ) -> ParsedVariantAttributes { + let mut deprecation_note = None; let mut input_skip = false; let mut output_skip = false; for attr in attributes.iter() { + if attr.path() == DEPRECATED { + match parse_deprecated_attr(attr) { + Ok(note) => deprecation_note = Some(note), + Err(err) => cx.syn_error(err), + } + } + if attr.path() != REFLECT { continue; } @@ -301,6 +318,7 @@ pub(crate) fn parse_variant_attributes( } ParsedVariantAttributes { + deprecation_note, input_skip, output_skip, } diff --git a/reflectapi-derive/src/tokenizable_schema.rs b/reflectapi-derive/src/tokenizable_schema.rs index 4fbd13ef..c84852f0 100644 --- a/reflectapi-derive/src/tokenizable_schema.rs +++ b/reflectapi-derive/src/tokenizable_schema.rs @@ -148,6 +148,10 @@ impl ToTokens for TokenizableVariant<'_> { let name = self.inner.name.as_str(); let serde_name = self.inner.serde_name.as_str(); let description = self.inner.description.as_str(); + let deprecation_note = self.inner.deprecation_note.as_ref().map_or_else( + || quote::quote! { None }, + |d| quote::quote! { Some(#d.into()) }, + ); let fields = self.inner.fields().map(TokenizableField::new); let fields = match self.inner.fields { @@ -173,6 +177,7 @@ impl ToTokens for TokenizableVariant<'_> { name: #name.into(), serde_name: #serde_name.into(), description: #description.into(), + deprecation_note: #deprecation_note, fields: #fields, discriminant: #discriminant, untagged: #untagged, @@ -234,6 +239,10 @@ impl ToTokens for TokenizableEnum<'_> { let name = self.inner.name.as_str(); let serde_name = self.inner.serde_name.as_str(); let description = self.inner.description.as_str(); + let deprecation_note = self.inner.deprecation_note.as_ref().map_or_else( + || quote::quote! { None }, + |d| quote::quote! { Some(#d.into()) }, + ); let parameters = self.inner.parameters().map(TokenizableTypeParameter::new); let representation = TokenizableRepresentation::new(&self.inner.representation); let variants = self.inner.variants().map(TokenizableVariant::new); @@ -244,6 +253,7 @@ impl ToTokens for TokenizableEnum<'_> { name: #name.into(), serde_name: #serde_name.into(), description: #description.into(), + deprecation_note: #deprecation_note, parameters: vec![#(#parameters),*], representation: #representation, variants: vec![#(#variants),*], @@ -296,6 +306,10 @@ impl ToTokens for TokenizableStruct<'_> { let name = self.inner.name.as_str(); let serde_name = self.inner.serde_name.as_str(); let description = self.inner.description.as_str(); + let deprecation_note = self.inner.deprecation_note.as_ref().map_or_else( + || quote::quote! { None }, + |d| quote::quote! { Some(#d.into()) }, + ); let parameters = self.inner.parameters().map(TokenizableTypeParameter::new); let fields = self.inner.fields().map(TokenizableField::new); let fields = match self.inner.fields { @@ -318,6 +332,7 @@ impl ToTokens for TokenizableStruct<'_> { name: #name.into(), serde_name: #serde_name.into(), description: #description.into(), + deprecation_note: #deprecation_note, parameters: vec![#(#parameters),*], fields: #fields, transparent: #transparent, diff --git a/reflectapi-schema/src/lib.rs b/reflectapi-schema/src/lib.rs index 056de5ae..24224570 100644 --- a/reflectapi-schema/src/lib.rs +++ b/reflectapi-schema/src/lib.rs @@ -900,6 +900,11 @@ pub struct Struct { #[serde(skip_serializing_if = "String::is_empty", default)] pub description: String, + /// Deprecation note. If none, struct is not deprecated. + /// If present as empty string, struct is deprecated without a note. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub deprecation_note: Option, + /// Generic type parameters, if any #[serde(skip_serializing_if = "Vec::is_empty", default)] pub parameters: Vec, @@ -924,6 +929,7 @@ impl Struct { name, serde_name: Default::default(), description: Default::default(), + deprecation_note: Default::default(), parameters: Default::default(), fields: Default::default(), transparent: Default::default(), @@ -949,6 +955,10 @@ impl Struct { self.description.as_str() } + pub fn deprecated(&self) -> bool { + self.deprecation_note.is_some() + } + pub fn parameters(&self) -> std::slice::Iter<'_, TypeParameter> { self.parameters.iter() } @@ -1266,6 +1276,11 @@ pub struct Enum { #[serde(skip_serializing_if = "String::is_empty", default)] pub description: String, + /// Deprecation note. If none, enum is not deprecated. + /// If present as empty string, enum is deprecated without a note. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub deprecation_note: Option, + /// Generic type parameters, if any #[serde(skip_serializing_if = "Vec::is_empty", default)] pub parameters: Vec, @@ -1289,6 +1304,7 @@ impl Enum { name, serde_name: Default::default(), description: Default::default(), + deprecation_note: Default::default(), parameters: Default::default(), representation: Default::default(), variants: Default::default(), @@ -1312,6 +1328,10 @@ impl Enum { self.description.as_str() } + pub fn deprecated(&self) -> bool { + self.deprecation_note.is_some() + } + pub fn parameters(&self) -> std::slice::Iter<'_, TypeParameter> { self.parameters.iter() } @@ -1339,6 +1359,11 @@ pub struct Variant { #[serde(skip_serializing_if = "String::is_empty", default)] pub description: String, + /// Deprecation note. If none, variant is not deprecated. + /// If present as empty string, variant is deprecated without a note. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub deprecation_note: Option, + pub fields: Fields, #[serde(skip_serializing_if = "Option::is_none", default)] pub discriminant: Option, @@ -1354,6 +1379,7 @@ impl Variant { name, serde_name: String::new(), description: String::new(), + deprecation_note: None, fields: Fields::None, discriminant: None, untagged: false, @@ -1376,6 +1402,10 @@ impl Variant { self.description.as_str() } + pub fn deprecated(&self) -> bool { + self.deprecation_note.is_some() + } + pub fn fields(&self) -> std::slice::Iter<'_, Field> { self.fields.iter() } diff --git a/reflectapi/src/codegen/openapi.rs b/reflectapi/src/codegen/openapi.rs index d5948405..40e8667a 100644 --- a/reflectapi/src/codegen/openapi.rs +++ b/reflectapi/src/codegen/openapi.rs @@ -878,6 +878,7 @@ impl Converter<'_> { name: variant.name().to_owned(), serde_name: variant.serde_name.to_owned(), description: sanitize_description(variant.description()), + deprecation_note: variant.deprecation_note.clone(), parameters: vec![], fields: variant.fields.clone(), transparent: false, diff --git a/reflectapi/src/codegen/rust.rs b/reflectapi/src/codegen/rust.rs index 6b81c96f..3f88884a 100644 --- a/reflectapi/src/codegen/rust.rs +++ b/reflectapi/src/codegen/rust.rs @@ -392,9 +392,21 @@ mod templates { } } + /// Render a type-level `#[deprecated]` attribute (with a trailing + /// newline) from a deprecation note, or an empty string if the item is + /// not deprecated. An empty note means "deprecated without a note". + fn __render_type_deprecated(deprecation_note: &Option) -> String { + match deprecation_note { + None => String::new(), + Some(note) if note.is_empty() => "#[deprecated]\n".into(), + Some(note) => format!("#[deprecated(note = \"{note}\")]\n"), + } + } + pub(super) struct __Struct { pub name: String, pub description: String, + pub deprecation_note: Option, pub fields: Vec<__Field>, pub is_tuple: bool, pub is_input_type: bool, @@ -435,9 +447,10 @@ mod templates { pub fn render(&self) -> String { let brackets = self.render_brackets(); let mut out = format!( - "\n{}{}\npub struct {} {}", + "\n{}{}\n{}pub struct {} {}", self.description, self.render_attributes_derive(), + __render_type_deprecated(&self.deprecation_note), self.name, brackets.0, ); @@ -467,6 +480,7 @@ mod templates { pub(super) struct __Enum { pub name: String, pub description: String, + pub deprecation_note: Option, pub variants: Vec<__Variant>, pub representation: crate::Representation, pub is_input_type: bool, @@ -520,9 +534,10 @@ mod templates { pub fn render(&self) -> anyhow::Result { let mut out = format!( - "\n{}{}\n{}pub enum {} {{", + "\n{}{}\n{}{}pub enum {} {{", self.description, self.render_attributes_derive(), + __render_type_deprecated(&self.deprecation_note), self.render_attributes(), self.name, ); @@ -553,6 +568,7 @@ mod templates { pub name: String, pub serde_name: String, pub description: String, + pub deprecation_note: Option, pub fields: Vec<__Field>, pub discriminant: Option, pub untagged: bool, @@ -591,11 +607,21 @@ mod templates { if self.untagged { attrs.push("untagged".into()); } - if attrs.is_empty() { - "".into() - } else { - format!("#[serde({})]\n ", attrs.join(", ")) + + let mut out = String::new(); + if !attrs.is_empty() { + out.push_str(&format!("#[serde({})]\n ", attrs.join(", "))); } + if let Some(deprecation_note) = &self.deprecation_note { + if deprecation_note.is_empty() { + out.push_str("#[deprecated]\n "); + } else { + out.push_str(&format!( + "#[deprecated(note = \"{deprecation_note}\")]\n " + )); + } + } + out } fn render_fields(&self) -> anyhow::Result { @@ -750,6 +776,7 @@ mod templates { pub(super) struct __Unit { pub name: String, pub description: String, + pub deprecation_note: Option, pub is_input_type: bool, pub is_output_type: bool, pub is_error_type: bool, @@ -779,9 +806,10 @@ mod templates { pub fn render(&self) -> String { let mut out = format!( - "\n{}{}\npub struct {};\n", + "\n{}{}\n{}pub struct {};\n", self.description, self.render_attributes_derive(), + __render_type_deprecated(&self.deprecation_note), self.name, ); @@ -1077,6 +1105,7 @@ fn __interface_types_from_function_group( __struct_name_from_parent_name_and_name(&group.parent, &name) ), description: "".into(), + deprecation_note: None, fields: Default::default(), is_tuple: false, is_input_type: false, @@ -1287,6 +1316,7 @@ fn __render_type( let unit_struct_template = templates::__Unit { name: type_name, description: __doc_to_ts_comments(&struct_def.description, 0), + deprecation_note: struct_def.deprecation_note.clone(), is_input_type, is_output_type, is_error_type, @@ -1312,6 +1342,7 @@ fn __render_type( let interface_template = templates::__Struct { name: type_name, description: __doc_to_ts_comments(&struct_def.description, 0), + deprecation_note: struct_def.deprecation_note.clone(), is_tuple: struct_def.is_tuple(), is_input_type, is_output_type, @@ -1346,6 +1377,7 @@ fn __render_type( let enum_template = templates::__Enum { name: type_name, description: __doc_to_ts_comments(&enum_def.description, 0), + deprecation_note: enum_def.deprecation_note.clone(), representation: enum_def.representation.clone(), is_input_type, is_output_type, @@ -1359,6 +1391,7 @@ fn __render_type( name: __name_to_pascal_case(variant.name()), serde_name: variant.serde_name().into(), description: __doc_to_ts_comments(&variant.description, 4), + deprecation_note: variant.deprecation_note.clone(), fields: variant .fields .iter() diff --git a/reflectapi/src/traits.rs b/reflectapi/src/traits.rs index 6ab001fa..4c5bc0ce 100644 --- a/reflectapi/src/traits.rs +++ b/reflectapi/src/traits.rs @@ -703,6 +703,7 @@ fn reflectapi_duration(schema: &mut crate::Typespace) -> crate::TypeReference { let type_def = crate::Struct { name: type_name.into(), description: "Time duration type".into(), + deprecation_note: Default::default(), fields: crate::Fields::Named(vec![ crate::Field::new("secs".into(), "u64".into()).with_required(true), crate::Field::new("nanos".into(), "u32".into()).with_required(true),