modelable 1.0.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.
Potentially problematic release.
This version of modelable might be problematic. Click here for more details.
- modelable/__init__.py +1 -0
- modelable/__main__.py +3 -0
- modelable/_pydantic_py314_compat.py +31 -0
- modelable/cli.py +41 -0
- modelable/commands/__init__.py +1 -0
- modelable/commands/apicurio.py +84 -0
- modelable/commands/codegen.py +241 -0
- modelable/commands/common.py +43 -0
- modelable/commands/compile.py +237 -0
- modelable/commands/create.py +164 -0
- modelable/commands/diff.py +82 -0
- modelable/commands/graph.py +53 -0
- modelable/commands/llm.py +564 -0
- modelable/commands/lsp.py +15 -0
- modelable/commands/runtime.py +37 -0
- modelable/commands/scenario.py +104 -0
- modelable/commands/spec.py +197 -0
- modelable/commands/workspace.py +240 -0
- modelable/compat/__init__.py +11 -0
- modelable/compat/checker.py +179 -0
- modelable/compat/diff.py +169 -0
- modelable/compiler/__init__.py +3 -0
- modelable/compiler/compiler.py +19 -0
- modelable/compiler/workspace.py +346 -0
- modelable/diagnostics/__init__.py +3 -0
- modelable/diagnostics/model.py +27 -0
- modelable/emitters/__init__.py +0 -0
- modelable/emitters/base.py +22 -0
- modelable/emitters/csharp.py +245 -0
- modelable/emitters/dbt_yaml.py +290 -0
- modelable/emitters/diagnostics.py +25 -0
- modelable/emitters/fhir.py +694 -0
- modelable/emitters/fhir_validator.py +36 -0
- modelable/emitters/go.py +334 -0
- modelable/emitters/java.py +264 -0
- modelable/emitters/json_schema.py +458 -0
- modelable/emitters/markdown.py +252 -0
- modelable/emitters/odcs.py +355 -0
- modelable/emitters/openlineage.py +315 -0
- modelable/emitters/openmetadata.py +258 -0
- modelable/emitters/python.py +282 -0
- modelable/emitters/rust.py +643 -0
- modelable/emitters/shapes.py +261 -0
- modelable/emitters/sql.py +266 -0
- modelable/emitters/targets.py +141 -0
- modelable/emitters/typescript.py +352 -0
- modelable/expressions/__init__.py +0 -0
- modelable/expressions/cel.py +547 -0
- modelable/governance/__init__.py +3 -0
- modelable/governance/checker.py +271 -0
- modelable/governance/por.py +46 -0
- modelable/grammar/__init__.py +1 -0
- modelable/grammar/modelable.lark +257 -0
- modelable/graph/__init__.py +5 -0
- modelable/graph/export.py +442 -0
- modelable/llm/__init__.py +43 -0
- modelable/llm/chat.py +255 -0
- modelable/llm/config.py +87 -0
- modelable/llm/context.py +194 -0
- modelable/llm/engine.py +976 -0
- modelable/llm/importers.py +1077 -0
- modelable/llm/provenance.py +84 -0
- modelable/llm/providers.py +182 -0
- modelable/llm/qa.py +126 -0
- modelable/llm/recommendations.py +33 -0
- modelable/llm/redaction.py +19 -0
- modelable/llm/render.py +279 -0
- modelable/llm/update_plan.py +101 -0
- modelable/llm/validation_help.py +10 -0
- modelable/lsp/__init__.py +3 -0
- modelable/lsp/__main__.py +4 -0
- modelable/lsp/code_actions.py +210 -0
- modelable/lsp/completion.py +480 -0
- modelable/lsp/definition.py +343 -0
- modelable/lsp/diagnostics.py +31 -0
- modelable/lsp/document_symbols.py +197 -0
- modelable/lsp/federation.py +261 -0
- modelable/lsp/folding.py +33 -0
- modelable/lsp/formatting.py +64 -0
- modelable/lsp/highlight.py +30 -0
- modelable/lsp/hover.py +370 -0
- modelable/lsp/inlay_hints.py +158 -0
- modelable/lsp/references.py +511 -0
- modelable/lsp/rename.py +564 -0
- modelable/lsp/semantic_tokens.py +412 -0
- modelable/lsp/server.py +370 -0
- modelable/lsp/workspace.py +83 -0
- modelable/lsp/workspace_symbols.py +104 -0
- modelable/parser/__init__.py +94 -0
- modelable/parser/ir.py +451 -0
- modelable/parser/parse.py +47 -0
- modelable/parser/transformer.py +798 -0
- modelable/parser/wire.py +68 -0
- modelable/planner/__init__.py +0 -0
- modelable/planner/lineage.py +91 -0
- modelable/planner/planner.py +134 -0
- modelable/planner/plans.py +122 -0
- modelable/py.typed +0 -0
- modelable/registry/__init__.py +9 -0
- modelable/registry/apicurio.py +166 -0
- modelable/registry/base.py +18 -0
- modelable/registry/factory.py +18 -0
- modelable/registry/index.py +419 -0
- modelable/registry/local.py +26 -0
- modelable/registry/oci.py +22 -0
- modelable/registry/resolver.py +213 -0
- modelable/registry/schema.sql +119 -0
- modelable/registry/signature.py +26 -0
- modelable/release.py +125 -0
- modelable/runtime/__init__.py +5 -0
- modelable/runtime/adapter/__init__.py +17 -0
- modelable/runtime/adapter/base.py +18 -0
- modelable/runtime/adapter/postgres.py +82 -0
- modelable/specs/__init__.py +23 -0
- modelable/specs/tracking.py +220 -0
- modelable/validation/__init__.py +3 -0
- modelable/validation/semantic.py +659 -0
- modelable-1.0.0.dist-info/METADATA +61 -0
- modelable-1.0.0.dist-info/RECORD +122 -0
- modelable-1.0.0.dist-info/WHEEL +4 -0
- modelable-1.0.0.dist-info/entry_points.txt +2 -0
- modelable-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from lsprotocol import types
|
|
6
|
+
|
|
7
|
+
from modelable.llm.context import parse_model_ref
|
|
8
|
+
from modelable.lsp.workspace import LspWorkspaceIndex
|
|
9
|
+
from modelable.registry.resolver import resolve_model_ref
|
|
10
|
+
|
|
11
|
+
_QUALIFIED_REF_PATTERN = re.compile(
|
|
12
|
+
r"(?P<domain>[A-Za-z_][A-Za-z0-9_]*)\.(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
|
|
13
|
+
)
|
|
14
|
+
_REF_TYPE_PATTERN = re.compile(r"ref\s*<\s*(?P<domain>[A-Za-z_][A-Za-z0-9_]*)\.(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*>")
|
|
15
|
+
_FIELD_REF_PATTERN = re.compile(r"(?P<alias>[A-Za-z_][A-Za-z0-9_]*)\.(?P<field>[A-Za-z_][A-Za-z0-9_]*)")
|
|
16
|
+
_DECL_PATTERN = re.compile(
|
|
17
|
+
r"^\s*(?P<kind>entity|aggregate|event|value|projection)\s+"
|
|
18
|
+
r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
|
|
19
|
+
)
|
|
20
|
+
_DOMAIN_PATTERN = re.compile(r'^\s*domain\s+(?:"(?P<quoted>[^"]+)"|(?P<name>[A-Za-z_][A-Za-z0-9_]*))')
|
|
21
|
+
_WORD_PATTERN = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
22
|
+
_MODEL_FIELD_PATTERN = re.compile(
|
|
23
|
+
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:"
|
|
24
|
+
)
|
|
25
|
+
_PROJECTION_FIELD_PATTERN = re.compile(
|
|
26
|
+
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_MODEL_DECL_KINDS = {"entity", "aggregate", "event", "value"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_definition(
|
|
34
|
+
index: LspWorkspaceIndex, uri: str, line: int, character: int
|
|
35
|
+
) -> types.Location | list[types.Location] | None:
|
|
36
|
+
source = index.documents.get(uri)
|
|
37
|
+
workspace = index.workspace
|
|
38
|
+
if source is None or workspace is None:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
lines = source.text.splitlines()
|
|
42
|
+
if line < 0 or line >= len(lines):
|
|
43
|
+
return None
|
|
44
|
+
text_line = lines[line]
|
|
45
|
+
|
|
46
|
+
for match in _QUALIFIED_REF_PATTERN.finditer(text_line):
|
|
47
|
+
if _contains(match.start(), match.end(), character):
|
|
48
|
+
ref = f"{match.group('domain')}.{match.group('name')}@{match.group('version')}"
|
|
49
|
+
location = _definition_for_qualified_ref(workspace, ref)
|
|
50
|
+
if location is not None:
|
|
51
|
+
return location
|
|
52
|
+
|
|
53
|
+
for match in _REF_TYPE_PATTERN.finditer(text_line):
|
|
54
|
+
if _contains(match.start(), match.end(), character):
|
|
55
|
+
location = _definition_for_unversioned_ref(workspace, match.group("domain"), match.group("name"))
|
|
56
|
+
if location is not None:
|
|
57
|
+
return location
|
|
58
|
+
|
|
59
|
+
for match in _FIELD_REF_PATTERN.finditer(text_line):
|
|
60
|
+
if _contains(match.start(), match.end(), character):
|
|
61
|
+
location = _definition_for_field_reference(
|
|
62
|
+
workspace,
|
|
63
|
+
source.text,
|
|
64
|
+
line,
|
|
65
|
+
match.group("alias"),
|
|
66
|
+
match.group("field"),
|
|
67
|
+
)
|
|
68
|
+
if location is not None:
|
|
69
|
+
return location
|
|
70
|
+
|
|
71
|
+
word = _word_at(text_line, character)
|
|
72
|
+
if word is None:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
scope = _current_scope(source.text, line)
|
|
76
|
+
if scope is None:
|
|
77
|
+
return None
|
|
78
|
+
domain_name, kind, name, version = scope
|
|
79
|
+
|
|
80
|
+
if word == name:
|
|
81
|
+
return _definition_for_decl(workspace, domain_name, kind, name, version)
|
|
82
|
+
|
|
83
|
+
if kind == "model":
|
|
84
|
+
return _definition_for_model_field(
|
|
85
|
+
workspace,
|
|
86
|
+
domain_name,
|
|
87
|
+
name,
|
|
88
|
+
version,
|
|
89
|
+
word,
|
|
90
|
+
)
|
|
91
|
+
return _definition_for_projection_field(
|
|
92
|
+
workspace,
|
|
93
|
+
domain_name,
|
|
94
|
+
name,
|
|
95
|
+
version,
|
|
96
|
+
word,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _definition_for_qualified_ref(workspace, ref: str) -> types.Location | None:
|
|
101
|
+
model_ref = parse_model_ref(ref)
|
|
102
|
+
domain = next((d for d in workspace.mdl.domains if d.name == model_ref.domain), None)
|
|
103
|
+
if domain is None:
|
|
104
|
+
return None
|
|
105
|
+
if model_ref.name in domain.models:
|
|
106
|
+
return _definition_for_decl(workspace, model_ref.domain, "model", model_ref.name, model_ref.version)
|
|
107
|
+
if model_ref.name in domain.projections:
|
|
108
|
+
return _definition_for_decl(workspace, model_ref.domain, "projection", model_ref.name, model_ref.version)
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _definition_for_unversioned_ref(workspace, domain_name: str, name: str) -> types.Location | None:
|
|
113
|
+
domain = next((d for d in workspace.mdl.domains if d.name == domain_name), None)
|
|
114
|
+
if domain is None:
|
|
115
|
+
return None
|
|
116
|
+
if name in domain.models:
|
|
117
|
+
latest = max(domain.models[name], key=lambda v: v.version)
|
|
118
|
+
return _definition_for_decl(workspace, domain_name, "model", name, latest.version)
|
|
119
|
+
if name in domain.projections:
|
|
120
|
+
latest = max(domain.projections[name], key=lambda v: v.version)
|
|
121
|
+
return _definition_for_decl(workspace, domain_name, "projection", name, latest.version)
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _definition_for_field_reference(
|
|
126
|
+
workspace,
|
|
127
|
+
text: str,
|
|
128
|
+
line: int,
|
|
129
|
+
alias: str,
|
|
130
|
+
field_name: str,
|
|
131
|
+
) -> types.Location | None:
|
|
132
|
+
scope = _current_scope(text, line)
|
|
133
|
+
if scope is None:
|
|
134
|
+
return None
|
|
135
|
+
domain_name, kind, name, version = scope
|
|
136
|
+
if kind == "model":
|
|
137
|
+
return _definition_for_model_field(workspace, domain_name, name, version, field_name)
|
|
138
|
+
|
|
139
|
+
domain = next((d for d in workspace.mdl.domains if d.name == domain_name), None)
|
|
140
|
+
if domain is None:
|
|
141
|
+
return None
|
|
142
|
+
versions = domain.projections.get(name)
|
|
143
|
+
if not versions:
|
|
144
|
+
return None
|
|
145
|
+
projection_version = next((item for item in versions if item.version == version), None)
|
|
146
|
+
if projection_version is None:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
for source_ref in [projection_version.source, *projection_version.joins]:
|
|
150
|
+
if source_ref.alias != alias:
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
resolved = resolve_model_ref(
|
|
154
|
+
workspace.mdl,
|
|
155
|
+
source_ref.model,
|
|
156
|
+
source_ref.version,
|
|
157
|
+
)
|
|
158
|
+
except LookupError:
|
|
159
|
+
break
|
|
160
|
+
return _definition_for_source_field(
|
|
161
|
+
workspace,
|
|
162
|
+
resolved.domain_name,
|
|
163
|
+
resolved.model_name,
|
|
164
|
+
resolved.version.version,
|
|
165
|
+
field_name,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return _definition_for_projection_field(workspace, domain_name, name, version, field_name)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _definition_for_decl(
|
|
172
|
+
workspace,
|
|
173
|
+
domain_name: str,
|
|
174
|
+
kind: str,
|
|
175
|
+
name: str,
|
|
176
|
+
version: int,
|
|
177
|
+
) -> types.Location | None:
|
|
178
|
+
for source in workspace.sources:
|
|
179
|
+
location = _find_decl_location(source.uri, source.text, domain_name, kind, name, version)
|
|
180
|
+
if location is not None:
|
|
181
|
+
return location
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _definition_for_model_field(
|
|
186
|
+
workspace,
|
|
187
|
+
domain_name: str,
|
|
188
|
+
model_name: str,
|
|
189
|
+
version: int,
|
|
190
|
+
field_name: str,
|
|
191
|
+
) -> types.Location | None:
|
|
192
|
+
return _definition_for_source_field(workspace, domain_name, model_name, version, field_name)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _definition_for_source_field(
|
|
196
|
+
workspace,
|
|
197
|
+
domain_name: str,
|
|
198
|
+
model_name: str,
|
|
199
|
+
version: int,
|
|
200
|
+
field_name: str,
|
|
201
|
+
) -> types.Location | None:
|
|
202
|
+
for source in workspace.sources:
|
|
203
|
+
for kind in ("model", "projection"):
|
|
204
|
+
location = _find_field_location(
|
|
205
|
+
source.uri,
|
|
206
|
+
source.text,
|
|
207
|
+
domain_name,
|
|
208
|
+
kind,
|
|
209
|
+
model_name,
|
|
210
|
+
version,
|
|
211
|
+
field_name,
|
|
212
|
+
)
|
|
213
|
+
if location is not None:
|
|
214
|
+
return location
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _definition_for_projection_field(
|
|
219
|
+
workspace,
|
|
220
|
+
domain_name: str,
|
|
221
|
+
projection_name: str,
|
|
222
|
+
version: int,
|
|
223
|
+
field_name: str,
|
|
224
|
+
) -> types.Location | None:
|
|
225
|
+
return _definition_for_source_field(workspace, domain_name, projection_name, version, field_name)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _find_decl_location(
|
|
229
|
+
uri: str,
|
|
230
|
+
text: str,
|
|
231
|
+
domain_name: str,
|
|
232
|
+
kind: str,
|
|
233
|
+
name: str,
|
|
234
|
+
version: int,
|
|
235
|
+
) -> types.Location | None:
|
|
236
|
+
current_domain: str | None = None
|
|
237
|
+
lines = text.splitlines()
|
|
238
|
+
for line_no, line in enumerate(lines):
|
|
239
|
+
domain_match = _DOMAIN_PATTERN.match(line)
|
|
240
|
+
if domain_match:
|
|
241
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
242
|
+
continue
|
|
243
|
+
decl_match = _DECL_PATTERN.match(line)
|
|
244
|
+
if not decl_match or current_domain != domain_name:
|
|
245
|
+
continue
|
|
246
|
+
decl_kind = decl_match.group("kind")
|
|
247
|
+
if kind == "model":
|
|
248
|
+
if decl_kind not in _MODEL_DECL_KINDS:
|
|
249
|
+
continue
|
|
250
|
+
elif decl_kind != kind:
|
|
251
|
+
continue
|
|
252
|
+
if decl_match.group("name") != name:
|
|
253
|
+
continue
|
|
254
|
+
if int(decl_match.group("version")) != version:
|
|
255
|
+
continue
|
|
256
|
+
return types.Location(
|
|
257
|
+
uri=uri,
|
|
258
|
+
range=types.Range(
|
|
259
|
+
start=types.Position(line=line_no, character=decl_match.start("name")),
|
|
260
|
+
end=types.Position(line=line_no, character=decl_match.end("name")),
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _find_field_location(
|
|
267
|
+
uri: str,
|
|
268
|
+
text: str,
|
|
269
|
+
domain_name: str,
|
|
270
|
+
kind: str,
|
|
271
|
+
name: str,
|
|
272
|
+
version: int,
|
|
273
|
+
field_name: str,
|
|
274
|
+
) -> types.Location | None:
|
|
275
|
+
current_domain: str | None = None
|
|
276
|
+
active = False
|
|
277
|
+
lines = text.splitlines()
|
|
278
|
+
pattern = _MODEL_FIELD_PATTERN if kind == "model" else _PROJECTION_FIELD_PATTERN
|
|
279
|
+
|
|
280
|
+
for line_no, line in enumerate(lines):
|
|
281
|
+
domain_match = _DOMAIN_PATTERN.match(line)
|
|
282
|
+
if domain_match:
|
|
283
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
284
|
+
active = False
|
|
285
|
+
continue
|
|
286
|
+
decl_match = _DECL_PATTERN.match(line)
|
|
287
|
+
if decl_match and current_domain == domain_name:
|
|
288
|
+
decl_kind = decl_match.group("kind")
|
|
289
|
+
active = (
|
|
290
|
+
(decl_kind in _MODEL_DECL_KINDS if kind == "model" else decl_kind == kind)
|
|
291
|
+
and decl_match.group("name") == name
|
|
292
|
+
and int(decl_match.group("version")) == version
|
|
293
|
+
)
|
|
294
|
+
continue
|
|
295
|
+
if not active:
|
|
296
|
+
continue
|
|
297
|
+
field_match = pattern.match(line)
|
|
298
|
+
if field_match and field_match.group("name") == field_name:
|
|
299
|
+
return types.Location(
|
|
300
|
+
uri=uri,
|
|
301
|
+
range=types.Range(
|
|
302
|
+
start=types.Position(line=line_no, character=field_match.start("name")),
|
|
303
|
+
end=types.Position(line=line_no, character=field_match.end("name")),
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
if line.strip() == "}":
|
|
307
|
+
active = False
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _current_scope(text: str, line: int) -> tuple[str, str, str, int] | None:
|
|
312
|
+
lines = text.splitlines()
|
|
313
|
+
current_domain: str | None = None
|
|
314
|
+
current_kind: str | None = None
|
|
315
|
+
current_name: str | None = None
|
|
316
|
+
current_version: int | None = None
|
|
317
|
+
for item in lines[: line + 1]:
|
|
318
|
+
domain_match = _DOMAIN_PATTERN.match(item)
|
|
319
|
+
if domain_match:
|
|
320
|
+
current_domain = domain_match.group("quoted") or domain_match.group("name")
|
|
321
|
+
current_kind = None
|
|
322
|
+
current_name = None
|
|
323
|
+
current_version = None
|
|
324
|
+
continue
|
|
325
|
+
decl_match = _DECL_PATTERN.match(item)
|
|
326
|
+
if decl_match and current_domain is not None:
|
|
327
|
+
current_kind = "model" if decl_match.group("kind") != "projection" else "projection"
|
|
328
|
+
current_name = decl_match.group("name")
|
|
329
|
+
current_version = int(decl_match.group("version"))
|
|
330
|
+
if current_domain and current_kind and current_name and current_version is not None:
|
|
331
|
+
return current_domain, current_kind, current_name, current_version
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _word_at(text_line: str, character: int) -> str | None:
|
|
336
|
+
for match in _WORD_PATTERN.finditer(text_line):
|
|
337
|
+
if _contains(match.start(), match.end(), character):
|
|
338
|
+
return match.group(0)
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _contains(start: int, end: int, character: int) -> bool:
|
|
343
|
+
return start <= character <= max(end - 1, start)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from lsprotocol import types
|
|
4
|
+
|
|
5
|
+
from modelable.diagnostics.model import Diagnostic
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_lsp_diagnostic(diagnostic: Diagnostic) -> types.Diagnostic:
|
|
9
|
+
severity_map = {
|
|
10
|
+
"error": types.DiagnosticSeverity.Error,
|
|
11
|
+
"warning": types.DiagnosticSeverity.Warning,
|
|
12
|
+
"information": types.DiagnosticSeverity.Information,
|
|
13
|
+
}
|
|
14
|
+
line = max((diagnostic.line or 1) - 1, 0)
|
|
15
|
+
column = max((diagnostic.column or 1) - 1, 0)
|
|
16
|
+
end_line = max((diagnostic.end_line or diagnostic.line or 1) - 1, 0)
|
|
17
|
+
end_column = max((diagnostic.end_column or diagnostic.column or 1) - 1, 0)
|
|
18
|
+
return types.Diagnostic(
|
|
19
|
+
message=diagnostic.message,
|
|
20
|
+
severity=severity_map[diagnostic.severity],
|
|
21
|
+
source="modelable",
|
|
22
|
+
code=diagnostic.code,
|
|
23
|
+
range=types.Range(
|
|
24
|
+
start=types.Position(line=line, character=column),
|
|
25
|
+
end=types.Position(line=end_line, character=end_column),
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_lsp_diagnostics(diagnostics: list[Diagnostic]) -> list[types.Diagnostic]:
|
|
31
|
+
return [to_lsp_diagnostic(diagnostic) for diagnostic in diagnostics]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from lsprotocol import types
|
|
6
|
+
|
|
7
|
+
from modelable.lsp.workspace import LspWorkspaceIndex
|
|
8
|
+
|
|
9
|
+
_DOMAIN_PATTERN = re.compile(r'^\s*domain\s+(?:"(?P<quoted>[^"]+)"|(?P<name>[A-Za-z_][A-Za-z0-9_]*))')
|
|
10
|
+
_DECL_PATTERN = re.compile(
|
|
11
|
+
r"^\s*(?P<kind>entity|aggregate|event|value|projection)\s+"
|
|
12
|
+
r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*@\s*(?P<version>\d+)"
|
|
13
|
+
)
|
|
14
|
+
_FIELD_PATTERN = re.compile(r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\??\s*:")
|
|
15
|
+
_PROJECTION_FIELD_PATTERN = re.compile(
|
|
16
|
+
r"^\s*(?:@[A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?\s+)*(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<-|=)"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_document_symbols(
|
|
21
|
+
index: LspWorkspaceIndex,
|
|
22
|
+
uri: str,
|
|
23
|
+
) -> list[types.DocumentSymbol] | None:
|
|
24
|
+
source = index.documents.get(uri)
|
|
25
|
+
if source is None or index.workspace is None:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
lines = source.text.splitlines()
|
|
29
|
+
symbols: list[types.DocumentSymbol] = []
|
|
30
|
+
current_domain: types.DocumentSymbol | None = None
|
|
31
|
+
current_decl: types.DocumentSymbol | None = None
|
|
32
|
+
current_domain_name: str | None = None
|
|
33
|
+
current_decl_name: str | None = None
|
|
34
|
+
current_decl_kind: str | None = None
|
|
35
|
+
current_decl_version: int | None = None
|
|
36
|
+
|
|
37
|
+
for line_no, line in enumerate(lines):
|
|
38
|
+
domain_match = _DOMAIN_PATTERN.match(line)
|
|
39
|
+
if domain_match:
|
|
40
|
+
current_domain_name = domain_match.group("quoted") or domain_match.group("name")
|
|
41
|
+
current_domain = _make_domain_symbol(
|
|
42
|
+
lines,
|
|
43
|
+
line_no,
|
|
44
|
+
current_domain_name,
|
|
45
|
+
)
|
|
46
|
+
symbols.append(current_domain)
|
|
47
|
+
current_decl = None
|
|
48
|
+
current_decl_name = None
|
|
49
|
+
current_decl_kind = None
|
|
50
|
+
current_decl_version = None
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
decl_match = _DECL_PATTERN.match(line)
|
|
54
|
+
if decl_match and current_domain is not None and current_domain_name is not None:
|
|
55
|
+
current_decl_name = decl_match.group("name")
|
|
56
|
+
current_decl_kind = decl_match.group("kind")
|
|
57
|
+
current_decl_version = int(decl_match.group("version"))
|
|
58
|
+
current_decl = _make_decl_symbol(
|
|
59
|
+
lines,
|
|
60
|
+
line_no,
|
|
61
|
+
current_domain_name,
|
|
62
|
+
current_decl_kind,
|
|
63
|
+
current_decl_name,
|
|
64
|
+
current_decl_version,
|
|
65
|
+
)
|
|
66
|
+
if current_domain.children is None:
|
|
67
|
+
current_domain.children = [current_decl]
|
|
68
|
+
else:
|
|
69
|
+
current_domain.children = [*current_domain.children, current_decl]
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if current_decl is None:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
field_match = _FIELD_PATTERN.match(line) or _PROJECTION_FIELD_PATTERN.match(line)
|
|
76
|
+
if field_match:
|
|
77
|
+
field_symbol = _make_field_symbol(
|
|
78
|
+
line_no,
|
|
79
|
+
field_match.start("name"),
|
|
80
|
+
field_match.end("name"),
|
|
81
|
+
field_match.group("name"),
|
|
82
|
+
line,
|
|
83
|
+
)
|
|
84
|
+
if current_decl.children is None:
|
|
85
|
+
current_decl.children = [field_symbol]
|
|
86
|
+
else:
|
|
87
|
+
current_decl.children = [*current_decl.children, field_symbol]
|
|
88
|
+
|
|
89
|
+
return symbols
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _make_domain_symbol(
|
|
93
|
+
lines: list[str],
|
|
94
|
+
line_no: int,
|
|
95
|
+
name: str,
|
|
96
|
+
) -> types.DocumentSymbol:
|
|
97
|
+
return types.DocumentSymbol(
|
|
98
|
+
name=name,
|
|
99
|
+
kind=types.SymbolKind.Module,
|
|
100
|
+
detail="domain",
|
|
101
|
+
range=types.Range(
|
|
102
|
+
start=types.Position(line=line_no, character=0),
|
|
103
|
+
end=types.Position(
|
|
104
|
+
line=_block_end_line(lines, line_no), character=len(lines[_block_end_line(lines, line_no)])
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
selection_range=types.Range(
|
|
108
|
+
start=types.Position(line=line_no, character=_domain_name_start(lines[line_no])),
|
|
109
|
+
end=types.Position(line=line_no, character=_domain_name_end(lines[line_no])),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _make_decl_symbol(
|
|
115
|
+
lines: list[str],
|
|
116
|
+
line_no: int,
|
|
117
|
+
domain_name: str,
|
|
118
|
+
kind: str,
|
|
119
|
+
name: str,
|
|
120
|
+
version: int,
|
|
121
|
+
) -> types.DocumentSymbol:
|
|
122
|
+
detail = f"{kind} @{version}"
|
|
123
|
+
return types.DocumentSymbol(
|
|
124
|
+
name=name,
|
|
125
|
+
kind=types.SymbolKind.Class,
|
|
126
|
+
detail=detail,
|
|
127
|
+
range=types.Range(
|
|
128
|
+
start=types.Position(line=line_no, character=0),
|
|
129
|
+
end=types.Position(
|
|
130
|
+
line=_block_end_line(lines, line_no), character=len(lines[_block_end_line(lines, line_no)])
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
selection_range=types.Range(
|
|
134
|
+
start=types.Position(line=line_no, character=_decl_name_start(lines[line_no])),
|
|
135
|
+
end=types.Position(line=line_no, character=_decl_name_end(lines[line_no])),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _make_field_symbol(
|
|
141
|
+
line_no: int,
|
|
142
|
+
start: int,
|
|
143
|
+
end: int,
|
|
144
|
+
name: str,
|
|
145
|
+
line: str,
|
|
146
|
+
) -> types.DocumentSymbol:
|
|
147
|
+
return types.DocumentSymbol(
|
|
148
|
+
name=name,
|
|
149
|
+
kind=types.SymbolKind.Field,
|
|
150
|
+
detail=line.strip(),
|
|
151
|
+
range=types.Range(
|
|
152
|
+
start=types.Position(line=line_no, character=0),
|
|
153
|
+
end=types.Position(line=line_no, character=len(line)),
|
|
154
|
+
),
|
|
155
|
+
selection_range=types.Range(
|
|
156
|
+
start=types.Position(line=line_no, character=start),
|
|
157
|
+
end=types.Position(line=line_no, character=end),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _block_end_line(lines: list[str], start_line: int) -> int:
|
|
163
|
+
depth = 0
|
|
164
|
+
for line_no in range(start_line, len(lines)):
|
|
165
|
+
depth += lines[line_no].count("{")
|
|
166
|
+
depth -= lines[line_no].count("}")
|
|
167
|
+
if line_no > start_line and depth <= 0:
|
|
168
|
+
return line_no
|
|
169
|
+
return max(len(lines) - 1, start_line)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _domain_name_start(line: str) -> int:
|
|
173
|
+
match = _DOMAIN_PATTERN.match(line)
|
|
174
|
+
if match is None:
|
|
175
|
+
return 0
|
|
176
|
+
return match.start("quoted") if match.group("quoted") is not None else match.start("name")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _domain_name_end(line: str) -> int:
|
|
180
|
+
match = _DOMAIN_PATTERN.match(line)
|
|
181
|
+
if match is None:
|
|
182
|
+
return 0
|
|
183
|
+
return match.end("quoted") if match.group("quoted") is not None else match.end("name")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _decl_name_start(line: str) -> int:
|
|
187
|
+
match = _DECL_PATTERN.match(line)
|
|
188
|
+
if match is None:
|
|
189
|
+
return 0
|
|
190
|
+
return match.start("name")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _decl_name_end(line: str) -> int:
|
|
194
|
+
match = _DECL_PATTERN.match(line)
|
|
195
|
+
if match is None:
|
|
196
|
+
return 0
|
|
197
|
+
return match.end("name")
|