1
+ import json
2
+ from packagedcode import models
3
+ from packageurl import PackageURL
4
+ import yaml
5
+
6
+ class ComponentJSONMetadataHandler (models .NonAssemblableDatafileHandler ):
7
+ """
8
+ Handle JSON metadata files for package analysis.
9
+ """
10
+ datasource_id = "json_metadata"
11
+ path_patterns = ("*component.json" ,)
12
+ default_package_type = "library"
13
+ description = "JSON package metadata file"
14
+
15
+ @classmethod
16
+ def parse (cls , location , package_only = False ):
17
+ """
18
+ Parse the JSON metadata file at `location` and yield PackageData.
19
+ """
20
+ with open (location , "r" , encoding = "utf-8" ) as f :
21
+ data = json .load (f )
22
+
23
+ name = data .get ('name' ) or data .get ('repo' , '' ).split ('/' )[- 1 ]
24
+ if not name :
25
+ return
26
+
27
+ namespace = None
28
+ if 'repo' in data and '/' in data ['repo' ]:
29
+ namespace , name = data ['repo' ].split ('/' , 1 )
30
+
31
+ package_data = dict (
32
+ datasource_id = cls .datasource_id ,
33
+ type = cls .default_package_type ,
34
+ name = name ,
35
+ namespace = namespace ,
36
+ version = data .get ('version' ),
37
+ description = data .get ('description' , '' ),
38
+ homepage_url = cls ._extract_homepage (data ),
39
+ keywords = data .get ('keywords' , []),
40
+ dependencies = cls ._process_dependencies (data ),
41
+ extracted_license_statement = cls ._extract_license_statement (data ),
42
+ extra_data = cls ._extract_extra_data (data )
43
+ )
44
+
45
+ if namespace and name :
46
+ try :
47
+ package_data ['purl' ] = PackageURL (
48
+ type = 'generic' ,
49
+ namespace = namespace ,
50
+ name = name ,
51
+ version = package_data .get ('version' )
52
+ ).to_string ()
53
+ except Exception :
54
+ pass
55
+
56
+ yield models .PackageData .from_data (package_data , package_only )
57
+
58
+ @staticmethod
59
+ def _extract_homepage (data ):
60
+ """
61
+ Extract homepage URL from various possible sources.
62
+ """
63
+ if data .get ('homepage' ):
64
+ return data ['homepage' ]
65
+
66
+ if data .get ('repo' ):
67
+ return f'https://github.com/{ data ["repo" ]} '
68
+
69
+ desc = data .get ('description' , '' )
70
+ if 'http' in desc :
71
+ urls = [word for word in desc .split () if word .startswith ('http' )]
72
+ return urls [0 ] if urls else None
73
+
74
+ return None
75
+
76
+ @staticmethod
77
+ def _process_dependencies (data ):
78
+ """
79
+ Process dependencies into DependentPackage objects.
80
+ """
81
+ dependencies = []
82
+
83
+ for dep_name , dep_version in data .get ('dependencies' , {}).items ():
84
+ try :
85
+ if '/' in dep_name :
86
+ namespace , name = dep_name .split ('/' , 1 )
87
+ else :
88
+ namespace , name = None , dep_name
89
+
90
+ purl = PackageURL (
91
+ type = 'generic' ,
92
+ namespace = namespace ,
93
+ name = name ,
94
+ version = dep_version
95
+ ).to_string ()
96
+
97
+ dependencies .append (
98
+ models .DependentPackage (
99
+ purl = purl ,
100
+ scope = 'runtime' ,
101
+ is_runtime = True ,
102
+ is_optional = False
103
+ )
104
+ )
105
+ except Exception :
106
+ continue
107
+
108
+ return dependencies
109
+
110
+ @classmethod
111
+ def _extract_license_statement (cls , data ):
112
+ """
113
+ Extract license statement similar to BuildpackHandler.
114
+
115
+ Handles various license formats:
116
+ - Simple string license
117
+ - Multiple licenses
118
+ - Complex license strings
119
+ """
120
+ license_field = data .get ('license' )
121
+ if not license_field :
122
+ return None
123
+
124
+ if isinstance (license_field , str ):
125
+ return yaml .dump ({"type" : license_field .strip ()}).strip ()
126
+
127
+ if isinstance (license_field , list ):
128
+ license_statements = [
129
+ yaml .dump ({"type" : lic .strip ()}).strip ()
130
+ for lic in license_field
131
+ if lic .strip ()
132
+ ]
133
+ return "\n " .join (license_statements ) if license_statements else None
134
+
135
+ return None
136
+
137
+ @staticmethod
138
+ def _extract_extra_data (data ):
139
+ """
140
+ Extract additional metadata not in core package data.
141
+ """
142
+ extra_fields = [
143
+ 'main' , 'scripts' , 'styles' , 'bin' ,
144
+ 'repository' , 'private' , 'dev' , 'development'
145
+ ]
146
+
147
+ return {
148
+ field : data [field ]
149
+ for field in extra_fields
150
+ if field in data
151
+ }
0 commit comments