1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4
5use crate::gts::{GtsID, GTS_URI_PREFIX};
6use crate::path_resolver::JsonPathResolver;
7use crate::schema_cast::{GtsEntityCastResult, SchemaCastError};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ValidationError {
11 #[serde(rename = "instancePath")]
12 pub instance_path: String,
13 #[serde(rename = "schemaPath")]
14 pub schema_path: String,
15 pub keyword: String,
16 pub message: String,
17 pub params: HashMap<String, Value>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub data: Option<Value>,
20}
21
22#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23pub struct ValidationResult {
24 pub errors: Vec<ValidationError>,
25}
26
27#[derive(Debug, Clone)]
28pub struct GtsFile {
29 pub path: String,
30 pub name: String,
31 pub content: Value,
32 pub sequences_count: usize,
33 pub sequence_content: HashMap<usize, Value>,
34 pub validation: ValidationResult,
35}
36
37impl GtsFile {
38 #[must_use]
39 pub fn new(path: String, name: String, content: Value) -> Self {
40 let sequence_content: HashMap<usize, Value> = if let Some(arr) = content.as_array() {
41 arr.iter()
42 .enumerate()
43 .map(|(i, v)| (i, v.clone()))
44 .collect()
45 } else {
46 [(0, content.clone())].into_iter().collect()
47 };
48 let sequences_count = sequence_content.len();
49
50 GtsFile {
51 path,
52 name,
53 content,
54 sequences_count,
55 sequence_content,
56 validation: ValidationResult::default(),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct GtsConfig {
63 pub entity_id_fields: Vec<String>,
64 pub schema_id_fields: Vec<String>,
65}
66
67impl Default for GtsConfig {
68 fn default() -> Self {
69 GtsConfig {
70 entity_id_fields: vec![
71 "$id".to_owned(),
72 "gtsId".to_owned(),
73 "gtsIid".to_owned(),
74 "gtsOid".to_owned(),
75 "gtsI".to_owned(),
76 "gts_id".to_owned(),
77 "gts_oid".to_owned(),
78 "gts_iid".to_owned(),
79 "id".to_owned(),
80 ],
81 schema_id_fields: vec![
82 "gtsTid".to_owned(),
83 "gtsType".to_owned(),
84 "gtsT".to_owned(),
85 "gts_t".to_owned(),
86 "gts_tid".to_owned(),
87 "gts_type".to_owned(),
88 "type".to_owned(),
89 "schema".to_owned(),
90 ],
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
96pub struct GtsRef {
97 pub id: String,
98 pub source_path: String,
99}
100
101#[derive(Debug, Clone)]
102pub struct GtsEntity {
103 pub gts_id: Option<GtsID>,
106 pub instance_id: Option<String>,
109 pub is_schema: bool,
111 pub file: Option<GtsFile>,
112 pub list_sequence: Option<usize>,
113 pub label: String,
114 pub content: Value,
115 pub gts_refs: Vec<GtsRef>,
116 pub validation: ValidationResult,
117 pub schema_id: Option<String>,
122 pub selected_entity_field: Option<String>,
123 pub selected_schema_id_field: Option<String>,
124 pub description: String,
125 pub schema_refs: Vec<GtsRef>,
126}
127
128impl GtsEntity {
129 #[allow(clippy::too_many_arguments)]
130 #[must_use]
131 pub fn new(
132 file: Option<GtsFile>,
133 list_sequence: Option<usize>,
134 content: &Value,
135 cfg: Option<&GtsConfig>,
136 gts_id: Option<GtsID>,
137 is_schema: bool,
138 label: String,
139 validation: Option<ValidationResult>,
140 schema_id: Option<String>,
141 ) -> Self {
142 let mut entity = GtsEntity {
143 file,
144 list_sequence,
145 content: content.clone(),
146 gts_id,
147 instance_id: None,
148 is_schema,
149 label,
150 validation: validation.unwrap_or_default(),
151 schema_id,
152 selected_entity_field: None,
153 selected_schema_id_field: None,
154 gts_refs: Vec::new(),
155 schema_refs: Vec::new(),
156 description: String::new(),
157 };
158
159 entity.is_schema = entity.has_schema_field();
162
163 if let Some(cfg) = cfg {
165 if entity.is_schema {
166 entity.extract_schema_ids(cfg);
168 } else {
169 entity.extract_instance_ids(cfg);
171 }
172 }
173
174 if let Some(ref file) = entity.file {
176 if let Some(seq) = entity.list_sequence {
177 entity.label = format!("{}#{seq}", file.name);
178 } else {
179 entity.label = file.name.clone();
180 }
181 } else if let Some(ref instance_id) = entity.instance_id {
182 entity.label = instance_id.clone();
183 } else if let Some(ref gts_id) = entity.gts_id {
184 entity.label = gts_id.id.clone();
185 } else if entity.label.is_empty() {
186 entity.label = String::new();
187 }
188
189 if let Some(obj) = content.as_object() {
191 if let Some(desc) = obj.get("description") {
192 if let Some(s) = desc.as_str() {
193 s.clone_into(&mut entity.description);
194 }
195 }
196 }
197
198 entity.gts_refs = entity.extract_gts_ids_with_paths();
200 if entity.is_schema {
201 entity.schema_refs = entity.extract_ref_strings_with_paths();
202 }
203
204 entity
205 }
206
207 fn has_schema_field(&self) -> bool {
210 if let Some(obj) = self.content.as_object() {
211 if let Some(schema_val) = obj.get("$schema") {
212 if let Some(schema_str) = schema_val.as_str() {
213 return !schema_str.is_empty();
214 }
215 }
216 }
217 false
218 }
219
220 fn extract_schema_ids(&mut self, cfg: &GtsConfig) {
225 if let Some(obj) = self.content.as_object() {
227 if let Some(id_val) = obj.get("$id") {
228 if let Some(id_str) = id_val.as_str() {
229 let trimmed = id_str.trim();
230
231 if trimmed.starts_with("gts.") {
234 return;
237 }
238
239 let normalized = trimmed.strip_prefix(GTS_URI_PREFIX).unwrap_or(trimmed);
240 if GtsID::is_valid(normalized) {
241 self.gts_id = GtsID::new(normalized).ok();
242 self.instance_id = Some(normalized.to_owned());
243 self.selected_entity_field = Some("$id".to_owned());
244 }
245 }
246 }
247
248 if let Some(schema_val) = obj.get("$schema") {
251 if let Some(schema_str) = schema_val.as_str() {
252 self.schema_id = Some(schema_str.to_owned());
253 self.selected_schema_id_field = Some("$schema".to_owned());
254 }
255 }
256
257 if let Some(ref gts_id) = self.gts_id {
259 if gts_id.gts_id_segments.len() > 1 {
260 let parent_segments: Vec<&str> = gts_id
263 .gts_id_segments
264 .iter()
265 .take(gts_id.gts_id_segments.len() - 1)
266 .map(|seg| seg.segment.as_str())
267 .collect();
268 if !parent_segments.is_empty() {
269 let parent_id = format!("gts.{}", parent_segments.join("~"));
274 let parent_id = if parent_id.ends_with('~') {
276 parent_id
277 } else {
278 format!("{parent_id}~")
279 };
280 if self
282 .schema_id
283 .as_ref()
284 .is_some_and(|s| s.starts_with("http"))
285 {
286 self.schema_id = Some(parent_id);
287 }
288 }
289 }
290 }
291 }
292
293 if self.gts_id.is_none() {
295 let idv = self.calc_json_entity_id_legacy(cfg);
296 if let Some(ref id) = idv {
297 if GtsID::is_valid(id) {
298 self.gts_id = GtsID::new(id).ok();
299 self.instance_id = Some(id.clone());
300 }
301 }
302 }
303 }
304
305 fn extract_instance_ids(&mut self, cfg: &GtsConfig) {
313 if self.content.as_object().is_none() {
315 return;
316 }
317
318 let id_value = self.get_id_field_value(cfg);
320
321 if let Some(ref id) = id_value {
323 if GtsID::is_valid(id) {
324 self.gts_id = GtsID::new(id).ok();
326 self.instance_id = Some(id.clone());
327
328 if let Some(ref gts_id) = self.gts_id {
337 if gts_id.gts_id_segments.len() > 1 {
339 if let Some(last_tilde) = gts_id.id.rfind('~') {
342 self.schema_id = Some(gts_id.id[..=last_tilde].to_string());
343 self.selected_schema_id_field = self.selected_entity_field.clone();
345 }
346 }
347 }
348 } else {
349 self.instance_id = Some(id.clone());
351 self.gts_id = None; }
353 }
354
355 if self.schema_id.is_none() {
359 self.schema_id = self.get_type_field_value(cfg);
360 }
361
362 if self.instance_id.is_none() {
364 if let Some(ref file) = self.file {
365 if let Some(seq) = self.list_sequence {
366 self.instance_id = Some(format!("{}#{}", file.path, seq));
367 } else {
368 self.instance_id = Some(file.path.clone());
369 }
370 }
371 }
372 }
373
374 fn get_id_field_value(&mut self, cfg: &GtsConfig) -> Option<String> {
376 for f in &cfg.entity_id_fields {
377 if f == "$schema" || f == "type" {
379 continue;
380 }
381 if let Some(v) = self.get_field_value(f) {
382 self.selected_entity_field = Some(f.clone());
383 return Some(v);
384 }
385 }
386 None
387 }
388
389 fn get_type_field_value(&mut self, cfg: &GtsConfig) -> Option<String> {
391 for f in &cfg.schema_id_fields {
392 if f == "$schema" {
394 continue;
395 }
396 if let Some(v) = self.get_field_value(f) {
397 if GtsID::is_valid(&v) && v.ends_with('~') {
399 self.selected_schema_id_field = Some(f.clone());
400 return Some(v);
401 }
402 }
403 }
404 None
405 }
406
407 fn calc_json_entity_id_legacy(&mut self, cfg: &GtsConfig) -> Option<String> {
409 self.first_non_empty_field(&cfg.entity_id_fields)
410 }
411
412 #[must_use]
413 pub fn resolve_path(&self, path: &str) -> JsonPathResolver {
414 let gts_id = self
415 .gts_id
416 .as_ref()
417 .map(|g| g.id.clone())
418 .unwrap_or_default();
419 JsonPathResolver::new(gts_id, self.content.clone()).resolve(path)
420 }
421
422 pub fn cast(
427 &self,
428 to_schema: &GtsEntity,
429 from_schema: &GtsEntity,
430 resolver: Option<&()>,
431 ) -> Result<GtsEntityCastResult, SchemaCastError> {
432 if self.is_schema {
433 if let (Some(self_id), Some(from_id)) = (&self.gts_id, &from_schema.gts_id) {
435 if self_id.id != from_id.id {
436 return Err(SchemaCastError::InternalError(format!(
437 "Internal error: {} != {}",
438 self_id.id, from_id.id
439 )));
440 }
441 }
442 }
443
444 if !to_schema.is_schema {
445 return Err(SchemaCastError::TargetMustBeSchema);
446 }
447
448 if !from_schema.is_schema {
449 return Err(SchemaCastError::SourceMustBeSchema);
450 }
451
452 let from_id = self
453 .gts_id
454 .as_ref()
455 .map(|g| g.id.clone())
456 .unwrap_or_default();
457 let to_id = to_schema
458 .gts_id
459 .as_ref()
460 .map(|g| g.id.clone())
461 .unwrap_or_default();
462
463 GtsEntityCastResult::cast(
464 &from_id,
465 &to_id,
466 &self.content,
467 &from_schema.content,
468 &to_schema.content,
469 resolver,
470 )
471 }
472
473 fn walk_and_collect<F>(content: &Value, collector: &mut Vec<GtsRef>, matcher: F)
474 where
475 F: Fn(&Value, &str) -> Option<GtsRef> + Copy,
476 {
477 fn walk<F>(node: &Value, current_path: &str, collector: &mut Vec<GtsRef>, matcher: F)
478 where
479 F: Fn(&Value, &str) -> Option<GtsRef> + Copy,
480 {
481 if let Some(match_result) = matcher(node, current_path) {
483 collector.push(match_result);
484 }
485
486 match node {
488 Value::Object(map) => {
489 for (k, v) in map {
490 let next_path = if current_path.is_empty() {
491 k.clone()
492 } else {
493 format!("{current_path}.{k}")
494 };
495 walk(v, &next_path, collector, matcher);
496 }
497 }
498 Value::Array(arr) => {
499 for (idx, item) in arr.iter().enumerate() {
500 let next_path = format!("{current_path}[{idx}]");
501 walk(item, &next_path, collector, matcher);
502 }
503 }
504 _ => {}
505 }
506 }
507
508 walk(content, "", collector, matcher);
509 }
510
511 fn deduplicate_by_id_and_path(items: Vec<GtsRef>) -> Vec<GtsRef> {
512 let mut seen = HashMap::new();
513 let mut result = Vec::new();
514
515 for item in items {
516 let key = format!("{}|{}", item.id, item.source_path);
517 if let std::collections::hash_map::Entry::Vacant(e) = seen.entry(key) {
518 e.insert(true);
519 result.push(item);
520 }
521 }
522
523 result
524 }
525
526 fn extract_gts_ids_with_paths(&self) -> Vec<GtsRef> {
527 let mut found = Vec::new();
528
529 let gts_id_matcher = |node: &Value, path: &str| -> Option<GtsRef> {
530 if let Some(s) = node.as_str() {
531 if GtsID::is_valid(s) {
532 return Some(GtsRef {
533 id: s.to_owned(),
534 source_path: if path.is_empty() {
535 "root".to_owned()
536 } else {
537 path.to_owned()
538 },
539 });
540 }
541 }
542 None
543 };
544
545 Self::walk_and_collect(&self.content, &mut found, gts_id_matcher);
546 Self::deduplicate_by_id_and_path(found)
547 }
548
549 fn extract_ref_strings_with_paths(&self) -> Vec<GtsRef> {
550 let mut refs = Vec::new();
551
552 let ref_matcher = |node: &Value, path: &str| -> Option<GtsRef> {
553 if let Some(obj) = node.as_object() {
554 if let Some(ref_val) = obj.get("$ref") {
555 if let Some(ref_str) = ref_val.as_str() {
556 let ref_path = if path.is_empty() {
557 "$ref".to_owned()
558 } else {
559 format!("{path}.$ref")
560 };
561 let normalized_ref = ref_str
563 .strip_prefix(GTS_URI_PREFIX)
564 .unwrap_or(ref_str)
565 .to_owned();
566 return Some(GtsRef {
567 id: normalized_ref,
568 source_path: ref_path,
569 });
570 }
571 }
572 }
573 None
574 };
575
576 Self::walk_and_collect(&self.content, &mut refs, ref_matcher);
577 Self::deduplicate_by_id_and_path(refs)
578 }
579
580 fn get_field_value(&self, field: &str) -> Option<String> {
581 if let Some(obj) = self.content.as_object() {
582 if let Some(v) = obj.get(field) {
583 if let Some(s) = v.as_str() {
584 let trimmed = s.trim();
585 if !trimmed.is_empty() {
586 if field == "$id" && self.is_schema && trimmed.starts_with("gts.") {
589 return None;
591 }
592
593 let normalized = if field == "$id" {
596 trimmed.strip_prefix(GTS_URI_PREFIX).unwrap_or(trimmed)
597 } else {
598 trimmed
599 };
600 return Some(normalized.to_owned());
601 }
602 }
603 }
604 }
605 None
606 }
607
608 fn first_non_empty_field(&mut self, fields: &[String]) -> Option<String> {
609 for f in fields {
611 if let Some(v) = self.get_field_value(f) {
612 if GtsID::is_valid(&v) {
613 self.selected_entity_field = Some(f.clone());
614 return Some(v);
615 }
616 }
617 }
618
619 for f in fields {
621 if let Some(v) = self.get_field_value(f) {
622 self.selected_entity_field = Some(f.clone());
623 return Some(v);
624 }
625 }
626
627 None
628 }
629
630 #[must_use]
635 pub fn effective_id(&self) -> Option<String> {
636 if let Some(ref gts_id) = self.gts_id {
638 return Some(gts_id.id.clone());
639 }
640 self.instance_id.clone()
642 }
643}
644
645#[cfg(test)]
646#[allow(clippy::unwrap_used, clippy::expect_used)]
647mod tests {
648 use super::*;
649 use serde_json::json;
650
651 #[test]
652 fn test_json_file_with_description() {
653 let content = json!({
654 "id": "gts.vendor.package.namespace.type.v1.0",
655 "description": "Test description"
656 });
657
658 let cfg = GtsConfig::default();
659 let entity = GtsEntity::new(
660 None,
661 None,
662 &content,
663 Some(&cfg),
664 None,
665 false,
666 String::new(),
667 None,
668 None,
669 );
670
671 assert_eq!(entity.description, "Test description");
672 }
673
674 #[test]
675 fn test_json_entity_with_file_and_sequence() {
676 let file_content = json!([
677 {"id": "gts.vendor.package.namespace.type.v1.0"},
678 {"id": "gts.vendor.package.namespace.type.v1.1"}
679 ]);
680
681 let file = GtsFile::new(
682 "/path/to/file.json".to_owned(),
683 "file.json".to_owned(),
684 file_content,
685 );
686
687 let entity_content = json!({"id": "gts.vendor.package.namespace.type.v1.0"});
688 let cfg = GtsConfig::default();
689
690 let entity = GtsEntity::new(
691 Some(file),
692 Some(0),
693 &entity_content,
694 Some(&cfg),
695 None,
696 false,
697 String::new(),
698 None,
699 None,
700 );
701
702 assert_eq!(entity.label, "file.json#0");
703 }
704
705 #[test]
706 fn test_json_entity_with_file_no_sequence() {
707 let file_content = json!({"id": "gts.vendor.package.namespace.type.v1.0"});
708
709 let file = GtsFile::new(
710 "/path/to/file.json".to_owned(),
711 "file.json".to_owned(),
712 file_content,
713 );
714
715 let entity_content = json!({"id": "gts.vendor.package.namespace.type.v1.0"});
716 let cfg = GtsConfig::default();
717
718 let entity = GtsEntity::new(
719 Some(file),
720 None,
721 &entity_content,
722 Some(&cfg),
723 None,
724 false,
725 String::new(),
726 None,
727 None,
728 );
729
730 assert_eq!(entity.label, "file.json");
731 }
732
733 #[test]
734 fn test_json_entity_extract_gts_ids() {
735 let content = json!({
736 "id": "gts.vendor.package.namespace.type.v1.0~a.b.c.d.v1",
737 "nested": {
738 "ref": "gts.other.package.namespace.type.v2.0~e.f.g.h.v2"
739 }
740 });
741
742 let cfg = GtsConfig::default();
743 let entity = GtsEntity::new(
744 None,
745 None,
746 &content,
747 Some(&cfg),
748 None,
749 false,
750 String::new(),
751 None,
752 None,
753 );
754
755 assert!(!entity.gts_refs.is_empty());
757 }
758
759 #[test]
760 fn test_json_entity_extract_ref_strings() {
761 let content = json!({
762 "$schema": "http://json-schema.org/draft-07/schema#",
763 "$ref": "gts://gts.vendor.package.namespace.type.v1.0~",
764 "properties": {
765 "user": {
766 "$ref": "gts://gts.other.package.namespace.type.v2.0~"
767 }
768 }
769 });
770
771 let cfg = GtsConfig::default();
772 let entity = GtsEntity::new(
773 None,
774 None,
775 &content,
776 Some(&cfg),
777 None,
778 false, String::new(),
780 None,
781 None,
782 );
783
784 assert!(entity.is_schema);
786 assert!(!entity.schema_refs.is_empty());
788 }
789
790 #[test]
791 fn test_json_entity_is_json_schema_entity() {
792 let schema_content = json!({
793 "$schema": "http://json-schema.org/draft-07/schema#",
794 "type": "object"
795 });
796
797 let entity = GtsEntity::new(
798 None,
799 None,
800 &schema_content,
801 None,
802 None,
803 false,
804 String::new(),
805 None,
806 None,
807 );
808
809 assert!(entity.is_schema);
810 }
811
812 #[test]
813 fn test_json_entity_instance_with_type_field() {
814 let content = json!({
819 "type": "gts.vendor.package.namespace.type.v1.0~",
820 "name": "test"
821 });
822
823 let cfg = GtsConfig::default();
824 let entity = GtsEntity::new(
825 None,
826 None,
827 &content,
828 Some(&cfg),
829 None,
830 false,
831 String::new(),
832 None,
833 None,
834 );
835
836 assert!(!entity.is_schema);
838 assert_eq!(
840 entity.schema_id,
841 Some("gts.vendor.package.namespace.type.v1.0~".to_owned())
842 );
843 assert!(entity.gts_id.is_none());
845 assert!(entity.instance_id.is_none());
846 }
847
848 #[test]
849 fn test_json_entity_with_custom_label() {
850 let content = json!({"name": "test"});
851
852 let entity = GtsEntity::new(
853 None,
854 None,
855 &content,
856 None,
857 None,
858 false,
859 "custom_label".to_owned(),
860 None,
861 None,
862 );
863
864 assert_eq!(entity.label, "custom_label");
865 }
866
867 #[test]
868 fn test_json_entity_empty_label_fallback() {
869 let content = json!({"name": "test"});
870
871 let entity = GtsEntity::new(
872 None,
873 None,
874 &content,
875 None,
876 None,
877 false,
878 String::new(),
879 None,
880 None,
881 );
882
883 assert_eq!(entity.label, "");
884 }
885
886 #[test]
887 fn test_validation_result_default() {
888 let result = ValidationResult::default();
889 assert!(result.errors.is_empty());
890 }
891
892 #[test]
893 fn test_validation_error_creation() {
894 let mut params = std::collections::HashMap::new();
895 params.insert("key".to_owned(), json!("value"));
896
897 let error = ValidationError {
898 instance_path: "/path".to_owned(),
899 schema_path: "/schema".to_owned(),
900 keyword: "required".to_owned(),
901 message: "test error".to_owned(),
902 params,
903 data: Some(json!({"test": "data"})),
904 };
905
906 assert_eq!(error.instance_path, "/path");
907 assert_eq!(error.message, "test error");
908 assert!(error.data.is_some());
909 }
910
911 #[test]
912 fn test_gts_config_entity_id_fields() {
913 let cfg = GtsConfig::default();
914 assert!(cfg.entity_id_fields.contains(&"id".to_owned()));
915 assert!(cfg.entity_id_fields.contains(&"$id".to_owned()));
916 assert!(cfg.entity_id_fields.contains(&"gtsId".to_owned()));
917 }
918
919 #[test]
920 fn test_gts_config_schema_id_fields() {
921 let cfg = GtsConfig::default();
922 assert!(cfg.schema_id_fields.contains(&"type".to_owned()));
923 assert!(cfg.schema_id_fields.contains(&"schema".to_owned()));
924 assert!(cfg.schema_id_fields.contains(&"gtsTid".to_owned()));
925 }
926
927 #[test]
928 fn test_json_entity_with_validation_result() {
929 let content = json!({"id": "gts.vendor.package.namespace.type.v1.0"});
930
931 let mut validation = ValidationResult::default();
932 validation.errors.push(ValidationError {
933 instance_path: "/test".to_owned(),
934 schema_path: "/schema/test".to_owned(),
935 keyword: "type".to_owned(),
936 message: "validation error".to_owned(),
937 params: std::collections::HashMap::new(),
938 data: None,
939 });
940
941 let entity = GtsEntity::new(
942 None,
943 None,
944 &content,
945 None,
946 None,
947 false,
948 String::new(),
949 Some(validation.clone()),
950 None,
951 );
952
953 assert_eq!(entity.validation.errors.len(), 1);
954 }
955
956 #[test]
957 fn test_json_entity_schema_id_field_selection() {
958 let content = json!({
959 "id": "gts.vendor.package.namespace.type.v1.0~instance.v1.0",
960 "type": "gts.vendor.package.namespace.type.v1.0~"
961 });
962
963 let cfg = GtsConfig::default();
964 let entity = GtsEntity::new(
965 None,
966 None,
967 &content,
968 Some(&cfg),
969 None,
970 false,
971 String::new(),
972 None,
973 None,
974 );
975
976 assert!(entity.selected_schema_id_field.is_some());
977 }
978
979 #[test]
980 fn test_json_entity_when_id_is_schema() {
981 let content = json!({
982 "id": "gts.vendor.package.namespace.type.v1.0~",
983 "$schema": "http://json-schema.org/draft-07/schema#"
984 });
985
986 let cfg = GtsConfig::default();
987 let entity = GtsEntity::new(
988 None,
989 None,
990 &content,
991 Some(&cfg),
992 None,
993 false,
994 String::new(),
995 None,
996 None,
997 );
998
999 assert_eq!(entity.selected_schema_id_field, Some("$schema".to_owned()));
1001 }
1002
1003 #[test]
1010 fn test_entity_with_gts_uri_prefix_in_id() {
1011 let content = json!({
1013 "$id": "gts://gts.vendor.package.namespace.type.v1.0~",
1014 "$schema": "http://json-schema.org/draft-07/schema#",
1015 "type": "object"
1016 });
1017
1018 let cfg = GtsConfig::default();
1019 let entity = GtsEntity::new(
1020 None,
1021 None,
1022 &content,
1023 Some(&cfg),
1024 None,
1025 false,
1026 String::new(),
1027 None,
1028 None,
1029 );
1030
1031 let gts_id = entity.gts_id.as_ref().expect("Entity should have a GTS ID");
1033 assert_eq!(gts_id.id, "gts.vendor.package.namespace.type.v1.0~");
1034 assert!(entity.is_schema, "Entity should be detected as a schema");
1035 }
1036
1037 #[test]
1038 fn test_entity_schema_id_extraction() {
1039 let content = json!({
1043 "id": "gts.vendor.package.namespace.type.v1~other.app.data.item.v1.0",
1044 "type": "gts.vendor.package.namespace.type.v1~"
1045 });
1046
1047 let cfg = GtsConfig::default();
1048 let entity = GtsEntity::new(
1049 None,
1050 None,
1051 &content,
1052 Some(&cfg),
1053 None,
1054 false,
1055 String::new(),
1056 None,
1057 None,
1058 );
1059
1060 let gts_id = entity.gts_id.as_ref().expect("Entity should have a GTS ID");
1061 assert_eq!(
1062 gts_id.id,
1063 "gts.vendor.package.namespace.type.v1~other.app.data.item.v1.0"
1064 );
1065
1066 let schema_id = entity
1067 .schema_id
1068 .as_ref()
1069 .expect("Entity should have a schema ID");
1070 assert_eq!(schema_id, "gts.vendor.package.namespace.type.v1~");
1071 }
1072
1073 #[test]
1074 fn test_is_json_schema_with_standard_schema() {
1075 let content = json!({
1077 "$id": "gts://gts.vendor.package.namespace.type.v1.0~",
1078 "$schema": "http://json-schema.org/draft-07/schema#"
1079 });
1080
1081 let entity = GtsEntity::new(
1082 None,
1083 None,
1084 &content,
1085 None,
1086 None,
1087 false,
1088 String::new(),
1089 None,
1090 None,
1091 );
1092
1093 assert!(
1094 entity.is_schema,
1095 "Entity with $schema should be detected as schema"
1096 );
1097 }
1098
1099 #[test]
1100 fn test_gts_colon_prefix_not_valid_in_id_field() {
1101 let content = json!({
1104 "$id": "gts:gts.vendor.package.namespace.type.v1.0~",
1105 "$schema": "http://json-schema.org/draft-07/schema#"
1106 });
1107
1108 let cfg = GtsConfig::default();
1109 let entity = GtsEntity::new(
1110 None,
1111 None,
1112 &content,
1113 Some(&cfg),
1114 None,
1115 false,
1116 String::new(),
1117 None,
1118 None,
1119 );
1120
1121 assert!(
1124 entity.gts_id.is_none(),
1125 "gts: prefix (without //) should not be stripped, resulting in invalid GTS ID"
1126 );
1127 }
1128
1129 #[test]
1130 fn test_gts_colon_prefix_not_valid_in_other_fields() {
1131 let content = json!({
1134 "id": "gts:gts.vendor.package.namespace.type.v1.0",
1135 "type": "gts:gts.vendor.package.namespace.type.v1~"
1136 });
1137
1138 let cfg = GtsConfig::default();
1139 let entity = GtsEntity::new(
1140 None,
1141 None,
1142 &content,
1143 Some(&cfg),
1144 None,
1145 false,
1146 String::new(),
1147 None,
1148 None,
1149 );
1150
1151 assert!(
1153 entity.gts_id.is_none(),
1154 "gts: prefix in 'id' field should not be valid"
1155 );
1156 }
1157
1158 #[test]
1159 fn test_gts_uri_prefix_only_stripped_from_dollar_id() {
1160 let content = json!({
1163 "id": "gts://gts.vendor.package.namespace.type.v1.0"
1164 });
1165
1166 let cfg = GtsConfig::default();
1167 let entity = GtsEntity::new(
1168 None,
1169 None,
1170 &content,
1171 Some(&cfg),
1172 None,
1173 false,
1174 String::new(),
1175 None,
1176 None,
1177 );
1178
1179 assert!(
1182 entity.gts_id.is_none(),
1183 "gts:// prefix in 'id' field (not $id) should not be stripped"
1184 );
1185 }
1186
1187 #[test]
1192 fn test_strict_schema_detection_requires_dollar_schema() {
1193 let content_with_schema = json!({
1195 "$schema": "http://json-schema.org/draft-07/schema#",
1196 "$id": "gts://gts.vendor.package.namespace.type.v1.0~",
1197 "type": "object"
1198 });
1199
1200 let entity_with_schema = GtsEntity::new(
1201 None,
1202 None,
1203 &content_with_schema,
1204 None,
1205 None,
1206 false,
1207 String::new(),
1208 None,
1209 None,
1210 );
1211 assert!(
1212 entity_with_schema.is_schema,
1213 "Document with $schema should be a schema"
1214 );
1215
1216 let content_without_schema = json!({
1218 "$id": "gts://gts.vendor.package.namespace.type.v1.0~",
1219 "type": "object"
1220 });
1221
1222 let entity_without_schema = GtsEntity::new(
1223 None,
1224 None,
1225 &content_without_schema,
1226 None,
1227 None,
1228 false,
1229 String::new(),
1230 None,
1231 None,
1232 );
1233 assert!(
1234 !entity_without_schema.is_schema,
1235 "Document without $schema should be an instance"
1236 );
1237 }
1238
1239 #[test]
1240 fn test_well_known_instance_with_chained_gts_id() {
1241 let content = json!({
1243 "id": "gts.x.core.events.type.v1~abc.app._.custom_event.v1.2"
1244 });
1245
1246 let cfg = GtsConfig::default();
1247 let entity = GtsEntity::new(
1248 None,
1249 None,
1250 &content,
1251 Some(&cfg),
1252 None,
1253 false,
1254 String::new(),
1255 None,
1256 None,
1257 );
1258
1259 assert!(!entity.is_schema, "Should be an instance");
1260 assert!(
1261 entity.gts_id.is_some(),
1262 "Well-known instance should have gts_id"
1263 );
1264 assert_eq!(
1265 entity.gts_id.as_ref().unwrap().id,
1266 "gts.x.core.events.type.v1~abc.app._.custom_event.v1.2"
1267 );
1268 assert_eq!(
1269 entity.instance_id,
1270 Some("gts.x.core.events.type.v1~abc.app._.custom_event.v1.2".to_owned())
1271 );
1272 assert_eq!(
1274 entity.schema_id,
1275 Some("gts.x.core.events.type.v1~".to_owned())
1276 );
1277 assert_eq!(entity.selected_entity_field, Some("id".to_owned()));
1278 assert_eq!(
1279 entity.selected_schema_id_field,
1280 Some("id".to_owned()),
1281 "selected_schema_id_field should be set when schema_id is derived from id field"
1282 );
1283 }
1284
1285 #[test]
1286 fn test_anonymous_instance_with_uuid_id() {
1287 let content = json!({
1289 "id": "7a1d2f34-5678-49ab-9012-abcdef123456",
1290 "type": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~"
1291 });
1292
1293 let cfg = GtsConfig::default();
1294 let entity = GtsEntity::new(
1295 None,
1296 None,
1297 &content,
1298 Some(&cfg),
1299 None,
1300 false,
1301 String::new(),
1302 None,
1303 None,
1304 );
1305
1306 assert!(!entity.is_schema, "Should be an instance");
1307 assert!(
1308 entity.gts_id.is_none(),
1309 "Anonymous instance should not have gts_id"
1310 );
1311 assert_eq!(
1312 entity.instance_id,
1313 Some("7a1d2f34-5678-49ab-9012-abcdef123456".to_owned())
1314 );
1315 assert_eq!(
1316 entity.schema_id,
1317 Some("gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~".to_owned())
1318 );
1319 assert_eq!(entity.selected_entity_field, Some("id".to_owned()));
1320 assert_eq!(entity.selected_schema_id_field, Some("type".to_owned()));
1321 }
1322
1323 #[test]
1324 fn test_effective_id_for_schema() {
1325 let content = json!({
1327 "$schema": "http://json-schema.org/draft-07/schema#",
1328 "$id": "gts://gts.vendor.package.namespace.type.v1.0~"
1329 });
1330
1331 let cfg = GtsConfig::default();
1332 let entity = GtsEntity::new(
1333 None,
1334 None,
1335 &content,
1336 Some(&cfg),
1337 None,
1338 false,
1339 String::new(),
1340 None,
1341 None,
1342 );
1343
1344 assert_eq!(
1345 entity.effective_id(),
1346 Some("gts.vendor.package.namespace.type.v1.0~".to_owned())
1347 );
1348 }
1349
1350 #[test]
1351 fn test_effective_id_for_well_known_instance() {
1352 let content = json!({
1354 "id": "gts.x.core.events.type.v1~abc.app._.custom_event.v1.2"
1355 });
1356
1357 let cfg = GtsConfig::default();
1358 let entity = GtsEntity::new(
1359 None,
1360 None,
1361 &content,
1362 Some(&cfg),
1363 None,
1364 false,
1365 String::new(),
1366 None,
1367 None,
1368 );
1369
1370 assert_eq!(
1371 entity.effective_id(),
1372 Some("gts.x.core.events.type.v1~abc.app._.custom_event.v1.2".to_owned())
1373 );
1374 }
1375
1376 #[test]
1377 fn test_effective_id_for_anonymous_instance() {
1378 let content = json!({
1380 "id": "7a1d2f34-5678-49ab-9012-abcdef123456",
1381 "type": "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~"
1382 });
1383
1384 let cfg = GtsConfig::default();
1385 let entity = GtsEntity::new(
1386 None,
1387 None,
1388 &content,
1389 Some(&cfg),
1390 None,
1391 false,
1392 String::new(),
1393 None,
1394 None,
1395 );
1396
1397 assert_eq!(
1398 entity.effective_id(),
1399 Some("7a1d2f34-5678-49ab-9012-abcdef123456".to_owned())
1400 );
1401 }
1402
1403 #[test]
1404 fn test_effective_id_returns_none_when_no_id() {
1405 let content = json!({
1407 "type": "gts.vendor.package.namespace.type.v1.0~",
1408 "name": "test"
1409 });
1410
1411 let cfg = GtsConfig::default();
1412 let entity = GtsEntity::new(
1413 None,
1414 None,
1415 &content,
1416 Some(&cfg),
1417 None,
1418 false,
1419 String::new(),
1420 None,
1421 None,
1422 );
1423
1424 assert_eq!(entity.effective_id(), None);
1425 }
1426
1427 #[test]
1428 fn test_well_known_instance_single_segment_no_schema_id() {
1429 let content = json!({
1432 "id": "gts.vendor.package.namespace.type.v1.0~a.b.c.d.v1"
1433 });
1434
1435 let cfg = GtsConfig::default();
1436 let entity = GtsEntity::new(
1437 None,
1438 None,
1439 &content,
1440 Some(&cfg),
1441 None,
1442 false,
1443 String::new(),
1444 None,
1445 None,
1446 );
1447
1448 assert!(!entity.is_schema);
1449 assert!(entity.gts_id.is_some());
1450 assert_eq!(
1451 entity.gts_id.as_ref().unwrap().id,
1452 "gts.vendor.package.namespace.type.v1.0~a.b.c.d.v1"
1453 );
1454 assert_eq!(
1456 entity.schema_id,
1457 Some("gts.vendor.package.namespace.type.v1.0~".to_owned())
1458 );
1459 }
1460
1461 #[test]
1462 fn test_extract_ref_strings_normalizes_gts_uri_prefix() {
1463 let content = json!({
1465 "$schema": "http://json-schema.org/draft-07/schema#",
1466 "$id": "gts://gts.vendor.package.namespace.type.v1.0~",
1467 "allOf": [
1468 {"$ref": "gts://gts.other.package.namespace.type.v2.0~"}
1469 ],
1470 "properties": {
1471 "nested": {
1472 "$ref": "gts://gts.third.package.namespace.type.v3.0~"
1473 }
1474 }
1475 });
1476
1477 let cfg = GtsConfig::default();
1478 let entity = GtsEntity::new(
1479 None,
1480 None,
1481 &content,
1482 Some(&cfg),
1483 None,
1484 false,
1485 String::new(),
1486 None,
1487 None,
1488 );
1489
1490 assert!(!entity.schema_refs.is_empty());
1492 assert!(
1493 entity
1494 .schema_refs
1495 .iter()
1496 .any(|r| r.id == "gts.other.package.namespace.type.v2.0~"),
1497 "Ref should be normalized (gts:// prefix stripped)"
1498 );
1499 assert!(
1500 entity
1501 .schema_refs
1502 .iter()
1503 .any(|r| r.id == "gts.third.package.namespace.type.v3.0~"),
1504 "Nested ref should be normalized"
1505 );
1506 assert!(
1508 !entity
1509 .schema_refs
1510 .iter()
1511 .any(|r| r.id.starts_with("gts://")),
1512 "No ref should contain gts:// prefix"
1513 );
1514 }
1515
1516 #[test]
1517 fn test_extract_ref_strings_preserves_local_refs() {
1518 let content = json!({
1520 "$schema": "http://json-schema.org/draft-07/schema#",
1521 "$id": "gts://gts.vendor.package.namespace.type.v1.0~",
1522 "$defs": {
1523 "Base": {"type": "object"}
1524 },
1525 "allOf": [
1526 {"$ref": "#/$defs/Base"}
1527 ]
1528 });
1529
1530 let cfg = GtsConfig::default();
1531 let entity = GtsEntity::new(
1532 None,
1533 None,
1534 &content,
1535 Some(&cfg),
1536 None,
1537 false,
1538 String::new(),
1539 None,
1540 None,
1541 );
1542
1543 assert!(
1545 entity.schema_refs.iter().any(|r| r.id == "#/$defs/Base"),
1546 "Local ref should be preserved"
1547 );
1548 }
1549
1550 #[test]
1551 fn test_instance_without_id_field_has_no_effective_id() {
1552 let content = json!({
1555 "type": "gts.vendor.package.namespace.type.v1.0~",
1556 "name": "test"
1557 });
1558
1559 let cfg = GtsConfig::default();
1560 let entity = GtsEntity::new(
1561 None,
1562 None,
1563 &content,
1564 Some(&cfg),
1565 None,
1566 false,
1567 String::new(),
1568 None,
1569 None,
1570 );
1571
1572 assert!(!entity.is_schema);
1573 assert_eq!(
1574 entity.effective_id(),
1575 None,
1576 "Instance without id should have no effective_id"
1577 );
1578 assert!(entity.instance_id.is_none());
1579 assert!(entity.gts_id.is_none());
1580 }
1581}