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 selects editors by Shape match over these, reusing the kernel
7//! matcher.
8
9use std::sync::Arc;
10
11use sim_kernel::{Cx, Expr, MatchScore, Result, Shape, ShapeDoc, ShapeMatch, Symbol, Value};
12use sim_shape::{ExactExprShape, OrShape, TableExtraPolicy, TableFieldSpec, TableShape};
13
14use crate::kinds::{INTENT_KINDS, INTENT_NAMESPACE, KIND_KEY, intent_kind};
15
16struct RankedShape {
17    symbol: Symbol,
18    name: String,
19    detail: String,
20    score: MatchScore,
21    inner: Arc<dyn Shape>,
22}
23
24impl RankedShape {
25    fn ranked(&self, mut matched: ShapeMatch) -> ShapeMatch {
26        if matched.accepted {
27            matched.score = self.score;
28        }
29        matched
30    }
31}
32
33impl Shape for RankedShape {
34    fn symbol(&self) -> Option<Symbol> {
35        Some(self.symbol.clone())
36    }
37
38    fn is_effectful(&self) -> bool {
39        self.inner.is_effectful()
40    }
41
42    fn is_total(&self) -> bool {
43        self.inner.is_total()
44    }
45
46    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
47        self.inner
48            .check_value(cx, value)
49            .map(|matched| self.ranked(matched))
50    }
51
52    fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
53        self.inner
54            .check_expr(cx, expr)
55            .map(|matched| self.ranked(matched))
56    }
57
58    fn describe(&self, _cx: &mut Cx) -> Result<ShapeDoc> {
59        Ok(ShapeDoc::new(self.name.clone()).with_detail(self.detail.clone()))
60    }
61}
62
63fn kind_field_shape(kind: Symbol) -> Arc<dyn Shape> {
64    Arc::new(TableShape::new(
65        vec![TableFieldSpec {
66            key: Symbol::new(KIND_KEY),
67            shape: Arc::new(ExactExprShape::new(Expr::Symbol(kind))),
68            required: true,
69        }],
70        TableExtraPolicy::Allow,
71    ))
72}
73
74fn ranked_shape(
75    symbol: Symbol,
76    name: impl Into<String>,
77    detail: impl Into<String>,
78    score: i32,
79    inner: Arc<dyn Shape>,
80) -> Arc<dyn Shape> {
81    Arc::new(RankedShape {
82        symbol,
83        name: name.into(),
84        detail: detail.into(),
85        score: MatchScore::exact(score),
86        inner,
87    })
88}
89
90/// The symbol for the umbrella `intent/Intent` Shape.
91pub fn intent_shape_symbol() -> Symbol {
92    Symbol::qualified(INTENT_NAMESPACE, "Intent")
93}
94
95pub(crate) fn intent_shape() -> Arc<dyn Shape> {
96    let choices = INTENT_KINDS
97        .iter()
98        .map(|name| kind_field_shape(intent_kind(name)))
99        .collect();
100    ranked_shape(
101        intent_shape_symbol(),
102        "Intent",
103        "any recognized Intent (a kind-tagged map)",
104        5,
105        Arc::new(OrShape::new(choices)),
106    )
107}
108
109fn intent_kind_shape(name: &str) -> (Symbol, Arc<dyn Shape>) {
110    let symbol = Symbol::qualified(INTENT_NAMESPACE, pascal_case(name));
111    let kind = intent_kind(name);
112    let shape = ranked_shape(
113        symbol.clone(),
114        symbol.name.to_string(),
115        format!("matches Intents tagged '{kind}'"),
116        20,
117        kind_field_shape(kind),
118    );
119    (symbol, shape)
120}
121
122/// Build `(symbol, shape)` registrations for the umbrella Shape plus every
123/// baseline Intent kind Shape.
124pub fn intent_shape_specs() -> Vec<(Symbol, Arc<dyn Shape>)> {
125    let mut specs: Vec<(Symbol, Arc<dyn Shape>)> = vec![(intent_shape_symbol(), intent_shape())];
126    for name in INTENT_KINDS {
127        specs.push(intent_kind_shape(name));
128    }
129    specs
130}
131
132/// Convert a kebab-case kind name to PascalCase for the Shape symbol.
133fn pascal_case(name: &str) -> String {
134    name.split('-')
135        .map(|word| {
136            let mut chars = word.chars();
137            match chars.next() {
138                Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
139                None => String::new(),
140            }
141        })
142        .collect()
143}