Skip to main content

phon_codegen/
lib.rs

1//! The codegen tool.
2//!
3//! phon schemas originate from Rust types via facet; codegen turns those schemas
4//! into source for the other languages a system speaks. For each target it emits
5//! two things: the type definitions a programmer writes against, and the schema
6//! itself as a constant — the self-describing phon bytes of the `Schema` value,
7//! which the peer parses at startup. A non-Rust peer never re-derives a schema
8//! from its generated types; the emitted bytes are the source of truth, so its
9//! `SchemaId` matches the Rust origin exactly
10//! (`r[codegen.schema-is-source-of-truth]`).
11//!
12//! This is the *data plane* codegen: types + schemas + ids. The RPC layer
13//! (services, clients, dispatchers, channels) is generated separately by the
14//! framework on top of this output.
15//!
16//! Spec: `docs/content/spec.md` — "Codegen".
17
18pub mod source;
19pub mod targets;
20pub mod typescript;
21
22pub use source::{Builder, Module, Root};
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27    use facet::Facet;
28    use phon_schema::{
29        DecodeLimits, SchemaBundleLimits, schema_bundle_from_bytes, schema_from_bytes,
30        schema_to_bytes,
31    };
32
33    #[derive(Facet)]
34    struct Point {
35        x: u32,
36        y: u32,
37    }
38
39    #[derive(Facet)]
40    #[repr(u8)]
41    #[allow(dead_code)]
42    enum Shape {
43        Circle(f64),
44        Rectangle { width: f64, height: f64 },
45        Point,
46    }
47
48    #[derive(Facet)]
49    #[allow(dead_code)]
50    struct Person {
51        name: String,
52        age: u32,
53        email: Option<String>,
54        tags: Vec<String>,
55        home: Point,
56        favorite: Shape,
57        big: u64,
58    }
59
60    fn module() -> Module {
61        Builder::new().add::<Person>().build().unwrap()
62    }
63
64    #[test]
65    // r[verify codegen.schema-is-source-of-truth]
66    fn every_schema_byte_blob_round_trips() {
67        let m = module();
68        assert!(!m.schemas.is_empty());
69        for s in &m.schemas {
70            let bytes = schema_to_bytes(s);
71            let (back, _) =
72                schema_from_bytes(&bytes, DecodeLimits::DEFAULT).expect("schema round-trips");
73            assert_eq!(&back, s, "schema {:#x} must round-trip", s.id.as_u64());
74        }
75    }
76
77    #[test]
78    fn emitted_schema_bundle_is_complete_and_admitted() {
79        let module = module();
80        let bytes = typescript::schema_bundle_bytes(&module).expect("schema bundle encodes");
81        let bundle = schema_bundle_from_bytes(&bytes, SchemaBundleLimits::default())
82            .expect("schema bundle admits");
83        assert_eq!(bundle.schemas(), module.schemas);
84    }
85
86    #[test]
87    // r[verify codegen.emits]
88    // r[verify codegen.schema-is-source-of-truth]
89    fn typescript_renders_the_expected_shapes() {
90        let ts = typescript::render(&module());
91
92        // Struct -> interface with the ergonomic scalar mapping.
93        assert!(
94            ts.contains("export interface Person {"),
95            "missing Person:\n{ts}"
96        );
97        assert!(ts.contains("name: string;"));
98        assert!(ts.contains("age: number;")); // u32 -> number
99        assert!(ts.contains("big: bigint;")); // u64 -> bigint
100        assert!(ts.contains("email: string | null;")); // Option<String>
101        assert!(ts.contains("tags: string[];")); // Vec<String>
102        assert!(ts.contains("home: Point;")); // nested struct by name
103        assert!(ts.contains("favorite: Shape;")); // enum by name
104
105        // Enum -> { tag, … } discriminated union (unit / newtype / struct variant).
106        assert!(ts.contains("export type Shape ="), "missing Shape:\n{ts}");
107        assert!(ts.contains(r#"{ tag: "Circle"; value: number }"#)); // newtype f64
108        assert!(ts.contains(r#"{ tag: "Rectangle"; width: number; height: number }"#));
109        assert!(ts.contains(r#"{ tag: "Point" }"#)); // unit
110
111        // The data plane: a registry built from schema-bytes + a root id constant.
112        assert!(ts.contains("export const registry = new Registry("));
113        assert!(ts.contains("export const schemaId = {"));
114        assert!(ts.contains("Person: 0x"));
115        assert!(ts.contains("export const schemaBundle = hexToBytes(\"50484f4e53434d31"));
116        assert!(!ts.contains("SCHEMA_BYTES"));
117        assert!(!ts.contains("schemaFromBytes"));
118    }
119}