1use crate::AdapterApplyError;
2use alembic_core::{FieldType, JsonMap, Key, TypeSchema, Uid};
3use anyhow::{anyhow, Result};
4use serde_json::{Map, Value};
5use std::collections::BTreeMap;
6
7pub fn build_key_from_schema(type_schema: &TypeSchema, attrs: &JsonMap) -> Result<Key> {
8 let mut map = BTreeMap::new();
9 for field in type_schema.key.keys() {
10 let Some(value) = attrs.get(field) else {
11 return Err(anyhow!("missing key field {field}"));
12 };
13 map.insert(field.clone(), value.clone());
14 }
15 Ok(Key::from(map))
16}
17
18pub fn build_request_body<Id, F>(
19 type_schema: &TypeSchema,
20 attrs: &JsonMap,
21 resolved: &BTreeMap<Uid, Id>,
22 encode_ref: F,
23) -> Result<Value>
24where
25 F: Fn(&Id) -> Value + Copy,
26{
27 let mut map = Map::new();
28 for (key, value) in attrs.iter() {
29 let field_schema = type_schema
30 .fields
31 .get(key)
32 .ok_or_else(|| anyhow!("missing schema for field {key}"))?;
33 if value.is_null() {
34 map.insert(key.clone(), Value::Null);
35 continue;
36 }
37 map.insert(
38 key.clone(),
39 resolve_value_for_type(&field_schema.r#type, value.clone(), resolved, encode_ref)?,
40 );
41 }
42 Ok(Value::Object(map))
43}
44
45pub fn resolve_value_for_type<Id, F>(
46 field_type: &FieldType,
47 value: Value,
48 resolved: &BTreeMap<Uid, Id>,
49 encode_ref: F,
50) -> Result<Value>
51where
52 F: Fn(&Id) -> Value + Copy,
53{
54 match field_type {
55 FieldType::Ref { .. } => resolve_ref_value(value, resolved, encode_ref),
56 FieldType::ListRef { .. } => resolve_list_ref_value(value, resolved, encode_ref),
57 FieldType::List { item } => resolve_list_value(item, value, resolved, encode_ref),
58 FieldType::Map { value: inner } => resolve_map_value(inner, value, resolved, encode_ref),
59 _ => Ok(value),
60 }
61}
62
63pub fn query_filters_from_key<Id>(
64 type_schema: &TypeSchema,
65 key: &Key,
66 resolved: &BTreeMap<Uid, Id>,
67) -> Result<Vec<(String, String)>>
68where
69 Id: ToString,
70{
71 let mut filters = Vec::new();
72 for (field, value) in key.iter() {
73 let field_schema = type_schema
74 .key
75 .get(field)
76 .ok_or_else(|| anyhow!("missing schema for key field {field}"))?;
77 add_query_filters(&mut filters, field, &field_schema.r#type, value, resolved)?;
78 }
79 Ok(filters)
80}
81
82fn resolve_ref_value<Id, F>(
83 value: Value,
84 resolved: &BTreeMap<Uid, Id>,
85 encode_ref: F,
86) -> Result<Value>
87where
88 F: Fn(&Id) -> Value + Copy,
89{
90 let Value::String(raw) = value else {
91 return Err(anyhow!("ref value must be a uuid string"));
92 };
93 let uid = Uid::parse_str(&raw).map_err(|_| anyhow!("ref value is not a uuid: {raw}"))?;
94 let id = resolved
95 .get(&uid)
96 .ok_or(AdapterApplyError::MissingRef { uid })?;
97 Ok(encode_ref(id))
98}
99
100fn resolve_list_ref_value<Id, F>(
101 value: Value,
102 resolved: &BTreeMap<Uid, Id>,
103 encode_ref: F,
104) -> Result<Value>
105where
106 F: Fn(&Id) -> Value + Copy,
107{
108 let Value::Array(items) = value else {
109 return Err(anyhow!("list_ref value must be an array"));
110 };
111 let mut out = Vec::with_capacity(items.len());
112 for item in items {
113 out.push(resolve_ref_value(item, resolved, encode_ref)?);
114 }
115 Ok(Value::Array(out))
116}
117
118fn resolve_list_value<Id, F>(
119 item_type: &FieldType,
120 value: Value,
121 resolved: &BTreeMap<Uid, Id>,
122 encode_ref: F,
123) -> Result<Value>
124where
125 F: Fn(&Id) -> Value + Copy,
126{
127 let Value::Array(items) = value else {
128 return Err(anyhow!("list value must be an array"));
129 };
130 let mut out = Vec::with_capacity(items.len());
131 for item in items {
132 out.push(resolve_value_for_type(
133 item_type, item, resolved, encode_ref,
134 )?);
135 }
136 Ok(Value::Array(out))
137}
138
139fn resolve_map_value<Id, F>(
140 value_type: &FieldType,
141 value: Value,
142 resolved: &BTreeMap<Uid, Id>,
143 encode_ref: F,
144) -> Result<Value>
145where
146 F: Fn(&Id) -> Value + Copy,
147{
148 let Value::Object(map) = value else {
149 return Err(anyhow!("map value must be an object"));
150 };
151 let mut out = Map::new();
152 for (key, value) in map {
153 out.insert(
154 key,
155 resolve_value_for_type(value_type, value, resolved, encode_ref)?,
156 );
157 }
158 Ok(Value::Object(out))
159}
160
161fn add_query_filters<Id>(
162 filters: &mut Vec<(String, String)>,
163 field: &str,
164 field_type: &FieldType,
165 value: &Value,
166 resolved: &BTreeMap<Uid, Id>,
167) -> Result<()>
168where
169 Id: ToString,
170{
171 match field_type {
172 FieldType::Ref { .. } => {
173 let id = resolve_query_ref(value, resolved)?;
174 filters.push((field.to_string(), id));
175 Ok(())
176 }
177 FieldType::ListRef { .. } => {
178 let Value::Array(items) = value else {
179 return Err(anyhow!("key field {field} must be an array"));
180 };
181 for item in items {
182 let id = resolve_query_ref(item, resolved)?;
183 filters.push((field.to_string(), id));
184 }
185 Ok(())
186 }
187 _ => {
188 let scalar = value_to_query_value(value)?;
189 filters.push((field.to_string(), scalar));
190 Ok(())
191 }
192 }
193}
194
195fn resolve_query_ref<Id>(value: &Value, resolved: &BTreeMap<Uid, Id>) -> Result<String>
196where
197 Id: ToString,
198{
199 let Value::String(raw) = value else {
200 return Err(anyhow!("ref value must be a uuid string"));
201 };
202 let uid = Uid::parse_str(raw).map_err(|_| anyhow!("ref value is not a uuid: {raw}"))?;
203 let id = resolved
204 .get(&uid)
205 .ok_or(AdapterApplyError::MissingRef { uid })?;
206 Ok(id.to_string())
207}
208
209fn value_to_query_value(value: &Value) -> Result<String> {
210 match value {
211 Value::String(raw) => Ok(raw.clone()),
212 Value::Number(num) => Ok(num.to_string()),
213 Value::Bool(value) => Ok(value.to_string()),
214 Value::Null => Err(anyhow!("key value is null")),
215 Value::Array(_) | Value::Object(_) => Err(anyhow!("key value must be scalar")),
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use alembic_core::{FieldSchema, FieldType, JsonMap, Key, TypeSchema};
223 use serde_json::json;
224 use uuid::Uuid;
225
226 fn field_schema(r#type: FieldType) -> FieldSchema {
227 FieldSchema {
228 r#type,
229 required: false,
230 nullable: false,
231 format: None,
232 pattern: None,
233 description: None,
234 }
235 }
236
237 fn simple_type_schema() -> TypeSchema {
238 TypeSchema {
239 key: BTreeMap::from([("slug".to_string(), field_schema(FieldType::Slug))]),
240 fields: BTreeMap::from([
241 ("name".to_string(), field_schema(FieldType::String)),
242 ("count".to_string(), field_schema(FieldType::Int)),
243 ]),
244 }
245 }
246
247 fn attrs(pairs: Vec<(&str, Value)>) -> JsonMap {
248 JsonMap::from(
249 pairs
250 .into_iter()
251 .map(|(k, v)| (k.to_string(), v))
252 .collect::<BTreeMap<_, _>>(),
253 )
254 }
255
256 fn empty_resolved() -> BTreeMap<Uid, i64> {
257 BTreeMap::new()
258 }
259
260 fn encode_ref(id: &i64) -> Value {
261 json!(id)
262 }
263
264 #[test]
267 fn build_key_from_schema_extracts_key_fields() {
268 let schema = simple_type_schema();
269 let a = attrs(vec![("slug", json!("fra1")), ("name", json!("FRA1"))]);
270 let key = build_key_from_schema(&schema, &a).unwrap();
271 assert_eq!(key.get("slug"), Some(&json!("fra1")));
272 assert_eq!(key.len(), 1);
273 }
274
275 #[test]
276 fn build_key_from_schema_composite_key() {
277 let schema = TypeSchema {
278 key: BTreeMap::from([
279 ("site".to_string(), field_schema(FieldType::String)),
280 ("name".to_string(), field_schema(FieldType::String)),
281 ]),
282 fields: BTreeMap::new(),
283 };
284 let a = attrs(vec![("site", json!("fra1")), ("name", json!("eth0"))]);
285 let key = build_key_from_schema(&schema, &a).unwrap();
286 assert_eq!(key.len(), 2);
287 assert_eq!(key.get("site"), Some(&json!("fra1")));
288 assert_eq!(key.get("name"), Some(&json!("eth0")));
289 }
290
291 #[test]
292 fn build_key_from_schema_missing_field_errors() {
293 let schema = simple_type_schema();
294 let a = attrs(vec![("name", json!("FRA1"))]);
295 let err = build_key_from_schema(&schema, &a).unwrap_err();
296 assert!(err.to_string().contains("missing key field slug"));
297 }
298
299 #[test]
302 fn build_request_body_scalar_fields() {
303 let schema = simple_type_schema();
304 let a = attrs(vec![("name", json!("FRA1")), ("count", json!(42))]);
305 let body = build_request_body(&schema, &a, &empty_resolved(), encode_ref).unwrap();
306 let obj = body.as_object().unwrap();
307 assert_eq!(obj.get("name"), Some(&json!("FRA1")));
308 assert_eq!(obj.get("count"), Some(&json!(42)));
309 }
310
311 #[test]
312 fn build_request_body_null_value_passes_through() {
313 let schema = simple_type_schema();
314 let a = attrs(vec![("name", Value::Null)]);
315 let body = build_request_body(&schema, &a, &empty_resolved(), encode_ref).unwrap();
316 assert_eq!(body.as_object().unwrap().get("name"), Some(&Value::Null));
317 }
318
319 #[test]
320 fn build_request_body_resolves_ref() {
321 let uid = Uuid::from_u128(1);
322 let schema = TypeSchema {
323 key: BTreeMap::new(),
324 fields: BTreeMap::from([(
325 "site".to_string(),
326 field_schema(FieldType::Ref {
327 target: "dcim.site".to_string(),
328 }),
329 )]),
330 };
331 let a = attrs(vec![("site", json!(uid.to_string()))]);
332 let mut resolved = BTreeMap::new();
333 resolved.insert(uid, 99_i64);
334 let body = build_request_body(&schema, &a, &resolved, encode_ref).unwrap();
335 assert_eq!(body.as_object().unwrap().get("site"), Some(&json!(99)));
336 }
337
338 #[test]
339 fn build_request_body_missing_schema_errors() {
340 let schema = simple_type_schema();
341 let a = attrs(vec![("nonexistent", json!("x"))]);
342 let err = build_request_body(&schema, &a, &empty_resolved(), encode_ref).unwrap_err();
343 assert!(err.to_string().contains("missing schema for field"));
344 }
345
346 #[test]
349 fn resolve_value_scalar_passthrough() {
350 let val = json!("hello");
351 let result = resolve_value_for_type(
352 &FieldType::String,
353 val.clone(),
354 &empty_resolved(),
355 encode_ref,
356 )
357 .unwrap();
358 assert_eq!(result, val);
359 }
360
361 #[test]
362 fn resolve_value_ref() {
363 let uid = Uuid::from_u128(5);
364 let mut resolved = BTreeMap::new();
365 resolved.insert(uid, 42_i64);
366 let result = resolve_value_for_type(
367 &FieldType::Ref {
368 target: "t".to_string(),
369 },
370 json!(uid.to_string()),
371 &resolved,
372 encode_ref,
373 )
374 .unwrap();
375 assert_eq!(result, json!(42));
376 }
377
378 #[test]
379 fn resolve_value_ref_missing_uid_errors() {
380 let uid = Uuid::from_u128(99);
381 let err = resolve_value_for_type(
382 &FieldType::Ref {
383 target: "t".to_string(),
384 },
385 json!(uid.to_string()),
386 &empty_resolved(),
387 encode_ref,
388 )
389 .unwrap_err();
390 assert!(err.to_string().contains("missing referenced uid"));
391 }
392
393 #[test]
394 fn resolve_value_ref_non_string_errors() {
395 let err = resolve_value_for_type(
396 &FieldType::Ref {
397 target: "t".to_string(),
398 },
399 json!(123),
400 &empty_resolved(),
401 encode_ref,
402 )
403 .unwrap_err();
404 assert!(err.to_string().contains("ref value must be a uuid string"));
405 }
406
407 #[test]
408 fn resolve_value_ref_invalid_uuid_errors() {
409 let err = resolve_value_for_type(
410 &FieldType::Ref {
411 target: "t".to_string(),
412 },
413 json!("not-a-uuid"),
414 &empty_resolved(),
415 encode_ref,
416 )
417 .unwrap_err();
418 assert!(err.to_string().contains("ref value is not a uuid"));
419 }
420
421 #[test]
422 fn resolve_value_list_ref() {
423 let uid1 = Uuid::from_u128(1);
424 let uid2 = Uuid::from_u128(2);
425 let mut resolved = BTreeMap::new();
426 resolved.insert(uid1, 10_i64);
427 resolved.insert(uid2, 20_i64);
428 let result = resolve_value_for_type(
429 &FieldType::ListRef {
430 target: "t".to_string(),
431 },
432 json!([uid1.to_string(), uid2.to_string()]),
433 &resolved,
434 encode_ref,
435 )
436 .unwrap();
437 assert_eq!(result, json!([10, 20]));
438 }
439
440 #[test]
441 fn resolve_value_list_ref_non_array_errors() {
442 let err = resolve_value_for_type(
443 &FieldType::ListRef {
444 target: "t".to_string(),
445 },
446 json!("not-array"),
447 &empty_resolved(),
448 encode_ref,
449 )
450 .unwrap_err();
451 assert!(err.to_string().contains("list_ref value must be an array"));
452 }
453
454 #[test]
455 fn resolve_value_list_scalars() {
456 let result = resolve_value_for_type(
457 &FieldType::List {
458 item: Box::new(FieldType::String),
459 },
460 json!(["a", "b"]),
461 &empty_resolved(),
462 encode_ref,
463 )
464 .unwrap();
465 assert_eq!(result, json!(["a", "b"]));
466 }
467
468 #[test]
469 fn resolve_value_list_of_refs() {
470 let uid = Uuid::from_u128(3);
471 let mut resolved = BTreeMap::new();
472 resolved.insert(uid, 7_i64);
473 let result = resolve_value_for_type(
474 &FieldType::List {
475 item: Box::new(FieldType::Ref {
476 target: "t".to_string(),
477 }),
478 },
479 json!([uid.to_string()]),
480 &resolved,
481 encode_ref,
482 )
483 .unwrap();
484 assert_eq!(result, json!([7]));
485 }
486
487 #[test]
488 fn resolve_value_list_non_array_errors() {
489 let err = resolve_value_for_type(
490 &FieldType::List {
491 item: Box::new(FieldType::String),
492 },
493 json!("not-array"),
494 &empty_resolved(),
495 encode_ref,
496 )
497 .unwrap_err();
498 assert!(err.to_string().contains("list value must be an array"));
499 }
500
501 #[test]
502 fn resolve_value_map_scalars() {
503 let result = resolve_value_for_type(
504 &FieldType::Map {
505 value: Box::new(FieldType::Int),
506 },
507 json!({"a": 1, "b": 2}),
508 &empty_resolved(),
509 encode_ref,
510 )
511 .unwrap();
512 let obj = result.as_object().unwrap();
513 assert_eq!(obj.get("a"), Some(&json!(1)));
514 assert_eq!(obj.get("b"), Some(&json!(2)));
515 }
516
517 #[test]
518 fn resolve_value_map_with_refs() {
519 let uid = Uuid::from_u128(4);
520 let mut resolved = BTreeMap::new();
521 resolved.insert(uid, 50_i64);
522 let result = resolve_value_for_type(
523 &FieldType::Map {
524 value: Box::new(FieldType::Ref {
525 target: "t".to_string(),
526 }),
527 },
528 json!({"x": uid.to_string()}),
529 &resolved,
530 encode_ref,
531 )
532 .unwrap();
533 assert_eq!(result.as_object().unwrap().get("x"), Some(&json!(50)));
534 }
535
536 #[test]
537 fn resolve_value_map_non_object_errors() {
538 let err = resolve_value_for_type(
539 &FieldType::Map {
540 value: Box::new(FieldType::String),
541 },
542 json!("not-object"),
543 &empty_resolved(),
544 encode_ref,
545 )
546 .unwrap_err();
547 assert!(err.to_string().contains("map value must be an object"));
548 }
549
550 #[test]
553 fn query_filters_scalar_key() {
554 let schema = simple_type_schema();
555 let key = Key::from(BTreeMap::from([("slug".to_string(), json!("fra1"))]));
556 let filters = query_filters_from_key(&schema, &key, &empty_resolved()).unwrap();
557 assert_eq!(filters, vec![("slug".to_string(), "fra1".to_string())]);
558 }
559
560 #[test]
561 fn query_filters_numeric_key() {
562 let schema = TypeSchema {
563 key: BTreeMap::from([("id".to_string(), field_schema(FieldType::Int))]),
564 fields: BTreeMap::new(),
565 };
566 let key = Key::from(BTreeMap::from([("id".to_string(), json!(42))]));
567 let filters = query_filters_from_key(&schema, &key, &empty_resolved()).unwrap();
568 assert_eq!(filters, vec![("id".to_string(), "42".to_string())]);
569 }
570
571 #[test]
572 fn query_filters_bool_key() {
573 let schema = TypeSchema {
574 key: BTreeMap::from([("active".to_string(), field_schema(FieldType::Bool))]),
575 fields: BTreeMap::new(),
576 };
577 let key = Key::from(BTreeMap::from([("active".to_string(), json!(true))]));
578 let filters = query_filters_from_key(&schema, &key, &empty_resolved()).unwrap();
579 assert_eq!(filters, vec![("active".to_string(), "true".to_string())]);
580 }
581
582 #[test]
583 fn query_filters_ref_key() {
584 let uid = Uuid::from_u128(10);
585 let schema = TypeSchema {
586 key: BTreeMap::from([(
587 "site".to_string(),
588 field_schema(FieldType::Ref {
589 target: "dcim.site".to_string(),
590 }),
591 )]),
592 fields: BTreeMap::new(),
593 };
594 let key = Key::from(BTreeMap::from([(
595 "site".to_string(),
596 json!(uid.to_string()),
597 )]));
598 let mut resolved = BTreeMap::new();
599 resolved.insert(uid, 77_i64);
600 let filters = query_filters_from_key(&schema, &key, &resolved).unwrap();
601 assert_eq!(filters, vec![("site".to_string(), "77".to_string())]);
602 }
603
604 #[test]
605 fn query_filters_list_ref_key() {
606 let uid1 = Uuid::from_u128(1);
607 let uid2 = Uuid::from_u128(2);
608 let schema = TypeSchema {
609 key: BTreeMap::from([(
610 "tags".to_string(),
611 field_schema(FieldType::ListRef {
612 target: "extras.tag".to_string(),
613 }),
614 )]),
615 fields: BTreeMap::new(),
616 };
617 let key = Key::from(BTreeMap::from([(
618 "tags".to_string(),
619 json!([uid1.to_string(), uid2.to_string()]),
620 )]));
621 let mut resolved = BTreeMap::new();
622 resolved.insert(uid1, 100_i64);
623 resolved.insert(uid2, 200_i64);
624 let filters = query_filters_from_key(&schema, &key, &resolved).unwrap();
625 assert_eq!(
626 filters,
627 vec![
628 ("tags".to_string(), "100".to_string()),
629 ("tags".to_string(), "200".to_string()),
630 ]
631 );
632 }
633
634 #[test]
635 fn query_filters_missing_key_schema_errors() {
636 let schema = simple_type_schema();
637 let key = Key::from(BTreeMap::from([("nonexistent".to_string(), json!("x"))]));
638 let err = query_filters_from_key(&schema, &key, &empty_resolved()).unwrap_err();
639 assert!(err.to_string().contains("missing schema for key field"));
640 }
641
642 #[test]
643 fn query_filters_null_scalar_errors() {
644 let schema = simple_type_schema();
645 let key = Key::from(BTreeMap::from([("slug".to_string(), Value::Null)]));
646 let err = query_filters_from_key(&schema, &key, &empty_resolved()).unwrap_err();
647 assert!(err.to_string().contains("key value is null"));
648 }
649
650 #[test]
651 fn query_filters_list_ref_non_array_errors() {
652 let schema = TypeSchema {
653 key: BTreeMap::from([(
654 "tags".to_string(),
655 field_schema(FieldType::ListRef {
656 target: "t".to_string(),
657 }),
658 )]),
659 fields: BTreeMap::new(),
660 };
661 let key = Key::from(BTreeMap::from([("tags".to_string(), json!("not-array"))]));
662 let err = query_filters_from_key(&schema, &key, &empty_resolved()).unwrap_err();
663 assert!(err.to_string().contains("key field tags must be an array"));
664 }
665}