Skip to main content

jpx_core/extensions/
ids.rs

1//! ID generation functions (nanoid, ulid).
2
3use 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
12// =============================================================================
13// nanoid(size?) -> string
14// =============================================================================
15
16defn!(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
33// =============================================================================
34// ulid() -> string
35// =============================================================================
36
37defn!(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
47// =============================================================================
48// ulid_timestamp(ulid) -> number (unix ms)
49// =============================================================================
50
51defn!(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
68/// Register ID functions filtered by the enabled set.
69pub 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        // ULID is 26 characters
130        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        // Generate a ULID and extract its timestamp
147        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        // ULID should be 26 characters of Crockford's Base32
175        assert_eq!(id.len(), 26);
176        // All characters should be valid Base32
177        assert!(id.chars().all(|c| c.is_ascii_alphanumeric()));
178    }
179}