2626Validate each type according to the PURL spec type definitions
2727"""
2828
29+ from enum import Enum
30+ from dataclasses import dataclass
31+
32+
33+ class ValidationSeverity (Enum ):
34+ ERROR = "error"
35+ WARNING = "warning"
36+ INFO = "info"
37+
38+
39+ @dataclass
40+ class ValidationMessage :
41+ severity : ValidationSeverity
42+ message : str
43+
2944
3045class TypeValidator :
3146 @classmethod
@@ -34,26 +49,45 @@ def validate(cls, purl, strict=False):
3449 purl = cls .normalize (purl )
3550
3651 if cls .namespace_requirement == "prohibited" and purl .namespace :
37- yield f"Namespace is prohibited for purl type: { cls .type !r} "
52+ yield ValidationMessage (
53+ severity = ValidationSeverity .ERROR ,
54+ message = f"Namespace is prohibited for purl type: { cls .type !r} " ,
55+ )
3856
3957 elif cls .namespace_requirement == "required" and not purl .namespace :
40- yield f"Namespace is required for purl type: { cls .type !r} "
58+ yield ValidationMessage (
59+ severity = ValidationSeverity .ERROR ,
60+ message = f"Namespace is required for purl type: { cls .type !r} " ,
61+ )
4162
4263 if purl .type == "cpan" :
4364 if purl .namespace and purl .namespace != purl .namespace .upper ():
44- yield f"Namespace must be uppercase for purl type: { cls .type !r} "
65+ yield ValidationMessage (
66+ severity = ValidationSeverity .WARNING ,
67+ message = f"Namespace must be uppercase for purl type: { cls .type !r} " ,
68+ )
69+ # TODO: Check pending CPAN PR and decide if we want to upgrade the type definition schema
4570 elif (
4671 not cls .namespace_case_sensitive
4772 and purl .namespace
4873 and purl .namespace .lower () != purl .namespace
4974 ):
50- yield f"Namespace is not lowercased for purl type: { cls .type !r} "
75+ yield ValidationMessage (
76+ severity = ValidationSeverity .WARNING ,
77+ message = f"Namespace is not lowercased for purl type: { cls .type !r} " ,
78+ )
5179
5280 if not cls .name_case_sensitive and purl .name and purl .name .lower () != purl .name :
53- yield f"Name is not lowercased for purl type: { cls .type !r} "
81+ yield ValidationMessage (
82+ severity = ValidationSeverity .WARNING ,
83+ message = f"Name is not lowercased for purl type: { cls .type !r} " ,
84+ )
5485
5586 if not cls .version_case_sensitive and purl .version and purl .version .lower () != purl .version :
56- yield f"Version is not lowercased for purl type: { cls .type !r} "
87+ yield ValidationMessage (
88+ severity = ValidationSeverity .WARNING ,
89+ message = f"Version is not lowercased for purl type: { cls .type !r} " ,
90+ )
5791
5892 messages = cls .validate_type (purl , strict = strict )
5993 if messages :
@@ -87,7 +121,8 @@ def normalize(cls, purl):
87121
88122 @classmethod
89123 def validate_type (cls , purl , strict = False ):
90- return
124+ if strict :
125+ yield from cls .validate_qualifiers (purl )
91126
92127 @classmethod
93128 def validate_qualifiers (cls , purl ):
@@ -100,9 +135,12 @@ def validate_qualifiers(cls, purl):
100135 disallowed = purl_qualifiers_keys - allowed_qualifiers_set
101136
102137 if disallowed :
103- yield (
104- f"Invalid qualifiers found: { ', ' .join (sorted (disallowed ))} . "
105- f"Allowed qualifiers are: { ', ' .join (sorted (allowed_qualifiers_set ))} "
138+ yield ValidationMessage (
139+ severity = ValidationSeverity .INFO ,
140+ message = (
141+ f"Invalid qualifiers found: { ', ' .join (sorted (disallowed ))} . "
142+ f"Allowed qualifiers are: { ', ' .join (sorted (allowed_qualifiers_set ))} "
143+ ),
106144 )
107145
108146
@@ -248,9 +286,15 @@ class CpanTypeValidator(TypeValidator):
248286 @classmethod
249287 def validate_type (cls , purl , strict = False ):
250288 if purl .namespace and "::" in purl .name :
251- yield f"Name must not contain '::' when Namespace is absent for purl type: { cls .type !r} "
289+ yield ValidationMessage (
290+ severity = ValidationSeverity .ERROR ,
291+ message = f"Name must not contain '::' when Namespace is present for purl type: { cls .type !r} " ,
292+ )
252293 if not purl .namespace and "-" in purl .name :
253- yield f"Name must not contain '-' when Namespace is absent for purl type: { cls .type !r} "
294+ yield ValidationMessage (
295+ severity = ValidationSeverity .ERROR ,
296+ message = f"Name must not contain '-' when Namespace is absent for purl type: { cls .type !r} " ,
297+ )
254298 messages = super ().validate_type (purl , strict )
255299 if messages :
256300 yield from messages
@@ -370,7 +414,10 @@ class HackageTypeValidator(TypeValidator):
370414 @classmethod
371415 def validate_type (cls , purl , strict = False ):
372416 if "_" in purl .name :
373- yield f"Name contains underscores but should be kebab-case for purl type: { cls .type !r} "
417+ yield ValidationMessage (
418+ severity = ValidationSeverity .WARNING ,
419+ message = f"Name cannot contain underscores for purl type:{ cls .type !r} " ,
420+ )
374421 messages = super ().validate_type (purl , strict )
375422 if messages :
376423 yield from messages
@@ -503,10 +550,17 @@ class PubTypeValidator(TypeValidator):
503550
504551 @classmethod
505552 def validate_type (cls , purl , strict = False ):
506- if any (not (c .islower () or c .isdigit () or c == "_" ) for c in purl .name ):
507- yield f"Name contains invalid characters but should only contain lowercase letters, digits, or underscores for purl type: { cls .type !r} "
553+ if not all (c .isalnum () or c == "_" for c in purl .name ):
554+ yield ValidationMessage (
555+ severity = ValidationSeverity .ERROR ,
556+ message = f"Name contains invalid characters but should only contain letters, digits, or underscores for purl type: { cls .type !r} " ,
557+ )
558+
508559 if " " in purl .name :
509- yield f"Name contains spaces but should use underscores instead for purl type: { cls .type !r} "
560+ yield ValidationMessage (
561+ severity = ValidationSeverity .ERROR ,
562+ message = f"Name contains spaces but should use underscores instead for purl type: { cls .type !r} " ,
563+ )
510564 messages = super ().validate_type (purl , strict )
511565 if messages :
512566 yield from messages
@@ -528,7 +582,10 @@ class PypiTypeValidator(TypeValidator):
528582 @classmethod
529583 def validate_type (cls , purl , strict = False ):
530584 if "_" in purl .name :
531- yield f"Name cannot contain `_` for purl type:{ cls .type !r} "
585+ yield ValidationMessage (
586+ severity = ValidationSeverity .WARNING ,
587+ message = f"Name cannot contain underscores for purl type:{ cls .type !r} " ,
588+ )
532589 messages = super ().validate_type (purl , strict )
533590 if messages :
534591 yield from messages
0 commit comments