1use sim_citizen::CitizenField;
2use sim_citizen_derive::Citizen;
3use sim_kernel::{ContentId, Error, Expr, Result, Symbol};
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum IntentStatus {
8 #[default]
11 Candidate,
12 Verified,
14 Golden,
16}
17
18impl CitizenField for IntentStatus {
19 fn encode_field(&self) -> Expr {
20 Expr::Symbol(self.as_symbol())
21 }
22
23 fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
24 let Expr::Symbol(symbol) = expr else {
25 return Err(sim_citizen::field_error(
26 field,
27 "expected intent status symbol",
28 ));
29 };
30 Self::from_symbol(symbol)
31 .ok_or_else(|| sim_citizen::field_error(field, format!("unknown status {symbol}")))
32 }
33}
34
35impl IntentStatus {
36 pub fn as_symbol(self) -> Symbol {
38 match self {
39 Self::Candidate => Symbol::qualified("forge", "candidate"),
40 Self::Verified => Symbol::qualified("forge", "verified"),
41 Self::Golden => Symbol::qualified("forge", "golden"),
42 }
43 }
44
45 pub fn from_symbol(symbol: &Symbol) -> Option<Self> {
47 if *symbol == Symbol::qualified("forge", "candidate") {
48 Some(Self::Candidate)
49 } else if *symbol == Symbol::qualified("forge", "verified") {
50 Some(Self::Verified)
51 } else if *symbol == Symbol::qualified("forge", "golden") {
52 Some(Self::Golden)
53 } else {
54 None
55 }
56 }
57}
58
59#[derive(Clone, Debug, PartialEq, Citizen)]
61#[citizen(symbol = "forge/CompiledIntent", version = 1)]
62pub struct CompiledIntent {
63 pub name: Symbol,
65 pub version: u32,
67 #[citizen(with = "content_id_field")]
69 pub source: ContentId,
70 #[citizen(with = "content_id_field")]
72 pub packet: ContentId,
73 pub verifiers: Vec<Symbol>,
75 #[citizen(with = "content_id_vec_field")]
77 pub probes: Vec<ContentId>,
78 pub status: IntentStatus,
80 #[citizen(with = "optional_content_id_field")]
82 pub compiler_card: Option<ContentId>,
83 #[citizen(with = "optional_content_id_field")]
85 pub approval: Option<ContentId>,
86}
87
88impl Default for CompiledIntent {
89 fn default() -> Self {
90 Self {
91 name: Symbol::qualified("forge", "fixture"),
92 version: 1,
93 source: fixture_content_id(1),
94 packet: fixture_content_id(2),
95 verifiers: Vec::new(),
96 probes: Vec::new(),
97 status: IntentStatus::Candidate,
98 compiler_card: None,
99 approval: None,
100 }
101 }
102}
103
104fn fixture_content_id(byte: u8) -> ContentId {
105 ContentId::from_bytes(Symbol::qualified("core", "sha256"), [byte; 32])
106}
107
108mod content_id_field {
109 use super::*;
110 use sim_value::build::entry as field;
111
112 pub(crate) fn encode(id: &ContentId) -> Expr {
113 Expr::Map(vec![
114 field("algorithm", Expr::Symbol(id.algorithm.clone())),
115 field("bytes", Expr::Bytes(id.bytes.to_vec())),
116 ])
117 }
118
119 pub(crate) fn decode(expr: &Expr) -> Result<ContentId> {
120 let Expr::Map(entries) = expr else {
121 return Err(Error::Eval("content id field must be a map".to_owned()));
122 };
123 let algorithm = required_symbol(entries, "algorithm")?;
124 let bytes = required_bytes(entries, "bytes")?;
125 Ok(ContentId::from_bytes(algorithm, bytes))
126 }
127
128 fn required_symbol(entries: &[(Expr, Expr)], name: &'static str) -> Result<Symbol> {
129 match required_entry(entries, name)? {
130 Expr::Symbol(symbol) => Ok(symbol.clone()),
131 _ => Err(sim_citizen::field_error(name, "expected symbol")),
132 }
133 }
134
135 fn required_bytes(entries: &[(Expr, Expr)], name: &'static str) -> Result<[u8; 32]> {
136 match required_entry(entries, name)? {
137 Expr::Bytes(bytes) if bytes.len() == 32 => {
138 let mut digest = [0_u8; 32];
139 digest.copy_from_slice(bytes);
140 Ok(digest)
141 }
142 Expr::Bytes(bytes) => Err(sim_citizen::field_error(
143 name,
144 format!("expected 32 digest bytes, found {}", bytes.len()),
145 )),
146 _ => Err(sim_citizen::field_error(name, "expected bytes")),
147 }
148 }
149
150 fn required_entry<'a>(entries: &'a [(Expr, Expr)], name: &'static str) -> Result<&'a Expr> {
151 let key = Expr::Symbol(Symbol::new(name.to_owned()));
152 entries
153 .iter()
154 .find_map(|(entry_key, value)| (entry_key == &key).then_some(value))
155 .ok_or_else(|| sim_citizen::field_error(name, "missing content id field"))
156 }
157}
158
159mod content_id_vec_field {
160 use super::*;
161
162 pub(crate) fn encode(ids: &[ContentId]) -> Expr {
163 Expr::List(ids.iter().map(content_id_field::encode).collect())
164 }
165
166 pub(crate) fn decode(expr: &Expr) -> Result<Vec<ContentId>> {
167 let Expr::List(items) = expr else {
168 return Err(Error::Eval(
169 "content id list field must be a list".to_owned(),
170 ));
171 };
172 items.iter().map(content_id_field::decode).collect()
173 }
174}
175
176mod optional_content_id_field {
177 use super::*;
178
179 pub(crate) fn encode(id: &Option<ContentId>) -> Expr {
180 id.as_ref()
181 .map(content_id_field::encode)
182 .unwrap_or(Expr::Nil)
183 }
184
185 pub(crate) fn decode(expr: &Expr) -> Result<Option<ContentId>> {
186 match expr {
187 Expr::Nil => Ok(None),
188 other => content_id_field::decode(other).map(Some),
189 }
190 }
191}