3939CAPITALIZED_TYPE = re .compile (r"\b(?:Create|Add|Update|Restore|Bulk|Search|Move|Set|Vote)\w+\b" )
4040IMPORT = re .compile (r"(?m)^import\s+(?!static\s)([\w.]+);" )
4141TYPE_TOKEN = re .compile (r"\b[A-Z][A-Za-z0-9_]+\b" )
42+ JAVA_IDENTIFIER = re .compile (r"\b[A-Za-z_$][A-Za-z0-9_$]*\b" )
4243PARAMETER_LOCATION = re .compile (
4344 r"@(PathParam|QueryParam|HeaderParam|CookieParam|FormParam|FormDataParam)"
4445 r"\(\s*\"([^\"]+)\"\s*\)"
@@ -91,6 +92,7 @@ class ParameterContract:
9192 name : str
9293 location : str
9394 java_type : str
95+ cardinality : str
9496 required : bool
9597 default : str | None
9698 model_hints : tuple [str , ...]
@@ -179,6 +181,46 @@ def _split_java_parameters(signature: str) -> list[str]:
179181 return chunks
180182
181183
184+ def _without_java_annotations (text : str ) -> str :
185+ """Remove annotations, including nested arguments, from a parameter declaration."""
186+
187+ output : list [str ] = []
188+ offset = 0
189+ while offset < len (text ):
190+ if text [offset ] != "@" :
191+ output .append (text [offset ])
192+ offset += 1
193+ continue
194+ offset += 1
195+ while offset < len (text ) and (text [offset ].isalnum () or text [offset ] in "_.$" ):
196+ offset += 1
197+ while offset < len (text ) and text [offset ].isspace ():
198+ offset += 1
199+ if offset < len (text ) and text [offset ] == "(" :
200+ depth = 1
201+ offset += 1
202+ in_string = False
203+ escaped = False
204+ while offset < len (text ) and depth :
205+ character = text [offset ]
206+ if in_string :
207+ if escaped :
208+ escaped = False
209+ elif character == "\\ " :
210+ escaped = True
211+ elif character == '"' :
212+ in_string = False
213+ elif character == '"' :
214+ in_string = True
215+ elif character == "(" :
216+ depth += 1
217+ elif character == ")" :
218+ depth -= 1
219+ offset += 1
220+ output .append (" " )
221+ return "" .join (output )
222+
223+
182224def _parameter_contracts (signature : str , imports : dict [str , str ]) -> tuple [ParameterContract , ...]:
183225 contracts : list [ParameterContract ] = []
184226 locations = {
@@ -200,14 +242,23 @@ def _parameter_contracts(signature: str, imports: dict[str, str]) -> tuple[Param
200242 name = annotation .group (2 ) if annotation else variable .group (1 )
201243 default_match = DEFAULT_VALUE .search (parameter )
202244 default = default_match .group (1 ) if default_match else None
203- type_tokens = TYPE_TOKEN .findall (parameter [: variable .start ()])
245+ declaration = _without_java_annotations (parameter [: variable .start ()])
246+ type_tokens = [
247+ token for token in JAVA_IDENTIFIER .findall (declaration ) if token not in {"final" }
248+ ]
204249 java_type = type_tokens [- 1 ] if type_tokens else "unknown"
250+ cardinality = (
251+ "array"
252+ if re .search (r"\b(?:Collection|List|Set)\s*<|\[\]" , parameter [: variable .start ()])
253+ else "single"
254+ )
205255 model_hints = tuple (sorted (_schema_type_hints (parameter , imports )))
206256 contracts .append (
207257 ParameterContract (
208258 name = name ,
209259 location = location ,
210260 java_type = java_type ,
261+ cardinality = cardinality ,
211262 required = location in {"path" , "body" } or "@NotNull" in parameter ,
212263 default = default ,
213264 model_hints = model_hints ,
@@ -381,6 +432,7 @@ def parse_resource_source(source_file: Path, resource_root: Path) -> list[Source
381432 name = "inherited_collection" ,
382433 location = "path" ,
383434 java_type = "String" ,
435+ cardinality = "single" ,
384436 required = True ,
385437 default = "entities" ,
386438 model_hints = (),
0 commit comments