33import sys
44import json
55from pathlib import Path
6- from typing import List
6+ from typing import Dict , List , Optional
77from frictionless import Schema
8- from helpers import PROFILE_PATH , TABLE_SCHEMA_PATHS
98
9+ THIS_SCRIPT_PATH = Path (__file__ ).parent
10+ REPOSITORY_ROOT_PATH = THIS_SCRIPT_PATH / ".."
11+ PROFILE_PATH = REPOSITORY_ROOT_PATH / "geolocator-dp-profile.json"
12+ TABLE_SCHEMA_PATHS = [
13+ REPOSITORY_ROOT_PATH / "observations-table-schema.json" ,
14+ REPOSITORY_ROOT_PATH / "tags-table-schema.json" ,
15+ REPOSITORY_ROOT_PATH / "measurements-table-schema.json" ,
16+ REPOSITORY_ROOT_PATH / "staps-table-schema.json" ,
17+ REPOSITORY_ROOT_PATH / "twilights-table-schema.json" ,
18+ REPOSITORY_ROOT_PATH / "paths-table-schema.json" ,
19+ REPOSITORY_ROOT_PATH / "edges-table-schema.json" ,
20+ REPOSITORY_ROOT_PATH / "pressurepaths-table-schema.json" ,
21+ ]
1022
11- def validate_json (filepath : Path ) -> bool :
23+ EXPECTED_SCHEMA_RESOURCES = {
24+ "tags" ,
25+ "observations" ,
26+ "measurements" ,
27+ "staps" ,
28+ "twilights" ,
29+ "paths" ,
30+ "edges" ,
31+ "pressurepaths" ,
32+ }
33+
34+ def load_json (filepath : Path ) -> Optional [dict ]:
1235 with open (filepath ) as file :
1336 try :
14- json .load (file )
37+ return json .load (file )
1538 except json .decoder .JSONDecodeError :
16- return False
17- else :
18- return True
39+ return None
1940
2041
2142def validate_schema (file_path : Path ) -> bool :
@@ -32,12 +53,124 @@ def get_schema_metadata_error_messages(file_path: Path) -> List[str]:
3253 return [err .message for err in report .errors ]
3354
3455
56+ def _listify_fields (fields ) -> List [str ]:
57+ if isinstance (fields , str ):
58+ return [fields ]
59+ if isinstance (fields , list ):
60+ return [field for field in fields if isinstance (field , str )]
61+ return []
62+
63+
64+ def _get_schema_fields (descriptor : dict ) -> set :
65+ fields = descriptor .get ("fields" , [])
66+ return {
67+ field .get ("name" )
68+ for field in fields
69+ if isinstance (field , dict ) and isinstance (field .get ("name" ), str )
70+ }
71+
72+
73+ def check_schema_coherence (schema_descriptors : Dict [Path , dict ]) -> bool :
74+ encountered_errors = False
75+ resource_to_paths : Dict [str , List [Path ]] = {}
76+
77+ # 1) filename <-> schema name consistency
78+ for schema_path , descriptor in schema_descriptors .items ():
79+ schema_name = descriptor .get ("name" )
80+ expected_name = schema_path .name .replace ("-table-schema.json" , "" )
81+
82+ if not isinstance (schema_name , str ):
83+ print (f"✕ { schema_path .name } : missing or non-string `name`" )
84+ encountered_errors = True
85+ continue
86+
87+ resource_to_paths .setdefault (schema_name , []).append (schema_path )
88+
89+ if schema_name != expected_name :
90+ print (
91+ f"✕ { schema_path .name } : schema `name` is `{ schema_name } ` but expected `{ expected_name } `"
92+ )
93+ encountered_errors = True
94+
95+ # 2) expected resources exist exactly once
96+ observed_resources = set (resource_to_paths .keys ())
97+ missing_resources = sorted (EXPECTED_SCHEMA_RESOURCES - observed_resources )
98+ unexpected_resources = sorted (observed_resources - EXPECTED_SCHEMA_RESOURCES )
99+
100+ for resource in missing_resources :
101+ print (f"✕ schema resources: missing expected resource `{ resource } `" )
102+ encountered_errors = True
103+
104+ for resource in unexpected_resources :
105+ print (f"✕ schema resources: unexpected resource `{ resource } `" )
106+ encountered_errors = True
107+
108+ for resource , paths in sorted (resource_to_paths .items ()):
109+ if len (paths ) > 1 :
110+ path_list = ", " .join (path .name for path in paths )
111+ print (
112+ f"✕ schema resources: resource `{ resource } ` is defined more than once ({ path_list } )"
113+ )
114+ encountered_errors = True
115+
116+ # 3) foreign keys reference existing resources and target fields
117+ fields_by_resource : Dict [str , set ] = {}
118+ for resource , paths in resource_to_paths .items ():
119+ if len (paths ) == 1 :
120+ fields_by_resource [resource ] = _get_schema_fields (schema_descriptors [paths [0 ]])
121+
122+ for schema_path , descriptor in schema_descriptors .items ():
123+ foreign_keys = descriptor .get ("foreignKeys" , [])
124+ if not isinstance (foreign_keys , list ):
125+ continue
126+
127+ for foreign_key in foreign_keys :
128+ if not isinstance (foreign_key , dict ):
129+ continue
130+
131+ reference = foreign_key .get ("reference" , {})
132+ if not isinstance (reference , dict ):
133+ print (f"✕ { schema_path .name } : malformed foreign key reference" )
134+ encountered_errors = True
135+ continue
136+
137+ target_resource = reference .get ("resource" )
138+ target_fields = _listify_fields (reference .get ("fields" ))
139+
140+ if not isinstance (target_resource , str ):
141+ print (f"✕ { schema_path .name } : foreign key reference missing `resource`" )
142+ encountered_errors = True
143+ continue
144+
145+ if target_resource not in resource_to_paths :
146+ print (
147+ f"✕ { schema_path .name } : foreign key references unknown resource `{ target_resource } `"
148+ )
149+ encountered_errors = True
150+ continue
151+
152+ target_resource_fields = fields_by_resource .get (target_resource , set ())
153+ missing_target_fields = [
154+ field for field in target_fields if field not in target_resource_fields
155+ ]
156+
157+ if missing_target_fields :
158+ fields_str = ", " .join (missing_target_fields )
159+ print (
160+ f"✕ { schema_path .name } : foreign key references missing field(s) in `{ target_resource } `: { fields_str } "
161+ )
162+ encountered_errors = True
163+
164+ return not encountered_errors
165+
166+
35167if __name__ == "__main__" :
36168 encountered_errors = False
169+ schema_descriptors : Dict [Path , dict ] = {}
37170
38171 print (PROFILE_PATH .name )
39- result = validate_json (PROFILE_PATH )
40- if result is True :
172+ profile_json = load_json (PROFILE_PATH )
173+ if profile_json is not None :
41174 print ("✔︎ valid JSON" )
42175 else :
43176 print ("✕ valid JSON" )
@@ -46,7 +179,9 @@ def get_schema_metadata_error_messages(file_path: Path) -> List[str]:
46179 for table_schema in TABLE_SCHEMA_PATHS :
47180 print (f"\n { table_schema .name } " )
48181
49- if validate_json (table_schema ):
182+ schema_json = load_json (table_schema )
183+ if schema_json is not None :
184+ schema_descriptors [table_schema ] = schema_json
50185 print ("✔︎ valid JSON" )
51186 if validate_schema (table_schema ):
52187 print ("✔︎ valid Table Schema" )
@@ -59,6 +194,12 @@ def get_schema_metadata_error_messages(file_path: Path) -> List[str]:
59194 print ("✕ valid JSON" )
60195 encountered_errors = True
61196
197+ print ("\n Schema coherence" )
198+ if check_schema_coherence (schema_descriptors ):
199+ print ("✔︎ schema coherence checks passed" )
200+ else :
201+ encountered_errors = True
202+
62203 if encountered_errors :
63204 print ("Errors were encountered" )
64205 sys .exit (1 )
0 commit comments