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