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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/lutaml/lml/cli.rb +1 -1
  3. data/lib/lutaml/lml/data_processor/attribute_processing.rb +1 -0
  4. data/lib/lutaml/lml/data_processor/instance_processing.rb +11 -2
  5. data/lib/lutaml/lml/data_processor/view_processing.rb +7 -6
  6. data/lib/lutaml/lml/document_builder.rb +1 -8
  7. data/lib/lutaml/lml/entity_types.rb +30 -0
  8. data/lib/lutaml/lml/executor/condition_evaluator.rb +1 -1
  9. data/lib/lutaml/lml/executor.rb +4 -5
  10. data/lib/lutaml/lml/format/adapter/standard_adapter.rb +1 -1
  11. data/lib/lutaml/lml/formatter/base.rb +7 -4
  12. data/lib/lutaml/lml/grammar/concerns/definitions.rb +29 -21
  13. data/lib/lutaml/lml/grammar/concerns/primitives.rb +11 -2
  14. data/lib/lutaml/lml/grammar/concerns/view_rules.rb +26 -8
  15. data/lib/lutaml/lml/grammar/instances.rb +5 -2
  16. data/lib/lutaml/lml/import_resolver.rb +45 -47
  17. data/lib/lutaml/lml/literal_value.rb +32 -0
  18. data/lib/lutaml/lml/model_compiler.rb +3 -4
  19. data/lib/lutaml/lml/models/association.rb +1 -1
  20. data/lib/lutaml/lml/models/constraint.rb +1 -1
  21. data/lib/lutaml/lml/models/data_type.rb +5 -1
  22. data/lib/lutaml/lml/models/diagram.rb +1 -1
  23. data/lib/lutaml/lml/models/document.rb +8 -7
  24. data/lib/lutaml/lml/models/enum.rb +5 -1
  25. data/lib/lutaml/lml/models/instance_collection.rb +1 -1
  26. data/lib/lutaml/lml/models/operation.rb +1 -1
  27. data/lib/lutaml/lml/models/package.rb +1 -1
  28. data/lib/lutaml/lml/models/primitive_type.rb +5 -1
  29. data/lib/lutaml/lml/models/top_element_attribute.rb +68 -5
  30. data/lib/lutaml/lml/models/uml_class.rb +11 -18
  31. data/lib/lutaml/lml/models/value.rb +1 -1
  32. data/lib/lutaml/lml/parser.rb +3 -8
  33. data/lib/lutaml/lml/pipeline.rb +9 -30
  34. data/lib/lutaml/lml/preprocessor.rb +26 -21
  35. data/lib/lutaml/lml/source.rb +49 -0
  36. data/lib/lutaml/lml/transform.rb +5 -1
  37. data/lib/lutaml/lml/types/text_type.rb +28 -0
  38. data/lib/lutaml/lml/types.rb +10 -0
  39. data/lib/lutaml/lml/version.rb +1 -1
  40. data/lib/lutaml/lml/view_resolution.rb +32 -0
  41. data/lib/lutaml/lml/view_resolver.rb +5 -5
  42. data/lib/lutaml/lml.rb +21 -0
  43. metadata +8 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d0abfcd0323da47f7c05a6df8de6ddd2a17b91a6c022f2d503372bf9ad3ad62
4
- data.tar.gz: 98a5f6ba03661fb78b761e590631aef8870171332cbebe9a51d173633fc1e4a5
3
+ metadata.gz: f72188a69457d2cf82445867f2b726dd621d63e82d79c7e56b01241e0d15ec84
4
+ data.tar.gz: 402135ddb2bf31953c21f6166c859c6b5f67d24d1a9e18e8369eb3646a413381
5
5
  SHA512:
6
- metadata.gz: 77d556ec76e8d1fc4d60b3d903211d39223f35ed04cbde561e812ca8eadd3ef93553364d36e404c59372add94eb24ca99132147446875eb2e47bebf1e9e90484
7
- data.tar.gz: 8124cc37dac4d70ef905a1b8edb80d23acc40efe1d73496c15414250fb13d15f5dc86ebf576ff674b32d4aa3dd9e5ff69a1c7a33df558f56925813ca064aa035
6
+ metadata.gz: d07e5279b6c0d983d1ba1384ef3579aa3d493fba0520aedadfd97ac11a42bfca1fd2e96e621e6551e8dd22bef52e30136779cdf88e8bbe81e611b762e1efa3cd
7
+ data.tar.gz: 20e402c37d1506fd86c4ded5fac0bb010dc58d3cdda8efcf3e92cd287ec8025331375310b7e40093741af136aaaf4f61d78a44456fe59b2d75dffe5e1d57568b
@@ -24,7 +24,7 @@ module Lutaml
24
24
  }.freeze
25
25
 
26
26
  PARSE_HANDLERS = {
27
- 'lutaml' => ->(path) { Lutaml::Lml::Parser.parse(File.new(path)) },
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|
@@ -14,6 +14,7 @@ module Lutaml
14
14
  end
15
15
 
16
16
  def process_attributes_array(obj)
17
+ return [] if obj.empty?
17
18
  return obj.map { |item| process_attributes(item) } unless single_key_hashes?(obj)
18
19
 
19
20
  obj.each_with_object({}) do |item, hash|
@@ -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
- result = public_send(INSTANCE_KEY_HANDLERS[key], instance[key])
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
- extract_entity_names(data)
10
+ entity_names_from(data)
9
11
  end
10
12
 
11
13
  def process_hide_list(data)
12
- extract_entity_names(data)
14
+ entity_names_from(data)
13
15
  end
14
16
 
15
17
  private
16
18
 
17
- def extract_entity_names(data)
18
- return data.map { |d| extract_entity_names(d) } if data.is_a?(Array)
19
- return data[:entity_name].to_s if data.is_a?(Hash) && data.key?(:entity_name)
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
- names = entity_names.is_a?(Array) ? entity_names : [entity_names]
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
@@ -13,7 +13,7 @@ module Lutaml
13
13
  #
14
14
  # Usage:
15
15
  # compiled = ModelCompiler.new.compile(models_file)
16
- # doc = Pipeline.call(instances_file, resolve: false)
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
- collection = doc.instances.collections
77
- return [] unless collection.is_a?(Collection)
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 ---
@@ -11,7 +11,7 @@ module Lutaml
11
11
  return data if data.is_a?(Hash)
12
12
 
13
13
  input = data.is_a?(IO) ? data : StringIO.new(data.to_s)
14
- doc = Lutaml::Lml::Pipeline.call(input, resolve: false)
14
+ doc = Lutaml::Lml.parse_document(input)
15
15
  instance_to_hash(doc.instance)
16
16
  end
17
17
 
@@ -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.name == name }
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
- def name
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.name
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
- return unless handler
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
- ((str("\\") >> any) | (str("}").absent? >> any))
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(:view_inner_definitions) do
148
- title_definition |
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
- class_definition.as(:classes) |
155
- enum_definition.as(:enums) |
156
- primitive_definition.as(:primitives) |
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
- quotes? >>
181
- match['a-zA-Z0-9_\- ,.:;'].repeat(1).as(:title) >>
182
- quotes?
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
- quotes? >>
188
- match['a-zA-Z0-9_\- ,.:;'].repeat(1).as(:caption) >>
189
- quotes?
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
- rule(:quoted_string) do
23
- str('"') >> (str('"').absent? >> any).repeat.as(:string) >> str('"')
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
- rule(:view_import_keyword) { str("import") >> spaces }
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
- view_import_keyword >>
14
- str('"') >> (str('"').absent? >> any).repeat.as(:path) >> str('"') >>
15
- whitespace?
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
- class_name.as(:entity_name) >>
20
- (spaces? >> str(",") >> spaces? >> class_name.as(:entity_name)).repeat
37
+ entity_name_atom >>
38
+ (spaces? >> str(",") >> spaces? >> entity_name_atom).repeat
21
39
  end
22
40
 
23
41
  rule(:show_directive) do
24
- str("show") >> spaces >> entity_name_list.as(:show_list) >> whitespace?
42
+ kw_show >> entity_name_list.as(:show_list) >> whitespace?
25
43
  end
26
44
 
27
45
  rule(:hide_directive) do
28
- str("hide") >> spaces >> entity_name_list.as(:hide_list) >> whitespace?
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) { require_block? >> (models | diagram_definitions | view_definitions | instances | instance) }
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(base_path)
9
- @base_path = base_path
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
- entities = {}
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, entities, associations, visited, @base_path)
22
+ resolve_import(import.path, entity_index, associations, visited, @base_dir)
19
23
  end
20
24
 
21
- collect_local_entities(document, entities, associations)
25
+ collect_local_entities(document, entity_index, associations)
22
26
 
23
- [entities.values, associations]
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, entities, associations, visited, base_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
- Dir.glob(abs_pattern).each do |file_path|
41
+ files.each do |file_path|
33
42
  next if visited.include?(file_path)
34
43
  visited.add(file_path)
35
44
 
36
- if view_file?(file_path)
37
- resolve_view_file(file_path, entities, associations, visited)
38
- else
39
- resolve_model_file(file_path, entities, associations)
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
- def resolve_view_file(file_path, entities, associations, visited)
45
- doc = Parser.parse_document(File.new(file_path))
46
-
47
- collect_local_entities(doc, entities, associations)
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
- warn "Skipping #{file_path}: #{e.message}"
54
- []
59
+ @failures << "cannot read import #{file_path}: #{e.message}"
60
+ Document.new
55
61
  end
56
62
 
57
- def resolve_model_file(file_path, entities, associations)
58
- content = File.read(file_path)
59
- wrapped = "diagram Fragment {\n#{content}\n}"
60
- doc = Parser.parse_document(StringIO.new(wrapped))
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
- def merge_entities(collection, entities)
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
- entities[entity.name] ||= entity
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 = Pipeline.call(input, resolve: false)
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 : Pipeline.call(input, resolve: false)
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 : Pipeline.call(input, resolve: false)
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, :string
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, :string
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, :string
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, :string
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"