metaobjects 0.9.0__py3-none-any.whl
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.
- metaobjects/__init__.py +75 -0
- metaobjects/agent_context/__init__.py +55 -0
- metaobjects/agent_context/_content/README.md +14 -0
- metaobjects/agent_context/_content/servers/csharp.meta.json +5 -0
- metaobjects/agent_context/_content/servers/java.meta.json +5 -0
- metaobjects/agent_context/_content/servers/kotlin.meta.json +5 -0
- metaobjects/agent_context/_content/servers/python.meta.json +5 -0
- metaobjects/agent_context/_content/servers/typescript.meta.json +5 -0
- metaobjects/agent_context/_content/skills/metaobjects-authoring/SKILL.md +301 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/SKILL.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/csharp.md +87 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/java.md +94 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/kotlin.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-codegen/references/typescript.md +135 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/SKILL.md +148 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/csharp.md +110 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/java.md +108 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/kotlin.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/python.md +116 -0
- metaobjects/agent_context/_content/skills/metaobjects-prompts/references/typescript.md +150 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/SKILL.md +130 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/java.md +96 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/kotlin.md +99 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/react.md +86 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/tanstack.md +119 -0
- metaobjects/agent_context/_content/skills/metaobjects-runtime-ui/references/typescript.md +92 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/SKILL.md +107 -0
- metaobjects/agent_context/_content/skills/metaobjects-verify/references/migration.md +72 -0
- metaobjects/agent_context/_content/templates/always-on.md.mustache +27 -0
- metaobjects/agent_context/assemble.py +133 -0
- metaobjects/agent_context/content_root.py +54 -0
- metaobjects/agent_context/scaffold.py +191 -0
- metaobjects/agent_context/types.py +44 -0
- metaobjects/attr_class_map.py +23 -0
- metaobjects/cli.py +696 -0
- metaobjects/codegen/__init__.py +0 -0
- metaobjects/codegen/config.py +11 -0
- metaobjects/codegen/constants.py +13 -0
- metaobjects/codegen/extract_delegate_emitter.py +384 -0
- metaobjects/codegen/extract_schema_emitter.py +139 -0
- metaobjects/codegen/format.py +31 -0
- metaobjects/codegen/fr010_field_mapping.py +220 -0
- metaobjects/codegen/generator.py +62 -0
- metaobjects/codegen/generator_registry.py +163 -0
- metaobjects/codegen/generators/__init__.py +0 -0
- metaobjects/codegen/generators/entity_model.py +263 -0
- metaobjects/codegen/generators/extractor_generator.py +317 -0
- metaobjects/codegen/generators/filter_allowlist_generator.py +309 -0
- metaobjects/codegen/generators/m2m_codegen.py +192 -0
- metaobjects/codegen/generators/output_parser_generator.py +272 -0
- metaobjects/codegen/generators/output_prompt_generator.py +192 -0
- metaobjects/codegen/generators/payload_vo_generator.py +672 -0
- metaobjects/codegen/generators/render_helper_generator.py +451 -0
- metaobjects/codegen/generators/router_generator.py +635 -0
- metaobjects/codegen/generators/template_generator.py +70 -0
- metaobjects/codegen/generators/tph_plan.py +120 -0
- metaobjects/codegen/generators/trace_helper_generator.py +336 -0
- metaobjects/codegen/instance_artifacts.py +15 -0
- metaobjects/codegen/output_format_spec_emitter.py +79 -0
- metaobjects/codegen/overwrite_policy.py +27 -0
- metaobjects/codegen/runner.py +110 -0
- metaobjects/codegen/runtime/__init__.py +6 -0
- metaobjects/codegen/runtime/filter_parser.py +193 -0
- metaobjects/codegen/type_map.py +84 -0
- metaobjects/core_types.py +809 -0
- metaobjects/datatype.py +19 -0
- metaobjects/documentation/__init__.py +28 -0
- metaobjects/documentation/doc_constants.py +20 -0
- metaobjects/documentation/doc_provider.py +20 -0
- metaobjects/documentation/doc_schema.py +24 -0
- metaobjects/errors.py +124 -0
- metaobjects/loader/__init__.py +0 -0
- metaobjects/loader/merge.py +287 -0
- metaobjects/loader/meta_data_loader.py +245 -0
- metaobjects/loader/sources/__init__.py +24 -0
- metaobjects/loader/sources/directory_source.py +50 -0
- metaobjects/loader/sources/file_source.py +41 -0
- metaobjects/loader/sources/meta_data_source.py +67 -0
- metaobjects/loader/sources/uri_source.py +56 -0
- metaobjects/loader/validate_discriminator.py +181 -0
- metaobjects/loader/validate_field_readonly.py +146 -0
- metaobjects/loader/validate_source_parameter_ref.py +159 -0
- metaobjects/loader/validate_source_physical_names.py +140 -0
- metaobjects/loader/validation_passes.py +1513 -0
- metaobjects/meta/__init__.py +1 -0
- metaobjects/meta/core/__init__.py +0 -0
- metaobjects/meta/core/attr/__init__.py +0 -0
- metaobjects/meta/core/attr/attr_constants.py +31 -0
- metaobjects/meta/core/attr/meta_attr.py +136 -0
- metaobjects/meta/core/field/__init__.py +0 -0
- metaobjects/meta/core/field/field_constants.py +105 -0
- metaobjects/meta/core/field/meta_field.py +76 -0
- metaobjects/meta/core/identity/__init__.py +0 -0
- metaobjects/meta/core/identity/identity_constants.py +19 -0
- metaobjects/meta/core/identity/meta_identity.py +8 -0
- metaobjects/meta/core/object/__init__.py +0 -0
- metaobjects/meta/core/object/meta_object.py +65 -0
- metaobjects/meta/core/object/meta_object_aware.py +43 -0
- metaobjects/meta/core/object/object_class_registry.py +56 -0
- metaobjects/meta/core/object/object_constants.py +13 -0
- metaobjects/meta/core/object/object_extract.py +400 -0
- metaobjects/meta/core/object/value_object.py +70 -0
- metaobjects/meta/core/relationship/__init__.py +0 -0
- metaobjects/meta/core/relationship/derive_m2m_fields.py +180 -0
- metaobjects/meta/core/relationship/meta_relationship.py +54 -0
- metaobjects/meta/core/relationship/relationship_constants.py +51 -0
- metaobjects/meta/core/validator/__init__.py +0 -0
- metaobjects/meta/core/validator/validator_constants.py +18 -0
- metaobjects/meta/meta_data.py +206 -0
- metaobjects/meta/meta_root.py +8 -0
- metaobjects/meta/persistence/__init__.py +0 -0
- metaobjects/meta/persistence/db/__init__.py +1 -0
- metaobjects/meta/persistence/db/db_constants.py +41 -0
- metaobjects/meta/persistence/db/db_provider.py +60 -0
- metaobjects/meta/persistence/origin/__init__.py +0 -0
- metaobjects/meta/persistence/origin/meta_origin.py +8 -0
- metaobjects/meta/persistence/origin/origin_constants.py +20 -0
- metaobjects/meta/persistence/source/__init__.py +0 -0
- metaobjects/meta/persistence/source/meta_source.py +137 -0
- metaobjects/meta/persistence/source/source_constants.py +115 -0
- metaobjects/meta/presentation/__init__.py +0 -0
- metaobjects/meta/presentation/layout/__init__.py +0 -0
- metaobjects/meta/presentation/layout/layout_constants.py +13 -0
- metaobjects/meta/presentation/layout/meta_layout.py +8 -0
- metaobjects/meta/presentation/view/__init__.py +0 -0
- metaobjects/meta/presentation/view/meta_view.py +8 -0
- metaobjects/meta/presentation/view/view_constants.py +22 -0
- metaobjects/meta/template/__init__.py +0 -0
- metaobjects/meta/template/meta_template.py +46 -0
- metaobjects/meta/template/template_constants.py +112 -0
- metaobjects/meta/template/template_provider.py +43 -0
- metaobjects/parser.py +380 -0
- metaobjects/parser_yaml.py +82 -0
- metaobjects/provider.py +111 -0
- metaobjects/py.typed +0 -0
- metaobjects/registry.py +210 -0
- metaobjects/registry_manifest.py +223 -0
- metaobjects/render/__init__.py +74 -0
- metaobjects/render/email_document.py +14 -0
- metaobjects/render/escapers.py +109 -0
- metaobjects/render/extract/__init__.py +59 -0
- metaobjects/render/extract/coerce.py +279 -0
- metaobjects/render/extract/extract.py +211 -0
- metaobjects/render/extract/extract_map.py +61 -0
- metaobjects/render/extract/json_forgiving_reader.py +203 -0
- metaobjects/render/extract/locate.py +65 -0
- metaobjects/render/extract/normalize.py +96 -0
- metaobjects/render/extract/strip.py +20 -0
- metaobjects/render/extract/types.py +332 -0
- metaobjects/render/extract/xml_forgiving_reader.py +162 -0
- metaobjects/render/filesystem_provider.py +51 -0
- metaobjects/render/prompt/__init__.py +32 -0
- metaobjects/render/prompt/output_format_renderer.py +340 -0
- metaobjects/render/prompt/output_format_spec.py +28 -0
- metaobjects/render/prompt/prompt_field.py +29 -0
- metaobjects/render/prompt/prompt_overrides.py +29 -0
- metaobjects/render/prompt/prompt_style.py +38 -0
- metaobjects/render/renderer.py +358 -0
- metaobjects/render/verify.py +266 -0
- metaobjects/runtime/__init__.py +39 -0
- metaobjects/runtime/llm_recorder.py +210 -0
- metaobjects/runtime/n2m_resolver.py +155 -0
- metaobjects/runtime/object_manager.py +715 -0
- metaobjects/runtime/tph.py +50 -0
- metaobjects/serializer_json.py +172 -0
- metaobjects/shared/__init__.py +0 -0
- metaobjects/shared/base_types.py +16 -0
- metaobjects/shared/separators.py +4 -0
- metaobjects/shared/structural.py +9 -0
- metaobjects/source/__init__.py +79 -0
- metaobjects/source/error_source.py +266 -0
- metaobjects/source/json_path.py +106 -0
- metaobjects/source/semantic_diff.py +98 -0
- metaobjects/source/yaml_positions.py +174 -0
- metaobjects/super_resolve.py +128 -0
- metaobjects/yaml_desugar.py +481 -0
- metaobjects-0.9.0.dist-info/METADATA +97 -0
- metaobjects-0.9.0.dist-info/RECORD +181 -0
- metaobjects-0.9.0.dist-info/WHEEL +4 -0
- metaobjects-0.9.0.dist-info/entry_points.txt +2 -0
- metaobjects-0.9.0.dist-info/licenses/LICENSE +189 -0
metaobjects/parser.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""JSON document -> node tree. Owns inline-vs-child attr syntax (ADR-0002)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from .errors import ErrorCode, MetaError
|
|
8
|
+
from .meta.core.attr.attr_constants import ATTR_SUBTYPE_STRINGARRAY
|
|
9
|
+
from .meta.meta_data import MetaData
|
|
10
|
+
from .meta.meta_root import MetaRoot
|
|
11
|
+
from .registry import TypeRegistry
|
|
12
|
+
from .shared.base_types import SUBTYPE_ROOT, TYPE_ATTR, TYPE_FIELD, TYPE_METADATA, TYPE_OBJECT
|
|
13
|
+
from .shared.separators import ATTR_PREFIX, FUSED_KEY_SEP
|
|
14
|
+
from .shared.structural import (
|
|
15
|
+
KEY_ABSTRACT,
|
|
16
|
+
KEY_CHILDREN,
|
|
17
|
+
KEY_EXTENDS,
|
|
18
|
+
KEY_IS_ARRAY,
|
|
19
|
+
KEY_NAME,
|
|
20
|
+
KEY_OVERLAY,
|
|
21
|
+
KEY_PACKAGE,
|
|
22
|
+
KEY_VALUE,
|
|
23
|
+
)
|
|
24
|
+
from .source import (
|
|
25
|
+
CodeSource,
|
|
26
|
+
ErrorSource,
|
|
27
|
+
JsonPathBuilder,
|
|
28
|
+
JsonSource,
|
|
29
|
+
LoaderWarning,
|
|
30
|
+
YamlPosition,
|
|
31
|
+
YamlSource,
|
|
32
|
+
)
|
|
33
|
+
from .source.yaml_positions import get_yaml_position
|
|
34
|
+
|
|
35
|
+
# Reserved structural body keys — authoring any of these with the @-prefix is a
|
|
36
|
+
# hard ERR_RESERVED_ATTR (ADR-0007). Detected inline as each @-key is processed.
|
|
37
|
+
_RESERVED_STRUCTURAL_KEYS: frozenset[str] = frozenset({
|
|
38
|
+
KEY_NAME,
|
|
39
|
+
KEY_PACKAGE,
|
|
40
|
+
KEY_EXTENDS,
|
|
41
|
+
KEY_ABSTRACT,
|
|
42
|
+
KEY_OVERLAY,
|
|
43
|
+
KEY_IS_ARRAY,
|
|
44
|
+
KEY_CHILDREN,
|
|
45
|
+
KEY_VALUE,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ParseResult:
|
|
51
|
+
root: MetaData
|
|
52
|
+
errors: list[MetaError] = field(default_factory=list)
|
|
53
|
+
warnings: list[str] = field(default_factory=list)
|
|
54
|
+
# FR5c — envelope-shaped warnings (e.g. WARN_DUPLICATE_DECLARATION)
|
|
55
|
+
# produced during parse. Distinct from the legacy ``warnings: list[str]``
|
|
56
|
+
# channel: those get wrapped in a ``WARN_LEGACY`` envelope at the loader
|
|
57
|
+
# boundary, while envelope warnings already carry their own ``code`` +
|
|
58
|
+
# ``source`` and surface unchanged. Empty by default.
|
|
59
|
+
envelope_warnings: list[LoaderWarning] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# FR5b — module-level source-format discriminant. Set at the top of
|
|
63
|
+
# :func:`parse_document` and read by :func:`_current_envelope`. Safe because
|
|
64
|
+
# parse_document is fully synchronous — no reentrancy within a single parse
|
|
65
|
+
# call. Mirrors TS's ``_currentFormat`` module-level state in parser-core.ts.
|
|
66
|
+
_current_source_format: str = "json"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _current_envelope(
|
|
70
|
+
source: str | None,
|
|
71
|
+
builder: JsonPathBuilder,
|
|
72
|
+
yaml_position: YamlPosition | None = None,
|
|
73
|
+
) -> ErrorSource:
|
|
74
|
+
"""Build a source envelope for the current parser location.
|
|
75
|
+
|
|
76
|
+
Returns :class:`CodeSource` when *source* is missing (parser invoked
|
|
77
|
+
without a source id — emitting a JsonSource with an empty file list
|
|
78
|
+
would violate the FR5a length-1 invariant). Mirrors the C# fallback in
|
|
79
|
+
``Parser.ParseState.CurrentSource``.
|
|
80
|
+
|
|
81
|
+
FR5b finalized 2026-05-27 — when the module-level
|
|
82
|
+
:data:`_current_source_format` is ``"yaml"`` (set by :func:`parse_document`'s
|
|
83
|
+
``source_format`` kwarg, supplied by :func:`parse_yaml`), emits a
|
|
84
|
+
:class:`YamlSource` (format ``"yaml"``) carrying the optional
|
|
85
|
+
*yaml_position*. Otherwise emits a :class:`JsonSource`.
|
|
86
|
+
"""
|
|
87
|
+
if source is None or source == "":
|
|
88
|
+
return CodeSource.DEFAULT
|
|
89
|
+
if _current_source_format == "yaml":
|
|
90
|
+
return YamlSource(
|
|
91
|
+
files=(source,),
|
|
92
|
+
json_path=builder.to_string(),
|
|
93
|
+
yaml_position=yaml_position,
|
|
94
|
+
)
|
|
95
|
+
return JsonSource(
|
|
96
|
+
files=(source,),
|
|
97
|
+
json_path=builder.to_string(),
|
|
98
|
+
yaml_position=yaml_position,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_document(
|
|
103
|
+
doc: object,
|
|
104
|
+
registry: TypeRegistry,
|
|
105
|
+
source: str,
|
|
106
|
+
*,
|
|
107
|
+
source_format: str = "json",
|
|
108
|
+
) -> ParseResult:
|
|
109
|
+
# FR5b — set the module-level source-format discriminant for the duration
|
|
110
|
+
# of this parse call. parse_yaml passes source_format="yaml" so every
|
|
111
|
+
# envelope emitted during this run is a YamlSource (format "yaml").
|
|
112
|
+
global _current_source_format
|
|
113
|
+
prior_format = _current_source_format
|
|
114
|
+
_current_source_format = source_format
|
|
115
|
+
try:
|
|
116
|
+
return _parse_document_inner(doc, registry, source)
|
|
117
|
+
finally:
|
|
118
|
+
_current_source_format = prior_format
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _parse_document_inner(doc: object, registry: TypeRegistry, source: str) -> ParseResult:
|
|
122
|
+
builder = JsonPathBuilder()
|
|
123
|
+
result = ParseResult(root=MetaRoot(TYPE_METADATA, SUBTYPE_ROOT, ""))
|
|
124
|
+
if not isinstance(doc, dict):
|
|
125
|
+
result.errors.append(MetaError(
|
|
126
|
+
"top-level is not an object",
|
|
127
|
+
ErrorCode.ERR_TOP_LEVEL_NOT_OBJECT,
|
|
128
|
+
source,
|
|
129
|
+
envelope=_current_envelope(source, builder),
|
|
130
|
+
))
|
|
131
|
+
return result
|
|
132
|
+
if len(doc) != 1:
|
|
133
|
+
result.errors.append(MetaError(
|
|
134
|
+
"expected one wrapper key",
|
|
135
|
+
ErrorCode.ERR_TOP_LEVEL_NOT_OBJECT,
|
|
136
|
+
source,
|
|
137
|
+
envelope=_current_envelope(source, builder),
|
|
138
|
+
))
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
(wrapper, body), = cast(list[tuple[str, object]], list(doc.items()))
|
|
142
|
+
builder.push_key(wrapper)
|
|
143
|
+
# FR5b — look up the root wrapper's YAML position (when the input was
|
|
144
|
+
# YAML-loaded). For JSON input, this returns None and the envelope
|
|
145
|
+
# remains a plain JsonSource with no yaml_position.
|
|
146
|
+
root_yaml_position = get_yaml_position(doc, wrapper)
|
|
147
|
+
node = _build(
|
|
148
|
+
wrapper, body, registry, source, result, builder,
|
|
149
|
+
yaml_position=root_yaml_position,
|
|
150
|
+
)
|
|
151
|
+
builder.pop()
|
|
152
|
+
if isinstance(node, MetaData):
|
|
153
|
+
result.root = node
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _build(
|
|
158
|
+
wrapper: str,
|
|
159
|
+
body: object,
|
|
160
|
+
registry: TypeRegistry,
|
|
161
|
+
source: str,
|
|
162
|
+
result: ParseResult,
|
|
163
|
+
builder: JsonPathBuilder,
|
|
164
|
+
ctx_pkg: str = "",
|
|
165
|
+
parent_type: str = "",
|
|
166
|
+
yaml_position: YamlPosition | None = None,
|
|
167
|
+
) -> MetaData | None:
|
|
168
|
+
"""Build a node from a fused-key wrapper and its body dict.
|
|
169
|
+
|
|
170
|
+
*ctx_pkg* is the effective package inherited from the nearest ancestor that
|
|
171
|
+
declared one. *parent_type* is the type of the immediate parent node (used
|
|
172
|
+
for the field-package-inheritance rule: fields NOT inside objects inherit the
|
|
173
|
+
context package, mirroring the TS/Java loader behaviour for abstract fields
|
|
174
|
+
declared at the root level).
|
|
175
|
+
|
|
176
|
+
*yaml_position* is the FR5b YAML line/col of the wrapper key (if the input
|
|
177
|
+
was YAML-loaded); ``None`` for JSON input. Stamped onto the constructed
|
|
178
|
+
node's source envelope.
|
|
179
|
+
"""
|
|
180
|
+
type_, _, sub_type = wrapper.partition(FUSED_KEY_SEP)
|
|
181
|
+
if not sub_type:
|
|
182
|
+
result.errors.append(MetaError(
|
|
183
|
+
f"node '{wrapper}' omits subType",
|
|
184
|
+
ErrorCode.ERR_MISSING_SUBTYPE,
|
|
185
|
+
source,
|
|
186
|
+
envelope=_current_envelope(source, builder, yaml_position),
|
|
187
|
+
))
|
|
188
|
+
return None
|
|
189
|
+
if not registry.has_type(type_):
|
|
190
|
+
result.errors.append(MetaError(
|
|
191
|
+
f"unknown type '{type_}'",
|
|
192
|
+
ErrorCode.ERR_UNKNOWN_TYPE,
|
|
193
|
+
source,
|
|
194
|
+
envelope=_current_envelope(source, builder, yaml_position),
|
|
195
|
+
))
|
|
196
|
+
return None
|
|
197
|
+
definition = registry.find(type_, sub_type)
|
|
198
|
+
if definition is None:
|
|
199
|
+
result.errors.append(MetaError(
|
|
200
|
+
f"unknown subType '{type_}.{sub_type}'",
|
|
201
|
+
ErrorCode.ERR_UNKNOWN_SUBTYPE,
|
|
202
|
+
source,
|
|
203
|
+
envelope=_current_envelope(source, builder, yaml_position),
|
|
204
|
+
))
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
body_dict: dict[str, object] = body if isinstance(body, dict) else {}
|
|
208
|
+
name = str(body_dict.get(KEY_NAME, "") or "")
|
|
209
|
+
node = definition.factory(type_, sub_type, name)
|
|
210
|
+
assert isinstance(node, MetaData)
|
|
211
|
+
# FR5a / ADR-0009 — every parser-constructed node carries its origin.
|
|
212
|
+
# FR5b — when YAML-sourced, the envelope also carries yaml_position.
|
|
213
|
+
node.set_source(_current_envelope(source, builder, yaml_position))
|
|
214
|
+
|
|
215
|
+
pkg = body_dict.get(KEY_PACKAGE)
|
|
216
|
+
# Capture the file-default package at PARSE time so cross-package
|
|
217
|
+
# fully-qualified ``extends`` resolves over the MERGED tree (where per-file
|
|
218
|
+
# root packages are no longer reachable via the parent chain). The node's
|
|
219
|
+
# own ``package`` if declared, else the inherited context package (the
|
|
220
|
+
# file's root package). Mirrors TS ``MetaData.fileDefaultPackage``.
|
|
221
|
+
node.file_default_package = (str(pkg) if pkg else None) or (ctx_pkg or None)
|
|
222
|
+
|
|
223
|
+
if pkg:
|
|
224
|
+
node.package = str(pkg)
|
|
225
|
+
elif type_ == TYPE_FIELD and parent_type != TYPE_OBJECT and ctx_pkg:
|
|
226
|
+
# Fields NOT inside objects inherit the context package.
|
|
227
|
+
# This covers abstract fields declared at root level (e.g. abstract field.enum).
|
|
228
|
+
# Mirrors TS parser-core.ts and Java BaseMetaDataParser.shouldInheritPackageFromParent.
|
|
229
|
+
node.package = ctx_pkg
|
|
230
|
+
else:
|
|
231
|
+
node.package = None
|
|
232
|
+
|
|
233
|
+
if body_dict.get(KEY_EXTENDS):
|
|
234
|
+
node.super_ref = str(body_dict[KEY_EXTENDS])
|
|
235
|
+
node.is_abstract = bool(body_dict.get(KEY_ABSTRACT, False))
|
|
236
|
+
node.is_overlay = bool(body_dict.get(KEY_OVERLAY, False))
|
|
237
|
+
node.is_array = bool(body_dict.get(KEY_IS_ARRAY, False))
|
|
238
|
+
|
|
239
|
+
for key, value in body_dict.items():
|
|
240
|
+
if key.startswith(ATTR_PREFIX):
|
|
241
|
+
attr_name = key[len(ATTR_PREFIX):]
|
|
242
|
+
# ADR-0007: @-prefixing a reserved structural body key is invalid.
|
|
243
|
+
# Detected inline as each @-attr key is processed (matches TS parser-core).
|
|
244
|
+
# FR5a: emit the envelope at the PARENT body level (do NOT push the
|
|
245
|
+
# offending @-key onto the path) — matches TS parser-core which calls
|
|
246
|
+
# errSource() without descending into the @-key. Pushing would emit
|
|
247
|
+
# a deeper jsonPath than the reference port.
|
|
248
|
+
if attr_name in _RESERVED_STRUCTURAL_KEYS:
|
|
249
|
+
result.errors.append(
|
|
250
|
+
MetaError(
|
|
251
|
+
f"node '{wrapper}' uses reserved structural key '{attr_name}' "
|
|
252
|
+
f"with @-prefix; bare '{attr_name}' is the canonical form",
|
|
253
|
+
ErrorCode.ERR_RESERVED_ATTR,
|
|
254
|
+
source,
|
|
255
|
+
envelope=_current_envelope(source, builder, yaml_position),
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
continue
|
|
259
|
+
schema = registry.attr_schema(type_, sub_type, attr_name)
|
|
260
|
+
# Array-valued attrs (the `string` + is_array model that replaced the
|
|
261
|
+
# `stringarray` subtype) coerce through the array string-attr class
|
|
262
|
+
# (bare-string → one-element list), keyed off the retired-as-a-subtype-
|
|
263
|
+
# but-kept-as-a-coercion `stringarray` class-map entry.
|
|
264
|
+
attr_sub_type = (
|
|
265
|
+
ATTR_SUBTYPE_STRINGARRAY
|
|
266
|
+
if schema is not None and schema.is_array
|
|
267
|
+
else (schema.value_type if schema else None)
|
|
268
|
+
)
|
|
269
|
+
node.set_attr(attr_name, value, sub_type=attr_sub_type)
|
|
270
|
+
# FR5a / ADR-0009 — stamp the just-constructed MetaAttribute node with
|
|
271
|
+
# its origin envelope. Mirrors C# Parser.cs:1039 (attrModel.SetSource).
|
|
272
|
+
# The attr's JsonPath points at the @-key on the parent body.
|
|
273
|
+
attr_node = node.own_meta_attr(attr_name)
|
|
274
|
+
if attr_node is not None:
|
|
275
|
+
# FR5b — the inline attr's YAML position is the body's
|
|
276
|
+
# position-by-key map entry for this canonical key (the
|
|
277
|
+
# desugar re-keys sigil-free attrs to @-prefixed form, so
|
|
278
|
+
# the lookup key matches `key` directly).
|
|
279
|
+
attr_yaml_pos = get_yaml_position(body_dict, key)
|
|
280
|
+
builder.push_key(key)
|
|
281
|
+
attr_node.set_source(
|
|
282
|
+
_current_envelope(source, builder, attr_yaml_pos),
|
|
283
|
+
)
|
|
284
|
+
builder.pop()
|
|
285
|
+
|
|
286
|
+
# The context package for children: use this node's own package if set, else inherit.
|
|
287
|
+
child_ctx_pkg = node.package or ctx_pkg
|
|
288
|
+
|
|
289
|
+
# Descend into children: push `children` key, then `[i]` index for each entry,
|
|
290
|
+
# then the child wrapper key, so JsonPath segments stack correctly.
|
|
291
|
+
children_entries = _iter_children(body_dict)
|
|
292
|
+
if children_entries:
|
|
293
|
+
builder.push_key(KEY_CHILDREN)
|
|
294
|
+
for idx, (entry_dict, cw, cbody) in enumerate(children_entries):
|
|
295
|
+
builder.push_index(idx)
|
|
296
|
+
builder.push_key(cw)
|
|
297
|
+
child_type, _, child_sub = cw.partition(FUSED_KEY_SEP)
|
|
298
|
+
# FR5b — the child wrapper's YAML position lives on the entry
|
|
299
|
+
# dict (the one-key wrapper holding `{cw: cbody}`). For JSON
|
|
300
|
+
# input, get_yaml_position returns None.
|
|
301
|
+
child_yaml_pos = get_yaml_position(entry_dict, cw)
|
|
302
|
+
if child_type == TYPE_ATTR:
|
|
303
|
+
_parse_attr_child(
|
|
304
|
+
node, child_sub, cbody, registry, source, result, builder,
|
|
305
|
+
yaml_position=child_yaml_pos,
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
child = _build(
|
|
309
|
+
cw, cbody, registry, source, result, builder,
|
|
310
|
+
ctx_pkg=child_ctx_pkg, parent_type=type_,
|
|
311
|
+
yaml_position=child_yaml_pos,
|
|
312
|
+
)
|
|
313
|
+
if child is not None:
|
|
314
|
+
node.add_child(child)
|
|
315
|
+
builder.pop()
|
|
316
|
+
builder.pop()
|
|
317
|
+
builder.pop()
|
|
318
|
+
|
|
319
|
+
return node
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _parse_attr_child(
|
|
323
|
+
parent: MetaData,
|
|
324
|
+
sub_type: str,
|
|
325
|
+
body: object,
|
|
326
|
+
registry: TypeRegistry,
|
|
327
|
+
source: str,
|
|
328
|
+
result: ParseResult,
|
|
329
|
+
builder: JsonPathBuilder,
|
|
330
|
+
yaml_position: YamlPosition | None = None,
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Handle a typed attr child: { "attr.<sub>": { "name": ..., "value": ... } }.
|
|
333
|
+
|
|
334
|
+
Attaches via set_attr (not add_child) — attrs are not structural children.
|
|
335
|
+
Uses the child's own sub_type to pick the correct attr class (coerce + desugar).
|
|
336
|
+
|
|
337
|
+
FR5b — *yaml_position* is the YAML line/col of the attr child's wrapper key
|
|
338
|
+
(when YAML-loaded); stamped on the resulting attribute node's source.
|
|
339
|
+
"""
|
|
340
|
+
body_dict: dict[str, object] = body if isinstance(body, dict) else {}
|
|
341
|
+
attr_name = body_dict.get(KEY_NAME)
|
|
342
|
+
if not isinstance(attr_name, str) or not attr_name:
|
|
343
|
+
result.errors.append(
|
|
344
|
+
MetaError(
|
|
345
|
+
f"attr child requires a non-empty 'name'",
|
|
346
|
+
ErrorCode.ERR_MISSING_REQUIRED_ATTR,
|
|
347
|
+
source,
|
|
348
|
+
envelope=_current_envelope(source, builder, yaml_position),
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
return
|
|
352
|
+
raw_value = body_dict.get(KEY_VALUE)
|
|
353
|
+
# Resolve the attr sub_type; fall back to base if unregistered.
|
|
354
|
+
resolved_sub = sub_type if registry.find(TYPE_ATTR, sub_type) is not None else None
|
|
355
|
+
parent.set_attr(attr_name, raw_value, sub_type=resolved_sub)
|
|
356
|
+
# FR5a / ADR-0009 — stamp the just-constructed MetaAttribute node with its
|
|
357
|
+
# origin envelope. Mirrors C# Parser.cs:1039 (attrModel.SetSource). The
|
|
358
|
+
# builder already points at the `attr.<sub>` wrapper (caller pushed it).
|
|
359
|
+
attr_node = parent.own_meta_attr(attr_name)
|
|
360
|
+
if attr_node is not None:
|
|
361
|
+
attr_node.set_source(_current_envelope(source, builder, yaml_position))
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _iter_children(
|
|
365
|
+
body: dict[str, object],
|
|
366
|
+
) -> list[tuple[dict[str, object], str, object]]:
|
|
367
|
+
"""Iterate over a body's `children` entries.
|
|
368
|
+
|
|
369
|
+
Returns a list of ``(entry_dict, wrapper_key, body_value)`` triples. The
|
|
370
|
+
``entry_dict`` is the original one-key wrapper from the children array,
|
|
371
|
+
retained so callers can read its FR5b YAML position-by-key map.
|
|
372
|
+
"""
|
|
373
|
+
raw = body.get(KEY_CHILDREN, [])
|
|
374
|
+
out: list[tuple[dict[str, object], str, object]] = []
|
|
375
|
+
if isinstance(raw, list):
|
|
376
|
+
for entry in raw:
|
|
377
|
+
if isinstance(entry, dict) and len(entry) == 1:
|
|
378
|
+
(cw, cbody), = cast(list[tuple[str, object]], list(entry.items()))
|
|
379
|
+
out.append((entry, cw, cbody))
|
|
380
|
+
return out
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""YAML authoring front-end: text -> desugar -> canonical -> shared tree-builder.
|
|
2
|
+
|
|
3
|
+
Mirrors the TS reference at server/typescript/packages/metadata/src/core/parser-yaml.ts.
|
|
4
|
+
|
|
5
|
+
parse_yaml is the YAML peer of parse_document (parser.py). It is the only
|
|
6
|
+
front-end that should ever see sugared YAML; downstream code (validators,
|
|
7
|
+
serializer, conformance) sees the same canonical-JSON-shaped tree regardless
|
|
8
|
+
of authoring format. ADR-0006 D4: canonical JSON is the cross-language
|
|
9
|
+
interchange; YAML is a sigil-free authoring front-end.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import yaml # type: ignore[import-untyped] # PyYAML ships no type stubs
|
|
14
|
+
|
|
15
|
+
from .errors import ErrorCode, MetaError, ParseError
|
|
16
|
+
from .parser import ParseResult, parse_document
|
|
17
|
+
from .registry import TypeRegistry
|
|
18
|
+
from .source.yaml_positions import parse_yaml_with_positions
|
|
19
|
+
from .yaml_desugar import desugar
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_yaml(text: str, registry: TypeRegistry, source: str) -> ParseResult:
|
|
23
|
+
"""Parse a YAML authoring document into a canonical-shaped tree.
|
|
24
|
+
|
|
25
|
+
Steps mirror TS parser-yaml.ts:
|
|
26
|
+
1. Strip UTF-8 BOM if present (consistent with parse_document).
|
|
27
|
+
2. parse_yaml_with_positions -> a Python object whose mappings carry
|
|
28
|
+
line/col positions for each key (FR5b). Functionally equivalent to
|
|
29
|
+
``yaml.safe_load`` for downstream consumers; YamlPositionMap is a
|
|
30
|
+
dict subclass.
|
|
31
|
+
3. desugar(raw, registry) -> canonical-JSON-shaped dict + CollectedErrors
|
|
32
|
+
(positions are preserved across Rules 1, 2, 4, 5).
|
|
33
|
+
4. parse_document(canonical, registry, source) -> ParseResult; the
|
|
34
|
+
parser detects position-tagged dicts and populates
|
|
35
|
+
``source.yaml_position`` on every node it builds.
|
|
36
|
+
5. Merge desugar errors (each carrying its own stable code, e.g.
|
|
37
|
+
ERR_YAML_COERCION) ahead of parse_document's errors.
|
|
38
|
+
|
|
39
|
+
PyYAML failures (YAMLError) are wrapped as ParseError(ERR_MALFORMED_YAML).
|
|
40
|
+
"""
|
|
41
|
+
# Strip UTF-8 BOM if present (consistent with the JSON file loader's
|
|
42
|
+
# `encoding="utf-8-sig"` behavior).
|
|
43
|
+
if text.startswith(""):
|
|
44
|
+
text = text[1:]
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
parsed = parse_yaml_with_positions(text)
|
|
48
|
+
except yaml.YAMLError as exc:
|
|
49
|
+
raise ParseError(f"Invalid YAML: {exc}", ErrorCode.ERR_MALFORMED_YAML) from exc
|
|
50
|
+
|
|
51
|
+
result = desugar(parsed, registry)
|
|
52
|
+
|
|
53
|
+
# If the desugar produced nothing usable, surface that up-front via a
|
|
54
|
+
# MetaError so the loader pipeline still terminates gracefully (parallels
|
|
55
|
+
# parse_document's top-level ERR_TOP_LEVEL_NOT_OBJECT path).
|
|
56
|
+
if not result.canonical:
|
|
57
|
+
first = result.errors[0] if result.errors else None
|
|
58
|
+
message = first.message if first is not None else "empty YAML document"
|
|
59
|
+
code = (
|
|
60
|
+
first.code
|
|
61
|
+
if first is not None and first.code is not None
|
|
62
|
+
else ErrorCode.ERR_MALFORMED_YAML
|
|
63
|
+
)
|
|
64
|
+
parse_result = parse_document({}, registry, source, source_format="yaml")
|
|
65
|
+
parse_result.errors.insert(0, MetaError(message, code, source))
|
|
66
|
+
return parse_result
|
|
67
|
+
|
|
68
|
+
# FR5b — pass source_format="yaml" so parse_document emits YamlSource
|
|
69
|
+
# envelopes (format "yaml") on every node and error from this YAML input.
|
|
70
|
+
parsed_result = parse_document(result.canonical, registry, source, source_format="yaml")
|
|
71
|
+
|
|
72
|
+
# Merge collected desugar errors ahead of parse_document's own errors.
|
|
73
|
+
desugar_metaerrors = [
|
|
74
|
+
MetaError(
|
|
75
|
+
err.message,
|
|
76
|
+
err.code if err.code is not None else ErrorCode.ERR_MALFORMED_YAML,
|
|
77
|
+
source,
|
|
78
|
+
)
|
|
79
|
+
for err in result.errors
|
|
80
|
+
]
|
|
81
|
+
parsed_result.errors = desugar_metaerrors + parsed_result.errors
|
|
82
|
+
return parsed_result
|
metaobjects/provider.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Composable type providers (ADR-0004). Subtypes self-register via @provider.register."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Callable, TypeVar
|
|
5
|
+
|
|
6
|
+
from .errors import ErrorCode, ParseError
|
|
7
|
+
from .registry import AttrSchema, ChildRule, TypeDefinition, TypeRegistry
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=type)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Provider:
|
|
13
|
+
def __init__(self, provider_id: str, dependencies: tuple[str, ...] = ()) -> None:
|
|
14
|
+
self.id = provider_id
|
|
15
|
+
self.dependencies = dependencies
|
|
16
|
+
self._defs: list[TypeDefinition] = []
|
|
17
|
+
# Post-register hook. Receives the composed registry so a DOMAIN provider
|
|
18
|
+
# can call ``registry.extend(...)`` to enrich types another (already-ordered)
|
|
19
|
+
# provider registered — mirroring TS's ``registerTypes(registry)`` and the
|
|
20
|
+
# ``dbProvider`` / ``templateProvider`` ``registry.extend`` loops. Set via
|
|
21
|
+
# ``on_register`` or by overriding ``register_types``.
|
|
22
|
+
self._on_register: Callable[[TypeRegistry], None] | None = None
|
|
23
|
+
|
|
24
|
+
def add(self, definition: TypeDefinition) -> None:
|
|
25
|
+
self._defs.append(definition)
|
|
26
|
+
|
|
27
|
+
def register(self, cls: T) -> T:
|
|
28
|
+
"""Class decorator: build a TypeDefinition from class attributes and add it."""
|
|
29
|
+
type_ = getattr(cls, "TYPE")
|
|
30
|
+
sub_type = getattr(cls, "SUBTYPE")
|
|
31
|
+
attrs: list[AttrSchema] = list(getattr(cls, "ATTRS", []))
|
|
32
|
+
child_rules: list[ChildRule] = list(getattr(cls, "CHILD_RULES", []))
|
|
33
|
+
|
|
34
|
+
def factory(t: str, s: str, n: str, _cls: T = cls) -> object:
|
|
35
|
+
return _cls(t, s, n)
|
|
36
|
+
|
|
37
|
+
self.add(
|
|
38
|
+
TypeDefinition(
|
|
39
|
+
type=type_,
|
|
40
|
+
sub_type=sub_type,
|
|
41
|
+
factory=factory,
|
|
42
|
+
attrs=attrs,
|
|
43
|
+
child_rules=child_rules,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
return cls
|
|
47
|
+
|
|
48
|
+
def on_register(self, hook: Callable[[TypeRegistry], None]) -> None:
|
|
49
|
+
"""Register a post-register hook that receives the composed registry.
|
|
50
|
+
|
|
51
|
+
Runs AFTER this provider's own ``add``-ed definitions are registered (so
|
|
52
|
+
a provider can both register and extend). The hook is the ergonomic way
|
|
53
|
+
for a DOMAIN provider to call ``registry.extend(...)`` without subclassing
|
|
54
|
+
``Provider``. Mirrors the TS provider's ``registerTypes(registry)`` body.
|
|
55
|
+
"""
|
|
56
|
+
self._on_register = hook
|
|
57
|
+
|
|
58
|
+
def register_types(self, registry: TypeRegistry) -> None:
|
|
59
|
+
for definition in self._defs:
|
|
60
|
+
registry.register(definition)
|
|
61
|
+
if self._on_register is not None:
|
|
62
|
+
self._on_register(registry)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compose_registry(providers: list[Provider]) -> TypeRegistry:
|
|
66
|
+
"""Topologically sort providers by dependency, then register each into a fresh registry."""
|
|
67
|
+
ordered = _topo_sort(providers)
|
|
68
|
+
registry = TypeRegistry()
|
|
69
|
+
for provider in ordered:
|
|
70
|
+
provider.register_types(registry)
|
|
71
|
+
return registry
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _topo_sort(providers: list[Provider]) -> list[Provider]:
|
|
75
|
+
by_id: dict[str, Provider] = {}
|
|
76
|
+
for provider in providers:
|
|
77
|
+
if provider.id in by_id:
|
|
78
|
+
raise ParseError(
|
|
79
|
+
f'Duplicate provider id "{provider.id}"', ErrorCode.ERR_PROVIDER_DUPLICATE_ID
|
|
80
|
+
)
|
|
81
|
+
by_id[provider.id] = provider
|
|
82
|
+
|
|
83
|
+
for provider in providers:
|
|
84
|
+
for dep in provider.dependencies:
|
|
85
|
+
if dep not in by_id:
|
|
86
|
+
raise ParseError(
|
|
87
|
+
f'Provider "{provider.id}" depends on missing "{dep}"',
|
|
88
|
+
ErrorCode.ERR_PROVIDER_MISSING_DEPENDENCY,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
ordered: list[Provider] = []
|
|
92
|
+
visited: dict[str, int] = {} # 0 = visiting, 1 = done
|
|
93
|
+
|
|
94
|
+
def visit(p: Provider) -> None:
|
|
95
|
+
state = visited.get(p.id)
|
|
96
|
+
if state == 1:
|
|
97
|
+
return
|
|
98
|
+
if state == 0:
|
|
99
|
+
raise ParseError(
|
|
100
|
+
f'Provider dependency cycle at "{p.id}"',
|
|
101
|
+
ErrorCode.ERR_PROVIDER_DEPENDENCY_CYCLE,
|
|
102
|
+
)
|
|
103
|
+
visited[p.id] = 0
|
|
104
|
+
for dep in p.dependencies:
|
|
105
|
+
visit(by_id[dep])
|
|
106
|
+
visited[p.id] = 1
|
|
107
|
+
ordered.append(p)
|
|
108
|
+
|
|
109
|
+
for provider in providers:
|
|
110
|
+
visit(provider)
|
|
111
|
+
return ordered
|
metaobjects/py.typed
ADDED
|
File without changes
|