Skip to main content

helix_dsl/
query_generator.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::path::{Path, PathBuf};
4
5/// Current wire-format version for generated query bundles.
6pub const QUERY_BUNDLE_VERSION: u32 = 3;
7
8/// Declared shape of a registered query parameter.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub enum QueryParamType {
11    /// Boolean parameter.
12    Bool,
13    /// 64-bit signed integer parameter.
14    I64,
15    /// 64-bit floating point parameter.
16    F64,
17    /// 32-bit floating point parameter.
18    F32,
19    /// UTF-8 string parameter.
20    String,
21    /// Raw bytes parameter.
22    Bytes,
23    /// Any nested `PropertyValue` payload.
24    Value,
25    /// Object/map payload.
26    Object,
27    /// Array payload whose elements have the given shape.
28    Array(Box<QueryParamType>),
29}
30
31/// Declared parameter for a registered query.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct QueryParameter {
34    /// Parameter name.
35    pub name: String,
36    /// Parameter shape.
37    pub ty: QueryParamType,
38}
39
40/// Versioned payload written to `queries.json`.
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub struct QueryBundle {
43    /// Wire-format version.
44    pub version: u32,
45    /// Read-only query routes by route name.
46    pub read_routes: BTreeMap<String, crate::ReadBatch>,
47    /// Write-capable query routes by route name.
48    pub write_routes: BTreeMap<String, crate::WriteBatch>,
49    /// Registered read-route parameter metadata.
50    pub read_parameters: BTreeMap<String, Vec<QueryParameter>>,
51    /// Registered write-route parameter metadata.
52    pub write_parameters: BTreeMap<String, Vec<QueryParameter>>,
53}
54
55impl Default for QueryBundle {
56    fn default() -> Self {
57        Self {
58            version: QUERY_BUNDLE_VERSION,
59            read_routes: BTreeMap::new(),
60            write_routes: BTreeMap::new(),
61            read_parameters: BTreeMap::new(),
62            write_parameters: BTreeMap::new(),
63        }
64    }
65}
66
67/// Registered read-query function.
68pub struct RegisteredReadQuery {
69    /// Route name.
70    pub name: &'static str,
71    /// Function that constructs the route AST.
72    pub build: fn() -> crate::ReadBatch,
73    /// Function that constructs declared parameter metadata.
74    pub parameters: fn() -> Vec<QueryParameter>,
75}
76
77/// Registered write-query function.
78pub struct RegisteredWriteQuery {
79    /// Route name.
80    pub name: &'static str,
81    /// Function that constructs the route AST.
82    pub build: fn() -> crate::WriteBatch,
83    /// Function that constructs declared parameter metadata.
84    pub parameters: fn() -> Vec<QueryParameter>,
85}
86
87inventory::collect!(RegisteredReadQuery);
88inventory::collect!(RegisteredWriteQuery);
89
90/// Errors returned while generating or loading query bundles.
91#[derive(Debug)]
92pub enum GenerateError {
93    /// More than one query registered the same route name.
94    DuplicateQueryName(String),
95    /// Failed to read or write bundle file.
96    Io(std::io::Error),
97    /// Failed to serialize or deserialize the bundle.
98    Json(sonic_rs::Error),
99    /// Bundle version is unsupported.
100    UnsupportedVersion {
101        /// Version read from payload.
102        found: u32,
103        /// Version required by this crate.
104        expected: u32,
105    },
106}
107
108impl std::fmt::Display for GenerateError {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            Self::DuplicateQueryName(name) => {
112                write!(f, "duplicate generated query name: {name}")
113            }
114            Self::Io(err) => write!(f, "io error: {err}"),
115            Self::Json(err) => write!(f, "json error: {err}"),
116            Self::UnsupportedVersion { found, expected } => {
117                write!(
118                    f,
119                    "unsupported query bundle version {found} (expected {expected})"
120                )
121            }
122        }
123    }
124}
125
126impl std::error::Error for GenerateError {}
127
128impl From<std::io::Error> for GenerateError {
129    fn from(value: std::io::Error) -> Self {
130        Self::Io(value)
131    }
132}
133
134impl From<sonic_rs::Error> for GenerateError {
135    fn from(value: sonic_rs::Error) -> Self {
136        Self::Json(value)
137    }
138}
139
140/// Build the in-memory query bundle from all `#[register]` registrations.
141pub fn build_query_bundle() -> Result<QueryBundle, GenerateError> {
142    let mut bundle = QueryBundle::default();
143
144    for registered in inventory::iter::<RegisteredReadQuery> {
145        if bundle.read_routes.contains_key(registered.name)
146            || bundle.write_routes.contains_key(registered.name)
147        {
148            return Err(GenerateError::DuplicateQueryName(
149                registered.name.to_string(),
150            ));
151        }
152
153        bundle
154            .read_routes
155            .insert(registered.name.to_string(), (registered.build)());
156        bundle
157            .read_parameters
158            .insert(registered.name.to_string(), (registered.parameters)());
159    }
160
161    for registered in inventory::iter::<RegisteredWriteQuery> {
162        if bundle.read_routes.contains_key(registered.name)
163            || bundle.write_routes.contains_key(registered.name)
164        {
165            return Err(GenerateError::DuplicateQueryName(
166                registered.name.to_string(),
167            ));
168        }
169
170        bundle
171            .write_routes
172            .insert(registered.name.to_string(), (registered.build)());
173        bundle
174            .write_parameters
175            .insert(registered.name.to_string(), (registered.parameters)());
176    }
177
178    Ok(bundle)
179}
180
181/// Serialize a query bundle to JSON bytes.
182pub fn serialize_query_bundle(bundle: &QueryBundle) -> Result<Vec<u8>, GenerateError> {
183    Ok(sonic_rs::to_vec_pretty(bundle)?)
184}
185
186/// Deserialize a query bundle from JSON bytes.
187pub fn deserialize_query_bundle(bytes: &[u8]) -> Result<QueryBundle, GenerateError> {
188    let bundle: QueryBundle = sonic_rs::from_slice(bytes)?;
189
190    if bundle.version != QUERY_BUNDLE_VERSION {
191        return Err(GenerateError::UnsupportedVersion {
192            found: bundle.version,
193            expected: QUERY_BUNDLE_VERSION,
194        });
195    }
196
197    Ok(bundle)
198}
199
200/// Write a query bundle to a file.
201pub fn write_query_bundle_to_path<P: AsRef<Path>>(
202    bundle: &QueryBundle,
203    path: P,
204) -> Result<(), GenerateError> {
205    let bytes = serialize_query_bundle(bundle)?;
206    std::fs::write(path, bytes)?;
207    Ok(())
208}
209
210/// Read a query bundle from a file.
211pub fn read_query_bundle_from_path<P: AsRef<Path>>(path: P) -> Result<QueryBundle, GenerateError> {
212    let bytes = std::fs::read(path)?;
213    deserialize_query_bundle(&bytes)
214}
215
216/// Generate `queries.json` in the current working directory.
217pub fn generate() -> Result<PathBuf, GenerateError> {
218    generate_to_path("queries.json")
219}
220
221/// Generate a query bundle and write it to the requested output path.
222pub fn generate_to_path<P: AsRef<Path>>(path: P) -> Result<PathBuf, GenerateError> {
223    let path = path.as_ref();
224    let bundle = build_query_bundle()?;
225    write_query_bundle_to_path(&bundle, path)?;
226    Ok(path.to_path_buf())
227}