golem_schema_derive/lib.rs
1// Copyright 2024-2026 Golem Cloud
2//
3// Licensed under the Golem Source License v1.1 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://license.golem.cloud/LICENSE
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Derive macros for the Golem schema model.
16//!
17//! Two derive macros are provided:
18//!
19//! - [`IntoSchema`] — emits an implementation of
20//! `golem_common::schema::conversion::IntoSchema`. It registers the type into a
21//! [`SchemaBuilder`](golem_common::schema::SchemaBuilder), returns a
22//! [`SchemaType`](golem_common::schema::SchemaType) describing how the
23//! type should appear at the call site (always a
24//! [`SchemaType::Ref`](golem_common::schema::SchemaType::Ref) for nominal
25//! user types), and converts Rust values into matching
26//! [`SchemaValue`](golem_common::schema::SchemaValue)s through `to_value`.
27//! - [`FromSchema`] — emits a `from_value` implementation that decodes a
28//! [`SchemaValue`](golem_common::schema::SchemaValue) produced against a
29//! compatible schema back into the original Rust value.
30//!
31//! The Rust-SDK default `type_id()` derivation normalizes a Rust path FQN
32//! into the language-independent dotted form (`a::b::c::Xyz` →
33//! `a.b.c.Xyz`). Generic instantiations append resolved type arguments
34//! inside `<…>`, recursively normalized:
35//! `my_crate::Container<my_crate::user::Profile>` becomes
36//! `my_crate.Container<my_crate.user.Profile>`. `#[schema(named = "…")]`
37//! overrides the base name; for a generic type the explicit name is also
38//! auto-suffixed with the resolved arguments so each instantiation gets
39//! its own definition.
40//!
41//! See the crate-level documentation of `golem_common::schema::conversion`
42//! for the full attribute surface and recursion semantics.
43
44extern crate proc_macro;
45
46mod codegen;
47mod expand;
48mod parse;
49
50use proc_macro::TokenStream;
51use syn::{DeriveInput, parse_macro_input};
52
53/// Converts a guest value directly to canonical wire nodes, including affine
54/// resource preflight. Does not construct or validate a schema model.
55#[proc_macro_derive(IntoWire, attributes(schema))]
56pub fn derive_into_wire(input: TokenStream) -> TokenStream {
57 let input = parse_macro_input!(input as DeriveInput);
58 codegen::wire::expand(&input, true)
59 .unwrap_or_else(syn::Error::into_compile_error)
60 .into()
61}
62
63/// Decodes canonical wire nodes directly into a concrete guest type.
64#[proc_macro_derive(FromWire, attributes(schema))]
65pub fn derive_from_wire(input: TokenStream) -> TokenStream {
66 let input = parse_macro_input!(input as DeriveInput);
67 codegen::wire::expand(&input, false)
68 .unwrap_or_else(syn::Error::into_compile_error)
69 .into()
70}
71
72/// Appends a concrete type definition directly to the flat WIT schema arena.
73#[proc_macro_derive(WireSchema, attributes(schema))]
74pub fn derive_wire_schema(input: TokenStream) -> TokenStream {
75 let input = parse_macro_input!(input as DeriveInput);
76 codegen::wire::expand_schema(&input)
77 .unwrap_or_else(syn::Error::into_compile_error)
78 .into()
79}
80
81#[proc_macro_derive(IntoSchema, attributes(schema))]
82pub fn derive_into_schema(input: TokenStream) -> TokenStream {
83 let input = parse_macro_input!(input as DeriveInput);
84 expand::expand_into_schema(input)
85 .unwrap_or_else(syn::Error::into_compile_error)
86 .into()
87}
88
89#[proc_macro_derive(FromSchema, attributes(schema))]
90pub fn derive_from_schema(input: TokenStream) -> TokenStream {
91 let input = parse_macro_input!(input as DeriveInput);
92 expand::expand_from_schema(input)
93 .unwrap_or_else(syn::Error::into_compile_error)
94 .into()
95}
96
97/// Emits a schema-only `poem_openapi::types::Type` implementation whose
98/// registered `MetaSchema` mirrors the type's **serde** wire representation,
99/// while `ToJSON`/`ParseFromJSON` keep delegating to `serde_json` (so the wire
100/// format is unchanged). Unsupported serde attributes are rejected at compile
101/// time as a drift guard.
102///
103/// This derive only emits `poem_openapi::*` token paths and is meant to be
104/// gated behind the consuming crate's `full` feature, e.g.
105/// `#[cfg_attr(feature = "full", derive(PoemSchema))]`.
106#[proc_macro_derive(PoemSchema, attributes(serde))]
107pub fn derive_poem_schema(input: TokenStream) -> TokenStream {
108 let input = parse_macro_input!(input as DeriveInput);
109 codegen::poem::expand_poem_schema(&input)
110 .unwrap_or_else(syn::Error::into_compile_error)
111 .into()
112}
113
114#[proc_macro_derive(Schema, attributes(schema))]
115pub fn derive_schema(input: TokenStream) -> TokenStream {
116 let input = parse_macro_input!(input as DeriveInput);
117
118 let into_schema = expand::expand_into_schema(input.clone());
119 let from_schema = expand::expand_from_schema(input);
120
121 match (into_schema, from_schema) {
122 (Ok(into_schema), Ok(from_schema)) => quote::quote! {
123 #into_schema
124 #from_schema
125 }
126 .into(),
127 (Err(err), Ok(_)) | (Ok(_), Err(err)) => err.into_compile_error().into(),
128 (Err(mut first), Err(second)) => {
129 first.combine(second);
130 first.into_compile_error().into()
131 }
132 }
133}