ssc_codegen 0.30.0__tar.gz → 0.31.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/PKG-INFO +1 -1
  2. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/pyproject.toml +1 -1
  3. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/__init__.py +6 -2
  4. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/struct.py +104 -50
  5. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/converters/js_pure.py +148 -179
  6. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/converters/py_bs4.py +62 -309
  7. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/converters/py_lxml.py +21 -17
  8. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/converters/py_parsel.py +12 -6
  9. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/converters/py_render.py +2 -2
  10. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/converters/py_slax.py +12 -6
  11. ssc_codegen-0.31.0/ssc_codegen/converters/runtime/__init__.py +7 -0
  12. ssc_codegen-0.31.0/ssc_codegen/converters/runtime/_helpers.py +35 -0
  13. ssc_codegen-0.31.0/ssc_codegen/converters/runtime/js_base.py +24 -0
  14. ssc_codegen-0.31.0/ssc_codegen/converters/runtime/py_base.py +67 -0
  15. ssc_codegen-0.31.0/ssc_codegen/converters/runtime/py_lxml.py +7 -0
  16. ssc_codegen-0.31.0/ssc_codegen/converters/runtime/py_rest.py +133 -0
  17. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/struct_parser.py +40 -16
  18. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/main.py +1 -1
  19. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/.gitignore +0 -0
  20. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/LICENSE +0 -0
  21. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/README.md +0 -0
  22. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/__init__.py +0 -0
  23. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/_logging.py +0 -0
  24. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/array.py +0 -0
  25. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/base.py +0 -0
  26. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/cast.py +0 -0
  27. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/control.py +0 -0
  28. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/extract.py +0 -0
  29. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/helpers.py +0 -0
  30. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/jsondef.py +0 -0
  31. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/module.py +0 -0
  32. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/predicate_containers.py +0 -0
  33. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/predicate_ops.py +0 -0
  34. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/regex.py +0 -0
  35. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/selectors.py +0 -0
  36. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/string.py +0 -0
  37. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/transform.py +0 -0
  38. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/typedef.py +0 -0
  39. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/ast/types.py +0 -0
  40. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/converters/base.py +0 -0
  41. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/converters/helpers.py +0 -0
  42. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/__init__.py +0 -0
  43. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/contexts.py +0 -0
  44. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/expressions.py +0 -0
  45. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/format.py +0 -0
  46. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/linter.py +0 -0
  47. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/module_handler.py +0 -0
  48. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/predicates.py +0 -0
  49. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/reader.py +0 -0
  50. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/core/type_checking.py +0 -0
  51. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/document_utils.py +0 -0
  52. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/exceptions.py +0 -0
  53. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/health.py +0 -0
  54. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/kdl/__init__.py +0 -0
  55. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/kdl/dict_reader.py +0 -0
  56. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/kdl/parser.py +0 -0
  57. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/kdl/reader.py +0 -0
  58. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/parsers/__init__.py +0 -0
  59. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/parsers/curl.py +0 -0
  60. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/parsers/http.py +0 -0
  61. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/pseudo_selectors.py +0 -0
  62. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/regex_utils.py +0 -0
  63. {ssc_codegen-0.30.0/ssc_codegen/converters → ssc_codegen-0.31.0/ssc_codegen}/request_spec.py +0 -0
  64. {ssc_codegen-0.30.0 → ssc_codegen-0.31.0}/ssc_codegen/selector_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssc_codegen
3
- Version: 0.30.0
3
+ Version: 0.31.0
4
4
  Summary: Python-dsl code converter to html parser for web scraping
5
5
  Project-URL: Documentation, https://github.com/vypivshiy/selector_schema_codegen#readme
6
6
  Project-URL: Issues, https://github.com/vypivshiy/selector_schema_codegen/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ssc_codegen"
3
- version = "0.30.0"
3
+ version = "0.31.0"
4
4
  description = "Python-dsl code converter to html parser for web scraping "
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -43,7 +43,10 @@ from .struct import (
43
43
  TableConfig,
44
44
  TableRow,
45
45
  TableMatchKey,
46
- RequestConfig,
46
+ RequestHttp,
47
+ MethodBase,
48
+ MethodFetch,
49
+ MethodRest,
47
50
  ErrorResponse,
48
51
  PlaceholderSpec,
49
52
  Field,
@@ -150,7 +153,8 @@ __all__ = [
150
153
  "Init", "InitField", "SplitDoc",
151
154
  "Key", "Value",
152
155
  "TableConfig", "TableRow", "TableMatchKey",
153
- "RequestConfig", "ErrorResponse", "PlaceholderSpec", "Field", "StartParse",
156
+ "RequestHttp", "MethodBase", "MethodFetch", "MethodRest",
157
+ "ErrorResponse", "PlaceholderSpec", "Field", "StartParse",
154
158
  # selectors
155
159
  "CssSelect", "CssSelectAll",
156
160
  "XpathSelect", "XpathSelectAll",
@@ -49,6 +49,106 @@ def parse_placeholder(match: "_re.Match[str]") -> PlaceholderSpec:
49
49
  )
50
50
 
51
51
 
52
+ # ── Request/Method nodes ──────────────────────────────────────────────────────
53
+
54
+
55
+ def _iter_http_strings(http: RequestHttp):
56
+ """Yield every string value that may contain placeholders."""
57
+ yield http.url
58
+ yield from http.headers.values()
59
+ yield from http.cookies.values()
60
+ yield from http.params.values()
61
+ if isinstance(http.body, str) and http.body:
62
+ yield http.body
63
+ elif isinstance(http.body, dict):
64
+ yield from _iter_dict_strings(http.body)
65
+
66
+
67
+ def _iter_dict_strings(d: dict):
68
+ for v in d.values():
69
+ if isinstance(v, str):
70
+ yield v
71
+ elif isinstance(v, dict):
72
+ yield from _iter_dict_strings(v)
73
+ elif isinstance(v, list):
74
+ for item in v:
75
+ if isinstance(item, str):
76
+ yield item
77
+
78
+
79
+ @dataclass
80
+ class RequestHttp(Node):
81
+ """Parsed HTTP request — child node of MethodBase.
82
+
83
+ Created once at parse time from ``parse_to_spec(raw_payload)``;
84
+ converters read fields directly instead of re-parsing raw_payload.
85
+ """
86
+
87
+ method: str = "GET"
88
+ url: str = ""
89
+ headers: dict[str, str] = field(default_factory=dict)
90
+ cookies: dict[str, str] = field(default_factory=dict)
91
+ params: dict[str, str] = field(default_factory=dict)
92
+ body_kind: str = "empty" # "empty" | "json" | "form" | "raw"
93
+ body: str | dict | None = None
94
+
95
+ @property
96
+ def placeholders(self) -> list[PlaceholderSpec]:
97
+ """Unique placeholders across all string fields."""
98
+ seen: set[str] = set()
99
+ result: list[PlaceholderSpec] = []
100
+ for text in _iter_http_strings(self):
101
+ for m in PLACEHOLDER_RE.finditer(text):
102
+ spec = parse_placeholder(m)
103
+ if spec.name not in seen:
104
+ seen.add(spec.name)
105
+ result.append(spec)
106
+ return result
107
+
108
+
109
+ @dataclass
110
+ class MethodBase(Node):
111
+ """Base class for @request method nodes."""
112
+
113
+ name: str = "" # method name suffix; "" = default fetch()
114
+
115
+ @property
116
+ def http_request(self) -> RequestHttp:
117
+ return [n for n in self.body if isinstance(n, RequestHttp)][0]
118
+
119
+ @property
120
+ def placeholders(self) -> list[PlaceholderSpec]:
121
+ return self.http_request.placeholders
122
+
123
+ @property
124
+ def placeholder_names(self) -> list[str]:
125
+ return [p.name for p in self.placeholders]
126
+
127
+
128
+ @dataclass
129
+ class MethodFetch(MethodBase):
130
+ """Fetch shortcut for regular schemas (Item/List/etc).
131
+
132
+ ``fetch()`` returns a parser instance constructed from the HTTP response body.
133
+ """
134
+
135
+ response_path: str = "" # dot-notation JSON path, e.g. "payload.html"
136
+ response_join: str = (
137
+ "" # join separator when response-path resolves to list[str]
138
+ )
139
+
140
+
141
+ @dataclass
142
+ class MethodRest(MethodBase):
143
+ """REST method for ``type=rest`` schemas.
144
+
145
+ Method returns ``Ok/TransportErr`` union; no HTML parsing.
146
+ """
147
+
148
+ doc: str = "" # per-method docstring
149
+ response_schema: str = "" # json schema name for typed 2xx response
150
+
151
+
52
152
  # ── Base struct ──────────────────────────────────────────────────────────────
53
153
 
54
154
 
@@ -68,15 +168,15 @@ class StructBase(Node):
68
168
  return self.body[0] # type: ignore
69
169
 
70
170
  @property
71
- def request_config(self) -> RequestConfig | None:
171
+ def request_config(self) -> MethodBase | None:
72
172
  for node in self.body:
73
- if isinstance(node, RequestConfig):
173
+ if isinstance(node, MethodBase):
74
174
  return node
75
175
  return None
76
176
 
77
177
  @property
78
- def request_configs(self) -> list[RequestConfig]:
79
- return [n for n in self.body if isinstance(n, RequestConfig)]
178
+ def request_configs(self) -> list[MethodBase]:
179
+ return [n for n in self.body if isinstance(n, MethodBase)]
80
180
 
81
181
  @property
82
182
  def use_request(self) -> bool:
@@ -350,52 +450,6 @@ class TableMatchKey(Node):
350
450
  ret: VariableType = field(default=VariableType.STRING)
351
451
 
352
452
 
353
- @dataclass
354
- class RequestConfig(Node):
355
- """
356
- Optional transport layer config for a struct.
357
- DSL: @request [name="suffix"] [response-path="..."] [response-join="..."]
358
- [response=JsonSchema] [doc="..."] \"""...\"""
359
-
360
- raw_payload stores the verbatim curl or raw HTTP string (with {{placeholders}}).
361
- Transport normalization (curl/HTTP parse → kwargs) happens at converter stage.
362
-
363
- name="" (unnamed) generates fetch()/async_fetch().
364
- name="by-id" generates fetch_by_id()/async_fetch_by_id() (Python)
365
- or fetchById() (JS).
366
-
367
- response_schema (type=rest only): json schema name for typed 2xx response.
368
- Empty string = void return.
369
- doc (type=rest only): per-method docstring.
370
- """
371
-
372
- raw_payload: str = ""
373
- response_path: str = "" # dot-notation JSON path, e.g. "payload.html"
374
- response_join: str = (
375
- "" # join separator when response-path resolves to list[str]
376
- )
377
- name: str = "" # method name suffix; "" = default fetch()
378
- response_schema: str = "" # type=rest: json schema for 2xx body
379
- doc: str = "" # type=rest: per-method docstring
380
-
381
- @property
382
- def placeholders(self) -> list[PlaceholderSpec]:
383
- """Unique placeholders in declaration order (dedup by name)."""
384
- seen: set[str] = set()
385
- result: list[PlaceholderSpec] = []
386
- for m in PLACEHOLDER_RE.finditer(self.raw_payload):
387
- spec = parse_placeholder(m)
388
- if spec.name not in seen:
389
- seen.add(spec.name)
390
- result.append(spec)
391
- return result
392
-
393
- @property
394
- def placeholder_names(self) -> list[str]:
395
- """Unique placeholder names in declaration order."""
396
- return [p.name for p in self.placeholders]
397
-
398
-
399
453
  @dataclass
400
454
  class ErrorResponse(Node):
401
455
  """
@@ -28,12 +28,13 @@ from ssc_codegen.converters.helpers import (
28
28
  to_snake_case,
29
29
  jsonify_path_to_segments,
30
30
  )
31
- from ssc_codegen.converters.request_spec import (
32
- parse_to_spec,
31
+ from ssc_codegen.request_spec import (
32
+ RequestSpec,
33
33
  normalize_placeholder_names,
34
34
  _PH,
35
35
  )
36
36
  from ssc_codegen.ast.struct import parse_placeholder
37
+ from ssc_codegen.converters.runtime.js_base import js_base_utility_lines
37
38
 
38
39
  JS_CONVERTER = BaseConverter(indent=" " * 2)
39
40
 
@@ -202,24 +203,7 @@ def _js_module_has_rest(node) -> bool:
202
203
  @JS_CONVERTER(a.Utilities)
203
204
  def pre_utilities(node: a.Utilities, _: ConverterContext):
204
205
  # TODO: wrong func _replMap
205
- lines = [
206
- "const UNMATCHED_TABLE_ROW = Symbol('UNMATCHED_TABLE_ROW');",
207
- "",
208
- "function _replMap(s, map) {",
209
- " for (const [k, v] of Object.entries(map)) s = s.split(k).join(v);",
210
- " return s;",
211
- "}",
212
- "",
213
- "function _normalizeText(s) { return s ? s.trim().replace(/\\s+/g, ' ') : ''; }",
214
- "",
215
- "function _unescapeText(s) {",
216
- " const el = document.createElement('textarea');",
217
- " el.innerHTML = s; return el.value;",
218
- "}",
219
- "",
220
- "function _rmPrefix(s, p) { return s.startsWith(p) ? s.slice(p.length) : s; }",
221
- "function _rmSuffix(s, p) { return s.endsWith(p) ? s.slice(0, -p.length) : s; }",
222
- ]
206
+ lines = js_base_utility_lines()
223
207
  if _js_module_has_rest(node):
224
208
  lines.extend(
225
209
  [
@@ -1767,32 +1751,6 @@ _JS_PRIM_JSDOC = {
1767
1751
  }
1768
1752
 
1769
1753
 
1770
- def _js_ph_jsdoc(ph: a.PlaceholderSpec) -> str:
1771
- """JSDoc type expression for a placeholder, e.g. 'number[]' or 'string | null'."""
1772
- t = _JS_PRIM_JSDOC[ph.type_name]
1773
- if ph.is_array:
1774
- t = f"{t}[]"
1775
- return t
1776
-
1777
-
1778
- def _js_signature_jsdoc(
1779
- placeholders: list[a.PlaceholderSpec], indent: str
1780
- ) -> list[str]:
1781
- """Build JSDoc lines describing the destructured params argument."""
1782
- if not placeholders:
1783
- return []
1784
- lines = [f"{indent}/**", f"{indent} * @param {{object}} params"]
1785
- required = [p for p in placeholders if not p.is_optional]
1786
- optional = [p for p in placeholders if p.is_optional]
1787
- for p in required + optional:
1788
- bracket_name = (
1789
- f"[params.{p.name}]" if p.is_optional else f"params.{p.name}"
1790
- )
1791
- lines.append(f"{indent} * @param {{{_js_ph_jsdoc(p)}}} {bracket_name}")
1792
- lines.append(f"{indent} */")
1793
- return lines
1794
-
1795
-
1796
1754
  def _js_render_json_body(raw: str) -> str:
1797
1755
  """Render JSON body template (with {{placeholders}}) as a JS template literal."""
1798
1756
  inner = raw.replace("\\", "\\\\").replace("`", "\\`")
@@ -1816,31 +1774,105 @@ def _js_name(name: str) -> str:
1816
1774
  return to_camel_case(to_snake_case(name))
1817
1775
 
1818
1776
 
1819
- def _js_rest_method(node: a.RequestConfig, ctx: ConverterContext) -> list[str]:
1820
- """Emit a REST client method for `struct type=rest`."""
1777
+ def _js_build_request_args(node: a.MethodBase, ctx: ConverterContext):
1778
+ """Build shared request parts for MethodRest/MethodFetch handlers."""
1779
+ http = node.http_request
1821
1780
  spec = normalize_placeholder_names(
1822
- parse_to_spec(node.raw_payload), _js_name
1781
+ RequestSpec(
1782
+ method=http.method,
1783
+ url=http.url,
1784
+ headers=dict(http.headers),
1785
+ cookies=dict(http.cookies),
1786
+ params=dict(http.params),
1787
+ body_kind=http.body_kind,
1788
+ body=http.body,
1789
+ ),
1790
+ _js_name,
1823
1791
  )
1824
1792
  http_client = ctx.meta.get("http_client", "fetch")
1825
1793
  ind = ctx.indent_char
1826
- i1 = ctx.indent
1827
- i2 = i1 + ind
1828
- i3 = i2 + ind
1829
- i4 = i3 + ind
1794
+ i1, i2, i3, i4 = (ctx.indent + ind * n for n in range(4))
1795
+
1796
+ # placeholder params (required first, optional last)
1797
+ ph_param = ""
1798
+ if spec.placeholders:
1799
+ ordered = [
1800
+ p.name
1801
+ for p in sorted(spec.placeholders, key=lambda p: p.is_optional)
1802
+ ]
1803
+ ph_param = ", {" + ", ".join(ordered) + "}"
1804
+
1805
+ # pre-body builders + resolved expressions
1806
+ pre_lines: list[str] = []
1807
+
1808
+ def _resolve(d, varname, builder_fn):
1809
+ if not d:
1810
+ return None
1811
+ if _js_dict_needs_builder(d):
1812
+ pre_lines.extend(builder_fn(varname, d, i3))
1813
+ return varname
1814
+ return _js_render_obj(d)
1815
+
1816
+ params_expr = _resolve(spec.params, "_params", _js_emit_params_builder)
1817
+ headers_expr = _resolve(spec.headers, "_headers", _js_emit_obj_builder)
1818
+ cookies_expr = _resolve(spec.cookies, "_cookies", _js_emit_obj_builder)
1819
+
1820
+ body_result = _js_render_body(spec)
1821
+ body_expr = body_result[1] if body_result else None
1822
+
1823
+ return (
1824
+ spec,
1825
+ pre_lines,
1826
+ ph_param,
1827
+ http_client,
1828
+ params_expr,
1829
+ headers_expr,
1830
+ cookies_expr,
1831
+ body_expr,
1832
+ i1,
1833
+ i2,
1834
+ i3,
1835
+ i4,
1836
+ )
1837
+
1838
+
1839
+ def _js_ok_payload_type(node: a.MethodRest) -> str:
1840
+ """Return the JSDoc type for the successful response payload."""
1841
+ if not node.response_schema:
1842
+ return "null"
1843
+ struct = node.parent
1844
+ module = struct.parent if struct is not None else None
1845
+ schema_type = f"{to_pascal_case(node.response_schema)}Json"
1846
+ if module is not None:
1847
+ for n in module.body:
1848
+ if isinstance(n, a.JsonDef) and n.name == node.response_schema:
1849
+ if n.is_array:
1850
+ return f"Array<{schema_type}>"
1851
+ break
1852
+ return schema_type
1853
+
1854
+
1855
+ @JS_CONVERTER(a.MethodRest)
1856
+ def pre_method_rest(node: a.MethodRest, ctx: ConverterContext):
1857
+ (
1858
+ spec,
1859
+ pre_lines,
1860
+ ph_param,
1861
+ http_client,
1862
+ params_expr,
1863
+ headers_expr,
1864
+ _cookies_expr,
1865
+ body_expr,
1866
+ i1,
1867
+ i2,
1868
+ i3,
1869
+ i4,
1870
+ ) = _js_build_request_args(node, ctx)
1830
1871
 
1831
1872
  raw_name = node.name or "fetch"
1832
1873
  method_name = to_camel_case(to_snake_case(raw_name))
1833
1874
  if raw_name == "fetch":
1834
1875
  method_name = "fetch"
1835
-
1836
- placeholders = spec.placeholders
1837
- if placeholders:
1838
- required = [p for p in placeholders if not p.is_optional]
1839
- optional = [p for p in placeholders if p.is_optional]
1840
- ordered_names = [p.name for p in required + optional]
1841
- ph_param = ", {" + ", ".join(ordered_names) + "}"
1842
- else:
1843
- ph_param = ""
1844
1876
  sig = f"static async {method_name}(client{ph_param})"
1845
1877
 
1846
1878
  parent = node.parent
@@ -1866,42 +1898,19 @@ def _js_rest_method(node: a.RequestConfig, ctx: ConverterContext) -> list[str]:
1866
1898
  for doc_line in node.doc.splitlines():
1867
1899
  lines.append(f"{i1} * {doc_line}")
1868
1900
  lines.append(f"{i1} */")
1869
- # JSDoc: params signature + return type (merged block)
1870
- jsdoc_lines: list[str] = [f"{i1}/**"]
1871
- if placeholders:
1872
- jsdoc_lines.append(f"{i1} * @param {{object}} params")
1873
- for p in [
1874
- *(p for p in placeholders if not p.is_optional),
1875
- *(p for p in placeholders if p.is_optional),
1876
- ]:
1877
- bracket_name = (
1878
- f"[params.{p.name}]" if p.is_optional else f"params.{p.name}"
1879
- )
1880
- jsdoc_lines.append(
1881
- f"{i1} * @param {{{_js_ph_jsdoc(p)}}} {bracket_name}"
1882
- )
1883
- jsdoc_lines.append(f"{i1} * @returns {{Promise<{return_union}>}}")
1884
- jsdoc_lines.append(f"{i1} */")
1885
- lines.extend(jsdoc_lines)
1901
+ # JSDoc: params signature + return type
1902
+ lines.append(f"{i1}/**")
1903
+ for p in sorted(spec.placeholders, key=lambda p: p.is_optional):
1904
+ t = _JS_PRIM_JSDOC[p.type_name]
1905
+ if p.is_array:
1906
+ t = f"{t}[]"
1907
+ bracket = f"[params.{p.name}]" if p.is_optional else f"params.{p.name}"
1908
+ lines.append(f"{i1} * @param {{{t}}} {bracket}")
1909
+ lines.append(f"{i1} * @returns {{Promise<{return_union}>}}")
1910
+ lines.append(f"{i1} */")
1886
1911
  lines.append(f"{i1}{sig} {{")
1887
1912
 
1888
- # Pre-body: local builders for params/headers if any entry is array or optional.
1889
- pre_lines: list[str] = []
1890
- params_var: str | None = None
1891
- headers_var: str | None = None
1892
- cookies_var: str | None = None
1893
- if spec.params and _js_dict_needs_builder(spec.params):
1894
- params_var = "_params"
1895
- pre_lines.extend(_js_emit_params_builder(params_var, spec.params, i3))
1896
- if spec.headers and _js_dict_needs_builder(spec.headers):
1897
- headers_var = "_headers"
1898
- pre_lines.extend(_js_emit_obj_builder(headers_var, spec.headers, i3))
1899
- if spec.cookies and _js_dict_needs_builder(spec.cookies):
1900
- cookies_var = "_cookies"
1901
- pre_lines.extend(_js_emit_obj_builder(cookies_var, spec.cookies, i3))
1902
-
1903
- # Everything below — response handling & request — goes inside try/catch
1904
- # so transport failures turn into TransportErr.
1913
+ # try/catch so transport failures turn into TransportErr.
1905
1914
  lines.append(f"{i2}let _resp;")
1906
1915
  lines.append(f"{i2}try {{")
1907
1916
  lines.extend(pre_lines)
@@ -1912,48 +1921,34 @@ def _js_rest_method(node: a.RequestConfig, ctx: ConverterContext) -> list[str]:
1912
1921
  f"{i4}url: {_js_render_value(spec.url)},",
1913
1922
  f"{i4}validateStatus: () => true,",
1914
1923
  ]
1915
- if spec.params:
1916
- params_expr = (
1917
- params_var if params_var else _js_render_obj(spec.params)
1918
- )
1924
+ if params_expr:
1919
1925
  req_props.append(f"{i4}params: {params_expr},")
1920
- if spec.headers:
1921
- headers_expr = (
1922
- headers_var if headers_var else _js_render_obj(spec.headers)
1923
- )
1926
+ if headers_expr:
1924
1927
  req_props.append(f"{i4}headers: {headers_expr},")
1925
- body_result = _js_render_body(spec)
1926
- if body_result:
1927
- _, body_expr = body_result
1928
+ if body_expr:
1928
1929
  req_props.append(f"{i4}data: {body_expr},")
1929
1930
  lines.append(f"{i3}_resp = await client.request({{")
1930
1931
  lines.extend(req_props)
1931
1932
  lines.append(f"{i3}}});")
1932
1933
  else:
1933
- if spec.params:
1934
+ if params_expr:
1934
1935
  url_inner = _PH.sub(
1935
1936
  lambda mm: "${" + mm.group(1) + "}",
1936
1937
  spec.url.replace("`", "\\`"),
1937
1938
  )
1938
- if params_var:
1939
- url_expr = f"`{url_inner}?${{{params_var}.toString()}}`"
1939
+ if params_expr == "_params":
1940
+ url_expr = f"`{url_inner}?${{{params_expr}.toString()}}`"
1940
1941
  else:
1941
- params_obj = _js_render_obj(spec.params)
1942
1942
  url_expr = (
1943
- f"`{url_inner}?${{new URLSearchParams({params_obj})}}`"
1943
+ f"`{url_inner}?${{new URLSearchParams({params_expr})}}`"
1944
1944
  )
1945
1945
  else:
1946
1946
  url_expr = _js_render_value(spec.url)
1947
1947
 
1948
1948
  options: list[str] = [f"{i4}method: {spec.method!r},"]
1949
- if spec.headers:
1950
- headers_expr = (
1951
- headers_var if headers_var else _js_render_obj(spec.headers)
1952
- )
1949
+ if headers_expr:
1953
1950
  options.append(f"{i4}headers: {headers_expr},")
1954
- body_result = _js_render_body(spec)
1955
- if body_result:
1956
- _, body_expr = body_result
1951
+ if body_expr:
1957
1952
  options.append(f"{i4}body: {body_expr},")
1958
1953
  lines.append(f"{i3}_resp = await client({url_expr}, {{")
1959
1954
  lines.extend(options)
@@ -1990,79 +1985,62 @@ def _js_rest_method(node: a.RequestConfig, ctx: ConverterContext) -> list[str]:
1990
1985
  return lines
1991
1986
 
1992
1987
 
1993
- def _js_ok_payload_type(node: a.RequestConfig) -> str:
1994
- """Return the JSDoc type for the successful response payload."""
1995
- if not node.response_schema:
1996
- return "null"
1997
- struct = node.parent
1998
- module = struct.parent if struct is not None else None
1999
- schema_type = f"{to_pascal_case(node.response_schema)}Json"
2000
- if module is not None:
2001
- for n in module.body:
2002
- if isinstance(n, a.JsonDef) and n.name == node.response_schema:
2003
- if n.is_array:
2004
- return f"Array<{schema_type}>"
2005
- break
2006
- return schema_type
2007
-
2008
-
2009
- def _js_fetch_method(node: a.RequestConfig, ctx: ConverterContext) -> list[str]:
2010
- """Generate non-REST async fetch method for JS (fetch API or axios)."""
2011
- spec = normalize_placeholder_names(
2012
- parse_to_spec(node.raw_payload), _js_name
2013
- )
2014
- http_client = ctx.meta.get("http_client", "fetch")
2015
-
2016
- ind = ctx.indent_char
2017
- i1 = ctx.indent
2018
- i2 = i1 + ind
2019
- i3 = i2 + ind
1988
+ @JS_CONVERTER(a.MethodFetch)
1989
+ def pre_method_fetch(node: a.MethodFetch, ctx: ConverterContext):
1990
+ (
1991
+ spec,
1992
+ _pre_lines,
1993
+ ph_param,
1994
+ http_client,
1995
+ params_expr,
1996
+ headers_expr,
1997
+ _cookies_expr,
1998
+ body_expr,
1999
+ i1,
2000
+ i2,
2001
+ i3,
2002
+ _i4,
2003
+ ) = _js_build_request_args(node, ctx)
2020
2004
 
2021
2005
  struct_name = to_pascal_case(node.parent.name) # type: ignore[union-attr]
2022
- ph_names = [p.name for p in spec.placeholders]
2023
2006
  method_name = "fetch" + (to_pascal_case(node.name) if node.name else "")
2024
- ph_param = (", {" + ", ".join(ph_names) + "}") if ph_names else ""
2025
- sig = f"static async {method_name}(client{ph_param})"
2026
2007
 
2027
2008
  def _response_lines(data_expr: str) -> list[str]:
2028
- lines: list[str] = []
2009
+ rl: list[str] = []
2029
2010
  if node.response_path:
2030
2011
  accessor = "".join(
2031
2012
  f"[{p!r}]" for p in node.response_path.split(".")
2032
2013
  )
2033
- lines.append(f"{i2}const _data = {data_expr};")
2014
+ rl.append(f"{i2}const _data = {data_expr};")
2034
2015
  if node.response_join:
2035
- lines.append(
2016
+ rl.append(
2036
2017
  f"{i2}const _body = _data{accessor}.join({node.response_join!r});"
2037
2018
  )
2038
2019
  else:
2039
- lines.append(f"{i2}const _body = _data{accessor};")
2020
+ rl.append(f"{i2}const _body = _data{accessor};")
2040
2021
  else:
2041
- lines.append(f"{i2}const _body = {data_expr};")
2042
- lines.append(f"{i2}return new {struct_name}(_body);")
2043
- return lines
2022
+ rl.append(f"{i2}const _body = {data_expr};")
2023
+ rl.append(f"{i2}return new {struct_name}(_body);")
2024
+ return rl
2044
2025
 
2045
- lines: list[str] = [f"{i1}{sig} {{"]
2026
+ lines: list[str] = [f"{i1}static async {method_name}(client{ph_param}) {{"]
2046
2027
 
2047
2028
  if http_client == "fetch":
2048
- if spec.params:
2029
+ if params_expr:
2049
2030
  url_inner = _PH.sub(r"${\1}", spec.url.replace("`", "\\`"))
2050
- params_obj = _js_render_obj(spec.params)
2051
- url_expr = f"`{url_inner}?${{new URLSearchParams({params_obj})}}`"
2031
+ url_expr = f"`{url_inner}?${{new URLSearchParams({params_expr})}}`"
2052
2032
  else:
2053
2033
  url_expr = _js_render_value(spec.url)
2054
2034
 
2055
2035
  options: list[str] = [f"{i3}method: {spec.method!r},"]
2056
- if spec.headers:
2057
- options.append(f"{i3}headers: {_js_render_obj(spec.headers)},")
2036
+ if headers_expr:
2037
+ options.append(f"{i3}headers: {headers_expr},")
2058
2038
  if spec.cookies:
2059
2039
  cookie_str = "; ".join(f"{k}={v}" for k, v in spec.cookies.items())
2060
2040
  options.append(
2061
2041
  f"{i3}// cookies: {cookie_str!r} /* set via headers or credentials */"
2062
2042
  )
2063
- body_result = _js_render_body(spec)
2064
- if body_result:
2065
- _, body_expr = body_result
2043
+ if body_expr:
2066
2044
  options.append(f"{i3}body: {body_expr},")
2067
2045
 
2068
2046
  lines.append(f"{i2}const _resp = await client({url_expr}, {{")
@@ -2081,15 +2059,13 @@ def _js_fetch_method(node: a.RequestConfig, ctx: ConverterContext) -> list[str]:
2081
2059
  f"{i3}method: {spec.method!r},",
2082
2060
  f"{i3}url: {_js_render_value(spec.url)},",
2083
2061
  ]
2084
- if spec.params:
2085
- req_props.append(f"{i3}params: {_js_render_obj(spec.params)},")
2086
- if spec.headers:
2087
- req_props.append(f"{i3}headers: {_js_render_obj(spec.headers)},")
2062
+ if params_expr:
2063
+ req_props.append(f"{i3}params: {params_expr},")
2064
+ if headers_expr:
2065
+ req_props.append(f"{i3}headers: {headers_expr},")
2088
2066
  if spec.cookies:
2089
2067
  req_props.append(f"{i3}// cookies: {_js_render_obj(spec.cookies)},")
2090
- body_result = _js_render_body(spec)
2091
- if body_result:
2092
- _, body_expr = body_result
2068
+ if body_expr:
2093
2069
  req_props.append(f"{i3}data: {body_expr},")
2094
2070
 
2095
2071
  lines.append(f"{i2}const _resp = await client.request({{")
@@ -2104,13 +2080,6 @@ def _js_fetch_method(node: a.RequestConfig, ctx: ConverterContext) -> list[str]:
2104
2080
  return lines
2105
2081
 
2106
2082
 
2107
- @JS_CONVERTER(a.RequestConfig)
2108
- def pre_request_config(node: a.RequestConfig, ctx: ConverterContext):
2109
- if isinstance(node.parent, a.StructRest):
2110
- return _js_rest_method(node, ctx)
2111
- return _js_fetch_method(node, ctx)
2112
-
2113
-
2114
2083
  @JS_CONVERTER(a.ErrorResponse)
2115
2084
  def pre_error_response(node: a.ErrorResponse, ctx: ConverterContext):
2116
2085
  return None