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

1"""package.json validation models.""" 

2 

3import re 

4from typing import Annotated 

5 

6from pydantic import BaseModel, EmailStr, Field, field_validator 

7 

8from somesy.core.types import HttpUrlStr 

9 

10 

11class PackageAuthor(BaseModel): 

12 """Package author model.""" 

13 

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 

19 

20 

21class PackageRepository(BaseModel): 

22 """Package repository model.""" 

23 

24 type: Annotated[str | None, Field(description="Repository type")] = None 

25 url: Annotated[str, Field(description="Repository url")] 

26 

27 

28class PackageLicense(BaseModel): 

29 """Package license model.""" 

30 

31 type: Annotated[str | None, Field(description="License type")] = None 

32 url: Annotated[str, Field(description="License url")] 

33 

34 

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-]+)*))?$" 

38 

39 

40class PackageJsonConfig(BaseModel): 

41 """Package.json config model.""" 

42 

43 model_config = {"populate_by_name": True} 

44 

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 

69 

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] 

81 

82 return PackageAuthor.model_validate( 

83 {"name": author_name, "email": author_email, "url": author_url} 

84 ) 

85 

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") 

92 

93 return v 

94 

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 

102 

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 

108 

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