Skip to main content

sim_lib_intent/
shapes.rs

1//! Shapes for Intent kinds.
2//!
3//! Each baseline Intent kind gets a Shape that matches a `kind`-tagged
4//! `Expr::Map` for that kind; an umbrella `intent/Intent` Shape matches any
5//! recognized Intent and is used as the `codec:intent` expression shape. Editor
6//! dispatch (WEBUI_4 P3) selects editors by Shape match over these, reusing the
7//! kernel matcher.
8
9use std::sync::Arc;
10
11use sim_kernel::{Cx, Expr, MatchScore, Result, Shape, ShapeDoc, ShapeMatch, Symbol, Value};
12
13use crate::kinds::{INTENT_KINDS, INTENT_NAMESPACE, intent_kind, is_known_kind};
14use crate::model::intent_kind_of;
15
16/// A Shape that accepts exactly one Intent kind.
17pub struct IntentKindShape {
18    kind: Symbol,
19    symbol: Symbol,
20}
21
22impl IntentKindShape {
23    /// Build the Shape for the Intent kind named `name` (e.g. `wire`).
24    pub fn new(name: &str) -> Self {
25        Self {
26            kind: intent_kind(name),
27            symbol: Symbol::qualified(INTENT_NAMESPACE, pascal_case(name)),
28        }
29    }
30}
31
32impl Shape for IntentKindShape {
33    fn symbol(&self) -> Option<Symbol> {
34        Some(self.symbol.clone())
35    }
36
37    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
38        let expr = value.object().as_expr(cx)?;
39        self.check_expr(cx, &expr)
40    }
41
42    fn check_expr(&self, _cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
43        match intent_kind_of(expr) {
44            Some(kind) if kind == self.kind => Ok(ShapeMatch::accept(MatchScore::exact(20))),
45            Some(kind) => Ok(ShapeMatch::reject(format!(
46                "Intent kind '{kind}' does not match '{}'",
47                self.kind
48            ))),
49            None => Ok(ShapeMatch::reject("value is not an Intent")),
50        }
51    }
52
53    fn describe(&self, _cx: &mut Cx) -> Result<ShapeDoc> {
54        Ok(ShapeDoc::new(self.symbol.name.to_string())
55            .with_detail(format!("matches Intents tagged '{}'", self.kind)))
56    }
57}
58
59/// The umbrella Shape that accepts any recognized Intent.
60pub struct IntentShape;
61
62impl Shape for IntentShape {
63    fn symbol(&self) -> Option<Symbol> {
64        Some(intent_shape_symbol())
65    }
66
67    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
68        let expr = value.object().as_expr(cx)?;
69        self.check_expr(cx, &expr)
70    }
71
72    fn check_expr(&self, _cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
73        match intent_kind_of(expr) {
74            Some(kind) if is_known_kind(&kind) => Ok(ShapeMatch::accept(MatchScore::exact(5))),
75            Some(kind) => Ok(ShapeMatch::reject(format!(
76                "unrecognized Intent kind '{kind}'"
77            ))),
78            None => Ok(ShapeMatch::reject("value is not an Intent")),
79        }
80    }
81
82    fn describe(&self, _cx: &mut Cx) -> Result<ShapeDoc> {
83        Ok(ShapeDoc::new("Intent").with_detail("any recognized Intent (a kind-tagged map)"))
84    }
85}
86
87/// The symbol for the umbrella `intent/Intent` Shape.
88pub fn intent_shape_symbol() -> Symbol {
89    Symbol::qualified(INTENT_NAMESPACE, "Intent")
90}
91
92/// Build `(symbol, shape)` registrations for the umbrella Shape plus every
93/// baseline Intent kind Shape.
94pub fn intent_shape_specs() -> Vec<(Symbol, Arc<dyn Shape>)> {
95    let mut specs: Vec<(Symbol, Arc<dyn Shape>)> =
96        vec![(intent_shape_symbol(), Arc::new(IntentShape))];
97    for name in INTENT_KINDS {
98        let shape = IntentKindShape::new(name);
99        specs.push((shape.symbol.clone(), Arc::new(shape)));
100    }
101    specs
102}
103
104/// Convert a kebab-case kind name to PascalCase for the Shape symbol.
105fn pascal_case(name: &str) -> String {
106    name.split('-')
107        .map(|word| {
108            let mut chars = word.chars();
109            match chars.next() {
110                Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
111                None => String::new(),
112            }
113        })
114        .collect()
115}