Skip to main content

paramodel_elements/
configuration.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Element configuration and exports.
5//!
6//! `Configuration` holds the element's authored parameter bindings —
7//! literal values or [`TokenExpr`] references. It is *not* a defaults
8//! map: per SRD-0007 D21 the resolver precedence for a parameter's
9//! trial value is `axis binding → element configuration → parameter
10//! default → error`.
11//!
12//! `Exports` maps user-defined export names (e.g. `service_addr`) to
13//! token expressions (`${self.ip}:4567`) that downstream elements can
14//! reference. Resolution happens at deploy time.
15
16use std::collections::BTreeMap;
17
18use crate::{ParameterName, Value, name_type};
19use serde::{Deserialize, Serialize};
20
21use crate::error::ElementError;
22
23// ---------------------------------------------------------------------------
24// TokenExpr — opaque at this layer.
25// ---------------------------------------------------------------------------
26
27/// A token-expression reference.
28///
29/// Opaque at the element layer: the grammar (`${self.ip}`,
30/// `${other_element.endpoint}`, `${{db:dockerRegistry}}`, …) lives in
31/// the test-plan / compilation SRDs, which own parsing and resolution.
32#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct TokenExpr(String);
34
35impl TokenExpr {
36    /// Construct a token expression. Rejects empty strings.
37    pub fn new(s: impl Into<String>) -> Result<Self, ElementError> {
38        let s = s.into();
39        if s.is_empty() {
40            return Err(ElementError::EmptyTokenExpr);
41        }
42        Ok(Self(s))
43    }
44
45    /// Borrow the raw expression source.
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50
51    /// Consume and return the raw source.
52    #[must_use]
53    pub fn into_inner(self) -> String {
54        self.0
55    }
56}
57
58impl std::fmt::Display for TokenExpr {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.write_str(&self.0)
61    }
62}
63
64impl Serialize for TokenExpr {
65    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
66        s.serialize_str(&self.0)
67    }
68}
69
70impl<'de> Deserialize<'de> for TokenExpr {
71    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
72        let s = String::deserialize(d)?;
73        Self::new(s).map_err(serde::de::Error::custom)
74    }
75}
76
77// ---------------------------------------------------------------------------
78// ConfigEntry and Configuration.
79// ---------------------------------------------------------------------------
80
81/// One configuration slot on an element.
82///
83/// `Literal` pins an immediate value; `Token` refers to a runtime-
84/// resolved expression. The compiler replaces `Token` entries with
85/// their resolved `Value`s before execution.
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87#[serde(tag = "kind", rename_all = "snake_case")]
88pub enum ConfigEntry {
89    /// A literal, typed value bound to the parameter.
90    Literal {
91        /// The pinned value.
92        value: Value,
93    },
94    /// A token expression resolved at plan-compile time.
95    Token {
96        /// The token-expression source.
97        expr: TokenExpr,
98    },
99}
100
101impl ConfigEntry {
102    /// Wrap a value as a literal entry.
103    #[must_use]
104    pub const fn literal(value: Value) -> Self {
105        Self::Literal { value }
106    }
107
108    /// Wrap a token expression as a token entry.
109    #[must_use]
110    pub const fn token(expr: TokenExpr) -> Self {
111        Self::Token { expr }
112    }
113
114    /// Does this entry need token resolution before use?
115    #[must_use]
116    pub const fn is_token(&self) -> bool {
117        matches!(self, Self::Token { .. })
118    }
119}
120
121/// The element's authored parameter bindings. Not a defaults map —
122/// see SRD-0007 D21.
123#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
124#[serde(transparent)]
125pub struct Configuration(BTreeMap<ParameterName, ConfigEntry>);
126
127impl Configuration {
128    /// Empty configuration.
129    #[must_use]
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Insert or replace an entry. Returns the previous entry for
135    /// `name`, if any.
136    pub fn insert(
137        &mut self,
138        name:  ParameterName,
139        entry: ConfigEntry,
140    ) -> Option<ConfigEntry> {
141        self.0.insert(name, entry)
142    }
143
144    /// Look up an entry.
145    #[must_use]
146    pub fn get(&self, name: &ParameterName) -> Option<&ConfigEntry> {
147        self.0.get(name)
148    }
149
150    /// Sorted-by-key iterator over `(name, entry)` pairs.
151    pub fn iter(&self) -> impl Iterator<Item = (&ParameterName, &ConfigEntry)> {
152        self.0.iter()
153    }
154
155    /// Sorted key iterator.
156    pub fn keys(&self) -> impl Iterator<Item = &ParameterName> {
157        self.0.keys()
158    }
159
160    /// Entry count.
161    #[must_use]
162    pub fn len(&self) -> usize {
163        self.0.len()
164    }
165
166    /// `true` when empty.
167    #[must_use]
168    pub fn is_empty(&self) -> bool {
169        self.0.is_empty()
170    }
171}
172
173impl FromIterator<(ParameterName, ConfigEntry)> for Configuration {
174    fn from_iter<I: IntoIterator<Item = (ParameterName, ConfigEntry)>>(iter: I) -> Self {
175        Self(iter.into_iter().collect())
176    }
177}
178
179// ---------------------------------------------------------------------------
180// ExportName + Exports.
181// ---------------------------------------------------------------------------
182
183name_type! {
184    /// Name of one exported value an element publishes. Identifier-style
185    /// (ASCII alphanumeric + `_-.`).
186    pub struct ExportName { kind: "ExportName" }
187}
188
189/// Map from export name to token expression.
190#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(transparent)]
192pub struct Exports(BTreeMap<ExportName, TokenExpr>);
193
194impl Exports {
195    /// Empty export map.
196    #[must_use]
197    pub fn new() -> Self {
198        Self::default()
199    }
200
201    /// Insert or replace an export. Returns the previous expression
202    /// for `name`, if any.
203    pub fn insert(&mut self, name: ExportName, expr: TokenExpr) -> Option<TokenExpr> {
204        self.0.insert(name, expr)
205    }
206
207    /// Look up an export.
208    #[must_use]
209    pub fn get(&self, name: &ExportName) -> Option<&TokenExpr> {
210        self.0.get(name)
211    }
212
213    /// Sorted-by-key iterator.
214    pub fn iter(&self) -> impl Iterator<Item = (&ExportName, &TokenExpr)> {
215        self.0.iter()
216    }
217
218    /// Sorted key iterator.
219    pub fn keys(&self) -> impl Iterator<Item = &ExportName> {
220        self.0.keys()
221    }
222
223    /// Export count.
224    #[must_use]
225    pub fn len(&self) -> usize {
226        self.0.len()
227    }
228
229    /// `true` when empty.
230    #[must_use]
231    pub fn is_empty(&self) -> bool {
232        self.0.is_empty()
233    }
234}
235
236impl FromIterator<(ExportName, TokenExpr)> for Exports {
237    fn from_iter<I: IntoIterator<Item = (ExportName, TokenExpr)>>(iter: I) -> Self {
238        Self(iter.into_iter().collect())
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::ParameterName;
246
247    fn pname(s: &str) -> ParameterName {
248        ParameterName::new(s).unwrap()
249    }
250
251    #[test]
252    fn token_expr_rejects_empty() {
253        assert!(TokenExpr::new("").is_err());
254        let t = TokenExpr::new("${self.ip}").unwrap();
255        assert_eq!(t.as_str(), "${self.ip}");
256    }
257
258    #[test]
259    fn config_entry_helpers() {
260        let lit = ConfigEntry::literal(Value::integer(pname("n"), 8, None));
261        let tok = ConfigEntry::token(TokenExpr::new("${self.ip}").unwrap());
262        assert!(!lit.is_token());
263        assert!(tok.is_token());
264    }
265
266    #[test]
267    fn configuration_iter_is_sorted_by_name() {
268        let mut c = Configuration::new();
269        c.insert(
270            pname("zebra"),
271            ConfigEntry::literal(Value::integer(pname("zebra"), 1, None)),
272        );
273        c.insert(
274            pname("apple"),
275            ConfigEntry::literal(Value::integer(pname("apple"), 2, None)),
276        );
277        let names: Vec<&str> = c.keys().map(ParameterName::as_str).collect();
278        assert_eq!(names, vec!["apple", "zebra"]);
279    }
280
281    #[test]
282    fn exports_insert_and_get() {
283        let mut e = Exports::new();
284        let n = ExportName::new("service_addr").unwrap();
285        let t = TokenExpr::new("${self.ip}:4567").unwrap();
286        e.insert(n.clone(), t.clone());
287        assert_eq!(e.get(&n), Some(&t));
288        assert_eq!(e.len(), 1);
289    }
290
291    #[test]
292    fn token_expr_serde_roundtrip() {
293        let t = TokenExpr::new("${foo.bar}").unwrap();
294        let json = serde_json::to_string(&t).unwrap();
295        assert_eq!(json, "\"${foo.bar}\"");
296        let back: TokenExpr = serde_json::from_str(&json).unwrap();
297        assert_eq!(t, back);
298    }
299
300    #[test]
301    fn token_expr_deserialise_rejects_empty() {
302        let err: Result<TokenExpr, _> = serde_json::from_str("\"\"");
303        assert!(err.is_err());
304    }
305
306    #[test]
307    fn config_entry_serde_roundtrip() {
308        let lit = ConfigEntry::literal(Value::integer(pname("n"), 8, None));
309        let json = serde_json::to_string(&lit).unwrap();
310        let back: ConfigEntry = serde_json::from_str(&json).unwrap();
311        assert_eq!(lit, back);
312    }
313}