Skip to main content

pylon_query/
lib.rs

1use pylon_kernel::{AppManifest, ManifestField, ManifestQuery};
2
3// ---------------------------------------------------------------------------
4// Query descriptor — runtime-facing query metadata
5// ---------------------------------------------------------------------------
6
7/// A query descriptor holds the contract for a single named query.
8/// It describes what inputs the query accepts, derived from the manifest.
9/// It does not implement execution.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct QueryDescriptor {
12    pub name: String,
13    pub input: Vec<InputField>,
14}
15
16/// An input field descriptor for a query or action.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct InputField {
19    pub name: String,
20    pub field_type: String,
21    pub optional: bool,
22}
23
24impl InputField {
25    pub fn from_manifest_field(f: &ManifestField) -> Self {
26        Self {
27            name: f.name.clone(),
28            field_type: f.field_type.clone(),
29            optional: f.optional,
30        }
31    }
32}
33
34impl QueryDescriptor {
35    pub fn from_manifest(mq: &ManifestQuery) -> Self {
36        Self {
37            name: mq.name.clone(),
38            input: mq
39                .input
40                .iter()
41                .map(InputField::from_manifest_field)
42                .collect(),
43        }
44    }
45}
46
47// ---------------------------------------------------------------------------
48// Query registry — all queries from a manifest
49// ---------------------------------------------------------------------------
50
51/// A registry of query descriptors, built from a manifest.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct QueryRegistry {
54    pub queries: Vec<QueryDescriptor>,
55}
56
57impl QueryRegistry {
58    pub fn from_manifest(manifest: &AppManifest) -> Self {
59        Self {
60            queries: manifest
61                .queries
62                .iter()
63                .map(QueryDescriptor::from_manifest)
64                .collect(),
65        }
66    }
67
68    pub fn get(&self, name: &str) -> Option<&QueryDescriptor> {
69        self.queries.iter().find(|q| q.name == name)
70    }
71
72    pub fn names(&self) -> Vec<&str> {
73        self.queries.iter().map(|q| q.name.as_str()).collect()
74    }
75}
76
77// ---------------------------------------------------------------------------
78// Tests
79// ---------------------------------------------------------------------------
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use pylon_kernel::ManifestField;
85
86    // Synthesizes a manifest with three queries in memory rather than
87    // reading examples/todo-app — the example app keeps queries empty
88    // by design (it CRUDs entities directly through the policy-gated
89    // entity API), so pinning these tests to it produced false
90    // failures. The shape mirrors what the SDK's buildManifest emits.
91    fn test_manifest() -> AppManifest {
92        fn f(name: &str, ty: &str, optional: bool) -> ManifestField {
93            ManifestField {
94                name: name.into(),
95                field_type: ty.into(),
96                optional,
97                unique: false,
98                crdt: None,
99            }
100        }
101        AppManifest {
102            manifest_version: 1,
103            name: "test".into(),
104            version: "0.0.0".into(),
105            entities: vec![],
106            routes: vec![],
107            actions: vec![],
108            policies: vec![],
109            queries: vec![
110                ManifestQuery {
111                    name: "todosByAuthor".into(),
112                    input: vec![f("authorId", "id(User)", false)],
113                },
114                ManifestQuery {
115                    name: "allTodos".into(),
116                    input: vec![f("done", "bool", true)],
117                },
118                ManifestQuery {
119                    name: "todoById".into(),
120                    input: vec![f("id", "id(Todo)", false)],
121                },
122            ],
123            auth: Default::default(),
124        }
125    }
126
127    #[test]
128    fn registry_from_manifest() {
129        let reg = QueryRegistry::from_manifest(&test_manifest());
130        assert_eq!(reg.queries.len(), 3);
131        assert_eq!(reg.names(), vec!["todosByAuthor", "allTodos", "todoById"]);
132    }
133
134    #[test]
135    fn get_query_by_name() {
136        let reg = QueryRegistry::from_manifest(&test_manifest());
137        let q = reg.get("todosByAuthor").unwrap();
138        assert_eq!(q.name, "todosByAuthor");
139        assert_eq!(q.input.len(), 1);
140        assert_eq!(q.input[0].name, "authorId");
141        assert_eq!(q.input[0].field_type, "id(User)");
142        assert!(!q.input[0].optional);
143    }
144
145    #[test]
146    fn get_query_with_optional_input() {
147        let reg = QueryRegistry::from_manifest(&test_manifest());
148        let q = reg.get("allTodos").unwrap();
149        assert_eq!(q.input.len(), 1);
150        assert_eq!(q.input[0].name, "done");
151        assert!(q.input[0].optional);
152    }
153
154    #[test]
155    fn get_missing_query_returns_none() {
156        let reg = QueryRegistry::from_manifest(&test_manifest());
157        assert!(reg.get("nonexistent").is_none());
158    }
159
160    #[test]
161    fn descriptor_from_manifest_query() {
162        let mq = ManifestQuery {
163            name: "test".into(),
164            input: vec![ManifestField {
165                name: "id".into(),
166                field_type: "string".into(),
167                optional: false,
168                unique: false,
169                crdt: None,
170            }],
171        };
172        let desc = QueryDescriptor::from_manifest(&mq);
173        assert_eq!(desc.name, "test");
174        assert_eq!(desc.input.len(), 1);
175        assert_eq!(desc.input[0].name, "id");
176    }
177}