2323 "visitor" : "visitor_id" ,
2424 "tag" : "tag_slug" ,
2525}
26+ REFERENCE_FIELDS = {
27+ "packet" : (("response_packet_id" , "response" ),),
28+ "response" : (("source_packet_id" , "packet" ),),
29+ "message" : (
30+ ("reply_to" , "message" ),
31+ ("response_message_id" , "message" ),
32+ ("related_packet" , "packet" ),
33+ ("related_response" , "response" ),
34+ ),
35+ "notification" : (("message_id" , "message" ),),
36+ }
2637
2738
28- def load_json (path : Path ):
39+ def load_json (path : Path ) -> dict :
2940 with path .open (encoding = "utf-8" ) as handle :
3041 return json .load (handle )
3142
@@ -75,7 +86,9 @@ def validate_path_fields(record: dict) -> list[str]:
7586 return errors
7687
7788
78- def validate (path : Path , enforce_filename : bool = False ) -> list [str ]:
89+ def validate_document (
90+ path : Path , enforce_filename : bool
91+ ) -> tuple [dict | None , list [str ]]:
7992 try :
8093 record = load_json (path )
8194 schema_path = schema_for (record )
@@ -96,9 +109,33 @@ def validate(path: Path, enforce_filename: bool = False) -> list[str]:
96109 for message in validate_filename (record , path , enforce_filename )
97110 )
98111 messages .extend (f"{ path } : { message } " for message in validate_path_fields (record ))
99- return messages
112+ return record , messages
100113 except (OSError , ValueError , json .JSONDecodeError ) as error :
101- return [f"{ path } : { error } " ]
114+ return None , [f"{ path } : { error } " ]
115+
116+
117+ def validate_references (records : list [tuple [Path , dict ]]) -> list [str ]:
118+ index : dict [str , set [str ]] = {}
119+ for _ , record in records :
120+ record_type = record .get ("record_type" )
121+ identifier_field = ID_FIELDS .get (record_type )
122+ identifier = record .get (identifier_field ) if identifier_field else None
123+ if isinstance (record_type , str ) and isinstance (identifier , str ):
124+ index .setdefault (record_type , set ()).add (identifier )
125+
126+ errors : list [str ] = []
127+ for path , record in records :
128+ record_type = record .get ("record_type" )
129+ for field , target_type in REFERENCE_FIELDS .get (record_type , ()):
130+ target_id = record .get (field )
131+ if target_id is None :
132+ continue
133+ if not isinstance (target_id , str ) or target_id not in index .get (target_type , set ()):
134+ errors .append (
135+ f"{ path } : { field } : no { target_type } record with ID { target_id !r} "
136+ "in the validation set"
137+ )
138+ return errors
102139
103140
104141def main () -> int :
@@ -107,6 +144,7 @@ def main() -> int:
107144 parser .add_argument ("--fixtures" , action = "store_true" )
108145 parser .add_argument ("--examples" , action = "store_true" )
109146 parser .add_argument ("--enforce-filename" , action = "store_true" )
147+ parser .add_argument ("--check-references" , action = "store_true" )
110148 args = parser .parse_args ()
111149
112150 paths = list (args .paths )
@@ -117,11 +155,16 @@ def main() -> int:
117155 if not paths :
118156 parser .error ("supply JSON paths, --fixtures, or --examples" )
119157
120- errors = [
121- item
122- for path in paths
123- for item in validate (path , enforce_filename = args .enforce_filename )
158+ documents = [
159+ (path , * validate_document (path , args .enforce_filename )) for path in paths
124160 ]
161+ errors = [item for _ , _ , messages in documents for item in messages ]
162+ if args .check_references :
163+ valid_records = [
164+ (path , record ) for path , record , _ in documents if record is not None
165+ ]
166+ errors .extend (validate_references (valid_records ))
167+
125168 print ("\n " .join (errors ) if errors else f"validated { len (paths )} record(s)" )
126169 return 1 if errors else 0
127170
0 commit comments