Skip to main content

miden_note_schema/
builder.rs

1//! String-path note storage builder.
2
3use std::collections::BTreeMap;
4
5use miden_field_repr::FeltWriter;
6
7use crate::{
8    CodecRegistry, Error, Felt, NoteStorage, NoteStorageSchema, PrimitiveType, Result, SchemaField,
9    SchemaType, SchemaTypeKind,
10    codec::{parse_felt, parse_unsigned, write_repr},
11    schema::normalize_name,
12    value::validate_encoding,
13};
14
15/// Builds note storage from normalized dotted string paths.
16pub struct NoteStorageBuilder<'a> {
17    schema: &'a NoteStorageSchema,
18    registry: &'a CodecRegistry,
19    values: BTreeMap<String, String>,
20}
21
22impl<'a> NoteStorageBuilder<'a> {
23    /// Creates an empty builder for a schema and codec registry.
24    pub(crate) fn new(schema: &'a NoteStorageSchema, registry: &'a CodecRegistry) -> Self {
25        Self {
26            schema,
27            registry,
28            values: BTreeMap::new(),
29        }
30    }
31
32    /// Sets a leaf value by kebab-case or snake_case dotted path.
33    ///
34    /// Options accept `none` or `some(<value>)`. Variants accept a case name or
35    /// `case-name(<value>)` when the selected case has a payload.
36    pub fn set(mut self, path: &str, value: impl AsRef<str>) -> Result<Self> {
37        let segments = normalize_path(path)?;
38        let normalized = segments.join(".");
39        let ty = resolve_path(self.schema.root(), &segments)?;
40        let value = value.as_ref();
41        reject_unsupported_constructor_path(ty, &normalized, value, self.registry)?;
42
43        if let Some(conflict) = self.values.keys().find(|existing| {
44            *existing == &normalized
45                || existing.starts_with(&format!("{normalized}."))
46                || normalized.starts_with(&format!("{existing}."))
47        }) {
48            return Err(Error::new(format!(
49                "path `{normalized}` conflicts with the existing value at `{conflict}`"
50            )));
51        }
52        self.values.insert(normalized, value.to_owned());
53        Ok(self)
54    }
55
56    /// Checks completeness and returns note storage in declaration order.
57    pub fn build(self) -> Result<NoteStorage> {
58        let mut felts = Vec::new();
59        encode_type(
60            self.schema.root(),
61            "",
62            &self.values,
63            self.registry,
64            &mut FeltWriter::new(&mut felts),
65        )?;
66        validate_encoding(self.schema.root(), &felts)
67            .map_err(|err| err.context("built note storage does not match its schema"))?;
68        NoteStorage::new(felts)
69            .map_err(|err| Error::new(format!("failed to create note storage: {err}")))
70    }
71}
72
73/// Rejects structural constructor shapes that need nested record text parsing.
74fn reject_unsupported_constructor_path(
75    ty: &SchemaType,
76    path: &str,
77    value: &str,
78    registry: &CodecRegistry,
79) -> Result<()> {
80    if ty.fqn().is_some_and(|fqn| registry.contains(fqn)) {
81        return Ok(());
82    }
83    match ty.kind() {
84        SchemaTypeKind::Option(payload)
85            if matches!(payload.kind(), SchemaTypeKind::Record(_))
86                && value.trim() != "none"
87                && !payload.fqn().is_some_and(|fqn| registry.contains(fqn)) =>
88        {
89            Err(Error::new(format!(
90                "path `{path}` selects an option with record payload `{}`; the string builder \
91                 does not support nested record constructors without a codec for that payload",
92                payload.fqn().or(payload.name()).unwrap_or("<anonymous record>")
93            )))
94        }
95        SchemaTypeKind::Variant(cases) => {
96            let (case_name, _) = parse_constructor(value)?;
97            let case_name = normalize_name(case_name);
98            let unsupported = cases.iter().find(|case| case.name() == case_name).and_then(|case| {
99                let payload = case.payload()?;
100                (matches!(payload.kind(), SchemaTypeKind::Record(_))
101                    && !payload.fqn().is_some_and(|fqn| registry.contains(fqn)))
102                .then_some(case.name())
103            });
104            if let Some(case_name) = unsupported {
105                Err(Error::new(format!(
106                    "path `{path}` selects variant case `{case_name}` with a record payload; the \
107                     string builder does not support nested record constructors without a codec \
108                     for that payload"
109                )))
110            } else {
111                Ok(())
112            }
113        }
114        _ => Ok(()),
115    }
116}
117
118/// Normalizes and validates a dotted field path.
119fn normalize_path(path: &str) -> Result<Vec<String>> {
120    // `split` always yields one segment, so only an empty segment can fail here.
121    let segments = path.split('.').map(normalize_name).collect::<Vec<_>>();
122    if segments.iter().any(String::is_empty) {
123        return Err(Error::new(format!("invalid empty note storage path `{path}`")));
124    }
125    Ok(segments)
126}
127
128/// Resolves a dotted path through structural record fields.
129fn resolve_path<'a>(mut ty: &'a SchemaType, segments: &[String]) -> Result<&'a SchemaType> {
130    let mut resolved = Vec::with_capacity(segments.len());
131    for segment in segments {
132        let SchemaTypeKind::Record(fields) = ty.kind() else {
133            return Err(Error::new(format!(
134                "path `{}` continues through non-record type `{}`",
135                resolved.join("."),
136                ty.fqn().or(ty.name()).unwrap_or("<anonymous>")
137            )));
138        };
139        let field = fields.iter().find(|field| field.name() == segment).ok_or_else(|| {
140            let prefix = if resolved.is_empty() {
141                "<root>".to_owned()
142            } else {
143                resolved.join(".")
144            };
145            Error::new(format!("record `{prefix}` has no field named `{segment}`"))
146        })?;
147        resolved.push(segment.clone());
148        ty = field.ty();
149    }
150    Ok(ty)
151}
152
153/// Encodes a schema type from complete path assignments.
154fn encode_type(
155    ty: &SchemaType,
156    path: &str,
157    values: &BTreeMap<String, String>,
158    registry: &CodecRegistry,
159    writer: &mut FeltWriter<'_>,
160) -> Result<()> {
161    if let Some(value) = values.get(path) {
162        let felts = encode_text_value(ty, value, registry)
163            .map_err(|err| err.context(format!("value at `{path}`")))?;
164        for felt in felts {
165            writer.write(felt);
166        }
167        return Ok(());
168    }
169
170    if let SchemaTypeKind::Record(fields) = ty.kind() {
171        let Some((fqn, codec)) = ty.fqn().and_then(|fqn| Some((fqn, registry.codec(fqn)?))) else {
172            return encode_record_fields(fields, path, values, registry, writer);
173        };
174        // A record assembled from child-path values must still satisfy its registered
175        // codec, or the builder would produce storage that the same registry rejects on
176        // decode. Encode the subtree separately so the codec can validate it as one value.
177        let mut subtree = Vec::new();
178        encode_record_fields(fields, path, values, registry, &mut FeltWriter::new(&mut subtree))?;
179        codec.validate(&subtree).map_err(|err| {
180            err.context(format!(
181                "codec `{fqn}` validation failed for the values assigned under `{}`",
182                if path.is_empty() { "<root>" } else { path }
183            ))
184        })?;
185        for felt in subtree {
186            writer.write(felt);
187        }
188        return Ok(());
189    }
190
191    Err(Error::new(format!(
192        "missing note storage value for `{}`",
193        if path.is_empty() { "<root>" } else { path }
194    )))
195}
196
197/// Encodes every field of a record from complete path assignments.
198fn encode_record_fields(
199    fields: &[SchemaField],
200    path: &str,
201    values: &BTreeMap<String, String>,
202    registry: &CodecRegistry,
203    writer: &mut FeltWriter<'_>,
204) -> Result<()> {
205    for field in fields {
206        let field_path = if path.is_empty() {
207            field.name().to_owned()
208        } else {
209            format!("{path}.{}", field.name())
210        };
211        encode_type(field.ty(), &field_path, values, registry, writer)?;
212    }
213    Ok(())
214}
215
216/// Encodes one direct string value.
217fn encode_text_value(ty: &SchemaType, value: &str, registry: &CodecRegistry) -> Result<Vec<Felt>> {
218    if let Some(fqn) = ty.fqn()
219        && let Some(codec) = registry.codec(fqn)
220    {
221        let felts = codec.parse(value)?;
222        codec
223            .validate(&felts)
224            .map_err(|err| err.context(format!("codec `{fqn}` validation failed")))?;
225        validate_encoding(ty, &felts)
226            .map_err(|err| err.context(format!("codec `{fqn}` changed the structural layout")))?;
227        return Ok(felts);
228    }
229
230    match ty.kind() {
231        SchemaTypeKind::Felt => Ok(write_repr(&parse_felt(value)?)),
232        SchemaTypeKind::Primitive(primitive) => encode_primitive(*primitive, value),
233        SchemaTypeKind::Option(payload) => encode_option(payload, value, registry),
234        SchemaTypeKind::Variant(cases) => {
235            let (case_name, payload_text) = parse_constructor(value)?;
236            let case_name = normalize_name(case_name);
237            let (ordinal, case) =
238                cases.iter().enumerate().find(|(_, case)| case.name() == case_name).ok_or_else(
239                    || {
240                        Error::new(format!(
241                            "variant `{}` has no case named `{case_name}`",
242                            ty.fqn().or(ty.name()).unwrap_or("<anonymous>")
243                        ))
244                    },
245                )?;
246            let ordinal = u32::try_from(ordinal)
247                .map_err(|_| Error::new("variant has more than u32::MAX cases"))?;
248            let mut felts = write_repr(&ordinal);
249            match (case.payload(), payload_text) {
250                (None, None) => {}
251                (None, Some(_)) => {
252                    return Err(Error::new(format!(
253                        "variant case `{case_name}` does not accept a payload"
254                    )));
255                }
256                (Some(_), None) => {
257                    return Err(Error::new(format!("variant case `{case_name}` needs a payload")));
258                }
259                (Some(payload), Some(payload_text)) => {
260                    felts.extend(encode_text_value(payload, payload_text, registry)?);
261                }
262            }
263            Ok(felts)
264        }
265        SchemaTypeKind::Record(_) => Err(Error::new(format!(
266            "type `{}` has no codec; set its leaf fields with dotted paths",
267            ty.fqn().or(ty.name()).unwrap_or("<anonymous record>")
268        ))),
269    }
270}
271
272/// Encodes an option tag and optional direct payload.
273fn encode_option(payload: &SchemaType, value: &str, registry: &CodecRegistry) -> Result<Vec<Felt>> {
274    let value = value.trim();
275    if value == "none" {
276        return Ok(write_repr(&0u32));
277    }
278    let (constructor, payload_text) = parse_constructor(value)?;
279    if normalize_name(constructor) != "some" {
280        return Err(Error::new("an option value must be `none` or `some(<value>)`"));
281    }
282    let payload_text =
283        payload_text.ok_or_else(|| Error::new("an option `some` value needs a payload"))?;
284    let mut felts = write_repr(&1u32);
285    felts.extend(encode_text_value(payload, payload_text, registry)?);
286    Ok(felts)
287}
288
289/// Encodes one supported primitive through `miden-field-repr`.
290fn encode_primitive(primitive: PrimitiveType, value: &str) -> Result<Vec<Felt>> {
291    match primitive {
292        PrimitiveType::U64 => Ok(write_repr(&parse_unsigned(value, "u64")?)),
293        PrimitiveType::U32 => {
294            let value = u32::try_from(parse_unsigned(value, "u32")?)
295                .map_err(|_| Error::new(format!("u32 value `{value}` is out of range")))?;
296            Ok(write_repr(&value))
297        }
298        PrimitiveType::U8 => {
299            let value = u8::try_from(parse_unsigned(value, "u8")?)
300                .map_err(|_| Error::new(format!("u8 value `{value}` is out of range")))?;
301            Ok(write_repr(&value))
302        }
303        PrimitiveType::Bool => {
304            let value = match value.trim() {
305                "true" | "1" => true,
306                "false" | "0" => false,
307                value => {
308                    return Err(Error::new(format!(
309                        "invalid bool `{value}`; expected true, false, 1, or 0"
310                    )));
311                }
312            };
313            Ok(write_repr(&value))
314        }
315    }
316}
317
318/// Splits `name` or `name(payload)` text without interpreting the payload.
319fn parse_constructor(value: &str) -> Result<(&str, Option<&str>)> {
320    let value = value.trim();
321    let Some(open) = value.find('(') else {
322        return Ok((value, None));
323    };
324    if !value.ends_with(')') {
325        return Err(Error::new(format!("value `{value}` has an unclosed payload")));
326    }
327    let name = value[..open].trim();
328    let payload = value[open + 1..value.len() - 1].trim();
329    if name.is_empty() || payload.is_empty() {
330        return Err(Error::new(format!("value `{value}` has an empty constructor or payload")));
331    }
332    Ok((name, Some(payload)))
333}