Skip to main content

amql_engine/
types.rs

1//! Domain newtypes for the AQL engine.
2//!
3//! Types shared with subcrates (`amql-selector`, `amql-mutate`) are re-exported
4//! from those crates to ensure type identity across the dependency graph.
5//! Engine-only types are defined here with the `define_newtype_string!` macro.
6
7use serde::{Deserialize, Serialize};
8use std::borrow::Borrow;
9use std::fmt;
10use std::ops::Deref;
11use std::path::{Path, PathBuf};
12
13// Re-export shared types from subcrates so the engine and its dependencies
14// agree on a single concrete type.
15pub use amql_mutate::{NodeKind, RelativePath};
16pub use amql_selector::{AttrName, TagName};
17
18macro_rules! define_newtype_string {
19    ($name:ident, $doc:expr) => {
20        #[doc = $doc]
21        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22        #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
23        #[cfg_attr(feature = "ts", derive(ts_rs::TS))]
24        #[cfg_attr(feature = "flow", derive(flowjs_rs::Flow))]
25        #[cfg_attr(feature = "ts", ts(export))]
26        #[cfg_attr(feature = "flow", flow(export))]
27        #[serde(transparent)]
28        pub struct $name(String);
29
30        impl Deref for $name {
31            type Target = str;
32            #[inline]
33            fn deref(&self) -> &str {
34                &self.0
35            }
36        }
37
38        impl AsRef<str> for $name {
39            #[inline]
40            fn as_ref(&self) -> &str {
41                &self.0
42            }
43        }
44
45        impl Borrow<str> for $name {
46            #[inline]
47            fn borrow(&self) -> &str {
48                &self.0
49            }
50        }
51
52        impl fmt::Display for $name {
53            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54                f.write_str(&self.0)
55            }
56        }
57
58        impl From<String> for $name {
59            #[inline]
60            fn from(s: String) -> Self {
61                Self(s)
62            }
63        }
64
65        impl From<&str> for $name {
66            #[inline]
67            fn from(s: &str) -> Self {
68                Self(s.to_owned())
69            }
70        }
71
72        impl PartialEq<str> for $name {
73            #[inline]
74            fn eq(&self, other: &str) -> bool {
75                self.0 == other
76            }
77        }
78
79        impl PartialEq<&str> for $name {
80            #[inline]
81            fn eq(&self, other: &&str) -> bool {
82                self.0 == *other
83            }
84        }
85
86        impl PartialEq<String> for $name {
87            #[inline]
88            fn eq(&self, other: &String) -> bool {
89                self.0 == *other
90            }
91        }
92    };
93}
94
95define_newtype_string!(Binding, "Annotation-to-code join key.");
96define_newtype_string!(Scope, "Query scope prefix (directory, file, or empty).");
97define_newtype_string!(CodeElementName, "Name of a code construct.");
98define_newtype_string!(SelectorStr, "Raw selector string before parsing.");
99
100// ---------------------------------------------------------------------------
101// ProjectRoot — hand-written because it wraps PathBuf, not String.
102// ---------------------------------------------------------------------------
103
104/// Absolute path to the project root directory.
105#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
106#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
107#[cfg_attr(feature = "flow", derive(flowjs_rs::Flow))]
108#[cfg_attr(feature = "ts", ts(export))]
109#[cfg_attr(feature = "flow", flow(export))]
110#[serde(transparent)]
111pub struct ProjectRoot(PathBuf);
112
113impl Deref for ProjectRoot {
114    type Target = Path;
115    #[inline]
116    fn deref(&self) -> &Path {
117        &self.0
118    }
119}
120
121impl AsRef<Path> for ProjectRoot {
122    #[inline]
123    fn as_ref(&self) -> &Path {
124        &self.0
125    }
126}
127
128impl fmt::Display for ProjectRoot {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        write!(f, "{}", self.0.display())
131    }
132}
133
134impl From<PathBuf> for ProjectRoot {
135    #[inline]
136    fn from(p: PathBuf) -> Self {
137        Self(p)
138    }
139}
140
141impl From<&Path> for ProjectRoot {
142    #[inline]
143    fn from(p: &Path) -> Self {
144        Self(p.to_path_buf())
145    }
146}
147
148impl ProjectRoot {
149    /// Join a relative path onto the project root.
150    #[inline]
151    pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
152        self.0.join(path)
153    }
154
155    /// Convert to the inner `PathBuf`.
156    #[inline]
157    pub fn to_path_buf(&self) -> PathBuf {
158        self.0.clone()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use rustc_hash::FxHashMap;
166
167    #[test]
168    fn string_newtype_behavior() {
169        // Arrange
170        let tag = TagName::from("function");
171        let opt_tag: Option<TagName> = Some(TagName::from("function"));
172        let mut map = FxHashMap::default();
173        map.insert(TagName::from("function"), 42);
174
175        // Act
176        let deref_val: &str = &tag;
177        let display_val = format!("{tag}");
178        let lookup_val = map.get("function");
179        let as_deref_val = opt_tag.as_deref();
180
181        // Assert
182        assert_eq!(deref_val, "function", "Deref should yield inner str");
183        assert_eq!(display_val, "function", "Display should show inner value");
184        assert_eq!(tag, "function", "PartialEq<&str> should work");
185        assert_eq!(tag, *"function", "PartialEq<str> should work");
186        assert_eq!(tag, "function".to_string(), "PartialEq<String> should work");
187        assert_eq!(lookup_val, Some(&42), "Borrow<str> should allow map lookup");
188        assert_eq!(
189            as_deref_val,
190            Some("function"),
191            "Option::as_deref should work"
192        );
193    }
194
195    #[test]
196    fn project_root_behavior() {
197        // Arrange
198        let root = ProjectRoot::from(PathBuf::from("/project"));
199
200        // Act
201        let joined = root.join("src/main.rs");
202        let display_val = format!("{root}");
203
204        // Assert
205        assert_eq!(
206            joined,
207            PathBuf::from("/project/src/main.rs"),
208            "join should produce correct path"
209        );
210        assert_eq!(display_val, "/project", "Display should show path");
211    }
212}