@@ -642,6 +642,65 @@ def __call__(self, components: SchemaPath) -> Iterator[ValidationError]:
642642 yield from self .schemas_validator (schemas )
643643
644644
645+ class TagsValidator (KeywordValidator ):
646+ def __call__ (self , tags : SchemaPath ) -> Iterator [ValidationError ]:
647+ seen : set [str ] = set ()
648+ for tag in tags :
649+ tag_name = (tag / "name" ).read_str ()
650+ if tag_name in seen :
651+ yield OpenAPIValidationError (
652+ f"Duplicate tag name '{ tag_name } '"
653+ )
654+ seen .add (tag_name )
655+
656+
657+ class OpenAPIV32TagsValidator (TagsValidator ):
658+ def __call__ (self , tags : SchemaPath ) -> Iterator [ValidationError ]:
659+ yield from super ().__call__ (tags )
660+
661+ seen : set [str ] = set ()
662+ parent_by_tag_name : dict [str , str | None ] = {}
663+ for tag in tags :
664+ tag_name = (tag / "name" ).read_str ()
665+ seen .add (tag_name )
666+
667+ if "parent" in tag :
668+ parent_by_tag_name [tag_name ] = (tag / "parent" ).read_str ()
669+ else :
670+ parent_by_tag_name [tag_name ] = None
671+
672+ for tag_name , parent in parent_by_tag_name .items ():
673+ if parent is not None and parent not in seen :
674+ yield OpenAPIValidationError (
675+ f"Tag '{ tag_name } ' references unknown parent tag '{ parent } '"
676+ )
677+
678+ reported_cycles : set [str ] = set ()
679+ for start_tag_name in parent_by_tag_name :
680+ tag_name = start_tag_name
681+ trail : list [str ] = []
682+ trail_pos : dict [str , int ] = {}
683+
684+ while True :
685+ if tag_name in trail_pos :
686+ cycle = trail [trail_pos [tag_name ] :] + [tag_name ]
687+ cycle_str = " -> " .join (cycle )
688+ if cycle_str not in reported_cycles :
689+ reported_cycles .add (cycle_str )
690+ yield OpenAPIValidationError (
691+ f"Circular tag hierarchy detected: { cycle_str } "
692+ )
693+ break
694+
695+ trail_pos [tag_name ] = len (trail )
696+ trail .append (tag_name )
697+
698+ parent = parent_by_tag_name .get (tag_name )
699+ if parent is None or parent not in seen :
700+ break
701+ tag_name = parent
702+
703+
645704class RootValidator (KeywordValidator ):
646705 @property
647706 def paths_validator (self ) -> PathsValidator :
@@ -652,6 +711,11 @@ def components_validator(self) -> ComponentsValidator:
652711 return cast (ComponentsValidator , self .registry ["components" ])
653712
654713 def __call__ (self , spec : SchemaPath ) -> Iterator [ValidationError ]:
714+ if "tags" in spec and "tags" in self .registry .keyword_validators :
715+ tags = spec / "tags"
716+ tags_validator = cast (Any , self .registry ["tags" ])
717+ yield from tags_validator (tags )
718+
655719 if "paths" in spec :
656720 paths = spec / "paths"
657721 yield from self .paths_validator (paths )
0 commit comments