Skip to main content

schema/
compiled.rs

1//! Compiling a configuration into a single, portable binary artifact.
2//!
3//! [`compile`] runs the whole load pipeline — read `flusso.toml`, parse every
4//! referenced schema, validate and convert into a [`Config`] — and wraps the
5//! result in a [`Compiled`] envelope. [`write`](fn@write) serializes that
6//! envelope to MessagePack; [`load_compiled`] reads it back.
7//!
8//! The point is deployment: a compiled artifact is one file that carries the
9//! full, validated configuration with no scattered YAML and no source tree. It
10//! holds no secret it wasn't given literally — `{ env = "VAR" }` references are
11//! preserved and resolved where the artifact runs.
12
13use std::path::Path;
14
15use serde::{Deserialize, Serialize};
16
17use crate::Config;
18use crate::loader::{self, LoadError};
19
20/// The artifact format version. Bumped on any incompatible change to the
21/// serialized shape so a binary refuses an artifact it can't read, rather than
22/// misinterpreting it.
23pub const FORMAT_VERSION: u8 = 1;
24
25/// A compiled configuration: the validated [`Config`] plus the provenance needed
26/// to read it safely.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Compiled {
29    /// Artifact format version, checked on load against [`FORMAT_VERSION`].
30    pub format_version: u8,
31    /// The `flusso` version that produced this artifact (informational).
32    pub flusso_version: String,
33    /// The fully-validated configuration.
34    pub config: Config,
35}
36
37#[derive(thiserror::Error, Debug)]
38pub enum CompileError {
39    #[error(transparent)]
40    Load(#[from] LoadError),
41    #[error("failed to read compiled config `{path}`: {source}")]
42    Read {
43        path: std::path::PathBuf,
44        #[source]
45        source: std::io::Error,
46    },
47    #[error("failed to write compiled config `{path}`: {source}")]
48    Write {
49        path: std::path::PathBuf,
50        #[source]
51        source: std::io::Error,
52    },
53    #[error("failed to encode compiled config: {0}")]
54    Encode(#[from] rmp_serde::encode::Error),
55    #[error("failed to decode compiled config: {0}")]
56    Decode(#[from] rmp_serde::decode::Error),
57    #[error(
58        "compiled config format version {got} is not supported by this build \
59         (expected {expected}); recompile with a matching `flusso`"
60    )]
61    VersionMismatch { got: u8, expected: u8 },
62}
63
64/// Compile a `flusso.toml` (and the schemas it references) into a [`Compiled`]
65/// envelope. Needs neither a database nor any secret to be set — schemas are
66/// self-describing and secrets are deferred.
67pub fn compile(config_path: impl AsRef<Path>) -> Result<Compiled, CompileError> {
68    let config = loader::load(config_path)?;
69    Ok(Compiled {
70        format_version: FORMAT_VERSION,
71        flusso_version: env!("CARGO_PKG_VERSION").to_owned(),
72        config,
73    })
74}
75
76/// Serialize a [`Compiled`] envelope to its MessagePack bytes.
77pub fn to_bytes(compiled: &Compiled) -> Result<Vec<u8>, CompileError> {
78    Ok(rmp_serde::to_vec_named(compiled)?)
79}
80
81/// Write a [`Compiled`] envelope to `path` as MessagePack.
82pub fn write(compiled: &Compiled, path: impl AsRef<Path>) -> Result<(), CompileError> {
83    let path = path.as_ref();
84    let bytes = to_bytes(compiled)?;
85    std::fs::write(path, bytes).map_err(|source| CompileError::Write {
86        path: path.to_path_buf(),
87        source,
88    })
89}
90
91/// Decode a [`Compiled`] envelope from MessagePack bytes, checking the format
92/// version.
93pub fn from_bytes(bytes: &[u8]) -> Result<Config, CompileError> {
94    let compiled: Compiled = rmp_serde::from_slice(bytes)?;
95    if compiled.format_version != FORMAT_VERSION {
96        return Err(CompileError::VersionMismatch {
97            got: compiled.format_version,
98            expected: FORMAT_VERSION,
99        });
100    }
101    Ok(compiled.config)
102}
103
104/// Read a compiled artifact from `path` and return its [`Config`].
105pub fn load_compiled(path: impl AsRef<Path>) -> Result<Config, CompileError> {
106    let path = path.as_ref();
107    let bytes = std::fs::read(path).map_err(|source| CompileError::Read {
108        path: path.to_path_buf(),
109        source,
110    })?;
111    from_bytes(&bytes)
112}