jpx_core/extensions/
ids.rs1use std::collections::HashSet;
4
5use serde_json::Value;
6
7use crate::functions::{Function, number_value};
8use crate::interpreter::SearchResult;
9use crate::registry::register_if_enabled;
10use crate::{Context, Runtime, arg, defn};
11
12defn!(NanoidFn, vec![], Some(arg!(number)));
17
18impl Function for NanoidFn {
19 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
20 self.signature.validate(args, ctx)?;
21
22 let id = if args.is_empty() {
23 nanoid::nanoid!()
24 } else {
25 let size = args[0].as_f64().unwrap_or(21.0) as usize;
26 nanoid::nanoid!(size)
27 };
28
29 Ok(Value::String(id))
30 }
31}
32
33defn!(UlidFn, vec![], None);
38
39impl Function for UlidFn {
40 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
41 self.signature.validate(args, ctx)?;
42 let id = ulid::Ulid::new().to_string();
43 Ok(Value::String(id))
44 }
45}
46
47defn!(UlidTimestampFn, vec![arg!(string)], None);
52
53impl Function for UlidTimestampFn {
54 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
55 self.signature.validate(args, ctx)?;
56 let ulid_str = args[0].as_str().unwrap();
57
58 match ulid::Ulid::from_string(ulid_str) {
59 Ok(id) => {
60 let ts = id.timestamp_ms();
61 Ok(number_value(ts as f64))
62 }
63 Err(_) => Ok(Value::Null),
64 }
65 }
66}
67
68pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
70 register_if_enabled(runtime, "nanoid", enabled, Box::new(NanoidFn::new()));
71 register_if_enabled(runtime, "ulid", enabled, Box::new(UlidFn::new()));
72 register_if_enabled(
73 runtime,
74 "ulid_timestamp",
75 enabled,
76 Box::new(UlidTimestampFn::new()),
77 );
78}
79
80#[cfg(test)]
81mod tests {
82 use crate::Runtime;
83 use serde_json::json;
84
85 fn setup_runtime() -> Runtime {
86 Runtime::builder()
87 .with_standard()
88 .with_all_extensions()
89 .build()
90 }
91
92 #[test]
93 fn test_nanoid_default() {
94 let runtime = setup_runtime();
95 let data = json!(null);
96 let expr = runtime.compile("nanoid()").unwrap();
97 let result = expr.search(&data).unwrap();
98 let id = result.as_str().unwrap();
99 assert_eq!(id.len(), 21);
100 }
101
102 #[test]
103 fn test_nanoid_custom_size() {
104 let runtime = setup_runtime();
105 let data = json!(null);
106 let expr = runtime.compile("nanoid(`10`)").unwrap();
107 let result = expr.search(&data).unwrap();
108 let id = result.as_str().unwrap();
109 assert_eq!(id.len(), 10);
110 }
111
112 #[test]
113 fn test_nanoid_unique() {
114 let runtime = setup_runtime();
115 let data = json!(null);
116 let expr = runtime.compile("nanoid()").unwrap();
117 let id1 = expr.search(&data).unwrap();
118 let id2 = expr.search(&data).unwrap();
119 assert_ne!(id1.as_str().unwrap(), id2.as_str().unwrap());
120 }
121
122 #[test]
123 fn test_ulid() {
124 let runtime = setup_runtime();
125 let data = json!(null);
126 let expr = runtime.compile("ulid()").unwrap();
127 let result = expr.search(&data).unwrap();
128 let id = result.as_str().unwrap();
129 assert_eq!(id.len(), 26);
131 }
132
133 #[test]
134 fn test_ulid_unique() {
135 let runtime = setup_runtime();
136 let data = json!(null);
137 let expr = runtime.compile("ulid()").unwrap();
138 let id1 = expr.search(&data).unwrap();
139 let id2 = expr.search(&data).unwrap();
140 assert_ne!(id1.as_str().unwrap(), id2.as_str().unwrap());
141 }
142
143 #[test]
144 fn test_ulid_timestamp() {
145 let runtime = setup_runtime();
146 let ulid = ulid::Ulid::new();
148 let ulid_str = ulid.to_string();
149 let expected_ts = ulid.timestamp_ms();
150
151 let data = json!(ulid_str);
152 let expr = runtime.compile("ulid_timestamp(@)").unwrap();
153 let result = expr.search(&data).unwrap();
154 assert_eq!(result.as_f64().unwrap(), expected_ts as f64);
155 }
156
157 #[test]
158 fn test_ulid_timestamp_invalid() {
159 let runtime = setup_runtime();
160 let data = json!("not-a-ulid");
161 let expr = runtime.compile("ulid_timestamp(@)").unwrap();
162 let result = expr.search(&data).unwrap();
163 assert!(result.is_null());
164 }
165
166 #[test]
167 fn test_ulid_format() {
168 let runtime = setup_runtime();
169 let data = json!(null);
170 let expr = runtime.compile("ulid()").unwrap();
171 let result = expr.search(&data).unwrap();
172 let id = result.as_str().unwrap();
173
174 assert_eq!(id.len(), 26);
176 assert!(id.chars().all(|c| c.is_ascii_alphanumeric()));
178 }
179
180 #[test]
181 fn test_nanoid_charset() {
182 let runtime = setup_runtime();
183 let data = json!(null);
184 let expr = runtime.compile("nanoid()").unwrap();
185 for _ in 0..10 {
187 let result = expr.search(&data).unwrap();
188 let id = result.as_str().unwrap();
189 assert!(
190 id.chars()
191 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-'),
192 "nanoid contains invalid character: {}",
193 id
194 );
195 }
196 }
197
198 #[test]
199 fn test_ulid_parseable() {
200 let runtime = setup_runtime();
201 let data = json!(null);
202 let expr = runtime.compile("ulid()").unwrap();
203 let result = expr.search(&data).unwrap();
204 let id = result.as_str().unwrap();
205 assert!(
207 ulid::Ulid::from_string(id).is_ok(),
208 "Generated ULID should be parseable: {}",
209 id
210 );
211 }
212
213 #[test]
214 fn test_ulid_timestamp_roundtrip() {
215 let runtime = setup_runtime();
216 let data = json!(null);
217 let ulid_expr = runtime.compile("ulid()").unwrap();
219 let ulid_val = ulid_expr.search(&data).unwrap();
220
221 let ts_expr = runtime.compile("ulid_timestamp(@)").unwrap();
222 let ts = ts_expr.search(&ulid_val).unwrap();
223 let ts_ms = ts.as_f64().unwrap() as u64;
224
225 let now_ms = std::time::SystemTime::now()
226 .duration_since(std::time::UNIX_EPOCH)
227 .unwrap()
228 .as_millis() as u64;
229
230 assert!(
232 now_ms - ts_ms < 1000,
233 "ULID timestamp should be recent: {} vs {}",
234 ts_ms,
235 now_ms
236 );
237 }
238}