lutaml-lml 0.1.2 → 0.1.3
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.
- checksums.yaml +4 -4
- data/lib/lutaml/lml/cli.rb +1 -1
- data/lib/lutaml/lml/data_processor/view_processing.rb +7 -6
- data/lib/lutaml/lml/document_builder.rb +1 -8
- data/lib/lutaml/lml/entity_types.rb +30 -0
- data/lib/lutaml/lml/executor.rb +1 -1
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +1 -1
- data/lib/lutaml/lml/formatter/base.rb +7 -4
- data/lib/lutaml/lml/grammar/concerns/definitions.rb +29 -21
- data/lib/lutaml/lml/grammar/concerns/primitives.rb +4 -1
- data/lib/lutaml/lml/grammar/concerns/view_rules.rb +27 -7
- data/lib/lutaml/lml/grammar/instances.rb +5 -2
- data/lib/lutaml/lml/import_resolver.rb +45 -47
- data/lib/lutaml/lml/model_compiler.rb +3 -4
- data/lib/lutaml/lml/models/association.rb +1 -1
- data/lib/lutaml/lml/models/constraint.rb +1 -1
- data/lib/lutaml/lml/models/data_type.rb +5 -1
- data/lib/lutaml/lml/models/diagram.rb +1 -1
- data/lib/lutaml/lml/models/document.rb +8 -7
- data/lib/lutaml/lml/models/enum.rb +5 -1
- data/lib/lutaml/lml/models/operation.rb +1 -1
- data/lib/lutaml/lml/models/package.rb +1 -1
- data/lib/lutaml/lml/models/primitive_type.rb +5 -1
- data/lib/lutaml/lml/models/top_element_attribute.rb +5 -5
- data/lib/lutaml/lml/models/uml_class.rb +11 -18
- data/lib/lutaml/lml/models/value.rb +1 -1
- data/lib/lutaml/lml/parser.rb +3 -8
- data/lib/lutaml/lml/pipeline.rb +9 -30
- data/lib/lutaml/lml/preprocessor.rb +26 -21
- data/lib/lutaml/lml/source.rb +49 -0
- data/lib/lutaml/lml/transform.rb +5 -1
- data/lib/lutaml/lml/types/text_type.rb +28 -0
- data/lib/lutaml/lml/types.rb +10 -0
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml/view_resolution.rb +32 -0
- data/lib/lutaml/lml/view_resolver.rb +5 -5
- data/lib/lutaml/lml.rb +18 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 848b5d75872b444720cee8fb6cdc2ae93db738657cb8ad13633d0317d8bd9808
|
|
4
|
+
data.tar.gz: f9718ba7428f4948d06bf8e489d55d78584c95086ebc4ff6ea6fb2bd0c0e41a9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: adae7746cdcf26e0709537239e7a4cc234c9287f49615eca76d44add023d11d317b30754ac9fef8d1dd6fbbc0899d19a77c284aa3726b6f8809b9f49ea440b4a
|
|
7
|
+
data.tar.gz: ac7189d5cef1e567485b38b56e23998961280600d157c037e8e21def73ee41e22f632ff82c0ab298c286319c00ab74d9a71e6d3fe0f4e9669ea8644cdd7b03ba
|
data/lib/lutaml/lml/cli.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Lutaml
|
|
|
24
24
|
}.freeze
|
|
25
25
|
|
|
26
26
|
PARSE_HANDLERS = {
|
|
27
|
-
'lutaml' => ->(path) { Lutaml::Lml
|
|
27
|
+
'lutaml' => ->(path) { Lutaml::Lml.parse(File.new(path)) },
|
|
28
28
|
'yaml' => ->(path) { Lutaml::Lml::YamlParser.parse(path.to_s) },
|
|
29
29
|
'yml' => ->(path) { Lutaml::Lml::YamlParser.parse(path.to_s) },
|
|
30
30
|
'exp' => lambda { |path|
|
|
@@ -4,20 +4,21 @@ module Lutaml
|
|
|
4
4
|
module Lml
|
|
5
5
|
class DataProcessor
|
|
6
6
|
module ViewProcessing
|
|
7
|
+
# entity_name_list parses to a Hash (one name) or an Array of
|
|
8
|
+
# Hashes (several); normalize both to one list of strings.
|
|
7
9
|
def process_show_list(data)
|
|
8
|
-
|
|
10
|
+
entity_names_from(data)
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def process_hide_list(data)
|
|
12
|
-
|
|
14
|
+
entity_names_from(data)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
private
|
|
16
18
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
data.to_s
|
|
19
|
+
def entity_names_from(data)
|
|
20
|
+
entries = data.is_a?(Array) ? data : [data]
|
|
21
|
+
entries.map { |entry| entry[:entity_name].to_s }
|
|
21
22
|
end
|
|
22
23
|
end
|
|
23
24
|
end
|
|
@@ -65,7 +65,6 @@ module Lutaml
|
|
|
65
65
|
|
|
66
66
|
def set_model_attributes(model, hash)
|
|
67
67
|
hash.each do |key, value|
|
|
68
|
-
value = sanitize_definition(value) if key == :definition
|
|
69
68
|
apply_attribute(model, key, value)
|
|
70
69
|
end
|
|
71
70
|
end
|
|
@@ -79,11 +78,6 @@ module Lutaml
|
|
|
79
78
|
hash
|
|
80
79
|
end
|
|
81
80
|
|
|
82
|
-
def sanitize_definition(value)
|
|
83
|
-
value.to_s.gsub(/\\}/, '}').gsub(/\\{/, '{')
|
|
84
|
-
.split("\n").map(&:strip).join("\n")
|
|
85
|
-
end
|
|
86
|
-
|
|
87
81
|
def apply_attribute(model, key, value)
|
|
88
82
|
return unless model.class.attributes.key?(key.to_sym)
|
|
89
83
|
|
|
@@ -131,8 +125,7 @@ module Lutaml
|
|
|
131
125
|
end
|
|
132
126
|
|
|
133
127
|
def build_view_filter(entity_names)
|
|
134
|
-
|
|
135
|
-
ViewFilter.new(entity_names: names.map(&:to_s))
|
|
128
|
+
ViewFilter.new(entity_names: Array(entity_names).map(&:to_s))
|
|
136
129
|
end
|
|
137
130
|
end
|
|
138
131
|
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# Single source of truth for entity types held on a Document. Each
|
|
6
|
+
# entity model declares self.entity_type (the Document collection
|
|
7
|
+
# name); consumers iterate this registry instead of hardcoding the
|
|
8
|
+
# type list in four places.
|
|
9
|
+
module EntityTypes
|
|
10
|
+
ALL = [UmlClass, Enum, DataType, PrimitiveType].freeze
|
|
11
|
+
|
|
12
|
+
def self.all
|
|
13
|
+
ALL
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.by_symbol
|
|
17
|
+
@by_symbol ||= ALL.each_with_object({}) { |klass, index| index[klass.entity_type] = klass }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.symbols
|
|
21
|
+
by_symbol.keys
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Entities that can own attributes/associations (excludes enums).
|
|
25
|
+
def self.classifiable
|
|
26
|
+
ALL.select(&:classifiable?)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/lutaml/lml/executor.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Lutaml
|
|
|
13
13
|
#
|
|
14
14
|
# Usage:
|
|
15
15
|
# compiled = ModelCompiler.new.compile(models_file)
|
|
16
|
-
# doc =
|
|
16
|
+
# doc = Lutaml::Lml.parse_document(instances_file)
|
|
17
17
|
# result = Executor.run(doc, compiled: compiled)
|
|
18
18
|
# result.instances # array of hydrated instance objects
|
|
19
19
|
# result.errors # array of validation error strings
|
|
@@ -9,7 +9,7 @@ module Lutaml
|
|
|
9
9
|
|
|
10
10
|
def find_by_name(name)
|
|
11
11
|
name = name.to_sym
|
|
12
|
-
all.detect { |formatter_class| formatter_class.
|
|
12
|
+
all.detect { |formatter_class| formatter_class.formatter_name == name }
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -34,7 +34,10 @@ module Lutaml
|
|
|
34
34
|
new(attributes).format(node)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
# Registry key for Formatter.find_by_name. Deliberately not
|
|
38
|
+
# `name` — shadowing Class#name breaks Ruby internals and any
|
|
39
|
+
# library that legitimately calls .name on these classes.
|
|
40
|
+
def formatter_name
|
|
38
41
|
to_s.split('::').last.downcase.to_sym
|
|
39
42
|
end
|
|
40
43
|
end
|
|
@@ -46,7 +49,7 @@ module Lutaml
|
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
def name
|
|
49
|
-
self.class.
|
|
52
|
+
self.class.formatter_name
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
attr_reader :type
|
|
@@ -61,7 +64,7 @@ module Lutaml
|
|
|
61
64
|
|
|
62
65
|
def dispatch_format(node)
|
|
63
66
|
handler = FORMAT_HANDLERS.find { |type, _| node.is_a?(type) }&.last
|
|
64
|
-
|
|
67
|
+
raise Lml::Error, "no format handler for #{node.class}" unless handler
|
|
65
68
|
|
|
66
69
|
public_send(handler, node)
|
|
67
70
|
end
|
|
@@ -55,13 +55,21 @@ module Lutaml
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# -- Definition
|
|
58
|
+
# Balanced-brace body: nested "{...}" and "\"-escapes are allowed,
|
|
59
|
+
# so definition text can contain format templates like "ZB{code}".
|
|
60
|
+
rule(:definition_content) do
|
|
61
|
+
(
|
|
62
|
+
(str("\\") >> any) |
|
|
63
|
+
(str("{") >> definition_content >> str("}")) |
|
|
64
|
+
(str("}").absent? >> any)
|
|
65
|
+
).repeat(0)
|
|
66
|
+
end
|
|
58
67
|
rule(:definition_body) do
|
|
59
68
|
spaces? >>
|
|
60
69
|
str("definition") >>
|
|
61
70
|
whitespace? >>
|
|
62
71
|
str("{") >>
|
|
63
|
-
(
|
|
64
|
-
.repeat.maybe.as(:definition) >>
|
|
72
|
+
definition_content.as(:definition) >>
|
|
65
73
|
str("}")
|
|
66
74
|
end
|
|
67
75
|
|
|
@@ -142,22 +150,20 @@ module Lutaml
|
|
|
142
150
|
end
|
|
143
151
|
rule(:diagram_definitions) { diagram_definition >> whitespace? }
|
|
144
152
|
|
|
153
|
+
# A model file: bare top-level definitions with no enclosing
|
|
154
|
+
# named block. Same inner grammar as a diagram body; labeled
|
|
155
|
+
# :members so the document builder sees document shape.
|
|
156
|
+
rule(:model_definitions) { diagram_inner_definition.repeat(1).as(:members) }
|
|
157
|
+
|
|
145
158
|
# -- View (extends diagram with import/show/hide)
|
|
146
159
|
rule(:view_keyword) { kw_view >> spaces? }
|
|
147
|
-
rule(:
|
|
148
|
-
|
|
149
|
-
caption_definition |
|
|
150
|
-
fontname_definition |
|
|
151
|
-
view_import.as(:view_imports) |
|
|
160
|
+
rule(:view_only_definitions) do
|
|
161
|
+
view_import.as(:view_imports) |
|
|
152
162
|
show_directive |
|
|
153
|
-
hide_directive
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
data_type_definition.as(:data_types) |
|
|
158
|
-
association_definition.as(:associations) |
|
|
159
|
-
comment_definition |
|
|
160
|
-
comment_multiline_definition
|
|
163
|
+
hide_directive
|
|
164
|
+
end
|
|
165
|
+
rule(:view_inner_definitions) do
|
|
166
|
+
view_only_definitions | diagram_inner_definitions
|
|
161
167
|
end
|
|
162
168
|
rule(:view_inner_definition) do
|
|
163
169
|
view_inner_definitions >> whitespace?
|
|
@@ -176,17 +182,19 @@ module Lutaml
|
|
|
176
182
|
|
|
177
183
|
# -- Metadata
|
|
178
184
|
rule(:title_keyword) { kw_title >> spaces }
|
|
185
|
+
# Quoted titles/captions accept any character (parentheses, commas,
|
|
186
|
+
# Unicode). Unquoted form keeps a conservative charset.
|
|
179
187
|
rule(:title_text) do
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
188
|
+
(str('"') >> (str('"').absent? >> any).repeat(1).as(:title) >> str('"')) |
|
|
189
|
+
(str("'") >> (str("'").absent? >> any).repeat(1).as(:title) >> str("'")) |
|
|
190
|
+
match['a-zA-Z0-9_\- ,.:;()+\'/'].repeat(1).as(:title)
|
|
183
191
|
end
|
|
184
192
|
rule(:title_definition) { title_keyword >> title_text }
|
|
185
193
|
rule(:caption_keyword) { kw_caption >> spaces }
|
|
186
194
|
rule(:caption_text) do
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
195
|
+
(str('"') >> (str('"').absent? >> any).repeat(1).as(:caption) >> str('"')) |
|
|
196
|
+
(str("'") >> (str("'").absent? >> any).repeat(1).as(:caption) >> str("'")) |
|
|
197
|
+
match['a-zA-Z0-9_\- ,.:;()+\'/'].repeat(1).as(:caption)
|
|
190
198
|
end
|
|
191
199
|
rule(:caption_definition) { caption_keyword >> caption_text }
|
|
192
200
|
|
|
@@ -19,8 +19,11 @@ module Lutaml
|
|
|
19
19
|
rule(:whitespace?) { whitespace.maybe }
|
|
20
20
|
rule(:newline) { match('[\r\n]') }
|
|
21
21
|
|
|
22
|
+
rule(:quoted_string_content) do
|
|
23
|
+
(str('"').absent? >> any).repeat
|
|
24
|
+
end
|
|
22
25
|
rule(:quoted_string) do
|
|
23
|
-
str('"') >>
|
|
26
|
+
str('"') >> quoted_string_content.as(:string) >> str('"')
|
|
24
27
|
end
|
|
25
28
|
rule(:boolean) { (str("true") | str("false")).as(:boolean) }
|
|
26
29
|
rule(:number) { (match("[0-9]").repeat(1) >> str(".") >> match("[0-9]").repeat(1)).as(:float) | match("[0-9]").repeat(1).as(:number) }
|
|
@@ -7,25 +7,45 @@ module Lutaml
|
|
|
7
7
|
module ViewRules
|
|
8
8
|
include Parslet
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# View-scoped keyword rules. `import` is named view_import to
|
|
11
|
+
# stay MECE with the instances-block kw_import keyword. The
|
|
12
|
+
# trailing `spaces` is the word boundary: `important` fails to
|
|
13
|
+
# match rather than parsing as `import`.
|
|
14
|
+
VIEW_KEYWORDS = {
|
|
15
|
+
"view_import" => "import",
|
|
16
|
+
"show" => "show",
|
|
17
|
+
"hide" => "hide",
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
VIEW_KEYWORDS.each do |rule_name, keyword|
|
|
21
|
+
rule("kw_#{rule_name}") { whitespace? >> str(keyword) >> spaces }
|
|
22
|
+
end
|
|
11
23
|
|
|
12
24
|
rule(:view_import) do
|
|
13
|
-
|
|
14
|
-
str('"') >>
|
|
25
|
+
kw_view_import >>
|
|
26
|
+
str('"') >> quoted_string_content.as(:path) >> str('"') >>
|
|
15
27
|
whitespace?
|
|
16
28
|
end
|
|
17
29
|
|
|
30
|
+
# Entity names in show/hide lists must be space-free: general
|
|
31
|
+
# class_name_chars allows spaces, which would swallow a
|
|
32
|
+
# following directive (`show Foo, Bar hide Baz` would parse
|
|
33
|
+
# "Bar hide Baz" as one name).
|
|
34
|
+
rule(:entity_name_atom) do
|
|
35
|
+
match('[a-zA-Z0-9_.:\-]').repeat(1).as(:entity_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
18
38
|
rule(:entity_name_list) do
|
|
19
|
-
|
|
20
|
-
(spaces? >> str(",") >> spaces? >>
|
|
39
|
+
entity_name_atom >>
|
|
40
|
+
(spaces? >> str(",") >> spaces? >> entity_name_atom).repeat
|
|
21
41
|
end
|
|
22
42
|
|
|
23
43
|
rule(:show_directive) do
|
|
24
|
-
|
|
44
|
+
kw_show >> entity_name_list.as(:show_list) >> whitespace?
|
|
25
45
|
end
|
|
26
46
|
|
|
27
47
|
rule(:hide_directive) do
|
|
28
|
-
|
|
48
|
+
kw_hide >> entity_name_list.as(:hide_list) >> whitespace?
|
|
29
49
|
end
|
|
30
50
|
end
|
|
31
51
|
end
|
|
@@ -30,8 +30,11 @@ module Lutaml
|
|
|
30
30
|
rule("kw_#{keyword}") { whitespace? >> str(keyword) }
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
# -- Root (Full: diagram + models + instances)
|
|
34
|
-
rule(:diagram)
|
|
33
|
+
# -- Root (Full: diagram + models + instances + bare model files)
|
|
34
|
+
rule(:diagram) do
|
|
35
|
+
require_block? >>
|
|
36
|
+
(models | diagram_definitions | view_definitions | instances | instance | model_definitions)
|
|
37
|
+
end
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
end
|
|
@@ -5,83 +5,81 @@ require "set"
|
|
|
5
5
|
module Lutaml
|
|
6
6
|
module Lml
|
|
7
7
|
class ImportResolver
|
|
8
|
-
def initialize(
|
|
9
|
-
@
|
|
8
|
+
def initialize(base_dir)
|
|
9
|
+
@base_dir = base_dir
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
# Returns [entity_index, associations] where entity_index maps
|
|
13
|
+
# entity name → entity. ViewResolver consumes the index without
|
|
14
|
+
# rebuilding it.
|
|
12
15
|
def resolve(document)
|
|
13
|
-
|
|
16
|
+
@failures = []
|
|
17
|
+
entity_index = {}
|
|
14
18
|
associations = []
|
|
15
19
|
visited = Set.new
|
|
16
20
|
|
|
17
21
|
document.view_imports.each do |import|
|
|
18
|
-
resolve_import(import.path,
|
|
22
|
+
resolve_import(import.path, entity_index, associations, visited, @base_dir)
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
collect_local_entities(document,
|
|
25
|
+
collect_local_entities(document, entity_index, associations)
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
raise ImportError, @failures.join("\n") if @failures.any?
|
|
28
|
+
|
|
29
|
+
[entity_index, associations]
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
private
|
|
27
33
|
|
|
28
|
-
def resolve_import(path,
|
|
29
|
-
base_dir = base_path ? File.dirname(base_path) : Dir.pwd
|
|
34
|
+
def resolve_import(path, entity_index, associations, visited, base_dir)
|
|
30
35
|
abs_pattern = File.expand_path(path, base_dir)
|
|
36
|
+
files = Dir.glob(abs_pattern)
|
|
37
|
+
if files.empty?
|
|
38
|
+
@failures << "import matched no files: #{path} (resolved to #{abs_pattern})"
|
|
39
|
+
end
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
files.each do |file_path|
|
|
33
42
|
next if visited.include?(file_path)
|
|
34
43
|
visited.add(file_path)
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
doc = parse_file(file_path)
|
|
46
|
+
collect_local_entities(doc, entity_index, associations)
|
|
47
|
+
|
|
48
|
+
doc.view_imports.each do |import|
|
|
49
|
+
resolve_import(import.path, entity_index, associations, visited, File.dirname(file_path))
|
|
40
50
|
end
|
|
41
51
|
end
|
|
42
52
|
end
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
doc.view_imports&.each do |import|
|
|
50
|
-
resolve_import(import.path, entities, associations, visited, file_path)
|
|
51
|
-
end
|
|
54
|
+
# The grammar root accepts every file shape (diagram, view,
|
|
55
|
+
# models block, bare definitions) — no content sniffing needed.
|
|
56
|
+
def parse_file(file_path)
|
|
57
|
+
File.open(file_path) { |file| Pipeline.call(file) }
|
|
52
58
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
@failures << "cannot read import #{file_path}: #{e.message}"
|
|
60
|
+
Document.new
|
|
55
61
|
end
|
|
56
62
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
collect_local_entities(doc, entities, associations)
|
|
63
|
-
rescue Errno::ENOENT, Errno::EACCES => e
|
|
64
|
-
warn "Skipping #{file_path}: #{e.message}"
|
|
65
|
-
[]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def view_file?(file_path)
|
|
69
|
-
head = File.read(file_path, 200, encoding: "UTF-8")
|
|
70
|
-
head.match?(/\b(view|diagram)\s+\w/)
|
|
71
|
-
rescue Errno::ENOENT, Errno::EACCES
|
|
72
|
-
false
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def collect_local_entities(doc, entities, associations)
|
|
76
|
-
merge_entities(doc.classes, entities)
|
|
77
|
-
merge_entities(doc.enums, entities)
|
|
78
|
-
merge_entities(doc.data_types, entities)
|
|
63
|
+
def collect_local_entities(doc, entity_index, associations)
|
|
64
|
+
EntityTypes.all.each do |type|
|
|
65
|
+
merge_entities(doc.public_send(type.entity_type), entity_index)
|
|
66
|
+
end
|
|
79
67
|
associations.concat(doc.associations.to_a)
|
|
80
68
|
end
|
|
81
69
|
|
|
82
|
-
|
|
70
|
+
# Re-importing the same entity through two paths is legitimate
|
|
71
|
+
# (dedup silently); the same name with different content is a
|
|
72
|
+
# conflict that must not resolve to a silently wrong diagram.
|
|
73
|
+
def merge_entities(collection, entity_index)
|
|
83
74
|
collection.each do |entity|
|
|
84
|
-
|
|
75
|
+
next if entity.name.nil?
|
|
76
|
+
|
|
77
|
+
existing = entity_index[entity.name]
|
|
78
|
+
if existing.nil?
|
|
79
|
+
entity_index[entity.name] = entity
|
|
80
|
+
elsif existing.to_hash != entity.to_hash
|
|
81
|
+
@failures << "entity '#{entity.name}' is defined differently in two imports"
|
|
82
|
+
end
|
|
85
83
|
end
|
|
86
84
|
end
|
|
87
85
|
end
|
|
@@ -15,7 +15,6 @@ module Lutaml
|
|
|
15
15
|
"DateTime" => :date_time,
|
|
16
16
|
"Time" => :time,
|
|
17
17
|
"Uri" => :string,
|
|
18
|
-
"Hash" => :hash,
|
|
19
18
|
}.freeze
|
|
20
19
|
|
|
21
20
|
# Tokens that denote unbounded cardinality in LML attribute
|
|
@@ -30,13 +29,13 @@ module Lutaml
|
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
def compile(input)
|
|
33
|
-
doc =
|
|
32
|
+
doc = Lutaml::Lml.parse_document(input)
|
|
34
33
|
compile_document(doc)
|
|
35
34
|
@compiled
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
def hydrate(input)
|
|
39
|
-
doc = input.is_a?(Document) ? input :
|
|
38
|
+
doc = input.is_a?(Document) ? input : Lutaml::Lml.parse_document(input)
|
|
40
39
|
compile_document(doc) unless @compiled.any?
|
|
41
40
|
return {} unless doc.instance
|
|
42
41
|
|
|
@@ -56,7 +55,7 @@ module Lutaml
|
|
|
56
55
|
#
|
|
57
56
|
# Returns an array of validation error strings (empty if all pass).
|
|
58
57
|
def validate(input, compiled: nil)
|
|
59
|
-
doc = input.is_a?(Document) ? input :
|
|
58
|
+
doc = input.is_a?(Document) ? input : Lutaml::Lml.parse_document(input)
|
|
60
59
|
if compiled
|
|
61
60
|
@compiled = compiled
|
|
62
61
|
else
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Association < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Constraint < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class DataType < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string, default: "dataType"
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -26,6 +26,10 @@ module Lutaml
|
|
|
26
26
|
def self.entity_type
|
|
27
27
|
:data_types
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
def self.classifiable?
|
|
31
|
+
true
|
|
32
|
+
end
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
end
|
|
@@ -4,7 +4,7 @@ module Lutaml
|
|
|
4
4
|
module Lml
|
|
5
5
|
class Diagram < Lutaml::Model::Serializable
|
|
6
6
|
attribute :name, :string
|
|
7
|
-
attribute :definition,
|
|
7
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
8
8
|
attribute :keyword, :string
|
|
9
9
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
10
10
|
attribute :visibility, :string, default: "public"
|
|
@@ -7,10 +7,10 @@ module Lutaml
|
|
|
7
7
|
attribute :name, :string
|
|
8
8
|
attribute :title, :string
|
|
9
9
|
attribute :caption, :string
|
|
10
|
-
attribute :groups, "Lutaml::Lml::Group", collection: true
|
|
10
|
+
attribute :groups, "Lutaml::Lml::Group", collection: true, default: -> { [] }
|
|
11
11
|
attribute :fidelity, "Lutaml::Lml::Fidelity"
|
|
12
12
|
attribute :fontname, :string
|
|
13
|
-
attribute :comments, :string, collection: true
|
|
13
|
+
attribute :comments, :string, collection: true, default: -> { [] }
|
|
14
14
|
|
|
15
15
|
attribute :classes, "Lutaml::Lml::UmlClass", collection: true, default: -> { [] }
|
|
16
16
|
attribute :data_types, "Lutaml::Lml::DataType", collection: true, default: -> { [] }
|
|
@@ -20,11 +20,12 @@ module Lutaml
|
|
|
20
20
|
attribute :associations, "Lutaml::Lml::Association", collection: true, default: -> { [] }
|
|
21
21
|
attribute :diagrams, "Lutaml::Lml::Diagram", collection: true, default: -> { [] }
|
|
22
22
|
|
|
23
|
-
# LML-specific
|
|
23
|
+
# LML-specific. instance/instances/fidelity/show_filter/hide_filter
|
|
24
|
+
# are deliberately default-less: nil means "absent".
|
|
24
25
|
attribute :instance, "Lutaml::Lml::Instance"
|
|
25
|
-
attribute :requires, :string, collection: true
|
|
26
|
+
attribute :requires, :string, collection: true, default: -> { [] }
|
|
26
27
|
attribute :instances, "Lutaml::Lml::InstanceCollection"
|
|
27
|
-
attribute :view_imports, "Lutaml::Lml::ViewImport", collection: true
|
|
28
|
+
attribute :view_imports, "Lutaml::Lml::ViewImport", collection: true, default: -> { [] }
|
|
28
29
|
attribute :show_filter, "Lutaml::Lml::ViewFilter"
|
|
29
30
|
attribute :hide_filter, "Lutaml::Lml::ViewFilter"
|
|
30
31
|
|
|
@@ -32,13 +33,13 @@ module Lutaml
|
|
|
32
33
|
# formatters that need to walk every classifiable type without
|
|
33
34
|
# caring which collection holds it.
|
|
34
35
|
def all_classes
|
|
35
|
-
|
|
36
|
+
EntityTypes.all.flat_map { |type| public_send(type.entity_type) }
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
# Class-like entities that can own associations. Excludes enums
|
|
39
40
|
# (which have no attributes/associations of their own).
|
|
40
41
|
def classifiable_classes
|
|
41
|
-
|
|
42
|
+
EntityTypes.classifiable.flat_map { |type| public_send(type.entity_type) }
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
end
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Enum < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string, default: "enumeration"
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -23,6 +23,10 @@ module Lutaml
|
|
|
23
23
|
def self.entity_type
|
|
24
24
|
:enums
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
def self.classifiable?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Operation < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
class Package < Lutaml::Model::Serializable
|
|
6
6
|
# From TopElement
|
|
7
7
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
8
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
9
|
attribute :keyword, :string
|
|
10
10
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
11
|
attribute :visibility, :string, default: "public"
|
|
@@ -7,7 +7,7 @@ module Lutaml
|
|
|
7
7
|
# accesses the same attributes as DataType via format_class.
|
|
8
8
|
attribute :name, :string
|
|
9
9
|
attribute :keyword, :string, default: "primitive"
|
|
10
|
-
attribute :definition,
|
|
10
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
11
11
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
12
12
|
attribute :visibility, :string, default: "public"
|
|
13
13
|
attribute :is_abstract, :boolean, default: false
|
|
@@ -21,6 +21,10 @@ module Lutaml
|
|
|
21
21
|
def self.entity_type
|
|
22
22
|
:primitives
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
def self.classifiable?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
24
28
|
end
|
|
25
29
|
end
|
|
26
30
|
end
|
|
@@ -16,16 +16,16 @@ module Lutaml
|
|
|
16
16
|
attribute :is_static, :boolean, default: false
|
|
17
17
|
attribute :is_read_only, :boolean, default: false
|
|
18
18
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
19
|
-
attribute :definition,
|
|
19
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
20
20
|
attribute :association, :string
|
|
21
21
|
attribute :default, :string
|
|
22
22
|
|
|
23
23
|
# LML-specific attributes
|
|
24
|
-
attribute :properties, "Lutaml::Lml::TopElementAttribute", collection: true, default: []
|
|
25
|
-
attribute :value, "Lutaml::Lml::TopElementAttribute", collection: true
|
|
26
|
-
attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true, default: []
|
|
24
|
+
attribute :properties, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
|
|
25
|
+
attribute :value, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
|
|
26
|
+
attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
|
|
27
27
|
attribute :extended, :boolean
|
|
28
|
-
attribute :instances, "Lutaml::Lml::Instance", collection: true, default: []
|
|
28
|
+
attribute :instances, "Lutaml::Lml::Instance", collection: true, default: -> { [] }
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
5
|
class UmlClass < Lutaml::Model::Serializable
|
|
6
|
-
# From TopElement
|
|
6
|
+
# From TopElement. TextType unescapes \{ \} and normalizes lines
|
|
7
|
+
# on every assignment (builder, yaml, direct).
|
|
7
8
|
attribute :name, :string
|
|
8
|
-
attribute :definition,
|
|
9
|
+
attribute :definition, Lutaml::Lml::Types::TextType
|
|
9
10
|
attribute :keyword, :string
|
|
10
11
|
attribute :stereotype, :string, collection: true, default: -> { [] }
|
|
11
12
|
attribute :visibility, :string, default: "public"
|
|
@@ -31,9 +32,7 @@ module Lutaml
|
|
|
31
32
|
map "name", to: :name
|
|
32
33
|
map "keyword", to: :keyword
|
|
33
34
|
map "is_abstract", to: :is_abstract
|
|
34
|
-
map "definition", to: :definition
|
|
35
|
-
to: :definition_to_yaml, from: :definition_from_yaml
|
|
36
|
-
}
|
|
35
|
+
map "definition", to: :definition
|
|
37
36
|
map "modifier", to: :modifier
|
|
38
37
|
map "stereotype", to: :stereotype
|
|
39
38
|
map "visibility", to: :visibility
|
|
@@ -42,6 +41,9 @@ module Lutaml
|
|
|
42
41
|
map "operations", to: :operations
|
|
43
42
|
map "constraints", to: :constraints
|
|
44
43
|
map "data_types", to: :data_types
|
|
44
|
+
# Owner-side defaulting is a parent-context rule ("an
|
|
45
|
+
# association nested under a class defaults its owner to that
|
|
46
|
+
# class"); only a mapping proc can see the containing model.
|
|
45
47
|
map "associations", to: :associations, with: {
|
|
46
48
|
to: :associations_to_yaml, from: :associations_from_yaml
|
|
47
49
|
}
|
|
@@ -63,22 +65,13 @@ module Lutaml
|
|
|
63
65
|
model.associations = associations
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
def definition_to_yaml(model, doc)
|
|
67
|
-
doc["definition"] = model.definition if model.definition
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def definition_from_yaml(model, value)
|
|
71
|
-
model.definition = value.to_s
|
|
72
|
-
.gsub(/\\}/, "}")
|
|
73
|
-
.gsub(/\\{/, "{")
|
|
74
|
-
.split("\n")
|
|
75
|
-
.map(&:strip)
|
|
76
|
-
.join("\n")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
68
|
def self.entity_type
|
|
80
69
|
:classes
|
|
81
70
|
end
|
|
71
|
+
|
|
72
|
+
def self.classifiable?
|
|
73
|
+
true
|
|
74
|
+
end
|
|
82
75
|
end
|
|
83
76
|
end
|
|
84
77
|
end
|
data/lib/lutaml/lml/parser.rb
CHANGED
|
@@ -5,18 +5,13 @@ require "parslet/convenience"
|
|
|
5
5
|
|
|
6
6
|
module Lutaml
|
|
7
7
|
module Lml
|
|
8
|
+
# The Parslet parser: raw LML text → parse tree. Pipeline-level
|
|
9
|
+
# entry points live on the Lutaml::Lml module (parse /
|
|
10
|
+
# parse_document).
|
|
8
11
|
class Parser < Parslet::Parser
|
|
9
12
|
include Grammar::Full
|
|
10
13
|
|
|
11
14
|
root(:diagram)
|
|
12
|
-
|
|
13
|
-
def self.parse(io)
|
|
14
|
-
Pipeline.call(io)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.parse_document(io)
|
|
18
|
-
Pipeline.call(io, resolve: false)
|
|
19
|
-
end
|
|
20
15
|
end
|
|
21
16
|
end
|
|
22
17
|
end
|
data/lib/lutaml/lml/pipeline.rb
CHANGED
|
@@ -5,26 +5,24 @@ require "parslet/convenience"
|
|
|
5
5
|
|
|
6
6
|
module Lutaml
|
|
7
7
|
module Lml
|
|
8
|
+
# Parse pipeline only: preprocess → parse → transform → process →
|
|
9
|
+
# build. View resolution and label enrichment are a separate,
|
|
10
|
+
# explicit post-parse step (ViewResolution) — file I/O for imports
|
|
11
|
+
# does not belong inside parsing.
|
|
8
12
|
class Pipeline
|
|
9
|
-
def self.call(input
|
|
10
|
-
new(input
|
|
13
|
+
def self.call(input)
|
|
14
|
+
new(input).call
|
|
11
15
|
end
|
|
12
16
|
|
|
13
|
-
def initialize(input
|
|
14
|
-
@input = input
|
|
15
|
-
@resolve = resolve
|
|
17
|
+
def initialize(input)
|
|
18
|
+
@input = Source.wrap(input)
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def call
|
|
19
22
|
data = Preprocessor.call(@input)
|
|
20
23
|
hash = parse_raw(data)
|
|
21
24
|
hash = DataProcessor.process(hash)
|
|
22
|
-
|
|
23
|
-
if @resolve
|
|
24
|
-
document = resolve_document(document)
|
|
25
|
-
AssociationLabelResolver.new.enrich(document)
|
|
26
|
-
end
|
|
27
|
-
document
|
|
25
|
+
build_document(hash)
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
private
|
|
@@ -40,25 +38,6 @@ module Lutaml
|
|
|
40
38
|
def build_document(hash)
|
|
41
39
|
DocumentBuilder.new(DocumentBuilder::DEFAULT_REGISTRY).build(:document, hash)
|
|
42
40
|
end
|
|
43
|
-
|
|
44
|
-
def resolve_document(document)
|
|
45
|
-
return document unless document.view_imports&.any?
|
|
46
|
-
|
|
47
|
-
base_path = @input.is_a?(StringIO) ? nil : @input.path
|
|
48
|
-
entities, associations = ImportResolver.new(base_path).resolve(document)
|
|
49
|
-
entities, associations = ViewResolver.new.resolve(document, entities, associations)
|
|
50
|
-
rebuild_document(document, entities, associations)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def rebuild_document(document, entities, associations)
|
|
54
|
-
grouped = entities.group_by { |e| e.class.entity_type }
|
|
55
|
-
|
|
56
|
-
document.classes = grouped[:classes] || []
|
|
57
|
-
document.enums = grouped[:enums] || []
|
|
58
|
-
document.data_types = grouped[:data_types] || []
|
|
59
|
-
document.associations = associations
|
|
60
|
-
document
|
|
61
|
-
end
|
|
62
41
|
end
|
|
63
42
|
end
|
|
64
43
|
end
|
|
@@ -6,7 +6,7 @@ module Lutaml
|
|
|
6
6
|
attr_reader :input_file
|
|
7
7
|
|
|
8
8
|
def initialize(input_file)
|
|
9
|
-
@input_file = input_file
|
|
9
|
+
@input_file = Source.wrap(input_file)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
class << self
|
|
@@ -16,37 +16,42 @@ module Lutaml
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def call
|
|
19
|
-
input_file.
|
|
20
|
-
include_root = input_file.is_a?(StringIO) ? Dir.pwd : File.dirname(input_file.path)
|
|
21
|
-
input_file.read.split("\n").reduce([]) do |res, line|
|
|
22
|
-
res.push(*process_dsl_line(include_root, line))
|
|
23
|
-
end.join("\n")
|
|
19
|
+
expand_lines(@input_file.read, @input_file.base_dir, [])
|
|
24
20
|
end
|
|
25
21
|
|
|
26
22
|
private
|
|
27
23
|
|
|
28
|
-
def
|
|
29
|
-
|
|
24
|
+
def expand_lines(text, base_dir, chain)
|
|
25
|
+
text.split(/\r?\n/)
|
|
26
|
+
.flat_map { |line| expand_line(line, base_dir, chain) }
|
|
27
|
+
.join("\n")
|
|
30
28
|
end
|
|
31
29
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
def expand_line(line, base_dir, chain)
|
|
31
|
+
cleaned = strip_comment(line)
|
|
32
|
+
path = include_path(cleaned, base_dir)
|
|
33
|
+
return [cleaned] unless path
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
raise Error, "circular include detected: #{path}" if chain.include?(path)
|
|
36
|
+
|
|
37
|
+
expand_lines(read_included(path), File.dirname(path), chain + [path])
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
return
|
|
40
|
+
def include_path(line, base_dir)
|
|
41
|
+
match = line.match(/^\s*include\s+(.+)/)
|
|
42
|
+
return nil unless match
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
File.expand_path(match[1].strip, base_dir)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def read_included(path)
|
|
48
|
+
File.read(path)
|
|
47
49
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
raise Error, "cannot read include #{path}: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def strip_comment(line)
|
|
54
|
+
line.sub(%r{//.*}, "")
|
|
50
55
|
end
|
|
51
56
|
end
|
|
52
57
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
# Normalizes pipeline input (String, StringIO, File, Tempfile) behind
|
|
8
|
+
# one interface: full text, filesystem path (or nil), and the
|
|
9
|
+
# directory imports/includes resolve against.
|
|
10
|
+
class Source
|
|
11
|
+
attr_reader :input
|
|
12
|
+
|
|
13
|
+
def self.wrap(input)
|
|
14
|
+
input.is_a?(Source) ? input : new(input)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(input)
|
|
18
|
+
@input = input
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def read
|
|
22
|
+
return @input if @input.is_a?(String)
|
|
23
|
+
|
|
24
|
+
@input.rewind
|
|
25
|
+
@input.read
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Filesystem path, or nil for anonymous input (String/StringIO).
|
|
29
|
+
def path
|
|
30
|
+
io = unwrapped
|
|
31
|
+
io.is_a?(File) ? io.path : nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def base_dir
|
|
35
|
+
path ? File.dirname(path) : Dir.pwd
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Tempfile delegates to File (it is not a File subclass on
|
|
41
|
+
# Ruby >= 3.4), so unwrap before type checks.
|
|
42
|
+
def unwrapped
|
|
43
|
+
io = @input
|
|
44
|
+
io = io.__getobj__ while io.is_a?(Delegator)
|
|
45
|
+
io
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/lutaml/lml/transform.rb
CHANGED
|
@@ -14,7 +14,11 @@ module Lutaml
|
|
|
14
14
|
rule(visibility_modifier: simple(:visibility_value)) do
|
|
15
15
|
VISIBILITY_MAP.fetch(visibility_value.to_s, 'public')
|
|
16
16
|
end
|
|
17
|
-
|
|
17
|
+
# Global scalar normalizer: Parslet applies this to every leaf in
|
|
18
|
+
# the parse tree (names may legally contain trailing spaces via
|
|
19
|
+
# class_name_chars). Binding is named :value to make the breadth
|
|
20
|
+
# explicit — it is not tied to any one grammar rule.
|
|
21
|
+
rule(simple(:value)) { value.nil? ? value : value.to_s.strip }
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
module Types
|
|
8
|
+
# LML `definition` body text. The grammar allows literal braces to
|
|
9
|
+
# be escaped (\{ \}) so they do not close the definition block;
|
|
10
|
+
# casting unescapes them and normalizes line indentation. One
|
|
11
|
+
# implementation shared by every attribute declared with this
|
|
12
|
+
# type — no per-model (de)serialization procs.
|
|
13
|
+
class TextType < Lutaml::Model::Type::String
|
|
14
|
+
def self.cast(value, options = {})
|
|
15
|
+
text = super
|
|
16
|
+
return nil if text.nil?
|
|
17
|
+
return text if Lutaml::Model::Utils.uninitialized?(text)
|
|
18
|
+
|
|
19
|
+
text.gsub(/\\}/, '}')
|
|
20
|
+
.gsub(/\\{/, '{')
|
|
21
|
+
.split("\n")
|
|
22
|
+
.map(&:strip)
|
|
23
|
+
.join("\n")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/lutaml/lml/version.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# Post-parse step: expand view imports, apply show/hide filters,
|
|
6
|
+
# rebuild the document's entity collections, and enrich association
|
|
7
|
+
# labels. Import expansion runs only when the document declares
|
|
8
|
+
# view imports; label enrichment always runs.
|
|
9
|
+
class ViewResolution
|
|
10
|
+
def self.call(document, base_dir)
|
|
11
|
+
document = resolve_imports(document, base_dir) if document.view_imports.any?
|
|
12
|
+
AssociationLabelResolver.new.enrich(document)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.resolve_imports(document, base_dir)
|
|
16
|
+
entity_index, associations = ImportResolver.new(base_dir).resolve(document)
|
|
17
|
+
entities, associations = ViewResolver.new.resolve(document, entity_index, associations)
|
|
18
|
+
rebuild_document(document, entities, associations)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.rebuild_document(document, entities, associations)
|
|
22
|
+
grouped = entities.group_by { |e| e.class.entity_type }
|
|
23
|
+
|
|
24
|
+
EntityTypes.symbols.each do |symbol|
|
|
25
|
+
document.public_send("#{symbol}=", grouped[symbol] || [])
|
|
26
|
+
end
|
|
27
|
+
document.associations = associations
|
|
28
|
+
document
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
5
|
class ViewResolver
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# entity_index is the ImportResolver's name → entity map.
|
|
7
|
+
def resolve(document, entity_index, associations)
|
|
8
|
+
visible = apply_filters(document, entity_index)
|
|
8
9
|
filtered_associations = filter_associations(associations, visible)
|
|
9
10
|
|
|
10
11
|
[visible, filtered_associations]
|
|
@@ -12,9 +13,8 @@ module Lutaml
|
|
|
12
13
|
|
|
13
14
|
private
|
|
14
15
|
|
|
15
|
-
def apply_filters(document,
|
|
16
|
-
names =
|
|
17
|
-
|
|
16
|
+
def apply_filters(document, entity_index)
|
|
17
|
+
names = entity_index.reject { |name, _| name.nil? }
|
|
18
18
|
names = apply_show_filter(names, document.show_filter)
|
|
19
19
|
names = apply_hide_filter(names, document.hide_filter)
|
|
20
20
|
|
data/lib/lutaml/lml.rb
CHANGED
|
@@ -14,21 +14,38 @@ module Lutaml
|
|
|
14
14
|
module Lml
|
|
15
15
|
class Error < Lutaml::Error; end
|
|
16
16
|
class ParsingError < Error; end
|
|
17
|
+
class ImportError < Error; end
|
|
17
18
|
|
|
18
19
|
def self.compile(input, namespace: nil)
|
|
19
20
|
ModelCompiler.new(namespace: namespace).compile(input)
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# Full entry point: parse, then resolve view imports and enrich
|
|
24
|
+
# association labels.
|
|
25
|
+
def self.parse(input)
|
|
26
|
+
source = Source.wrap(input)
|
|
27
|
+
document = Pipeline.call(source)
|
|
28
|
+
ViewResolution.call(document, source.base_dir)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Parse only — no import resolution, no label enrichment.
|
|
32
|
+
def self.parse_document(input)
|
|
33
|
+
Pipeline.call(input)
|
|
34
|
+
end
|
|
35
|
+
|
|
22
36
|
# Top-level autoloads
|
|
23
37
|
autoload :Parser, "lutaml/lml/parser"
|
|
24
38
|
autoload :Pipeline, "lutaml/lml/pipeline"
|
|
25
39
|
autoload :Preprocessor, "lutaml/lml/preprocessor"
|
|
40
|
+
autoload :Source, "lutaml/lml/source"
|
|
26
41
|
autoload :Transform, "lutaml/lml/transform"
|
|
27
42
|
autoload :DataProcessor, "lutaml/lml/data_processor"
|
|
28
43
|
autoload :DocumentBuilder, "lutaml/lml/document_builder"
|
|
29
44
|
autoload :ModelCompiler, "lutaml/lml/model_compiler"
|
|
30
45
|
autoload :ImportResolver, "lutaml/lml/import_resolver"
|
|
31
46
|
autoload :ViewResolver, "lutaml/lml/view_resolver"
|
|
47
|
+
autoload :ViewResolution, "lutaml/lml/view_resolution"
|
|
48
|
+
autoload :EntityTypes, "lutaml/lml/entity_types"
|
|
32
49
|
autoload :AssociationLabelResolver, "lutaml/lml/association_label_resolver"
|
|
33
50
|
autoload :Executor, "lutaml/lml/executor"
|
|
34
51
|
autoload :YamlParser, "lutaml/lml/yaml_parser"
|
|
@@ -64,5 +81,6 @@ module Lutaml
|
|
|
64
81
|
# Namespaces with their own autoloads
|
|
65
82
|
autoload :Grammar, "lutaml/lml/grammar"
|
|
66
83
|
autoload :Format, "lutaml/lml/format"
|
|
84
|
+
autoload :Types, "lutaml/lml/types"
|
|
67
85
|
end
|
|
68
86
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-lml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -87,6 +87,7 @@ files:
|
|
|
87
87
|
- lib/lutaml/lml/data_processor/value_processing.rb
|
|
88
88
|
- lib/lutaml/lml/data_processor/view_processing.rb
|
|
89
89
|
- lib/lutaml/lml/document_builder.rb
|
|
90
|
+
- lib/lutaml/lml/entity_types.rb
|
|
90
91
|
- lib/lutaml/lml/executor.rb
|
|
91
92
|
- lib/lutaml/lml/executor/adapter_helpers.rb
|
|
92
93
|
- lib/lutaml/lml/executor/condition_evaluator.rb
|
|
@@ -150,8 +151,12 @@ files:
|
|
|
150
151
|
- lib/lutaml/lml/parser.rb
|
|
151
152
|
- lib/lutaml/lml/pipeline.rb
|
|
152
153
|
- lib/lutaml/lml/preprocessor.rb
|
|
154
|
+
- lib/lutaml/lml/source.rb
|
|
153
155
|
- lib/lutaml/lml/transform.rb
|
|
156
|
+
- lib/lutaml/lml/types.rb
|
|
157
|
+
- lib/lutaml/lml/types/text_type.rb
|
|
154
158
|
- lib/lutaml/lml/version.rb
|
|
159
|
+
- lib/lutaml/lml/view_resolution.rb
|
|
155
160
|
- lib/lutaml/lml/view_resolver.rb
|
|
156
161
|
- lib/lutaml/lml/yaml_parser.rb
|
|
157
162
|
homepage: https://github.com/lutaml/lutaml-lml
|