Skip to main content

skilj_codegen/
lib.rs

1//! `build.rs` codegen for skilj's own declarative bounded-context
2//! format, Codeberg issue #5's "narrower cut" (see `docs/architecture.md`
3//! §17 for the full design, and §16 for the prototype that scoped it
4//! down to this). A `.skilj.toml` file describes one bounded context's
5//! event/command type *shapes* only, fields, DCB tags and
6//! `rest_trigger_allowed`, and this crate turns that into real Rust:
7//! one payload struct and one `EventType`/`CommandType` impl per
8//! declared type, plus the shared per-bounded-context event enum and
9//! its `BoundedContextEvent` impl.
10//!
11//! **Deliberately narrow, not a gap to widen casually.**
12//! `sensitive_fields`, the event creation-origin flags
13//! (`external_creation_allowed`/`direct_creation_allowed`/
14//! `event_read_allowed`), scheduling, `#[requires_role]`, and every
15//! `Projection` concept are real, legitimate parts of the plugin API
16//! this format doesn't cover - the §16 prototype's own recommendation
17//! was to prove the mechanism on exactly what a real conversion needed
18//! (`skilj-demo/src/banking.rs`, which uses none of those), not to
19//! guess ahead of a second real use case.
20//!
21//! **`decide()`/`project()` stay hand-written Rust, always.** A
22//! generated `CommandType::decide()` is one line, delegating to a
23//! plain free function (`decide_<snake_case(NAME)>`) the including
24//! module is expected to already define - see `emit::emit_command_type`'s
25//! own doc comment. This crate never sees, needs, or could sensibly
26//! generate real domain logic.
27//!
28//! **How a consumer uses this**: from its own `build.rs`, call
29//! [`generate`] on a `.skilj.toml` file's contents, write the result to
30//! `$OUT_DIR`, and `include!()` it from the hand-written module that
31//! also defines the `decide_*` functions. See
32//! `skilj-demo/build.rs`/`skilj-demo/src/banking.rs` for the real,
33//! working example.
34
35mod emit;
36mod spec;
37
38pub use spec::{BoundedContextSpec, CommandTypeSpec, EventTypeSpec, FieldSpec, FieldType};
39
40#[derive(Debug)]
41pub enum Error {
42    /// The `.skilj.toml` file itself doesn't parse, or doesn't match
43    /// the expected shape - a real user-facing error, surfaced through
44    /// `build.rs` as a build failure with `toml`'s own message.
45    Toml(toml::de::Error),
46    /// The `TokenStream` this crate emitted doesn't parse as a valid
47    /// `syn::File` - a bug in this crate's own `emit` module, never a
48    /// user error; nothing about a well-formed `.skilj.toml` file
49    /// should be able to trigger this.
50    Generated(syn::Error),
51}
52
53impl std::fmt::Display for Error {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            Error::Toml(e) => write!(f, "invalid .skilj.toml: {e}"),
57            Error::Generated(e) => {
58                write!(f, "skilj-codegen generated code that failed to parse - this is a bug in skilj-codegen itself, not in your .skilj.toml: {e}")
59            }
60        }
61    }
62}
63
64impl std::error::Error for Error {}
65
66/// Parses `toml_source` (a `.skilj.toml` file's own contents) and
67/// returns real, `prettyplease`-formatted Rust source - genuinely
68/// readable when a consumer's own `build.rs` writes it to `$OUT_DIR`
69/// for debugging, not a minified one-liner. See this crate's own root
70/// doc comment for what the output covers.
71pub fn generate(toml_source: &str) -> Result<String, Error> {
72    let spec: BoundedContextSpec = toml::from_str(toml_source).map_err(Error::Toml)?;
73    let tokens = emit::emit(&spec);
74    let file: syn::File = syn::parse2(tokens).map_err(Error::Generated)?;
75    Ok(prettyplease::unparse(&file))
76}