Skip to main content

pylon_kernel/
lib.rs

1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5pub mod clock;
6pub mod errors;
7pub mod util;
8
9pub use clock::{Clock, MockClock, SystemClock};
10
11pub const VERSION: &str = env!("CARGO_PKG_VERSION");
12
13// ---------------------------------------------------------------------------
14// Exit codes
15// ---------------------------------------------------------------------------
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ExitCode {
19    Ok = 0,
20    Error = 1,
21    Usage = 64,
22    Unavailable = 69,
23}
24
25impl ExitCode {
26    pub const fn as_i32(self) -> i32 {
27        self as i32
28    }
29}
30
31// ---------------------------------------------------------------------------
32// Severity & Span — shared diagnostic primitives
33// ---------------------------------------------------------------------------
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum Severity {
38    Error,
39    Warning,
40    Info,
41}
42
43impl fmt::Display for Severity {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Severity::Error => f.write_str("error"),
47            Severity::Warning => f.write_str("warning"),
48            Severity::Info => f.write_str("info"),
49        }
50    }
51}
52
53/// Optional source location for a diagnostic.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct Span {
56    pub file: String,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub line: Option<u32>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub column: Option<u32>,
61}
62
63// ---------------------------------------------------------------------------
64// Diagnostic — structured, machine-readable error/warning
65// ---------------------------------------------------------------------------
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct Diagnostic {
69    pub severity: Severity,
70    pub code: String,
71    pub message: String,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub span: Option<Span>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub hint: Option<String>,
76}
77
78impl fmt::Display for Diagnostic {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(f, "[{}] {}: {}", self.severity, self.code, self.message)?;
81        if let Some(hint) = &self.hint {
82            write!(f, " (hint: {hint})")?;
83        }
84        Ok(())
85    }
86}
87
88// ---------------------------------------------------------------------------
89// AppManifest — canonical manifest shape
90// ---------------------------------------------------------------------------
91
92pub const MANIFEST_VERSION: u32 = 1;
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct AppManifest {
96    pub manifest_version: u32,
97    pub name: String,
98    pub version: String,
99    pub entities: Vec<ManifestEntity>,
100    pub routes: Vec<ManifestRoute>,
101    #[serde(default)]
102    pub queries: Vec<ManifestQuery>,
103    #[serde(default)]
104    pub actions: Vec<ManifestAction>,
105    #[serde(default)]
106    pub policies: Vec<ManifestPolicy>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct ManifestEntity {
111    pub name: String,
112    pub fields: Vec<ManifestField>,
113    pub indexes: Vec<ManifestIndex>,
114    #[serde(default, skip_serializing_if = "Vec::is_empty")]
115    pub relations: Vec<ManifestRelation>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct ManifestRelation {
120    pub name: String,
121    pub target: String,
122    pub field: String,
123    #[serde(default)]
124    pub many: bool,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct ManifestField {
129    pub name: String,
130    #[serde(rename = "type")]
131    pub field_type: String,
132    pub optional: bool,
133    pub unique: bool,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct ManifestIndex {
138    pub name: String,
139    pub fields: Vec<String>,
140    pub unique: bool,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct ManifestRoute {
145    pub path: String,
146    pub mode: String,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub query: Option<String>,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub auth: Option<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct ManifestQuery {
155    pub name: String,
156    #[serde(default, skip_serializing_if = "Vec::is_empty")]
157    pub input: Vec<ManifestField>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct ManifestAction {
162    pub name: String,
163    #[serde(default, skip_serializing_if = "Vec::is_empty")]
164    pub input: Vec<ManifestField>,
165}
166
167/// Row-level access policy attached to an entity or action.
168///
169/// `allow` is the legacy single-gate expression used for every kind of
170/// access. The optional `allow_*` fields let callers differentiate read
171/// from write from delete. When a per-action field is present it wins;
172/// otherwise the engine falls back to `allow`. That keeps old manifests
173/// working unchanged while enabling finer-grained ownership rules —
174/// "anyone can read, only the author can edit or delete."
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
176pub struct ManifestPolicy {
177    pub name: String,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub entity: Option<String>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub action: Option<String>,
182    #[serde(default, skip_serializing_if = "String::is_empty")]
183    pub allow: String,
184    /// Overrides `allow` for reads (pull, list, get). Optional.
185    #[serde(default, rename = "allowRead", skip_serializing_if = "Option::is_none")]
186    pub allow_read: Option<String>,
187    /// Overrides `allow` for inserts. Optional; falls back to `allow_write`
188    /// then `allow`.
189    #[serde(
190        default,
191        rename = "allowInsert",
192        skip_serializing_if = "Option::is_none"
193    )]
194    pub allow_insert: Option<String>,
195    /// Overrides `allow`/`allow_write` for updates. Optional.
196    #[serde(
197        default,
198        rename = "allowUpdate",
199        skip_serializing_if = "Option::is_none"
200    )]
201    pub allow_update: Option<String>,
202    /// Overrides `allow`/`allow_write` for deletes. Optional.
203    #[serde(
204        default,
205        rename = "allowDelete",
206        skip_serializing_if = "Option::is_none"
207    )]
208    pub allow_delete: Option<String>,
209    /// Shared fallback for any write (insert/update/delete) when the
210    /// more-specific field isn't set. Optional.
211    #[serde(
212        default,
213        rename = "allowWrite",
214        skip_serializing_if = "Option::is_none"
215    )]
216    pub allow_write: Option<String>,
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn exit_code_values() {
225        assert_eq!(ExitCode::Ok.as_i32(), 0);
226        assert_eq!(ExitCode::Error.as_i32(), 1);
227        assert_eq!(ExitCode::Usage.as_i32(), 64);
228        assert_eq!(ExitCode::Unavailable.as_i32(), 69);
229    }
230
231    #[test]
232    fn severity_display() {
233        assert_eq!(format!("{}", Severity::Error), "error");
234        assert_eq!(format!("{}", Severity::Warning), "warning");
235        assert_eq!(format!("{}", Severity::Info), "info");
236    }
237
238    #[test]
239    fn diagnostic_display_without_hint() {
240        let d = Diagnostic {
241            severity: Severity::Error,
242            code: "TEST".into(),
243            message: "something failed".into(),
244            span: None,
245            hint: None,
246        };
247        assert_eq!(format!("{d}"), "[error] TEST: something failed");
248    }
249
250    #[test]
251    fn diagnostic_display_with_hint() {
252        let d = Diagnostic {
253            severity: Severity::Warning,
254            code: "WARN".into(),
255            message: "check this".into(),
256            span: None,
257            hint: Some("try again".into()),
258        };
259        assert_eq!(
260            format!("{d}"),
261            "[warning] WARN: check this (hint: try again)"
262        );
263    }
264
265    #[test]
266    fn manifest_version_constant() {
267        assert_eq!(MANIFEST_VERSION, 1);
268    }
269}