77from pathlib import Path
88from typing import Any
99
10- from jsonschema .validators import validator_for
11-
1210from .entities import GtsEntity , GtsFile
13- from .gts import GtsID
11+ from .gts import GTS_PREFIX , GTS_URI_PREFIX , GtsID
1412from .store import GtsStore
1513
14+ _X_GTS_REF_KEYWORD = "x-gts-ref"
15+
1616
1717@dataclass
1818class GtsJsonValidationIssue :
@@ -67,8 +67,12 @@ def __init__(self, path: str, cfg: Any) -> None:
6767 def validate (self ) -> GtsJsonValidationResult :
6868 for file_path in self ._json_files ():
6969 self ._read_file (file_path )
70- self ._validate_json_schemas ()
70+ self ._check_schema_field_type ()
7171 store = self ._register_gts_entities ()
72+ schemas_count , instances_count = self ._count_schema_instance ()
73+ self .result .schemas = schemas_count
74+ self .result .instances = instances_count
75+ self .result .gts_entities = schemas_count + instances_count
7276 self ._validate_schemas (store )
7377 self ._validate_instances (store )
7478 return self .result
@@ -87,22 +91,35 @@ def _json_files(self) -> list[Path]:
8791 return []
8892
8993 files : list [Path ] = []
90- for root , dirs , names in os .walk (resolved , followlinks = True ):
91- dirs [:] = [
92- name for name in dirs if name not in {"node_modules" , "dist" , "build" }
93- ]
94- files .extend (
95- Path (root , name ).resolve (strict = False )
96- for name in names
97- if Path (name ).suffix .lower () == ".json"
98- )
99- return sorted (set (files ))
94+ seen : set [Path ] = set ()
95+ for root , _dirs , names in os .walk (resolved , followlinks = True ):
96+ for name in names :
97+ if Path (name ).suffix .lower () == ".json" :
98+ rp = Path (root , name ).resolve (strict = False )
99+ if rp not in seen :
100+ seen .add (rp )
101+ files .append (rp )
102+ return sorted (files )
103+
104+ @staticmethod
105+ def _is_gts_marker (text : str ) -> bool :
106+ return (
107+ GTS_PREFIX in text or GTS_URI_PREFIX in text or _X_GTS_REF_KEYWORD in text
108+ )
100109
101110 def _read_file (self , file_path : Path ) -> None :
111+ try :
112+ content_str = file_path .read_text (encoding = "utf-8" )
113+ except Exception as error : # noqa: BLE001 - report this document and continue
114+ self ._issue (file_path , "json" , str (error ))
115+ return
116+
117+ if not self ._is_gts_marker (content_str ):
118+ return
102119 self .result .files += 1
120+
103121 try :
104- with file_path .open (encoding = "utf-8" ) as source :
105- content = json .load (source )
122+ content = json .loads (content_str )
106123 except Exception as error : # noqa: BLE001 - report this document and continue
107124 self ._issue (file_path , "json" , str (error ))
108125 return
@@ -120,15 +137,14 @@ def _read_file(self, file_path: Path) -> None:
120137 )
121138 )
122139
123- def _validate_json_schemas (self ) -> None :
140+ def _check_schema_field_type (self ) -> None :
124141 for entity in self .entities :
125142 content = entity .content
126- if not isinstance (content , dict ) or "$schema" not in content :
143+ if not isinstance (content , dict ):
127144 continue
128- try :
129- validator_for (content ).check_schema (content )
130- except Exception as error : # noqa: BLE001 - report this document and continue
131- self ._issue (entity , "json-schema" , str (error ))
145+ schema_val = content .get ("$schema" )
146+ if schema_val is not None and not isinstance (schema_val , str ):
147+ self ._issue (entity , "json-schema" , "$schema must be a string" )
132148
133149 def _register_gts_entities (self ) -> GtsStore :
134150 store = GtsStore (reader = None ) # type: ignore[arg-type]
@@ -138,9 +154,12 @@ def _register_gts_entities(self) -> GtsStore:
138154 continue
139155 key = self ._registry_key (entity )
140156 if key is None :
141- self ._issue (
142- entity , "registry" , "GTS-related document has no registrable GTS ID"
143- )
157+ if entity .is_schema :
158+ self ._issue (
159+ entity ,
160+ "registry" ,
161+ "GTS schema has a malformed or non-GTS $id" ,
162+ )
144163 continue
145164 if not entity .is_schema and entity .selected_entity_field is None :
146165 raw_id = entity .raw_id
@@ -152,17 +171,28 @@ def _register_gts_entities(self) -> GtsStore:
152171 continue
153172 keys .add (key )
154173 store .register (entity )
155- self .result .gts_entities += 1
174+ return store
175+
176+ def _count_schema_instance (self ) -> tuple [int , int ]:
177+ schemas = 0
178+ instances = 0
179+ for entity in self .entities :
180+ if self ._registry_key (entity ) is None :
181+ continue
156182 if entity .is_schema :
157- self . result . schemas += 1
183+ schemas += 1
158184 else :
159- self . result . instances += 1
160- return store
185+ instances += 1
186+ return schemas , instances
161187
162188 @staticmethod
163189 def _is_gts_related (value : Any ) -> bool :
164190 if isinstance (value , str ):
165- return "gts." in value
191+ return (
192+ GTS_PREFIX in value
193+ or GTS_URI_PREFIX in value
194+ or _X_GTS_REF_KEYWORD in value
195+ )
166196 if isinstance (value , dict ):
167197 return any (
168198 GtsJsonValidator ._is_gts_related (item ) for item in value .values ()
@@ -187,43 +217,58 @@ def _registry_key(entity: GtsEntity) -> str | None:
187217 return None
188218
189219 def _validate_schemas (self , store : GtsStore ) -> None :
190- schemas = sorted (
191- (
192- entity
193- for entity in self .entities
194- if entity .is_schema
195- and entity .gts_id
196- and store .get (entity .gts_id .id ) is entity
197- ),
198- key = self ._schema_depth ,
199- )
200- for stage , depth in (("base-type" , 1 ), ("derived-type" , None )):
201- for entity in schemas :
202- if (depth == 1 ) != (self ._schema_depth (entity ) == 1 ):
203- continue
204- gts_id = entity .gts_id
205- if not gts_id :
206- continue
207- try :
208- store .validate_schema (gts_id .id )
209- except Exception as error : # noqa: BLE001 - report this document and continue
210- self ._issue (entity , stage , str (error ))
220+ pending : list [tuple [int , str , str , int | None , GtsEntity ]] = []
221+ for entity in self .entities :
222+ if not entity .is_schema or not entity .gts_id :
223+ continue
224+ gid = entity .gts_id
225+ if store .get (gid .id ) is not entity :
226+ continue
227+ depth = len (gid .gts_id_segments )
228+ file = entity .file .path if entity .file else entity .label
229+ pending .append ((depth , gid .id , file , entity .list_sequence , entity ))
230+ pending .sort (key = lambda t : (t [0 ], t [1 ], t [2 ], t [3 ] if t [3 ] is not None else - 1 ))
231+
232+ for depth , _gts_id , _file , _idx , entity in pending :
233+ stage = "base-type" if depth <= 1 else "derived-type"
234+ try :
235+ store .validate_schema (entity .gts_id .id ) # type: ignore[union-attr]
236+ except Exception as error : # noqa: BLE001 - report this document and continue
237+ self ._issue (entity , stage , str (error ))
211238
212239 @staticmethod
213240 def _schema_depth (entity : GtsEntity ) -> int :
214241 return len (entity .gts_id .gts_id_segments ) if entity .gts_id else 0
215242
243+ @staticmethod
244+ def _entity_depth (entity : GtsEntity ) -> int :
245+ if entity .gts_id :
246+ return len (entity .gts_id .gts_id_segments )
247+ if entity .type_id and GtsID .is_valid (entity .type_id ):
248+ return len (GtsID (entity .type_id ).gts_id_segments )
249+ return 0
250+
216251 def _validate_instances (self , store : GtsStore ) -> None :
252+ pending : list [tuple [int , str , str , int | None , str ]] = []
217253 for entity in self .entities :
254+ if entity .is_schema :
255+ continue
218256 key = self ._registry_key (entity )
219- if (
220- entity .is_schema
221- or key is None
222- or not self ._is_gts_related (entity .content )
223- ):
257+ if key is None :
258+ continue
259+ depth = self ._entity_depth (entity )
260+ gts_id_str = entity .gts_id .id if entity .gts_id else ""
261+ file = entity .file .path if entity .file else entity .label
262+ pending .append ((depth , gts_id_str , file , entity .list_sequence , key ))
263+ pending .sort (key = lambda t : (t [0 ], t [1 ], t [2 ], t [3 ] if t [3 ] is not None else - 1 ))
264+
265+ for _depth , _gts_id , _file , _idx , registry_key in pending :
266+ # Find the entity for error reporting
267+ entity = store .get (registry_key )
268+ if entity is None :
224269 continue
225270 try :
226- store .validate_instance (key )
271+ store .validate_instance (registry_key )
227272 except Exception as error : # noqa: BLE001 - report this document and continue
228273 self ._issue (entity , "instance" , str (error ))
229274
0 commit comments