ssc_codegen 0.32.3__tar.gz → 0.34.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.34.0}/PKG-INFO +11 -8
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/README.md +10 -7
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/pyproject.toml +1 -1
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/__init__.py +4 -0
- ssc_codegen-0.34.0/ssc_codegen/ast/function.py +28 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/types.py +1 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/expressions.py +70 -37
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/linter.py +73 -6
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/module_handler.py +31 -1
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/reader.py +6 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/struct_parser.py +124 -5
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/health.py +16 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/visitor.py +81 -14
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/javascript/visitor.py +63 -8
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/rest.py +2 -2
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/visitor.py +67 -13
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/traversal/utils.py +21 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/traversal/walker.py +4 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/.gitignore +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/LICENSE +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/_logging.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/array.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/cast.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/control.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/extract.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/helpers.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/jsondef.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/module.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/predicate_containers.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/predicate_ops.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/regex.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/rest.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/selectors.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/string.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/struct.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/ast/typedef.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/contexts.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/format.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/predicates.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/rest_artifacts.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/core/type_checking.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/exceptions.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/explore.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/generation/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/generation/builder.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/generation/runtime.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/main.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/naming.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/parsers/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/parsers/curl.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/parsers/http.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/regex_utils.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/request_spec.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/http_libs/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/http_libs/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/http_libs/nethttp.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/literals.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/regex.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/rest.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/golang/runtime.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/javascript/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/javascript/http_libs/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/javascript/http_libs/axios.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/javascript/http_libs/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/javascript/http_libs/fetch.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/javascript/rest.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/profile.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/html_libs/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/html_libs/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/html_libs/bs4.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/html_libs/lxml.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/html_libs/parsel.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/html_libs/slax.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/http_libs/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/http_libs/aiohttp.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/http_libs/base.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/http_libs/httpx.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/python/http_libs/requests.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/resolver.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/targets/spec.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/traversal/__init__.py +0 -0
- {ssc_codegen-0.32.3 → ssc_codegen-0.34.0}/ssc_codegen/traversal/context.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssc_codegen
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.34.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
|
|
@@ -20,6 +20,8 @@ from .module import (
|
|
|
20
20
|
Utilities,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
from .function import FunctionDef
|
|
24
|
+
|
|
23
25
|
from .typedef import TypeDef, TypeDefField
|
|
24
26
|
|
|
25
27
|
from .jsondef import JsonDef, JsonDefField
|
|
@@ -147,6 +149,8 @@ __all__ = [
|
|
|
147
149
|
"CodeEndHook",
|
|
148
150
|
"Docstring",
|
|
149
151
|
"Utilities",
|
|
152
|
+
# function
|
|
153
|
+
"FunctionDef",
|
|
150
154
|
# typedef
|
|
151
155
|
"TypeDef",
|
|
152
156
|
"TypeDefField",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .base import Node
|
|
6
|
+
from .types import TypeInfo, VariableType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class FunctionDef(Node):
|
|
11
|
+
"""Module-level single-value extraction function.
|
|
12
|
+
|
|
13
|
+
DSL: ``fn <name> { pipeline }`` or ``(raw)fn <name> { pipeline }``.
|
|
14
|
+
|
|
15
|
+
Generates a standalone function instead of a class.
|
|
16
|
+
``is_raw`` selects the document type: HTML (DOCUMENT) or plain text (STRING).
|
|
17
|
+
``ret_type_info`` is resolved after pipeline build (AUTO → concrete type).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name: str = ""
|
|
21
|
+
is_raw: bool = False
|
|
22
|
+
doc: str = ""
|
|
23
|
+
accept_type_info: TypeInfo = field(
|
|
24
|
+
default_factory=lambda: TypeInfo(base=VariableType.DOCUMENT)
|
|
25
|
+
)
|
|
26
|
+
ret_type_info: TypeInfo = field(
|
|
27
|
+
default_factory=lambda: TypeInfo(base=VariableType.AUTO)
|
|
28
|
+
)
|
|
@@ -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 (
|
|
@@ -18,6 +19,7 @@ from ssc_codegen.ast import (
|
|
|
18
19
|
Field,
|
|
19
20
|
Filter,
|
|
20
21
|
Fmt,
|
|
22
|
+
FunctionDef,
|
|
21
23
|
Index,
|
|
22
24
|
InitField,
|
|
23
25
|
JsonDef,
|
|
@@ -103,6 +105,7 @@ FieldLikeNode: TypeAlias = (
|
|
|
103
105
|
| Value
|
|
104
106
|
| Field
|
|
105
107
|
| InitField
|
|
108
|
+
| FunctionDef
|
|
106
109
|
)
|
|
107
110
|
|
|
108
111
|
|
|
@@ -206,7 +209,7 @@ def resolve_index_types(
|
|
|
206
209
|
the element base with is_array=False.
|
|
207
210
|
"""
|
|
208
211
|
if parent.body:
|
|
209
|
-
prev = parent
|
|
212
|
+
prev = _prev_type_info(parent)
|
|
210
213
|
ret_ti = TypeInfo(base=prev.ret_type_info.base)
|
|
211
214
|
return prev.ret_type_info, ret_ti, prev.is_array
|
|
212
215
|
auto = TypeInfo(base=VariableType.AUTO)
|
|
@@ -420,18 +423,26 @@ def parse_expressions(
|
|
|
420
423
|
raise BuildTimeError(f"Unknown expression: {node.name}")
|
|
421
424
|
lint.push(node.name)
|
|
422
425
|
lint_pipeline_op(node, lint)
|
|
423
|
-
# Guard: ops in _REQUIRES_PREV access parent.body[-1]
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
+
# Guard: ops in _REQUIRES_PREV access parent.body[-1] in their
|
|
427
|
+
# handlers. Emit a diagnostic and skip when the pipeline is empty —
|
|
428
|
+
# UNLESS the parent starts with STRING (RAW struct fields), where
|
|
429
|
+
# string ops are valid as the first operation.
|
|
426
430
|
if not parent.body and node.name in _REQUIRES_PREV:
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
431
|
+
parent_accept = getattr(parent, "accept_type_info", None)
|
|
432
|
+
if (
|
|
433
|
+
parent_accept is not None
|
|
434
|
+
and parent_accept.base == VariableType.STRING
|
|
435
|
+
):
|
|
436
|
+
pass # RAW struct field — string ops OK as first node
|
|
437
|
+
else:
|
|
438
|
+
lint.error(
|
|
439
|
+
node,
|
|
440
|
+
message=(f"'{node.name}' requires a preceding operation"),
|
|
441
|
+
code="E100",
|
|
442
|
+
hint="add a producer first, e.g. `css '.x'; text`",
|
|
443
|
+
)
|
|
444
|
+
lint.pop()
|
|
445
|
+
continue
|
|
435
446
|
# Guard: handlers reading node.args[i] without a bounds check would
|
|
436
447
|
# crash with `tuple index out of range` when the user omits args.
|
|
437
448
|
required_args = _MIN_ARGS.get(node.name)
|
|
@@ -471,8 +482,8 @@ def parse_expressions(
|
|
|
471
482
|
)
|
|
472
483
|
parent.ret_type_info = last_ti
|
|
473
484
|
|
|
474
|
-
# Populate type_info on Field/Value/InitField
|
|
475
|
-
if isinstance(parent, (Field, Value, InitField)):
|
|
485
|
+
# Populate type_info on Field/Value/InitField/FunctionDef
|
|
486
|
+
if isinstance(parent, (Field, Value, InitField, FunctionDef)):
|
|
476
487
|
last_ret = last_ti.base
|
|
477
488
|
is_arr = (
|
|
478
489
|
parent.body[-2].is_array if len(parent.body) >= 2 else False
|
|
@@ -504,6 +515,28 @@ _ExpressionHandler = Callable[
|
|
|
504
515
|
_EXPRESSION_HANDLERS: dict[str, _ExpressionHandler] = {}
|
|
505
516
|
|
|
506
517
|
|
|
518
|
+
def _prev_type_info(
|
|
519
|
+
parent: FieldLikeNode,
|
|
520
|
+
) -> SimpleNamespace:
|
|
521
|
+
"""Return a namespace with ``ret_type_info`` and ``is_array`` for the
|
|
522
|
+
pipeline cursor.
|
|
523
|
+
|
|
524
|
+
When the pipeline is non-empty, uses the last node's ret type.
|
|
525
|
+
When empty (first op), falls back to the parent's accept type —
|
|
526
|
+
this lets RAW struct fields start directly with string ops
|
|
527
|
+
(re/split/fmt/etc.) since their document is already a STRING.
|
|
528
|
+
"""
|
|
529
|
+
if parent.body:
|
|
530
|
+
prev = parent.body[-1]
|
|
531
|
+
return SimpleNamespace(
|
|
532
|
+
ret_type_info=prev.ret_type_info, is_array=prev.is_array
|
|
533
|
+
)
|
|
534
|
+
return SimpleNamespace(
|
|
535
|
+
ret_type_info=parent.accept_type_info,
|
|
536
|
+
is_array=parent.accept_type_info.is_array,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
|
|
507
540
|
# Ops whose handlers access ``parent.body[-1]`` unconditionally and therefore
|
|
508
541
|
# require a preceding node. Used by ``parse_expressions`` to emit a clean
|
|
509
542
|
# E100 diagnostic instead of crashing with IndexError on an empty pipeline.
|
|
@@ -659,7 +692,7 @@ def _expr_text(
|
|
|
659
692
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
660
693
|
):
|
|
661
694
|
if parent.body:
|
|
662
|
-
prev = parent
|
|
695
|
+
prev = _prev_type_info(parent)
|
|
663
696
|
accept_ti = prev.ret_type_info
|
|
664
697
|
is_arr = prev.is_array
|
|
665
698
|
else:
|
|
@@ -678,7 +711,7 @@ def _expr_raw(
|
|
|
678
711
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
679
712
|
):
|
|
680
713
|
if parent.body:
|
|
681
|
-
prev = parent
|
|
714
|
+
prev = _prev_type_info(parent)
|
|
682
715
|
accept_ti = prev.ret_type_info
|
|
683
716
|
is_arr = prev.is_array
|
|
684
717
|
else:
|
|
@@ -697,7 +730,7 @@ def _expr_attr(
|
|
|
697
730
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
698
731
|
):
|
|
699
732
|
if parent.body:
|
|
700
|
-
prev = parent
|
|
733
|
+
prev = _prev_type_info(parent)
|
|
701
734
|
accept_ti = prev.ret_type_info
|
|
702
735
|
is_arr = prev.is_array
|
|
703
736
|
else:
|
|
@@ -727,7 +760,7 @@ def _expr_trim(
|
|
|
727
760
|
if node.args
|
|
728
761
|
else ""
|
|
729
762
|
)
|
|
730
|
-
prev = parent
|
|
763
|
+
prev = _prev_type_info(parent)
|
|
731
764
|
return Trim(
|
|
732
765
|
parent=parent,
|
|
733
766
|
accept_type_info=prev.ret_type_info,
|
|
@@ -749,7 +782,7 @@ def _expr_ltrim(
|
|
|
749
782
|
if node.args
|
|
750
783
|
else ""
|
|
751
784
|
)
|
|
752
|
-
prev = parent
|
|
785
|
+
prev = _prev_type_info(parent)
|
|
753
786
|
return Ltrim(
|
|
754
787
|
parent=parent,
|
|
755
788
|
accept_type_info=prev.ret_type_info,
|
|
@@ -771,7 +804,7 @@ def _expr_rtrim(
|
|
|
771
804
|
if node.args
|
|
772
805
|
else ""
|
|
773
806
|
)
|
|
774
|
-
prev = parent
|
|
807
|
+
prev = _prev_type_info(parent)
|
|
775
808
|
return Rtrim(
|
|
776
809
|
parent=parent,
|
|
777
810
|
accept_type_info=prev.ret_type_info,
|
|
@@ -785,7 +818,7 @@ def _expr_rtrim(
|
|
|
785
818
|
def _expr_norm_space(
|
|
786
819
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
787
820
|
):
|
|
788
|
-
prev = parent
|
|
821
|
+
prev = _prev_type_info(parent)
|
|
789
822
|
return NormalizeSpace(
|
|
790
823
|
parent=parent,
|
|
791
824
|
accept_type_info=prev.ret_type_info,
|
|
@@ -799,7 +832,7 @@ def _expr_rm_prefix(
|
|
|
799
832
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
800
833
|
):
|
|
801
834
|
substr = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
802
|
-
prev = parent
|
|
835
|
+
prev = _prev_type_info(parent)
|
|
803
836
|
return RmPrefix(
|
|
804
837
|
parent=parent,
|
|
805
838
|
accept_type_info=prev.ret_type_info,
|
|
@@ -814,7 +847,7 @@ def _expr_rm_suffix(
|
|
|
814
847
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
815
848
|
):
|
|
816
849
|
substr = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
817
|
-
prev = parent
|
|
850
|
+
prev = _prev_type_info(parent)
|
|
818
851
|
return RmSuffix(
|
|
819
852
|
parent=parent,
|
|
820
853
|
accept_type_info=prev.ret_type_info,
|
|
@@ -829,7 +862,7 @@ def _expr_rm_prefix_suffix(
|
|
|
829
862
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
830
863
|
):
|
|
831
864
|
substr = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
832
|
-
prev = parent
|
|
865
|
+
prev = _prev_type_info(parent)
|
|
833
866
|
return RmPrefixSuffix(
|
|
834
867
|
parent=parent,
|
|
835
868
|
accept_type_info=prev.ret_type_info,
|
|
@@ -844,7 +877,7 @@ def _expr_fmt(
|
|
|
844
877
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
845
878
|
):
|
|
846
879
|
tmpl = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
847
|
-
prev = parent
|
|
880
|
+
prev = _prev_type_info(parent)
|
|
848
881
|
return Fmt(
|
|
849
882
|
parent=parent,
|
|
850
883
|
accept_type_info=prev.ret_type_info,
|
|
@@ -858,7 +891,7 @@ def _expr_fmt(
|
|
|
858
891
|
def _expr_repl(
|
|
859
892
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
860
893
|
):
|
|
861
|
-
prev = parent
|
|
894
|
+
prev = _prev_type_info(parent)
|
|
862
895
|
if node.children:
|
|
863
896
|
items = {
|
|
864
897
|
str(child.name): str(child.args[0].value) for child in node.children
|
|
@@ -890,7 +923,7 @@ def _expr_repl(
|
|
|
890
923
|
def _expr_lower(
|
|
891
924
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
892
925
|
):
|
|
893
|
-
prev = parent
|
|
926
|
+
prev = _prev_type_info(parent)
|
|
894
927
|
return Lower(
|
|
895
928
|
parent=parent,
|
|
896
929
|
accept_type_info=prev.ret_type_info,
|
|
@@ -903,7 +936,7 @@ def _expr_lower(
|
|
|
903
936
|
def _expr_upper(
|
|
904
937
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
905
938
|
):
|
|
906
|
-
prev = parent
|
|
939
|
+
prev = _prev_type_info(parent)
|
|
907
940
|
return Upper(
|
|
908
941
|
parent=parent,
|
|
909
942
|
accept_type_info=prev.ret_type_info,
|
|
@@ -936,7 +969,7 @@ def _expr_join(
|
|
|
936
969
|
def _expr_unescape(
|
|
937
970
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
938
971
|
):
|
|
939
|
-
prev = parent
|
|
972
|
+
prev = _prev_type_info(parent)
|
|
940
973
|
return Unescape(
|
|
941
974
|
parent=parent,
|
|
942
975
|
accept_type_info=prev.ret_type_info,
|
|
@@ -954,7 +987,7 @@ def _expr_re(
|
|
|
954
987
|
):
|
|
955
988
|
raw = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
956
989
|
pattern = normalize_regex_pattern(raw)
|
|
957
|
-
prev = parent
|
|
990
|
+
prev = _prev_type_info(parent)
|
|
958
991
|
return Re(
|
|
959
992
|
parent=parent,
|
|
960
993
|
pattern=pattern,
|
|
@@ -978,7 +1011,7 @@ def _expr_re_all(
|
|
|
978
1011
|
def _expr_re_sub(
|
|
979
1012
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
980
1013
|
):
|
|
981
|
-
prev = parent
|
|
1014
|
+
prev = _prev_type_info(parent)
|
|
982
1015
|
raw = ctx.property_defines.get(node.args[0].value, node.args[0].value)
|
|
983
1016
|
pattern = normalize_regex_pattern(raw)
|
|
984
1017
|
repl = cast(
|
|
@@ -1045,7 +1078,7 @@ def _expr_slice(
|
|
|
1045
1078
|
):
|
|
1046
1079
|
start, end = int(node.args[0].value), int(node.args[1].value)
|
|
1047
1080
|
if parent.body:
|
|
1048
|
-
prev = parent
|
|
1081
|
+
prev = _prev_type_info(parent)
|
|
1049
1082
|
return Slice(
|
|
1050
1083
|
parent=parent,
|
|
1051
1084
|
start=start,
|
|
@@ -1079,7 +1112,7 @@ def _expr_unique(
|
|
|
1079
1112
|
def _expr_to_int(
|
|
1080
1113
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
1081
1114
|
):
|
|
1082
|
-
prev = parent
|
|
1115
|
+
prev = _prev_type_info(parent)
|
|
1083
1116
|
return ToInt(
|
|
1084
1117
|
parent=parent,
|
|
1085
1118
|
accept_type_info=prev.ret_type_info,
|
|
@@ -1092,7 +1125,7 @@ def _expr_to_int(
|
|
|
1092
1125
|
def _expr_to_float(
|
|
1093
1126
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
1094
1127
|
):
|
|
1095
|
-
prev = parent
|
|
1128
|
+
prev = _prev_type_info(parent)
|
|
1096
1129
|
return ToFloat(
|
|
1097
1130
|
parent=parent,
|
|
1098
1131
|
accept_type_info=prev.ret_type_info,
|
|
@@ -1108,7 +1141,7 @@ def _expr_to_bool(
|
|
|
1108
1141
|
# Guard against empty parent.body is enforced centrally in
|
|
1109
1142
|
# parse_expressions via _REQUIRES_PREV — handler is reached only when a
|
|
1110
1143
|
# preceding node exists.
|
|
1111
|
-
prev = parent
|
|
1144
|
+
prev = _prev_type_info(parent)
|
|
1112
1145
|
return ToBool(parent=parent, accept_type_info=prev.ret_type_info)
|
|
1113
1146
|
|
|
1114
1147
|
|
|
@@ -1164,7 +1197,7 @@ def _expr_fallback(
|
|
|
1164
1197
|
node: KdlNode, parent: FieldLikeNode, ctx: ParseContext, lint: LintContext
|
|
1165
1198
|
):
|
|
1166
1199
|
value = [] if not node.args else node.args[0].value
|
|
1167
|
-
prev = parent
|
|
1200
|
+
prev = _prev_type_info(parent)
|
|
1168
1201
|
prev_ti = prev.ret_type_info
|
|
1169
1202
|
fb = Fallback(
|
|
1170
1203
|
parent=parent,
|
|
@@ -1189,7 +1222,7 @@ def _expr_filter(
|
|
|
1189
1222
|
ret_type_info=TypeInfo(base=VariableType.DOCUMENT, is_array=True),
|
|
1190
1223
|
is_array=True,
|
|
1191
1224
|
)
|
|
1192
|
-
prev = parent
|
|
1225
|
+
prev = _prev_type_info(parent)
|
|
1193
1226
|
return Filter(
|
|
1194
1227
|
parent=parent,
|
|
1195
1228
|
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"}),
|
|
@@ -165,6 +168,7 @@ def lint_module(
|
|
|
165
168
|
_lint_defines(doc, source_path, diags, children_defines)
|
|
166
169
|
_lint_json_defs(doc, source_path, diags, children_defines)
|
|
167
170
|
_lint_structs(doc, source_path, diags, children_defines)
|
|
171
|
+
_lint_fns(doc, source_path, diags)
|
|
168
172
|
_lint_nested_topdown(doc, source_path, diags)
|
|
169
173
|
_lint_json_refs_topdown(doc, source_path, diags, children_defines)
|
|
170
174
|
return diags
|
|
@@ -175,7 +179,7 @@ def _lint_top_level(
|
|
|
175
179
|
source_path: str,
|
|
176
180
|
diags: list[ReadDiagnostic],
|
|
177
181
|
) -> None:
|
|
178
|
-
for node in doc.select(":root:not(@doc, json, struct, define, import)"):
|
|
182
|
+
for node in doc.select(":root:not(@doc, json, struct, fn, define, import)"):
|
|
179
183
|
diags.append(
|
|
180
184
|
_error(
|
|
181
185
|
node,
|
|
@@ -375,6 +379,69 @@ def _lint_structs(
|
|
|
375
379
|
_lint_single_struct(node, source_path, diags, children_defines)
|
|
376
380
|
|
|
377
381
|
|
|
382
|
+
# Directives allowed inside ``fn`` / ``(raw)fn`` body.
|
|
383
|
+
_FN_ALLOWED_RESERVED: frozenset[str] = frozenset({"@doc"})
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _lint_fns(
|
|
387
|
+
doc: KdlDocument,
|
|
388
|
+
source_path: str,
|
|
389
|
+
diags: list[ReadDiagnostic],
|
|
390
|
+
) -> None:
|
|
391
|
+
"""Structural lint for ``fn`` / ``(raw)fn`` directives."""
|
|
392
|
+
for node in doc.select("fn:root"):
|
|
393
|
+
name = _node_arg(node, 0)
|
|
394
|
+
if not name:
|
|
395
|
+
diags.append(
|
|
396
|
+
_error(
|
|
397
|
+
node,
|
|
398
|
+
"'fn' requires a name",
|
|
399
|
+
source_path,
|
|
400
|
+
code="E001",
|
|
401
|
+
hint='example: fn page_title { css "h1" { text } }',
|
|
402
|
+
)
|
|
403
|
+
)
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
annotation = node.type_annotation
|
|
407
|
+
if annotation and annotation != "(raw)":
|
|
408
|
+
diags.append(
|
|
409
|
+
_error(
|
|
410
|
+
node,
|
|
411
|
+
f"unsupported fn annotation '{annotation}' — only '(raw)' is recognized",
|
|
412
|
+
source_path,
|
|
413
|
+
code="E400",
|
|
414
|
+
hint="use 'fn { ... }' (HTML) or '(raw)fn { ... }' (plain text)",
|
|
415
|
+
)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
ops = list(node.children)
|
|
419
|
+
if not ops:
|
|
420
|
+
diags.append(
|
|
421
|
+
_error(
|
|
422
|
+
node,
|
|
423
|
+
f"fn '{name}' must contain at least one pipeline operation",
|
|
424
|
+
source_path,
|
|
425
|
+
code="E001",
|
|
426
|
+
hint='example: fn title { css "h1" { text } }',
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
for child in ops:
|
|
431
|
+
if (
|
|
432
|
+
child.name.startswith("@")
|
|
433
|
+
and child.name not in _FN_ALLOWED_RESERVED
|
|
434
|
+
):
|
|
435
|
+
diags.append(
|
|
436
|
+
_error(
|
|
437
|
+
child,
|
|
438
|
+
f"'{child.name}' is not allowed inside fn — use a struct for init/check/request/etc.",
|
|
439
|
+
source_path,
|
|
440
|
+
code="E203",
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
378
445
|
def _lint_nested_topdown(
|
|
379
446
|
doc: KdlDocument,
|
|
380
447
|
source_path: str,
|
|
@@ -5,12 +5,15 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from ssc_codegen.ast import (
|
|
8
|
+
FunctionDef,
|
|
8
9
|
JsonDef,
|
|
9
10
|
Module,
|
|
10
11
|
StructBase,
|
|
11
12
|
Struct,
|
|
12
13
|
StructRest,
|
|
13
14
|
StructType,
|
|
15
|
+
VariableType,
|
|
16
|
+
TypeInfo,
|
|
14
17
|
)
|
|
15
18
|
from ssc_codegen.exceptions import BuildTimeError
|
|
16
19
|
from kdlquery import KdlNode, parse as kdl_parse
|
|
@@ -23,7 +26,11 @@ from ssc_codegen.core.contexts import (
|
|
|
23
26
|
ParseContext,
|
|
24
27
|
)
|
|
25
28
|
from ssc_codegen.core.expressions import resolve_define_references
|
|
26
|
-
from ssc_codegen.core.struct_parser import
|
|
29
|
+
from ssc_codegen.core.struct_parser import (
|
|
30
|
+
parse_function,
|
|
31
|
+
parse_json_fields,
|
|
32
|
+
parse_struct,
|
|
33
|
+
)
|
|
27
34
|
|
|
28
35
|
_KDL_TEXT_ENCODING = "utf-8-sig"
|
|
29
36
|
|
|
@@ -42,6 +49,7 @@ def handle_struct(
|
|
|
42
49
|
"dict": StructType.DICT,
|
|
43
50
|
"table": StructType.TABLE,
|
|
44
51
|
"rest": StructType.REST,
|
|
52
|
+
"raw": StructType.RAW,
|
|
45
53
|
}
|
|
46
54
|
st = struct_type_map.get(type_)
|
|
47
55
|
if st is None:
|
|
@@ -64,6 +72,28 @@ def handle_struct(
|
|
|
64
72
|
return struct
|
|
65
73
|
|
|
66
74
|
|
|
75
|
+
def handle_function(
|
|
76
|
+
node: KdlNode, module: Module, ctx: ParseContext, lint: LintContext
|
|
77
|
+
) -> FunctionDef:
|
|
78
|
+
raw_annotation = node.type_annotation
|
|
79
|
+
is_raw = raw_annotation == "(raw)"
|
|
80
|
+
if not node.args:
|
|
81
|
+
raise BuildTimeError("'fn' requires a name")
|
|
82
|
+
name = str(node.args[0].value)
|
|
83
|
+
fn = FunctionDef(
|
|
84
|
+
parent=module,
|
|
85
|
+
name=name,
|
|
86
|
+
is_raw=is_raw,
|
|
87
|
+
accept_type_info=(
|
|
88
|
+
TypeInfo(base=VariableType.STRING)
|
|
89
|
+
if is_raw
|
|
90
|
+
else TypeInfo(base=VariableType.DOCUMENT)
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
parse_function(node.children, fn, ctx, lint)
|
|
94
|
+
return fn
|
|
95
|
+
|
|
96
|
+
|
|
67
97
|
def handle_json(
|
|
68
98
|
node: KdlNode, module: Module, ctx: ParseContext, lint: LintContext
|
|
69
99
|
) -> JsonDef:
|
|
@@ -16,6 +16,7 @@ from ssc_codegen.core.expressions import typedef_from_struct
|
|
|
16
16
|
from ssc_codegen.core.rest_artifacts import rest_artifacts_from_struct
|
|
17
17
|
from ssc_codegen.core.module_handler import (
|
|
18
18
|
handle_define,
|
|
19
|
+
handle_function,
|
|
19
20
|
handle_json,
|
|
20
21
|
handle_struct,
|
|
21
22
|
resolve_imports,
|
|
@@ -68,6 +69,7 @@ def parse_module(
|
|
|
68
69
|
module.source_file = source_path.name if source_path else ""
|
|
69
70
|
structs: list = []
|
|
70
71
|
typedefs: list = []
|
|
72
|
+
functions: list = []
|
|
71
73
|
|
|
72
74
|
for node in top_nodes:
|
|
73
75
|
lint.push(node.name)
|
|
@@ -79,6 +81,9 @@ def parse_module(
|
|
|
79
81
|
struct = handle_struct(node, module, ctx, lint)
|
|
80
82
|
typedefs.append(typedef_from_struct(struct, module))
|
|
81
83
|
structs.append(struct)
|
|
84
|
+
elif node.name == "fn":
|
|
85
|
+
fn = handle_function(node, module, ctx, lint)
|
|
86
|
+
functions.append(fn)
|
|
82
87
|
elif node.name in ("define", "import"):
|
|
83
88
|
pass # already handled
|
|
84
89
|
else:
|
|
@@ -95,6 +100,7 @@ def parse_module(
|
|
|
95
100
|
if isinstance(struct, StructRest):
|
|
96
101
|
module.body.extend(rest_artifacts_from_struct(struct, module))
|
|
97
102
|
module.body.append(struct)
|
|
103
|
+
module.body.extend(functions)
|
|
98
104
|
|
|
99
105
|
# merge lint diagnostics into walker diagnostics
|
|
100
106
|
diagnostics.extend(lint.diagnostics)
|