Coverage for src/somesy/package_json/models.py: 88%
64 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-04-30 09:42 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2024-04-30 09:42 +0000
1"""package.json validation models."""
2import re
3from typing import List, Optional, Union
5from pydantic import BaseModel, EmailStr, Field, field_validator
6from typing_extensions import Annotated
8from somesy.core.types import HttpUrlStr
11class PackageAuthor(BaseModel):
12 """Package author model."""
14 name: Annotated[Optional[str], Field(description="Author name")]
15 email: Annotated[Optional[EmailStr], Field(description="Author email")] = None
16 url: Annotated[
17 Optional[HttpUrlStr], Field(description="Author website or orcid page")
18 ] = None
21class PackageRepository(BaseModel):
22 """Package repository model."""
24 type: Annotated[Optional[str], Field(description="Repository type")] = None
25 url: Annotated[str, Field(description="Repository url")]
28class PackageLicense(BaseModel):
29 """Package license model."""
31 type: Annotated[Optional[str], Field(description="License type")] = None
32 url: Annotated[str, Field(description="License url")]
35NPM_PKG_AUTHOR = r"^(.*?)\s*(?:<([^>]+)>)?\s*(?:\(([^)]+)\))?$"
36NPM_PKG_NAME = r"^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$"
37NPM_PKG_VERSION = 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-]+)*))?$" # noqa: E501
40class PackageJsonConfig(BaseModel):
41 """Package.json config model."""
43 model_config = dict(populate_by_name=True)
45 name: Annotated[str, Field(description="Package name")]
46 version: Annotated[str, Field(description="Package version")]
47 description: Annotated[
48 Optional[str], Field(description="Package description")
49 ] = None
50 author: Annotated[
51 Optional[Union[str, PackageAuthor]], Field(description="Package author")
52 ] = None
53 maintainers: Annotated[
54 Optional[List[Union[str, PackageAuthor]]],
55 Field(description="Package maintainers"),
56 ] = None
57 contributors: Annotated[
58 Optional[List[Union[str, PackageAuthor]]],
59 Field(description="Package contributors"),
60 ] = None
61 license: Annotated[
62 Optional[Union[str, PackageLicense]], Field(description="Package license")
63 ] = None
64 repository: Annotated[
65 Optional[Union[PackageRepository, str]], Field(description="Package repository")
66 ] = None
67 homepage: Annotated[
68 Optional[HttpUrlStr], Field(description="Package homepage")
69 ] = None
70 keywords: Annotated[
71 Optional[List[str]], Field(description="Keywords that describe the package")
72 ] = None
74 # convert package author to dict if it is a string
75 @classmethod
76 def convert_author(cls, author: str) -> PackageAuthor:
77 """Convert author string to PackageAuthor model."""
78 # parse author string to "name <email> (url)" format with regex
79 author_match = re.match(NPM_PKG_AUTHOR, author)
80 if not author_match:
81 raise ValueError(f"Invalid author format: {author}")
82 author_name = author_match[1]
83 author_email = author_match[2]
84 author_url = author_match[3]
86 return PackageAuthor(name=author_name, email=author_email, url=author_url)
88 @field_validator("name")
89 @classmethod
90 def validate_name(cls, v):
91 """Validate package name."""
92 if re.match(NPM_PKG_NAME, v) is None:
93 raise ValueError("Invalid name")
95 return v
97 @field_validator("version")
98 @classmethod
99 def validate_version(cls, v):
100 """Validate package version."""
101 if re.match(NPM_PKG_VERSION, v) is None:
102 raise ValueError("Invalid version")
103 return v
105 @field_validator("author")
106 @classmethod
107 def validate_author(cls, v):
108 """Validate package author."""
109 return cls.convert_author(v) if isinstance(v, str) else v
111 @field_validator("maintainers", "contributors")
112 @classmethod
113 def validate_people(cls, v):
114 """Validate package maintainers and contributors."""
115 people = []
116 for p in v:
117 if isinstance(p, str):
118 people.append(cls.convert_author(p))
119 else:
120 people.append(p)
121 return people