Skip to main content

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}