lutaml-lml 0.1.2 → 0.1.4
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/attribute_processing.rb +1 -0
- data/lib/lutaml/lml/data_processor/instance_processing.rb +11 -2
- 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/condition_evaluator.rb +1 -1
- data/lib/lutaml/lml/executor.rb +4 -5
- 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 +11 -2
- data/lib/lutaml/lml/grammar/concerns/view_rules.rb +26 -8
- data/lib/lutaml/lml/grammar/instances.rb +5 -2
- data/lib/lutaml/lml/import_resolver.rb +45 -47
- data/lib/lutaml/lml/literal_value.rb +32 -0
- 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/instance_collection.rb +1 -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 +68 -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 +21 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f72188a69457d2cf82445867f2b726dd621d63e82d79c7e56b01241e0d15ec84
|
|
4
|
+
data.tar.gz: 402135ddb2bf31953c21f6166c859c6b5f67d24d1a9e18e8369eb3646a413381
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d07e5279b6c0d983d1ba1384ef3579aa3d493fba0520aedadfd97ac11a42bfca1fd2e96e621e6551e8dd22bef52e30136779cdf88e8bbe81e611b762e1efa3cd
|
|
7
|
+
data.tar.gz: 20e402c37d1506fd86c4ded5fac0bb010dc58d3cdda8efcf3e92cd287ec8025331375310b7e40093741af136aaaf4f61d78a44456fe59b2d75dffe5e1d57568b
|
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|
|
|
@@ -26,8 +26,7 @@ module Lutaml
|
|
|
26
26
|
key = INSTANCE_KEY_HANDLERS.keys.find { |k| instance.key?(k) }
|
|
27
27
|
next unless key
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
key == :instance ? (acc[:instances] << result) : (acc[key] = result)
|
|
29
|
+
append_result(acc, key, public_send(INSTANCE_KEY_HANDLERS[key], instance[key]))
|
|
31
30
|
end
|
|
32
31
|
end
|
|
33
32
|
|
|
@@ -57,6 +56,16 @@ module Lutaml
|
|
|
57
56
|
def handle_instance_template(value, result)
|
|
58
57
|
result[:template] = process_attributes(value[:attributes])
|
|
59
58
|
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def append_result(acc, key, result)
|
|
63
|
+
case key
|
|
64
|
+
when :instance then acc[:instances] << result
|
|
65
|
+
when :collections then (acc[:collections] ||= []) << result
|
|
66
|
+
else (acc[key] ||= []).concat(result)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
60
69
|
end
|
|
61
70
|
end
|
|
62
71
|
end
|
|
@@ -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
|
|
@@ -30,8 +30,8 @@ module Lutaml
|
|
|
30
30
|
# Evaluate all validation conditions against a collection of instances.
|
|
31
31
|
# Returns an array of error strings (empty if all pass).
|
|
32
32
|
def self.evaluate(collection, instances)
|
|
33
|
-
return [] unless collection.validations&.any?
|
|
34
33
|
return [] unless collection.is_a?(Collection)
|
|
34
|
+
return [] unless collection.validations&.any?
|
|
35
35
|
|
|
36
36
|
new(instances).evaluate_all(collection.validations)
|
|
37
37
|
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
|
|
@@ -73,10 +73,9 @@ module Lutaml
|
|
|
73
73
|
def validate_collections(doc, instances)
|
|
74
74
|
return [] unless doc.instances&.collections
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
ConditionEvaluator.evaluate(collection, instances)
|
|
76
|
+
Array(doc.instances.collections).flat_map do |collection|
|
|
77
|
+
ConditionEvaluator.evaluate(collection, instances)
|
|
78
|
+
end
|
|
80
79
|
end
|
|
81
80
|
|
|
82
81
|
# --- Export ---
|
|
@@ -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,9 +19,18 @@ module Lutaml
|
|
|
19
19
|
rule(:whitespace?) { whitespace.maybe }
|
|
20
20
|
rule(:newline) { match('[\r\n]') }
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
# Body of a string delimited by `char`, captured as `label`.
|
|
23
|
+
def quoted(char, label)
|
|
24
|
+
str(char) >> (str(char).absent? >> any).repeat.as(label) >> str(char)
|
|
24
25
|
end
|
|
26
|
+
|
|
27
|
+
# A string in either quote style. Delimiters must match, so `"a'`
|
|
28
|
+
# stays a parse error.
|
|
29
|
+
def any_quoted(label)
|
|
30
|
+
quoted('"', label) | quoted("'", label)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
rule(:quoted_string) { any_quoted(:string) }
|
|
25
34
|
rule(:boolean) { (str("true") | str("false")).as(:boolean) }
|
|
26
35
|
rule(:number) { (match("[0-9]").repeat(1) >> str(".") >> match("[0-9]").repeat(1)).as(:float) | match("[0-9]").repeat(1).as(:number) }
|
|
27
36
|
rule(:variable) { (quoted_string | match("[a-zA-Z0-9_]").repeat(1)) }
|
|
@@ -7,25 +7,43 @@ 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
|
-
|
|
15
|
-
|
|
25
|
+
kw_view_import >> any_quoted(:path) >> whitespace?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Entity names in show/hide lists must be space-free: general
|
|
29
|
+
# class_name_chars allows spaces, which would swallow a
|
|
30
|
+
# following directive (`show Foo, Bar hide Baz` would parse
|
|
31
|
+
# "Bar hide Baz" as one name).
|
|
32
|
+
rule(:entity_name_atom) do
|
|
33
|
+
match('[a-zA-Z0-9_.:\-]').repeat(1).as(:entity_name)
|
|
16
34
|
end
|
|
17
35
|
|
|
18
36
|
rule(:entity_name_list) do
|
|
19
|
-
|
|
20
|
-
(spaces? >> str(",") >> spaces? >>
|
|
37
|
+
entity_name_atom >>
|
|
38
|
+
(spaces? >> str(",") >> spaces? >> entity_name_atom).repeat
|
|
21
39
|
end
|
|
22
40
|
|
|
23
41
|
rule(:show_directive) do
|
|
24
|
-
|
|
42
|
+
kw_show >> entity_name_list.as(:show_list) >> whitespace?
|
|
25
43
|
end
|
|
26
44
|
|
|
27
45
|
rule(:hide_directive) do
|
|
28
|
-
|
|
46
|
+
kw_hide >> entity_name_list.as(:hide_list) >> whitespace?
|
|
29
47
|
end
|
|
30
48
|
end
|
|
31
49
|
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
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
# The type of an LML attribute literal: a scalar, a list, or a map.
|
|
8
|
+
#
|
|
9
|
+
# This exists as a subclass rather than using Lutaml::Model::Type::Value
|
|
10
|
+
# directly because lutaml-model's key-value serializer tests the declared
|
|
11
|
+
# attribute type with a strict `attribute_type < Type::Value`. Type::Value
|
|
12
|
+
# is not a strict subclass of itself, so declaring it hands the serializer
|
|
13
|
+
# the raw wrapper object: JSON calls to_json(state) on it and raises
|
|
14
|
+
# ArgumentError, and Psych emits a !ruby/object tag that from_yaml then
|
|
15
|
+
# refuses to load. A subclass puts the attribute on the working path, where
|
|
16
|
+
# the serializer asks the wrapper for its value instead.
|
|
17
|
+
#
|
|
18
|
+
# Deleting this class needs two upstream changes, not one: that `<` widened
|
|
19
|
+
# to `<=`, and the serializer no longer re-wrapping a value it already
|
|
20
|
+
# wrapped. Widening `<` alone still leaves the nested wrapper below.
|
|
21
|
+
class LiteralValue < Lutaml::Model::Type::Value
|
|
22
|
+
# The serializer re-wraps an already-wrapped value before asking it for a
|
|
23
|
+
# format, so #value has to see through that extra layer. Every to_<format>
|
|
24
|
+
# method lutaml-model generates reads through here, which is why this is
|
|
25
|
+
# one override rather than one per format.
|
|
26
|
+
def value
|
|
27
|
+
wrapped = super
|
|
28
|
+
wrapped.is_a?(LiteralValue) ? wrapped.value : wrapped
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
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"
|