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