|
| 1 | +"""package.json validation models.""" |
| 2 | +import re |
| 3 | +from typing import List, Optional, Union |
| 4 | + |
| 5 | +from pydantic import AnyUrl, BaseModel, EmailStr, ValidationError, validator |
| 6 | + |
| 7 | + |
| 8 | +class PackageAuthor(BaseModel): |
| 9 | + """Package author model.""" |
| 10 | + |
| 11 | + name: Optional[str] |
| 12 | + email: Optional[EmailStr] |
| 13 | + url: Optional[AnyUrl] |
| 14 | + |
| 15 | + |
| 16 | +class PackageRepository(BaseModel): |
| 17 | + """Package repository model.""" |
| 18 | + |
| 19 | + type: str |
| 20 | + url: Optional[str] |
| 21 | + |
| 22 | + |
| 23 | +class PackageLicense(BaseModel): |
| 24 | + """Package license model.""" |
| 25 | + |
| 26 | + type: str |
| 27 | + url: Optional[str] |
| 28 | + |
| 29 | + |
| 30 | +class PackageJsonConfig(BaseModel): |
| 31 | + """Package.json config model.""" |
| 32 | + |
| 33 | + name: str |
| 34 | + version: str |
| 35 | + description: Optional[str] |
| 36 | + author: Optional[Union[str, PackageAuthor]] |
| 37 | + maintainers: Optional[List[Union[str, PackageAuthor]]] |
| 38 | + contributors: Optional[List[Union[str, PackageAuthor]]] |
| 39 | + license: Optional[Union[str, PackageLicense]] |
| 40 | + repository: Optional[PackageRepository] |
| 41 | + homepage: Optional[AnyUrl] |
| 42 | + keywords: Optional[List[str]] |
| 43 | + |
| 44 | + # convert package author to dict if it is a string |
| 45 | + @classmethod |
| 46 | + def convert_author(cls, author: str) -> PackageAuthor: |
| 47 | + """Convert author string to PackageAuthor model.""" |
| 48 | + # parse author string to "name <email> (url)" format with regex |
| 49 | + author_regex = r"^(.*?)\s*(?:<([^>]+)>)?\s*(?:\(([^)]+)\))?$" |
| 50 | + author_match = re.match(author_regex, author) |
| 51 | + if not author_match: |
| 52 | + raise ValidationError(f"Invalid author format: {author}") |
| 53 | + author_name = author_match[1] |
| 54 | + author_email = author_match[2] |
| 55 | + author_url = author_match[3] |
| 56 | + |
| 57 | + return PackageAuthor(name=author_name, email=author_email, url=author_url) |
| 58 | + |
| 59 | + @validator("name") |
| 60 | + def validate_name(cls, v): |
| 61 | + """Validate package name.""" |
| 62 | + pattern = r"^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$" |
| 63 | + if re.match(pattern, v) is None: |
| 64 | + raise ValidationError("Invalid name") |
| 65 | + |
| 66 | + return v |
| 67 | + |
| 68 | + @validator("version") |
| 69 | + def validate_version(cls, v): |
| 70 | + """Validate package version.""" |
| 71 | + # pattern for npm version |
| 72 | + pattern = r"^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" |
| 73 | + if re.match(pattern, v) is None: |
| 74 | + raise ValidationError("Invalid version") |
| 75 | + return v |
| 76 | + |
| 77 | + @validator("author") |
| 78 | + def validate_author(cls, v): |
| 79 | + """Validate package author.""" |
| 80 | + return cls.convert_author(v) if isinstance(v, str) else v |
| 81 | + |
| 82 | + @validator("maintainers", "contributors") |
| 83 | + def validate_people(cls, v): |
| 84 | + """Validate package maintainers and contributors.""" |
| 85 | + people = [] |
| 86 | + for p in v: |
| 87 | + if isinstance(p, str): |
| 88 | + people.append(cls.convert_author(p)) |
| 89 | + else: |
| 90 | + people.append(p) |
| 91 | + return people |
| 92 | + |
| 93 | + class Config: |
| 94 | + """Pydantic config.""" |
| 95 | + |
| 96 | + allow_population_by_field_name = True |
0 commit comments