thalo_schema/
compiler.rs

1#![allow(unused_must_use)]
2
3use std::{collections::BTreeMap, env, fmt::Write, fs, path::Path};
4
5use crate::{
6    schema::{self, Aggregate, Command, Event},
7    Error,
8};
9
10/// Compile schemas into Rust code.
11///
12/// # Example
13///
14/// ```
15/// // build.rs
16///
17/// fn main() -> Result<(), Box<dyn std::error::Error>> {
18///     thalo_schema::configure()
19///         .add_schema_file("./bank-account.yaml")?
20///         .compile()?;
21///
22///     Ok(())
23/// }
24/// ```
25#[derive(Default)]
26pub struct Compiler {
27    schemas: Vec<Aggregate>,
28}
29
30impl Compiler {
31    /// Creates a new compiler instance.
32    pub fn new() -> Self {
33        Compiler {
34            schemas: Vec::new(),
35        }
36    }
37
38    /// Adds a schema.
39    pub fn add_schema(mut self, schema: Aggregate) -> Self {
40        self.schemas.push(schema);
41        self
42    }
43
44    /// Add a schema from a yaml file.
45    pub fn add_schema_file<P: AsRef<Path>>(self, path: P) -> Result<Self, Error> {
46        let content = fs::read(path)?;
47        let aggregate: Aggregate = serde_yaml::from_slice(&content)?;
48        Ok(self.add_schema(aggregate))
49    }
50
51    /// Add a schema from yaml string.
52    pub fn add_schema_str(self, content: &str) -> Result<Self, Error> {
53        let aggregate: Aggregate = serde_yaml::from_str(content)?;
54        Ok(self.add_schema(aggregate))
55    }
56
57    /// Compile schemas into Rust code and save in OUT_DIR.
58    pub fn compile(self) -> Result<(), Error> {
59        let out_dir = env::var("OUT_DIR").unwrap();
60
61        for schema in self.schemas {
62            let code = Self::compile_schema(&schema);
63            fs::write(format!("{}/{}.rs", out_dir, schema.name), code)?;
64        }
65
66        Ok(())
67    }
68
69    fn compile_schema(schema: &Aggregate) -> String {
70        let mut code = String::new();
71
72        Self::compile_schema_events(&mut code, &schema.name, &schema.events);
73
74        Self::compile_schema_errors(&mut code, &schema.name, &schema.errors);
75
76        Self::compile_schema_commands(&mut code, &schema.name, &schema.commands);
77
78        code
79    }
80
81    fn compile_schema_events(code: &mut String, name: &str, events: &BTreeMap<String, Event>) {
82        writeln!(
83            code,
84            "#[derive(Clone, Debug, serde::Deserialize, thalo::event::EventType, PartialEq, serde::Serialize)]"
85        );
86        writeln!(code, "pub enum {}Event {{", name);
87
88        for event_name in events.keys() {
89            writeln!(code, "    {}({0}Event),", event_name);
90        }
91
92        writeln!(code, "}}\n");
93
94        for (event_name, event) in events {
95            writeln!(code, "#[derive(Clone, Debug, serde::Deserialize, thalo::event::Event, PartialEq, serde::Serialize)]");
96            writeln!(
97                code,
98                r#"#[thalo(parent = "{}Event", variant = "{}")]"#,
99                name, event_name
100            );
101            writeln!(code, "pub struct {}Event {{", event_name);
102            for field in &event.fields {
103                writeln!(code, "    pub {}: {},", field.name, field.field_type);
104            }
105            writeln!(code, "}}\n");
106        }
107    }
108
109    fn compile_schema_errors(
110        code: &mut String,
111        name: &str,
112        errors: &BTreeMap<String, schema::Error>,
113    ) {
114        writeln!(code, "#[derive(Clone, Debug, thiserror::Error, PartialEq)]");
115        writeln!(code, "pub enum {}Error {{", name);
116
117        for (error_name, error) in errors {
118            writeln!(code, r#"    #[error("{}")]"#, error.message);
119            writeln!(code, "    {},", error_name);
120        }
121
122        writeln!(code, "}}");
123    }
124
125    fn compile_schema_commands(
126        code: &mut String,
127        name: &str,
128        commands: &BTreeMap<String, Command>,
129    ) {
130        writeln!(code, "pub trait {}Command {{", name);
131
132        for (command_name, command) in commands {
133            write!(code, "    fn {}(&self", command_name);
134
135            for arg in &command.args {
136                write!(code, ", {}: {}", arg.name, arg.type_field);
137            }
138
139            write!(code, ") -> ");
140
141            if !command.infallible {
142                write!(code, "std::result::Result<");
143            }
144
145            if command.event.is_some() && command.optional {
146                write!(code, "std::option::Option<");
147            }
148
149            match &command.event {
150                Some((event_name, _)) => {
151                    write!(code, "{}Event", event_name);
152                }
153                None => {
154                    write!(code, "Vec<{}Event>", name);
155                }
156            }
157
158            if command.event.is_some() && command.optional {
159                write!(code, ">");
160            }
161
162            if !command.infallible {
163                write!(code, ", {}Error>", name);
164            }
165
166            writeln!(code, ";");
167        }
168
169        writeln!(code, "}}");
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use std::{collections::BTreeMap, fs};
176
177    use crate::{
178        schema::{Aggregate, Event, Field},
179        Compiler,
180    };
181
182    #[test]
183    fn it_compile_schema_events() {
184        let mut events = BTreeMap::new();
185        events.insert(
186            "AccountOpened".to_string(),
187            Event {
188                fields: vec![Field {
189                    name: "initial_balance".to_string(),
190                    field_type: "f64".to_string(),
191                }],
192            },
193        );
194        events.insert(
195            "DepositedFunds".to_string(),
196            Event {
197                fields: vec![Field {
198                    name: "amount".to_string(),
199                    field_type: "f64".to_string(),
200                }],
201            },
202        );
203        events.insert(
204            "WithdrewFunds".to_string(),
205            Event {
206                fields: vec![Field {
207                    name: "amount".to_string(),
208                    field_type: "f64".to_string(),
209                }],
210            },
211        );
212
213        let mut code = String::new();
214        Compiler::compile_schema_events(&mut code, "BankAccount", &events);
215
216        assert_eq!(
217            code,
218            r#"#[derive(Clone, Debug, serde::Deserialize, thalo::event::EventType, PartialEq, serde::Serialize)]
219pub enum BankAccountEvent {
220    AccountOpened(AccountOpenedEvent),
221    DepositedFunds(DepositedFundsEvent),
222    WithdrewFunds(WithdrewFundsEvent),
223}
224
225#[derive(Clone, Debug, serde::Deserialize, thalo::event::Event, PartialEq, serde::Serialize)]
226#[thalo(parent = "BankAccountEvent", variant = "AccountOpened")]
227pub struct AccountOpenedEvent {
228    pub initial_balance: f64,
229}
230
231#[derive(Clone, Debug, serde::Deserialize, thalo::event::Event, PartialEq, serde::Serialize)]
232#[thalo(parent = "BankAccountEvent", variant = "DepositedFunds")]
233pub struct DepositedFundsEvent {
234    pub amount: f64,
235}
236
237#[derive(Clone, Debug, serde::Deserialize, thalo::event::Event, PartialEq, serde::Serialize)]
238#[thalo(parent = "BankAccountEvent", variant = "WithdrewFunds")]
239pub struct WithdrewFundsEvent {
240    pub amount: f64,
241}
242
243"#
244        )
245    }
246
247    #[test]
248    fn it_compile_schema_commands() {
249        let config = fs::read_to_string("./bank-account.yaml").unwrap();
250        let schema: Aggregate = serde_yaml::from_str(&config).unwrap();
251
252        let mut code = String::new();
253        Compiler::compile_schema_commands(&mut code, &schema.name, &schema.commands);
254
255        println!("{}", code);
256    }
257}