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//! [`Schema`] derives both schema-model conversions and the direct wire traits.
18//! The conversion traits are also available as individual derives:
19//!
20//! - [`IntoSchema`] — emits an implementation of
21//! `golem_common::schema::conversion::IntoSchema`. It registers the type into a
22//! [`SchemaBuilder`](golem_common::schema::SchemaBuilder), returns a
23//! [`SchemaType`](golem_common::schema::SchemaType) describing how the
24//! type should appear at the call site (always a
25//! [`SchemaType::Ref`](golem_common::schema::SchemaType::Ref) for nominal
26//! user types), and converts Rust values into matching
27//! [`SchemaValue`](golem_common::schema::SchemaValue)s through `to_value`.
28//! - [`FromSchema`] — emits a `from_value` implementation that decodes a
29//! [`SchemaValue`](golem_common::schema::SchemaValue) produced against a
30//! compatible schema back into the original Rust value.
31//!
32//! The Rust-SDK default `type_id()` derivation normalizes a Rust path FQN
33//! into the language-independent dotted form (`a::b::c::Xyz` →
34//! `a.b.c.Xyz`). Generic instantiations append resolved type arguments
35//! inside `<…>`, recursively normalized:
36//! `my_crate::Container<my_crate::user::Profile>` becomes
37//! `my_crate.Container<my_crate.user.Profile>`. `#[schema(named = "…")]`
38//! overrides the base name; for a generic type the explicit name is also
39//! auto-suffixed with the resolved arguments so each instantiation gets
40//! its own definition.
41//!
42//! See the crate-level documentation of `golem_common::schema::conversion`
43//! for the full attribute surface and recursion semantics.
44
45extern crate proc_macro;
46
47mod codegen;
48mod expand;
49mod parse;
50
51use proc_macro::TokenStream;
52use syn::{DeriveInput, parse_macro_input};
53
54/// Converts a guest value directly to canonical wire nodes, including affine
55/// resource preflight. Does not construct or validate a schema model.
56#[proc_macro_derive(IntoWire, attributes(schema))]
57pub fn derive_into_wire(input: TokenStream) -> TokenStream {
58 let input = parse_macro_input!(input as DeriveInput);
59 codegen::wire::expand(&input, true)
60 .unwrap_or_else(syn::Error::into_compile_error)
61 .into()
62}
63
64/// Decodes canonical wire nodes directly into a concrete guest type.
65#[proc_macro_derive(FromWire, attributes(schema))]
66pub fn derive_from_wire(input: TokenStream) -> TokenStream {
67 let input = parse_macro_input!(input as DeriveInput);
68 codegen::wire::expand(&input, false)
69 .unwrap_or_else(syn::Error::into_compile_error)
70 .into()
71}
72
73/// Appends a concrete type definition directly to the flat WIT schema arena.
74#[proc_macro_derive(WireSchema, attributes(schema))]
75pub fn derive_wire_schema(input: TokenStream) -> TokenStream {
76 let input = parse_macro_input!(input as DeriveInput);
77 codegen::wire::expand_schema(&input)
78 .unwrap_or_else(syn::Error::into_compile_error)
79 .into()
80}
81
82#[proc_macro_derive(IntoSchema, attributes(schema))]
83pub fn derive_into_schema(input: TokenStream) -> TokenStream {
84 let input = parse_macro_input!(input as DeriveInput);
85 expand::expand_into_schema(input)
86 .unwrap_or_else(syn::Error::into_compile_error)
87 .into()
88}
89
90#[proc_macro_derive(FromSchema, attributes(schema))]
91pub fn derive_from_schema(input: TokenStream) -> TokenStream {
92 let input = parse_macro_input!(input as DeriveInput);
93 expand::expand_from_schema(input)
94 .unwrap_or_else(syn::Error::into_compile_error)
95 .into()
96}
97
98/// Emits a schema-only `poem_openapi::types::Type` implementation whose
99/// registered `MetaSchema` mirrors the type's **serde** wire representation,
100/// while `ToJSON`/`ParseFromJSON` keep delegating to `serde_json` (so the wire
101/// format is unchanged). Unsupported serde attributes are rejected at compile
102/// time as a drift guard.
103///
104/// This derive only emits `poem_openapi::*` token paths and is meant to be
105/// gated behind the consuming crate's `full` feature, e.g.
106/// `#[cfg_attr(feature = "full", derive(PoemSchema))]`.
107#[proc_macro_derive(PoemSchema, attributes(serde))]
108pub fn derive_poem_schema(input: TokenStream) -> TokenStream {
109 let input = parse_macro_input!(input as DeriveInput);
110 codegen::poem::expand_poem_schema(&input)
111 .unwrap_or_else(syn::Error::into_compile_error)
112 .into()
113}
114
115/// Derives `IntoSchema`, `FromSchema`, `IntoWire`, `FromWire`, and `WireSchema`.
116/// Requires the schema crate's `guest` feature for the direct wire traits.
117#[proc_macro_derive(Schema, attributes(schema))]
118pub fn derive_schema(input: TokenStream) -> TokenStream {
119 let input = parse_macro_input!(input as DeriveInput);
120
121 let expansions = [
122 expand::expand_into_schema(input.clone()),
123 expand::expand_from_schema(input.clone()),
124 codegen::wire::expand(&input, true),
125 codegen::wire::expand(&input, false),
126 codegen::wire::expand_schema(&input),
127 ];
128 let mut output = proc_macro2::TokenStream::new();
129 let mut errors: Option<syn::Error> = None;
130 for expansion in expansions {
131 match expansion {
132 Ok(tokens) => output.extend(tokens),
133 Err(error) => match &mut errors {
134 Some(errors) => errors.combine(error),
135 None => errors = Some(error),
136 },
137 }
138 }
139 errors.map_or(output, syn::Error::into_compile_error).into()
140}