Skip to main content

a2ui_base/catalog/
schema_only.rs

1//! Schema-only function placeholder for inline catalogs.
2//!
3//! Functions declared in an inline catalog have a schema (argument shape and
4//! return type) but no native Rust implementation. To let the existing
5//! `handle_call_function` machinery discover and uniformly reject calls to
6//! such functions, we register a [`SchemaOnlyFunction`] in the catalog's
7//! function map. Its [`execute`](FunctionImplementation::execute) always errors.
8
9use std::collections::HashMap;
10
11use super::function_api::{FunctionImplementation, ReturnType};
12use crate::error::A2uiError;
13use crate::model::data_context::DataContext;
14
15/// A function that carries a schema but has no native implementation.
16///
17/// This is used to represent functions declared in *inline catalogs* that the
18/// server sends as part of capabilities negotiation. The client knows the
19/// function exists (and its declared return type) but cannot execute it.
20pub struct SchemaOnlyFunction {
21    name: &'static str,
22    return_type: ReturnType,
23}
24
25impl SchemaOnlyFunction {
26    /// Create a new schema-only function from a runtime `String` name.
27    ///
28    /// The name is leaked via `Box::leak` to produce the `&'static str`
29    /// required by [`FunctionImplementation::name`]. This leak is **bounded**:
30    /// inline catalogs are registered exactly once at startup (via
31    /// [`MessageProcessor::register_inline_catalog`](crate::message_processor::MessageProcessor::register_inline_catalog))
32    /// and never unloaded for the lifetime of the processor, so the leaked
33    /// memory is proportional to the (small, fixed) set of inline functions a
34    /// client advertises — it does not grow unboundedly.
35    pub fn new(name: String, return_type: ReturnType) -> Self {
36        // Bounded leak: inline catalogs are registered once at startup.
37        let leaked: &'static str = Box::leak(name.into_boxed_str());
38        Self {
39            name: leaked,
40            return_type,
41        }
42    }
43}
44
45impl FunctionImplementation for SchemaOnlyFunction {
46    fn name(&self) -> &'static str {
47        self.name
48    }
49
50    fn return_type(&self) -> ReturnType {
51        self.return_type
52    }
53
54    fn execute(
55        &self,
56        _args: &HashMap<String, serde_json::Value>,
57        _context: &DataContext,
58    ) -> Result<serde_json::Value, A2uiError> {
59        Err(A2uiError::NoNativeImplementation(self.name.to_string()))
60    }
61}
62
63/// Parse a return-type string (as found in a catalog's `returnType` field)
64/// into the [`ReturnType`] enum. Unknown strings map to [`ReturnType::Any`].
65pub fn parse_return_type(s: &str) -> ReturnType {
66    match s {
67        "string" => ReturnType::String,
68        "number" => ReturnType::Number,
69        "boolean" => ReturnType::Boolean,
70        "array" => ReturnType::Array,
71        "object" => ReturnType::Object,
72        "void" => ReturnType::Void,
73        _ => ReturnType::Any, // "any" and anything unexpected
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn name_and_return_type_round_trip() {
83        let f = SchemaOnlyFunction::new("shout".to_string(), ReturnType::String);
84        assert_eq!(f.name(), "shout");
85        assert_eq!(f.return_type(), ReturnType::String);
86    }
87
88    #[test]
89    fn execute_errors() {
90        let f = SchemaOnlyFunction::new("noop".to_string(), ReturnType::Void);
91        let dm = crate::model::data_model::DataModel::new();
92        let empty: HashMap<String, Box<dyn FunctionImplementation>> = HashMap::new();
93        let ctx = DataContext::new(&dm, &empty);
94        let args = HashMap::new();
95        let res = f.execute(&args, &ctx);
96        assert!(res.is_err());
97        let err = res.unwrap_err();
98        assert!(
99            err.to_string().contains("no native implementation"),
100            "unexpected error: {err}"
101        );
102    }
103
104    #[test]
105    fn parse_return_type_variants() {
106        assert_eq!(parse_return_type("string"), ReturnType::String);
107        assert_eq!(parse_return_type("number"), ReturnType::Number);
108        assert_eq!(parse_return_type("boolean"), ReturnType::Boolean);
109        assert_eq!(parse_return_type("array"), ReturnType::Array);
110        assert_eq!(parse_return_type("object"), ReturnType::Object);
111        assert_eq!(parse_return_type("void"), ReturnType::Void);
112        assert_eq!(parse_return_type("any"), ReturnType::Any);
113        assert_eq!(parse_return_type("bogus"), ReturnType::Any);
114    }
115}