ssc_codegen 0.32.3__tar.gz → 0.33.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.
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/PKG-INFO +11 -8
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/README.md +10 -7
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/pyproject.toml +1 -1
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/types.py +1 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/expressions.py +66 -35
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/linter.py +8 -5
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/module_handler.py +1 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/struct_parser.py +82 -5
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/health.py +16 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/visitor.py +51 -11
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/javascript/visitor.py +37 -8
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/rest.py +2 -2
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/visitor.py +41 -13
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/traversal/utils.py +13 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/.gitignore +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/LICENSE +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/_logging.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/array.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/cast.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/control.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/extract.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/helpers.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/jsondef.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/module.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/predicate_containers.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/predicate_ops.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/regex.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/rest.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/selectors.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/string.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/struct.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/ast/typedef.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/contexts.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/format.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/predicates.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/reader.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/rest_artifacts.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/core/type_checking.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/exceptions.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/explore.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/generation/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/generation/builder.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/generation/runtime.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/main.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/naming.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/parsers/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/parsers/curl.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/parsers/http.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/regex_utils.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/request_spec.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/http_libs/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/http_libs/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/http_libs/nethttp.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/literals.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/regex.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/rest.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/golang/runtime.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/javascript/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/javascript/http_libs/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/javascript/http_libs/axios.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/javascript/http_libs/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/javascript/http_libs/fetch.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/javascript/rest.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/profile.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/html_libs/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/html_libs/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/html_libs/bs4.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/html_libs/lxml.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/html_libs/parsel.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/html_libs/slax.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/http_libs/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/http_libs/aiohttp.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/http_libs/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/http_libs/httpx.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/python/http_libs/requests.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/resolver.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/spec.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/traversal/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/traversal/context.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/traversal/walker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssc_codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.33.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
|
|
@@ -69,7 +69,7 @@ uv tool install ssc_codegen
|
|
|
69
69
|
Generate Python parser:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
|
-
ssc-gen generate books.kdl -
|
|
72
|
+
ssc-gen generate python books.kdl -L bs4 -o ./output
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
## Usage
|
|
@@ -78,22 +78,25 @@ ssc-gen generate books.kdl -l python -L bs4 -o ./output
|
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
# single file (Python + bs4)
|
|
81
|
-
ssc-gen generate schema.kdl -
|
|
81
|
+
ssc-gen generate python schema.kdl -L bs4 -o ./output
|
|
82
82
|
|
|
83
83
|
# all .kdl files in a directory (JavaScript)
|
|
84
|
-
ssc-gen generate examples/ -
|
|
84
|
+
ssc-gen generate js examples/ -o ./output
|
|
85
|
+
|
|
86
|
+
# Go (goquery + net/http)
|
|
87
|
+
ssc-gen generate go schema.kdl -o ./output
|
|
85
88
|
|
|
86
89
|
# with custom package name
|
|
87
|
-
ssc-gen generate schema.kdl -
|
|
90
|
+
ssc-gen generate python schema.kdl -L bs4 -o ./parsers --package scraper
|
|
88
91
|
|
|
89
92
|
# with @request support (REST/HTTP codegen)
|
|
90
|
-
ssc-gen generate schema.kdl -
|
|
93
|
+
ssc-gen generate python schema.kdl -L bs4 -o ./out --http-client httpx
|
|
91
94
|
|
|
92
95
|
# extract helper functions into a separate runtime module
|
|
93
|
-
ssc-gen generate schema.kdl -
|
|
96
|
+
ssc-gen generate python schema.kdl -L bs4 -o ./out -R
|
|
94
97
|
```
|
|
95
98
|
|
|
96
|
-
Languages
|
|
99
|
+
Languages: `generate python`, `generate js`, `generate go`.
|
|
97
100
|
HTML libraries (`--lib / -L`, Python only): `bs4` (default), `lxml`, `parsel`, `slax`.
|
|
98
101
|
|
|
99
102
|
### Lint schemas
|
|
@@ -37,7 +37,7 @@ uv tool install ssc_codegen
|
|
|
37
37
|
Generate Python parser:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
|
-
ssc-gen generate books.kdl -
|
|
40
|
+
ssc-gen generate python books.kdl -L bs4 -o ./output
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## Usage
|
|
@@ -46,22 +46,25 @@ ssc-gen generate books.kdl -l python -L bs4 -o ./output
|
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
48
|
# single file (Python + bs4)
|
|
49
|
-
ssc-gen generate schema.kdl -
|
|
49
|
+
ssc-gen generate python schema.kdl -L bs4 -o ./output
|
|
50
50
|
|
|
51
51
|
# all .kdl files in a directory (JavaScript)
|
|
52
|
-
ssc-gen generate examples/ -
|
|
52
|
+
ssc-gen generate js examples/ -o ./output
|
|
53
|
+
|
|
54
|
+
# Go (goquery + net/http)
|
|
55
|
+
ssc-gen generate go schema.kdl -o ./output
|
|
53
56
|
|
|
54
57
|
# with custom package name
|
|
55
|
-
ssc-gen generate schema.kdl -
|
|
58
|
+
ssc-gen generate python schema.kdl -L bs4 -o ./parsers --package scraper
|
|
56
59
|
|
|
57
60
|
# with @request support (REST/HTTP codegen)
|
|
58
|
-
ssc-gen generate schema.kdl -
|
|
61
|
+
ssc-gen generate python schema.kdl -L bs4 -o ./out --http-client httpx
|
|
59
62
|
|
|
60
63
|
# extract helper functions into a separate runtime module
|
|
61
|
-
ssc-gen generate schema.kdl -
|
|
64
|
+
ssc-gen generate python schema.kdl -L bs4 -o ./out -R
|
|
62
65
|
```
|
|
63
66
|
|
|
64
|
-
Languages
|
|
67
|
+
Languages: `generate python`, `generate js`, `generate go`.
|
|
65
68
|
HTML libraries (`--lib / -L`, Python only): `bs4` (default), `lxml`, `parsel`, `slax`.
|
|
66
69
|
|
|
67
70
|
### Lint schemas
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import ast as _py_ast
|
|
6
6
|
import re as _re
|
|
7
7
|
from collections.abc import Sequence
|
|
8
|
+
from types import SimpleNamespace
|
|
8
9
|
from typing import Any, Callable, TypeAlias
|
|
9
10
|
|
|
10
11
|
from ssc_codegen.ast import (
|
|
@@ -206,7 +207,7 @@ def resolve_index_types(
|
|
|
206
207
|
the element base with is_array=False.
|
|
207
208
|
"""
|
|
208
209
|
if parent.body:
|
|
209
|
-
prev = parent
|
|
210
|
+
prev = _prev_type_info(parent)
|
|
210
211
|
ret_ti = TypeInfo(base=prev.ret_type_info.base)
|
|
211
212
|
return prev.ret_type_info, ret_ti, prev.is_array
|
|
212
213
|
auto = TypeInfo(base=VariableType.AUTO)
|
|
@@ -420,18 +421,26 @@ def parse_expressions(
|
|
|
420
421
|
raise BuildTimeError(f"Unknown expression: {node.name}")
|
|
421
422
|
lint.push(node.name)
|
|
422
423
|
lint_pipeline_op(node, lint)
|
|
423
|
-
# Guard: ops in _REQUIRES_PREV access parent.body[-1]
|
|
424
|
-
#
|
|
425
|
-
#
|
|
424
|
+
# Guard: ops in _REQUIRES_PREV access parent.body[-1] in their
|
|
425
|
+
# handlers. Emit a diagnostic and skip when the pipeline is empty —
|
|
426
|
+
# UNLESS the parent starts with STRING (RAW struct fields), where
|
|
427
|
+
# string ops are valid as the first operation.
|
|
426
428
|
if not parent.body and node.name in _REQUIRES_PREV:
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
429
|
+
parent_accept = getattr(parent, "accept_type_info", None)
|
|
430
|
+
if (
|
|
431
|
+
parent_accept is not None
|
|
432
|
+
and parent_accept.base == VariableType.STRING
|
|
433
|
+
):
|
|
434
|
+
pass # RAW struct field — string ops OK as first node
|
|
435
|
+
else:
|
|
436
|
+
lint.error(
|
|
437
|
+
node,
|
|
438
|
+
message=(f"'{node.name}' requires a preceding operation"),
|
|
439
|
+
code="E100",
|
|
440
|
+
hint="add a producer first, e.g. `css '.x'; text`",
|
|
441
|
+
)
|
|
442
|
+
lint.pop()
|
|
443
|
+
continue
|
|
435
444
|
# Guard: handlers reading node.args[i] without a bounds check would
|
|
436
445
|
# crash with `tuple index out of range` when the user omits args.
|
|
437
446
|
required_args = _MIN_ARGS.get(node.name)
|
|
@@ -504,6 +513,28 @@ _ExpressionHandler = Callable[
|
|
|
504
513
|
_EXPRESSION_HANDLERS: dict[str, _ExpressionHandler] = {}
|
|
505
514
|
|
|
506
515
|
|
|
516
|
+
def _prev_type_info(
|
|
517
|
+
parent: FieldLikeNode,
|
|
518
|
+
) -> SimpleNamespace:
|
|
519
|
+
"""Return a namespace with ``ret_type_info`` and ``is_array`` for the
|
|
520
|
+
pipeline cursor.
|
|
521
|
+
|
|
522
|
+
When the pipeline is non-empty, uses the last node's ret type.
|
|
523
|
+
When empty (first op), falls back to the parent's accept type —
|
|
524
|
+
this lets RAW struct fields start directly with string ops
|
|
525
|
+
(re/split/fmt/etc.) since their document is already a STRING.
|
|
526
|
+
"""
|
|
527
|
+
if parent.body:
|
|
528
|
+
prev = parent.body[-1]
|
|
529
|
+
return SimpleNamespace(
|
|
530
|
+
ret_type_info=prev.ret_type_info, is_array=prev.is_array
|
|
531
|
+
)
|
|
532
|
+
return SimpleNamespace(
|
|
533
|
+
ret_type_info=parent.accept_type_info,
|
|
534
|
+
is_array=parent.accept_type_info.is_array,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
507
538
|
# Ops whose handlers access ``parent.body[-1]`` unconditionally and therefore
|
|
508
539
|
# require a preceding node. Used by ``parse_expressions`` to emit a clean
|
|
509
540
|
# E100 diagnostic instead of crashing with IndexError on an empty pipeline.
|
|
@@ -659,7 +690,7 @@ def _expr_text(
|
|
|
659
690
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
660
691
|
):
|
|
661
692
|
if parent.body:
|
|
662
|
-
prev = parent
|
|
693
|
+
prev = _prev_type_info(parent)
|
|
663
694
|
accept_ti = prev.ret_type_info
|
|
664
695
|
is_arr = prev.is_array
|
|
665
696
|
else:
|
|
@@ -678,7 +709,7 @@ def _expr_raw(
|
|
|
678
709
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
679
710
|
):
|
|
680
711
|
if parent.body:
|
|
681
|
-
prev = parent
|
|
712
|
+
prev = _prev_type_info(parent)
|
|
682
713
|
accept_ti = prev.ret_type_info
|
|
683
714
|
is_arr = prev.is_array
|
|
684
715
|
else:
|
|
@@ -697,7 +728,7 @@ def _expr_attr(
|
|
|
697
728
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
698
729
|
):
|
|
699
730
|
if parent.body:
|
|
700
|
-
prev = parent
|
|
731
|
+
prev = _prev_type_info(parent)
|
|
701
732
|
accept_ti = prev.ret_type_info
|
|
702
733
|
is_arr = prev.is_array
|
|
703
734
|
else:
|
|
@@ -727,7 +758,7 @@ def _expr_trim(
|
|
|
727
758
|
if node.args
|
|
728
759
|
else ""
|
|
729
760
|
)
|
|
730
|
-
prev = parent
|
|
761
|
+
prev = _prev_type_info(parent)
|
|
731
762
|
return Trim(
|
|
732
763
|
parent=parent,
|
|
733
764
|
accept_type_info=prev.ret_type_info,
|
|
@@ -749,7 +780,7 @@ def _expr_ltrim(
|
|
|
749
780
|
if node.args
|
|
750
781
|
else ""
|
|
751
782
|
)
|
|
752
|
-
prev = parent
|
|
783
|
+
prev = _prev_type_info(parent)
|
|
753
784
|
return Ltrim(
|
|
754
785
|
parent=parent,
|
|
755
786
|
accept_type_info=prev.ret_type_info,
|
|
@@ -771,7 +802,7 @@ def _expr_rtrim(
|
|
|
771
802
|
if node.args
|
|
772
803
|
else ""
|
|
773
804
|
)
|
|
774
|
-
prev = parent
|
|
805
|
+
prev = _prev_type_info(parent)
|
|
775
806
|
return Rtrim(
|
|
776
807
|
parent=parent,
|
|
777
808
|
accept_type_info=prev.ret_type_info,
|
|
@@ -785,7 +816,7 @@ def _expr_rtrim(
|
|
|
785
816
|
def _expr_norm_space(
|
|
786
817
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
787
818
|
):
|
|
788
|
-
prev = parent
|
|
819
|
+
prev = _prev_type_info(parent)
|
|
789
820
|
return NormalizeSpace(
|
|
790
821
|
parent=parent,
|
|
791
822
|
accept_type_info=prev.ret_type_info,
|
|
@@ -799,7 +830,7 @@ def _expr_rm_prefix(
|
|
|
799
830
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
800
831
|
):
|
|
801
832
|
substr = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
802
|
-
prev = parent
|
|
833
|
+
prev = _prev_type_info(parent)
|
|
803
834
|
return RmPrefix(
|
|
804
835
|
parent=parent,
|
|
805
836
|
accept_type_info=prev.ret_type_info,
|
|
@@ -814,7 +845,7 @@ def _expr_rm_suffix(
|
|
|
814
845
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
815
846
|
):
|
|
816
847
|
substr = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
817
|
-
prev = parent
|
|
848
|
+
prev = _prev_type_info(parent)
|
|
818
849
|
return RmSuffix(
|
|
819
850
|
parent=parent,
|
|
820
851
|
accept_type_info=prev.ret_type_info,
|
|
@@ -829,7 +860,7 @@ def _expr_rm_prefix_suffix(
|
|
|
829
860
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
830
861
|
):
|
|
831
862
|
substr = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
832
|
-
prev = parent
|
|
863
|
+
prev = _prev_type_info(parent)
|
|
833
864
|
return RmPrefixSuffix(
|
|
834
865
|
parent=parent,
|
|
835
866
|
accept_type_info=prev.ret_type_info,
|
|
@@ -844,7 +875,7 @@ def _expr_fmt(
|
|
|
844
875
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
845
876
|
):
|
|
846
877
|
tmpl = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
847
|
-
prev = parent
|
|
878
|
+
prev = _prev_type_info(parent)
|
|
848
879
|
return Fmt(
|
|
849
880
|
parent=parent,
|
|
850
881
|
accept_type_info=prev.ret_type_info,
|
|
@@ -858,7 +889,7 @@ def _expr_fmt(
|
|
|
858
889
|
def _expr_repl(
|
|
859
890
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
860
891
|
):
|
|
861
|
-
prev = parent
|
|
892
|
+
prev = _prev_type_info(parent)
|
|
862
893
|
if node.children:
|
|
863
894
|
items = {
|
|
864
895
|
str(child.name): str(child.args[0].value) for child in node.children
|
|
@@ -890,7 +921,7 @@ def _expr_repl(
|
|
|
890
921
|
def _expr_lower(
|
|
891
922
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
892
923
|
):
|
|
893
|
-
prev = parent
|
|
924
|
+
prev = _prev_type_info(parent)
|
|
894
925
|
return Lower(
|
|
895
926
|
parent=parent,
|
|
896
927
|
accept_type_info=prev.ret_type_info,
|
|
@@ -903,7 +934,7 @@ def _expr_lower(
|
|
|
903
934
|
def _expr_upper(
|
|
904
935
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
905
936
|
):
|
|
906
|
-
prev = parent
|
|
937
|
+
prev = _prev_type_info(parent)
|
|
907
938
|
return Upper(
|
|
908
939
|
parent=parent,
|
|
909
940
|
accept_type_info=prev.ret_type_info,
|
|
@@ -936,7 +967,7 @@ def _expr_join(
|
|
|
936
967
|
def _expr_unescape(
|
|
937
968
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
938
969
|
):
|
|
939
|
-
prev = parent
|
|
970
|
+
prev = _prev_type_info(parent)
|
|
940
971
|
return Unescape(
|
|
941
972
|
parent=parent,
|
|
942
973
|
accept_type_info=prev.ret_type_info,
|
|
@@ -954,7 +985,7 @@ def _expr_re(
|
|
|
954
985
|
):
|
|
955
986
|
raw = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
956
987
|
pattern = normalize_regex_pattern(raw)
|
|
957
|
-
prev = parent
|
|
988
|
+
prev = _prev_type_info(parent)
|
|
958
989
|
return Re(
|
|
959
990
|
parent=parent,
|
|
960
991
|
pattern=pattern,
|
|
@@ -978,7 +1009,7 @@ def _expr_re_all(
|
|
|
978
1009
|
def _expr_re_sub(
|
|
979
1010
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
980
1011
|
):
|
|
981
|
-
prev = parent
|
|
1012
|
+
prev = _prev_type_info(parent)
|
|
982
1013
|
raw = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
983
1014
|
pattern = normalize_regex_pattern(raw)
|
|
984
1015
|
repl = cast(
|
|
@@ -1045,7 +1076,7 @@ def _expr_slice(
|
|
|
1045
1076
|
):
|
|
1046
1077
|
start, end = int(node.args[0].value), int(node.args[1].value)
|
|
1047
1078
|
if parent.body:
|
|
1048
|
-
prev = parent
|
|
1079
|
+
prev = _prev_type_info(parent)
|
|
1049
1080
|
return Slice(
|
|
1050
1081
|
parent=parent,
|
|
1051
1082
|
start=start,
|
|
@@ -1079,7 +1110,7 @@ def _expr_unique(
|
|
|
1079
1110
|
def _expr_to_int(
|
|
1080
1111
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
1081
1112
|
):
|
|
1082
|
-
prev = parent
|
|
1113
|
+
prev = _prev_type_info(parent)
|
|
1083
1114
|
return ToInt(
|
|
1084
1115
|
parent=parent,
|
|
1085
1116
|
accept_type_info=prev.ret_type_info,
|
|
@@ -1092,7 +1123,7 @@ def _expr_to_int(
|
|
|
1092
1123
|
def _expr_to_float(
|
|
1093
1124
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
1094
1125
|
):
|
|
1095
|
-
prev = parent
|
|
1126
|
+
prev = _prev_type_info(parent)
|
|
1096
1127
|
return ToFloat(
|
|
1097
1128
|
parent=parent,
|
|
1098
1129
|
accept_type_info=prev.ret_type_info,
|
|
@@ -1108,7 +1139,7 @@ def _expr_to_bool(
|
|
|
1108
1139
|
# Guard against empty parent.body is enforced centrally in
|
|
1109
1140
|
# parse_expressions via _REQUIRES_PREV — handler is reached only when a
|
|
1110
1141
|
# preceding node exists.
|
|
1111
|
-
prev = parent
|
|
1142
|
+
prev = _prev_type_info(parent)
|
|
1112
1143
|
return ToBool(parent=parent, accept_type_info=prev.ret_type_info)
|
|
1113
1144
|
|
|
1114
1145
|
|
|
@@ -1164,7 +1195,7 @@ def _expr_fallback(
|
|
|
1164
1195
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
1165
1196
|
):
|
|
1166
1197
|
value = [] if not node.args else node.args[0].value
|
|
1167
|
-
prev = parent
|
|
1198
|
+
prev = _prev_type_info(parent)
|
|
1168
1199
|
prev_ti = prev.ret_type_info
|
|
1169
1200
|
fb = Fallback(
|
|
1170
1201
|
parent=parent,
|
|
@@ -1189,7 +1220,7 @@ def _expr_filter(
|
|
|
1189
1220
|
ret_type_info=TypeInfo(base=VariableType.DOCUMENT, is_array=True),
|
|
1190
1221
|
is_array=True,
|
|
1191
1222
|
)
|
|
1192
|
-
prev = parent
|
|
1223
|
+
prev = _prev_type_info(parent)
|
|
1193
1224
|
return Filter(
|
|
1194
1225
|
parent=parent,
|
|
1195
1226
|
accept_type_info=prev.ret_type_info,
|
|
@@ -49,7 +49,7 @@ def _node_args(node: KdlNode) -> list[str]:
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
_VALID_STRUCT_TYPES = frozenset(
|
|
52
|
-
{"item", "list", "dict", "table", "flat", "rest"}
|
|
52
|
+
{"item", "list", "dict", "table", "flat", "rest", "raw"}
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
_REQUIRED_RESERVED: dict[str, frozenset[str]] = {
|
|
@@ -59,15 +59,18 @@ _REQUIRED_RESERVED: dict[str, frozenset[str]] = {
|
|
|
59
59
|
"table": frozenset({"@table", "@rows", "@match", "@value"}),
|
|
60
60
|
"flat": frozenset(),
|
|
61
61
|
"rest": frozenset({"@request"}),
|
|
62
|
+
"raw": frozenset(),
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
_RESERVED_ALLOWED: dict[str, frozenset[str] | None] = {
|
|
65
66
|
"@request": None,
|
|
66
67
|
"@doc": None,
|
|
67
|
-
"@pre-validate": frozenset(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"@
|
|
68
|
+
"@pre-validate": frozenset(
|
|
69
|
+
{"item", "list", "dict", "table", "flat", "raw"}
|
|
70
|
+
),
|
|
71
|
+
"@check": frozenset({"item", "list", "dict", "table", "flat", "raw"}),
|
|
72
|
+
"@init": frozenset({"item", "list", "dict", "table", "flat", "raw"}),
|
|
73
|
+
"@split-doc": frozenset({"list", "dict", "raw"}),
|
|
71
74
|
"@key": frozenset({"dict"}),
|
|
72
75
|
"@value": frozenset({"dict", "table"}),
|
|
73
76
|
"@table": frozenset({"table"}),
|
|
@@ -6,7 +6,11 @@ from collections.abc import Sequence
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from ssc_codegen.ast import (
|
|
9
|
+
Attr,
|
|
9
10
|
CheckMethod,
|
|
11
|
+
CssRemove,
|
|
12
|
+
CssSelect,
|
|
13
|
+
CssSelectAll,
|
|
10
14
|
ErrorResponse,
|
|
11
15
|
Field,
|
|
12
16
|
InitField,
|
|
@@ -18,6 +22,7 @@ from ssc_codegen.ast import (
|
|
|
18
22
|
MethodRest,
|
|
19
23
|
Node,
|
|
20
24
|
PreValidate,
|
|
25
|
+
Raw,
|
|
21
26
|
SplitDoc,
|
|
22
27
|
StartParse,
|
|
23
28
|
StructBase,
|
|
@@ -27,9 +32,13 @@ from ssc_codegen.ast import (
|
|
|
27
32
|
TableConfig,
|
|
28
33
|
TableMatchKey,
|
|
29
34
|
TableRows,
|
|
35
|
+
Text,
|
|
30
36
|
TypeInfo,
|
|
31
37
|
Value,
|
|
32
38
|
VariableType,
|
|
39
|
+
XpathRemove,
|
|
40
|
+
XpathSelect,
|
|
41
|
+
XpathSelectAll,
|
|
33
42
|
)
|
|
34
43
|
from ssc_codegen.request_spec import parse_to_http
|
|
35
44
|
from kdlquery import KdlNode
|
|
@@ -38,6 +47,41 @@ from ssc_codegen.core.contexts import LintContext, ParseContext, WalkCtx
|
|
|
38
47
|
from ssc_codegen.core.expressions import parse_expressions
|
|
39
48
|
from ssc_codegen.core.type_checking import check_pipeline_types
|
|
40
49
|
|
|
50
|
+
# AST node types forbidden in (raw)struct — they require a DOM document.
|
|
51
|
+
_RAW_FORBIDDEN_OPS = (
|
|
52
|
+
CssSelect,
|
|
53
|
+
CssSelectAll,
|
|
54
|
+
CssRemove,
|
|
55
|
+
XpathSelect,
|
|
56
|
+
XpathSelectAll,
|
|
57
|
+
XpathRemove,
|
|
58
|
+
Text,
|
|
59
|
+
Attr,
|
|
60
|
+
Raw,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _struct_start_type(struct: StructBase) -> VariableType:
|
|
65
|
+
"""Return the pipeline start type for a struct's fields."""
|
|
66
|
+
if isinstance(struct, Struct) and struct.type == StructType.RAW:
|
|
67
|
+
return VariableType.STRING
|
|
68
|
+
return VariableType.DOCUMENT
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _lint_raw_forbidden_ops(expr: Node, lint: LintContext) -> None:
|
|
72
|
+
"""Recursively check that no HTML-only ops appear in a RAW struct node."""
|
|
73
|
+
for child in expr.body:
|
|
74
|
+
if isinstance(child, _RAW_FORBIDDEN_OPS):
|
|
75
|
+
lint.error(
|
|
76
|
+
child, # type: ignore[arg-type]
|
|
77
|
+
message=(
|
|
78
|
+
f"HTML operation '{type(child).__name__}' is forbidden in"
|
|
79
|
+
" (raw)struct — the document is a plain string, not a DOM"
|
|
80
|
+
),
|
|
81
|
+
code="E001",
|
|
82
|
+
)
|
|
83
|
+
_lint_raw_forbidden_ops(child, lint)
|
|
84
|
+
|
|
41
85
|
|
|
42
86
|
def parse_struct(
|
|
43
87
|
kdl_nodes: Sequence[KdlNode],
|
|
@@ -56,7 +100,11 @@ def parse_struct(
|
|
|
56
100
|
_parse_init_fields(node.children, parent, ctx, lint)
|
|
57
101
|
elif node.name == "@pre-validate":
|
|
58
102
|
expr = PreValidate(parent=parent)
|
|
103
|
+
if isinstance(parent, Struct) and parent.type == StructType.RAW:
|
|
104
|
+
expr.accept_type_info = TypeInfo(base=VariableType.STRING)
|
|
59
105
|
parse_expressions(node.children, expr, ctx, lint)
|
|
106
|
+
if isinstance(parent, Struct) and parent.type == StructType.RAW:
|
|
107
|
+
_lint_raw_forbidden_ops(expr, lint)
|
|
60
108
|
parent.body.append(expr)
|
|
61
109
|
elif node.name == "@check":
|
|
62
110
|
if not node.args:
|
|
@@ -68,11 +116,22 @@ def parse_struct(
|
|
|
68
116
|
continue
|
|
69
117
|
check_name = str(node.args[0].value)
|
|
70
118
|
expr = CheckMethod(parent=parent, name=check_name)
|
|
119
|
+
if isinstance(parent, Struct) and parent.type == StructType.RAW:
|
|
120
|
+
expr.accept_type_info = TypeInfo(base=VariableType.STRING)
|
|
71
121
|
parse_expressions(node.children, expr, ctx, lint)
|
|
122
|
+
if isinstance(parent, Struct) and parent.type == StructType.RAW:
|
|
123
|
+
_lint_raw_forbidden_ops(expr, lint)
|
|
72
124
|
parent.body.append(expr)
|
|
73
125
|
elif node.name == "@split-doc":
|
|
74
126
|
expr = SplitDoc(parent=parent)
|
|
127
|
+
if isinstance(parent, Struct) and parent.type == StructType.RAW:
|
|
128
|
+
expr.accept_type_info = TypeInfo(base=VariableType.STRING)
|
|
129
|
+
expr.ret_type_info = TypeInfo(
|
|
130
|
+
base=VariableType.STRING, is_array=True
|
|
131
|
+
)
|
|
75
132
|
parse_expressions(node.children, expr, ctx, lint)
|
|
133
|
+
if isinstance(parent, Struct) and parent.type == StructType.RAW:
|
|
134
|
+
_lint_raw_forbidden_ops(expr, lint)
|
|
76
135
|
parent.body.append(expr)
|
|
77
136
|
elif node.name == "@key":
|
|
78
137
|
expr = Key(parent=parent)
|
|
@@ -218,12 +277,21 @@ def parse_struct(
|
|
|
218
277
|
)
|
|
219
278
|
parent.body.append(err)
|
|
220
279
|
else:
|
|
280
|
+
is_raw = (
|
|
281
|
+
isinstance(parent, Struct) and parent.type == StructType.RAW
|
|
282
|
+
)
|
|
221
283
|
if isinstance(parent, Struct) and parent.type == StructType.TABLE:
|
|
222
284
|
expr = Field(
|
|
223
285
|
parent=parent,
|
|
224
286
|
name=node.name,
|
|
225
287
|
accept_type_info=TypeInfo(base=VariableType.STRING),
|
|
226
288
|
)
|
|
289
|
+
elif is_raw:
|
|
290
|
+
expr = Field(
|
|
291
|
+
parent=parent,
|
|
292
|
+
name=node.name,
|
|
293
|
+
accept_type_info=TypeInfo(base=VariableType.STRING),
|
|
294
|
+
)
|
|
227
295
|
else:
|
|
228
296
|
expr = Field(parent=parent, name=node.name)
|
|
229
297
|
ops = list(node.children)
|
|
@@ -231,8 +299,13 @@ def parse_struct(
|
|
|
231
299
|
# Type inference for regular fields
|
|
232
300
|
if ops and not (len(ops) == 1 and ops[0].name == "nested"):
|
|
233
301
|
check_pipeline_types(
|
|
234
|
-
ops,
|
|
302
|
+
ops,
|
|
303
|
+
ctx,
|
|
304
|
+
lint,
|
|
305
|
+
start_type=_struct_start_type(parent),
|
|
235
306
|
)
|
|
307
|
+
if is_raw:
|
|
308
|
+
_lint_raw_forbidden_ops(expr, lint)
|
|
236
309
|
parent.body.append(expr)
|
|
237
310
|
|
|
238
311
|
if not isinstance(parent, StructRest):
|
|
@@ -321,18 +394,22 @@ def _parse_init_fields(
|
|
|
321
394
|
prev_ctx = lint.walk_context
|
|
322
395
|
lint.walk_context = WalkCtx.INIT_BLOCK
|
|
323
396
|
init = parent.init
|
|
397
|
+
is_raw = parent.type == StructType.RAW
|
|
398
|
+
start_type = _struct_start_type(parent)
|
|
324
399
|
for node in kdl_nodes:
|
|
325
400
|
lint.push(node.name)
|
|
326
401
|
lint.init_fields.add(node.name)
|
|
327
402
|
expr = InitField(parent=parent, name=node.name)
|
|
403
|
+
if is_raw:
|
|
404
|
+
expr.accept_type_info = TypeInfo(base=VariableType.STRING)
|
|
328
405
|
parse_expressions(node.children, expr, ctx, lint)
|
|
329
406
|
if expr.body:
|
|
330
407
|
expr.ret_type_info = expr.body[-1].ret_type_info
|
|
331
408
|
ops = list(node.children)
|
|
332
|
-
ret = check_pipeline_types(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
409
|
+
ret = check_pipeline_types(ops, ctx, lint, start_type=start_type)
|
|
410
|
+
lint.inferred_define_types[node.name] = (start_type, ret)
|
|
411
|
+
if is_raw:
|
|
412
|
+
_lint_raw_forbidden_ops(expr, lint)
|
|
336
413
|
parent.body.append(expr)
|
|
337
414
|
init.body.append(InitFieldCall(parent=init, name=node.name))
|
|
338
415
|
lint.pop()
|
|
@@ -25,6 +25,8 @@ from ssc_codegen.ast import (
|
|
|
25
25
|
TableRows,
|
|
26
26
|
TableMatchKey,
|
|
27
27
|
StructBase,
|
|
28
|
+
Struct,
|
|
29
|
+
StructType,
|
|
28
30
|
Module,
|
|
29
31
|
)
|
|
30
32
|
from ssc_codegen.ast.base import Node as AstNode
|
|
@@ -285,6 +287,20 @@ def check_struct_health(
|
|
|
285
287
|
soup = BeautifulSoup(html, "lxml")
|
|
286
288
|
result = HealthResult(struct_name=struct.name)
|
|
287
289
|
|
|
290
|
+
# RAW structs have no HTML selectors — skip health-check entirely.
|
|
291
|
+
if isinstance(struct, Struct) and struct.type == StructType.RAW:
|
|
292
|
+
result.checks.append(
|
|
293
|
+
SelectorCheck(
|
|
294
|
+
path=struct.name,
|
|
295
|
+
selector_type="raw",
|
|
296
|
+
query="",
|
|
297
|
+
matches=0,
|
|
298
|
+
status="warn",
|
|
299
|
+
message="(raw)struct has no HTML selectors to check",
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
return result
|
|
303
|
+
|
|
288
304
|
# collect selectors + nested refs, then recurse into nested structs
|
|
289
305
|
visited: set[str] = set()
|
|
290
306
|
_check_struct_recursive(
|
|
@@ -418,6 +418,9 @@ class GoVisitor(BaseWalker):
|
|
|
418
418
|
return f"[]{name}Type"
|
|
419
419
|
case ST.FLAT:
|
|
420
420
|
return "[]string"
|
|
421
|
+
case ST.RAW:
|
|
422
|
+
has_split = any(isinstance(n, SplitDoc) for n in struct.body)
|
|
423
|
+
return f"[]{name}Type" if has_split else f"{name}Type"
|
|
421
424
|
case _:
|
|
422
425
|
return "any"
|
|
423
426
|
|
|
@@ -516,7 +519,7 @@ class GoVisitor(BaseWalker):
|
|
|
516
519
|
return [f"type {name}Type = map[string]{vt}", ""]
|
|
517
520
|
case ST.TABLE:
|
|
518
521
|
return [f"type {name}Type = map[string]any", ""]
|
|
519
|
-
case ST.ITEM | ST.LIST:
|
|
522
|
+
case ST.ITEM | ST.LIST | ST.RAW:
|
|
520
523
|
lines = [f"type {name}Type struct {{"]
|
|
521
524
|
lines.extend(self.walk_children(node, ctx))
|
|
522
525
|
lines.append("}")
|
|
@@ -611,7 +614,9 @@ class GoVisitor(BaseWalker):
|
|
|
611
614
|
else f"{ctx.indent}//"
|
|
612
615
|
)
|
|
613
616
|
lines.append(f"type {name} struct {{")
|
|
614
|
-
|
|
617
|
+
is_raw = isinstance(node, Struct) and node.type == ST.RAW
|
|
618
|
+
sel_type = "string" if is_raw else "*goquery.Selection"
|
|
619
|
+
lines.append(f"\tsel {sel_type}")
|
|
615
620
|
init_fields = self._collect_init_fields(node)
|
|
616
621
|
for cn, gt, _ in init_fields:
|
|
617
622
|
lines.append(f"\t{cn} {gt}")
|
|
@@ -633,18 +638,28 @@ class GoVisitor(BaseWalker):
|
|
|
633
638
|
) -> list[str]:
|
|
634
639
|
i = ctx.indent
|
|
635
640
|
i2 = ctx.deeper().indent
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
641
|
+
is_raw = isinstance(node, Struct) and node.type == ST.RAW
|
|
642
|
+
if is_raw:
|
|
643
|
+
lines: list[str] = [
|
|
644
|
+
f"func New{name}(input string) *{name} {{",
|
|
645
|
+
f"{i2}{rcv} := &{name}{{sel: input}}",
|
|
646
|
+
]
|
|
647
|
+
else:
|
|
648
|
+
lines = [
|
|
649
|
+
f"func New{name}(input string) (*{name}, error) {{",
|
|
650
|
+
f"{i2}doc, err := goquery.NewDocumentFromReader(strings.NewReader(input))",
|
|
651
|
+
f"{i2}if err != nil {{",
|
|
652
|
+
f"{i2}\treturn nil, err",
|
|
653
|
+
f"{i2}}}",
|
|
654
|
+
f"{i2}{rcv} := &{name}{{sel: doc.Selection}}",
|
|
655
|
+
]
|
|
644
656
|
init_fields = self._collect_init_fields(node)
|
|
645
657
|
if init_fields:
|
|
646
658
|
lines.append(f"{i2}{rcv}.init()")
|
|
647
|
-
|
|
659
|
+
if is_raw:
|
|
660
|
+
lines.append(f"{i2}return {rcv}")
|
|
661
|
+
else:
|
|
662
|
+
lines.append(f"{i2}return {rcv}, nil")
|
|
648
663
|
lines.append(f"{i}}}")
|
|
649
664
|
lines.append("")
|
|
650
665
|
return lines
|
|
@@ -935,6 +950,31 @@ class GoVisitor(BaseWalker):
|
|
|
935
950
|
lines.append(f"{i2}}}")
|
|
936
951
|
else:
|
|
937
952
|
lines.append(f"{i2}return {name}Type{{}}")
|
|
953
|
+
case ST.RAW:
|
|
954
|
+
if node.use_split_doc:
|
|
955
|
+
lines.append(f"{i2}rows := {rcv}.splitDoc({rcv}.sel)")
|
|
956
|
+
lines.append(
|
|
957
|
+
f"{i2}result := make([]{name}Type, 0, len(rows))"
|
|
958
|
+
)
|
|
959
|
+
lines.append(f"{i2}for _, item := range rows {{")
|
|
960
|
+
lines.append(f"{i3}result = append(result, {name}Type{{")
|
|
961
|
+
for f in node.fields:
|
|
962
|
+
fn = _go_field_name(f.name)
|
|
963
|
+
mn = _go_method_name(f.name)
|
|
964
|
+
lines.append(f"{i4}{fn}: {rcv}.{mn}(item),")
|
|
965
|
+
lines.append(f"{i3}}})")
|
|
966
|
+
lines.append(f"{i2}}}")
|
|
967
|
+
lines.append(f"{i2}return result")
|
|
968
|
+
else:
|
|
969
|
+
if node.fields:
|
|
970
|
+
lines.append(f"{i2}return {name}Type{{")
|
|
971
|
+
for f in node.fields:
|
|
972
|
+
fn = _go_field_name(f.name)
|
|
973
|
+
mn = _go_method_name(f.name)
|
|
974
|
+
lines.append(f"{i3}{fn}: {rcv}.{mn}({rcv}.sel),")
|
|
975
|
+
lines.append(f"{i2}}}")
|
|
976
|
+
else:
|
|
977
|
+
lines.append(f"{i2}return {name}Type{{}}")
|
|
938
978
|
case ST.LIST:
|
|
939
979
|
lines.append(f"{i2}rows := {rcv}.splitDoc({rcv}.sel)")
|
|
940
980
|
lines.append(
|
|
@@ -465,14 +465,22 @@ class JsVisitor(BaseWalker):
|
|
|
465
465
|
if isinstance(node.parent, StructRest):
|
|
466
466
|
return []
|
|
467
467
|
i1, i2, i3 = ctx.indent, ctx.indent * 2, ctx.indent * 3
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
468
|
+
struct = node.parent
|
|
469
|
+
is_raw = isinstance(struct, Struct) and struct.type == ST.RAW
|
|
470
|
+
if is_raw:
|
|
471
|
+
lines = [
|
|
472
|
+
f"{i1}constructor(document) {{",
|
|
473
|
+
f"{i2}this._doc = document;",
|
|
474
|
+
]
|
|
475
|
+
else:
|
|
476
|
+
lines = [
|
|
477
|
+
f"{i1}constructor(document) {{",
|
|
478
|
+
f"{i2}if (typeof document === 'string') {{",
|
|
479
|
+
f"{i3}this._doc = (new DOMParser()).parseFromString(document, 'text/html');",
|
|
480
|
+
f"{i2}}} else {{",
|
|
481
|
+
f"{i3}this._doc = document;",
|
|
482
|
+
f"{i2}}}",
|
|
483
|
+
]
|
|
476
484
|
lines.extend(self.walk_children(node, ctx))
|
|
477
485
|
lines.append(f"{i1}}}")
|
|
478
486
|
return lines
|
|
@@ -574,6 +582,9 @@ class JsVisitor(BaseWalker):
|
|
|
574
582
|
ret_type = f"Array<{name}Type>"
|
|
575
583
|
elif st == ST.FLAT:
|
|
576
584
|
ret_type = "Array<string>"
|
|
585
|
+
elif st == ST.RAW:
|
|
586
|
+
has_split = any(isinstance(n, SplitDoc) for n in struct.body)
|
|
587
|
+
ret_type = f"Array<{name}Type>" if has_split else f"{name}Type"
|
|
577
588
|
else:
|
|
578
589
|
ret_type = f"{name}Type"
|
|
579
590
|
i1, i2, i3 = ctx.indent, ctx.indent * 2, ctx.indent * 3
|
|
@@ -642,6 +653,24 @@ class JsVisitor(BaseWalker):
|
|
|
642
653
|
)
|
|
643
654
|
lines.append(f"{i2}}}")
|
|
644
655
|
lines.append(f"{i2}return _result;")
|
|
656
|
+
elif st == ST.RAW:
|
|
657
|
+
has_split = node.use_split_doc
|
|
658
|
+
if has_split:
|
|
659
|
+
lines.append(
|
|
660
|
+
f"{i2}return Array.from(this._splitDoc(this._doc)).map(i => ({{"
|
|
661
|
+
)
|
|
662
|
+
for f in node.fields:
|
|
663
|
+
n = to_camel_case(f.name)
|
|
664
|
+
lines.append(f"{i3}{n}: this.{_js_method_name(f.name)}(i),")
|
|
665
|
+
lines.append(f"{i2}}}));")
|
|
666
|
+
else:
|
|
667
|
+
lines.append(f"{i2}return {{")
|
|
668
|
+
for f in node.fields:
|
|
669
|
+
n = to_camel_case(f.name)
|
|
670
|
+
lines.append(
|
|
671
|
+
f"{i3}{n}: this.{_js_method_name(f.name)}(this._doc),"
|
|
672
|
+
)
|
|
673
|
+
lines.append(f"{i2}}};")
|
|
645
674
|
lines.append(f"{i1}}}")
|
|
646
675
|
return lines
|
|
647
676
|
|
|
@@ -29,8 +29,8 @@ from ssc_codegen.targets.python.http_libs.base import HttpLibStrategy
|
|
|
29
29
|
from ssc_codegen.traversal.context import WalkContext
|
|
30
30
|
from ssc_codegen.traversal.utils import (
|
|
31
31
|
dict_needs_builder,
|
|
32
|
+
module_has_html_struct,
|
|
32
33
|
module_has_rest,
|
|
33
|
-
module_is_rest_only,
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
|
|
@@ -245,7 +245,7 @@ def runtime_export_names(
|
|
|
245
245
|
- Module with REST structs: also import REST names.
|
|
246
246
|
"""
|
|
247
247
|
names: list[str] = list(_RUNTIME_ALWAYS_EXPORT_NAMES)
|
|
248
|
-
if
|
|
248
|
+
if module_has_html_struct(module):
|
|
249
249
|
names.extend(_RUNTIME_HTML_EXPORT_NAMES)
|
|
250
250
|
if need_fallback:
|
|
251
251
|
names.append("FALLBACK_HTML_STR")
|
|
@@ -125,8 +125,8 @@ from ssc_codegen.ast import (
|
|
|
125
125
|
from ssc_codegen.naming import to_pascal_case, to_snake_case
|
|
126
126
|
from ssc_codegen.traversal.utils import (
|
|
127
127
|
jsonify_path_to_segments,
|
|
128
|
+
module_has_html_struct,
|
|
128
129
|
module_has_rest,
|
|
129
|
-
module_is_rest_only,
|
|
130
130
|
module_uses_http,
|
|
131
131
|
)
|
|
132
132
|
from ssc_codegen.generation.builder import ModuleBuilder
|
|
@@ -340,6 +340,11 @@ class PythonVisitor(BaseWalker):
|
|
|
340
340
|
return self.ARRAY_TYPE_FMT.format(f"{name}Type")
|
|
341
341
|
case ST.FLAT:
|
|
342
342
|
return self.ARRAY_TYPE_FMT.format(self.TYPES[VT.STRING])
|
|
343
|
+
case ST.RAW:
|
|
344
|
+
has_split = any(isinstance(n, SplitDoc) for n in struct.body)
|
|
345
|
+
if has_split:
|
|
346
|
+
return self.ARRAY_TYPE_FMT.format(f"{name}Type")
|
|
347
|
+
return f"{name}Type"
|
|
343
348
|
case _:
|
|
344
349
|
return self.TYPES[VT.STRING]
|
|
345
350
|
|
|
@@ -350,10 +355,10 @@ class PythonVisitor(BaseWalker):
|
|
|
350
355
|
if node.doc:
|
|
351
356
|
lines.extend(['"""', node.doc, '"""'])
|
|
352
357
|
has_rest = module_has_rest(node)
|
|
353
|
-
is_rest_only = module_is_rest_only(node)
|
|
354
358
|
uses_http = module_uses_http(node)
|
|
359
|
+
has_html = module_has_html_struct(node)
|
|
355
360
|
runtime = ctx.meta.get("runtime_module")
|
|
356
|
-
if
|
|
361
|
+
if has_html:
|
|
357
362
|
for line in self._dom.parser_imports:
|
|
358
363
|
self._builder.require_import(line)
|
|
359
364
|
self._builder.require_import(
|
|
@@ -419,8 +424,8 @@ class PythonVisitor(BaseWalker):
|
|
|
419
424
|
lines.extend(self._builder.imports)
|
|
420
425
|
lines.append("")
|
|
421
426
|
mod = node.parent
|
|
422
|
-
|
|
423
|
-
if
|
|
427
|
+
has_html = isinstance(mod, Module) and module_has_html_struct(mod)
|
|
428
|
+
if has_html:
|
|
424
429
|
lines.extend(
|
|
425
430
|
[
|
|
426
431
|
"class UnmatchedTableRow:",
|
|
@@ -481,7 +486,7 @@ class PythonVisitor(BaseWalker):
|
|
|
481
486
|
return self.walk_children(node, ctx)
|
|
482
487
|
case ST.FLAT:
|
|
483
488
|
return [f"{name}Type = List[str]"]
|
|
484
|
-
case ST.ITEM | ST.LIST | ST.TABLE:
|
|
489
|
+
case ST.ITEM | ST.LIST | ST.TABLE | ST.RAW:
|
|
485
490
|
lines = [f'{name}Type = TypedDict("{name}Type", {{']
|
|
486
491
|
lines.extend(self.walk_children(node, ctx))
|
|
487
492
|
lines.append("})")
|
|
@@ -552,13 +557,21 @@ class PythonVisitor(BaseWalker):
|
|
|
552
557
|
ctx.deeper().indent,
|
|
553
558
|
ctx.deeper().deeper().indent,
|
|
554
559
|
)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
560
|
+
struct = node.parent
|
|
561
|
+
is_raw = isinstance(struct, Struct) and struct.type == ST.RAW
|
|
562
|
+
if is_raw:
|
|
563
|
+
lines = [
|
|
564
|
+
f"{i}def __init__(self, document: str):",
|
|
565
|
+
f"{i2}self._doc = document",
|
|
566
|
+
]
|
|
567
|
+
else:
|
|
568
|
+
lines = [
|
|
569
|
+
f"{i}def __init__(self, document: {self._dom.init_arg_type}):",
|
|
570
|
+
f"{i2}if isinstance(document, str):",
|
|
571
|
+
f"{i3}self._doc = {self._dom.init_from_str_expr}",
|
|
572
|
+
f"{i2}else:",
|
|
573
|
+
f"{i3}self._doc = document",
|
|
574
|
+
]
|
|
562
575
|
lines.extend(self.walk_children(node, ctx))
|
|
563
576
|
return lines
|
|
564
577
|
|
|
@@ -713,6 +726,21 @@ class PythonVisitor(BaseWalker):
|
|
|
713
726
|
lines.append(f"{i4}_result[{fn!r}] = _{fn}")
|
|
714
727
|
lines.append(f"{i4}continue")
|
|
715
728
|
lines.append(f"{i2}return _result")
|
|
729
|
+
case ST.RAW:
|
|
730
|
+
if node.use_split_doc:
|
|
731
|
+
lines.append(f"{i2}return [{{")
|
|
732
|
+
for field in node.fields:
|
|
733
|
+
fn = to_snake_case(field.name)
|
|
734
|
+
lines.append(f"{i2}{fn!r}: self._parse_{fn}(i),")
|
|
735
|
+
lines.append(f"{i3}}} for i in self._split_doc(self._doc)]")
|
|
736
|
+
else:
|
|
737
|
+
lines.append(f"{i2}return {{")
|
|
738
|
+
for field in node.fields:
|
|
739
|
+
fn = to_snake_case(field.name)
|
|
740
|
+
lines.append(
|
|
741
|
+
f"{i3}{fn!r}: self._parse_{fn}(self._doc),"
|
|
742
|
+
)
|
|
743
|
+
lines.append(f"{i3}}}")
|
|
716
744
|
return lines
|
|
717
745
|
|
|
718
746
|
# === REST / FETCH (delegate to rest.py) ===
|
|
@@ -13,8 +13,10 @@ from ssc_codegen.ast import (
|
|
|
13
13
|
PlaceholderSpec,
|
|
14
14
|
PlaceholderTemplate,
|
|
15
15
|
PreValidate,
|
|
16
|
+
Struct,
|
|
16
17
|
StructBase,
|
|
17
18
|
StructRest,
|
|
19
|
+
StructType,
|
|
18
20
|
)
|
|
19
21
|
|
|
20
22
|
|
|
@@ -46,6 +48,17 @@ def module_is_rest_only(module: Module) -> bool:
|
|
|
46
48
|
return len(structs) == 0 or all(isinstance(s, StructRest) for s in structs)
|
|
47
49
|
|
|
48
50
|
|
|
51
|
+
def module_has_html_struct(module: Module) -> bool:
|
|
52
|
+
"""True if the module has at least one HTML-parsing struct (not REST, not RAW)."""
|
|
53
|
+
for n in module.body:
|
|
54
|
+
if isinstance(n, StructRest):
|
|
55
|
+
continue
|
|
56
|
+
if isinstance(n, Struct):
|
|
57
|
+
if n.type != StructType.RAW:
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
49
62
|
def err_subclass_name(struct_name: str, err: ErrorResponse) -> str:
|
|
50
63
|
"""Deterministic error-subclass name from struct name + error spec."""
|
|
51
64
|
from ssc_codegen.core.rest_artifacts import (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ssc_codegen-0.32.3 → ssc_codegen-0.33.0}/ssc_codegen/targets/javascript/http_libs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|