lutaml-lml 0.1.8 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d249342f8fa771fc6acc926a4a4fa75eb2f2532ccd4461ea0452c7930d6a0372
4
- data.tar.gz: 7a852a63a20c02124a7c34c66501f749801711968239ef8e7fd7d92df4b96abb
3
+ metadata.gz: 8bdefb219dd99a2335669d57b9e7a79a21d822485ebc1a3748d915dee88d2901
4
+ data.tar.gz: a63770177d2c8ff0b85e0d30caa6c9033529c75eee9738fbe59b25dc7c1d30eb
5
5
  SHA512:
6
- metadata.gz: 2acbed3dc1c0da892d6344ec7ff87147d13d24de03b44ae8c8ff3ba136a3941a20ac6b95e4d1e4445c52b91ca0997e1d7c9a7425fd92713b954874ce2dd0abf3
7
- data.tar.gz: a9f5844e875eba16ef0f3bdcb2f10ff62fe377935cce06f35c7e92571a8e7cfa9a43782919b2661f161cb4be6fffe73b67fc10bbcb9c56a73ab7ec36b2daec48
6
+ metadata.gz: 3e12f920e7b614aa4902cd953e311fdde4dae2cd93a15bd9c5fe10b1c0ca234412a8228ca6cf2f0d22956ddb67cfd681127dedac417d43cd9b0c5b962e3e777a
7
+ data.tar.gz: c7a727c6d617731e160d1ad6bebbba053cd6216a10c93c2d177f391881f0405f0304b3bd7a28230c18cd85cddb3383ff7639a348b36489f142d1e6c6e052ec76
@@ -186,13 +186,22 @@ module Lutaml
186
186
  input_path = Pathname.new(path_string)
187
187
  return report_file_error(input_path, 'File does not exist') unless input_path.exist?
188
188
 
189
- parse_document(input_path)
190
- say "✓ #{input_path}", :green
191
- nil
189
+ document = parse_document(input_path)
190
+ violations = Lutaml::Lml::Validator.violations(document)
191
+ if violations.any?
192
+ report_file_error(input_path, violations_message(violations))
193
+ else
194
+ say "✓ #{input_path}", :green
195
+ nil
196
+ end
192
197
  rescue StandardError => e
193
198
  report_file_error(input_path, e.message)
194
199
  end
195
200
 
201
+ def violations_message(violations)
202
+ violations.map { |v| "[#{v.rule}] #{v.message}" }.join('; ')
203
+ end
204
+
196
205
  def report_file_error(input_path, message)
197
206
  say "✗ #{input_path}: #{message}", :red
198
207
  "#{input_path}: #{message}"
@@ -15,18 +15,52 @@ module Lutaml
15
15
 
16
16
  def process_attributes_array(obj)
17
17
  return [] if obj.empty?
18
- return obj.map { |item| process_attributes(item) } unless single_key_hashes?(obj)
18
+ return obj.map { |item| process_attributes(item) } unless named_attribute_entries?(obj)
19
+ return merge_named_entries(obj) unless single_key_hashes?(obj)
19
20
 
21
+ obj.each_with_object({}) { |item, hash| append_uniform_entry(hash, item) }
22
+ end
23
+
24
+ def append_uniform_entry(hash, item)
25
+ hash[:properties] ||= []
26
+ if item.key?(:properties)
27
+ hash[:properties] << process_attributes(item[:properties])
28
+ else
29
+ hash.merge!(process_attributes(item))
30
+ end
31
+ end
32
+
33
+ # Attribute-definition groups split across capture depths (e.g. the
34
+ # comma form: {name, type} beside a nested {value_set}) combine into
35
+ # one attribute entry. Key/value pair lists (import/export maps)
36
+ # carry :key and are never merged.
37
+ def named_attribute_entries?(obj)
38
+ obj.any? { |e| e.is_a?(Hash) && e.key?(:name) } &&
39
+ obj.all? { |e| e.is_a?(Hash) && !e.key?(:key) }
40
+ end
41
+
42
+ COLLECTION_KEYS = %i[properties attributes instances].freeze
43
+
44
+ def merge_named_entries(obj)
20
45
  obj.each_with_object({}) do |item, hash|
21
- hash[:properties] ||= []
22
- if item.key?(:properties)
23
- hash[:properties] << process_attributes(item[:properties])
24
- else
25
- hash.merge!(process_attributes(item))
46
+ process_attributes(item).each do |key, value|
47
+ hash[key] = merge_attribute_value(hash[key], key, value)
26
48
  end
27
49
  end
28
50
  end
29
51
 
52
+ def merge_attribute_value(existing, key, value)
53
+ return merge_collection_value(existing, value) if COLLECTION_KEYS.include?(key)
54
+ return existing + value if existing.is_a?(Array) && value.is_a?(Array)
55
+
56
+ value
57
+ end
58
+
59
+ def merge_collection_value(existing, value)
60
+ current = existing.is_a?(Array) ? existing : (existing ? [existing] : [])
61
+ current + (value.is_a?(Array) ? value : [value])
62
+ end
63
+
30
64
  def process_attributes_hash(obj)
31
65
  result = extract_name_and_value(obj)
32
66
  convert_instance_type(result)
@@ -34,18 +68,17 @@ module Lutaml
34
68
  result[:literal] = true if obj.key?(:type_literal)
35
69
  apply_nested_attributes(result, obj)
36
70
  apply_remaining_keys(result, obj)
71
+ translate_values_argument(result)
37
72
  result
38
73
  end
39
74
 
40
- private
41
-
42
75
  def single_key_hashes?(arr)
43
76
  arr.all? { |e| e.is_a?(Hash) && e.keys.size == 1 }
44
77
  end
45
78
 
46
79
  def extract_name_and_value(obj)
47
80
  result = {}
48
- result[:name] = obj[:key] if obj.key?(:key)
81
+ result[:name] = strip_argument_colon(obj[:key]) if obj.key?(:key)
49
82
 
50
83
  if obj.key?(:comments)
51
84
  result[:name] = 'Comment'
@@ -74,9 +107,16 @@ module Lutaml
74
107
  obj.each do |key, value|
75
108
  next if EXCLUDED_PASS_THROUGH_KEYS.include?(key)
76
109
 
110
+ value = strip_argument_colon(value) if key == :name
77
111
  result[key] = value unless result.key?(key)
78
112
  end
79
113
  end
114
+
115
+ # `default: "Clear"` uses the colon separator (RS 3001 §Attribute);
116
+ # the colon lands in the captured name and must not.
117
+ def strip_argument_colon(name)
118
+ name.to_s.sub(/:\z/, "")
119
+ end
80
120
  end
81
121
  end
82
122
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ class DataProcessor
6
+ # RS 3001 §Value/§Attribute argument normalization: the canonical
7
+ # `value` construct and the attribute `values` argument.
8
+ module ValueDeclarationProcessing
9
+ # `value <name> { block }` is the canonical enum member; the bare
10
+ # identifier member is its documented shortcut. Normalize into the
11
+ # member shape the shortcut produces: the value block's `definition`
12
+ # lines become the member description and its attribute pairs become
13
+ # member payload properties.
14
+ def process_value_declaration(decl)
15
+ inner = { name: unwrap_string(decl[:value_name]) }
16
+ Array(decl[:members]).each do |member|
17
+ normalize_value_member(inner, member)
18
+ end
19
+ { attributes: inner }
20
+ end
21
+
22
+ # `values <enum>` types the attribute with the named enum;
23
+ # `values { v1, v2 }` carries an inline value set that the compiler
24
+ # expands into an anonymous enum.
25
+ def translate_values_argument(result)
26
+ if result.key?(:value_set_ref)
27
+ ref = unwrap_string(result.delete(:value_set_ref))
28
+ result[:type] = ref unless result[:type]
29
+ end
30
+ return unless result.key?(:value_set)
31
+
32
+ result[:value_set] = Array(result[:value_set][:items]).map { |item| unwrap_data_value(item[:item]) }
33
+ end
34
+
35
+ private
36
+
37
+ def normalize_value_member(inner, member)
38
+ if member.key?(:definition)
39
+ (inner[:attributes] ||= []) << { name: 'description', type: unwrap_string(member[:definition]) }
40
+ elsif member.key?(:instance)
41
+ (inner[:instances] ||= []).concat(Array(member[:instance]))
42
+ elsif member.key?(:attributes)
43
+ (inner[:properties] ||= []) << member[:attributes]
44
+ elsif member.key?(:key)
45
+ (inner[:properties] ||= []) << extract_name_and_value(member)
46
+ end
47
+ end
48
+
49
+ def unwrap_data_value(item)
50
+ return item unless item.is_a?(Hash)
51
+
52
+ item[:string] || item[:number] || item[:float] ||
53
+ item[:boolean] || item[:name] || item
54
+ end
55
+
56
+ def unwrap_string(value)
57
+ value.is_a?(Hash) && value.key?(:string) ? value[:string] : value
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -5,12 +5,14 @@ module Lutaml
5
5
  class DataProcessor
6
6
  autoload :ValueProcessing, "lutaml/lml/data_processor/value_processing"
7
7
  autoload :AttributeProcessing, "lutaml/lml/data_processor/attribute_processing"
8
+ autoload :ValueDeclarationProcessing, "lutaml/lml/data_processor/value_declaration_processing"
8
9
  autoload :InstanceProcessing, "lutaml/lml/data_processor/instance_processing"
9
10
  autoload :CollectionProcessing, "lutaml/lml/data_processor/collection_processing"
10
11
  autoload :ViewProcessing, "lutaml/lml/data_processor/view_processing"
11
12
 
12
13
  include ValueProcessing
13
14
  include AttributeProcessing
15
+ include ValueDeclarationProcessing
14
16
  include InstanceProcessing
15
17
  include CollectionProcessing
16
18
  include ViewProcessing
@@ -20,6 +22,7 @@ module Lutaml
20
22
  instances: :process_instances,
21
23
  instance: :process_instance,
22
24
  attributes: :process_attributes,
25
+ value_decl: :process_value_declaration,
23
26
  show_list: :process_show_list,
24
27
  hide_list: :process_hide_list
25
28
  }.freeze
@@ -24,7 +24,9 @@ module Lutaml
24
24
  mapping: ::Lutaml::Lml::Mapping,
25
25
  string_format: ::Lutaml::Lml::StringFormat,
26
26
  derived_field: ::Lutaml::Lml::DerivedField,
27
- member_field: ::Lutaml::Lml::MemberField
27
+ member_field: ::Lutaml::Lml::MemberField,
28
+ namespace: ::Lutaml::Lml::Namespace,
29
+ serialization_mapping: ::Lutaml::Lml::SerializationMapping
28
30
  }.freeze
29
31
 
30
32
  attr_reader :registry
@@ -36,7 +38,7 @@ module Lutaml
36
38
  FACTORY_KEYS = %i[
37
39
  document package class enum data_type diagram view_import view_filter
38
40
  attribute association operation constraint value cardinality mapping
39
- string_format derived_field
41
+ string_format derived_field namespace serialization_mapping
40
42
  ].freeze
41
43
 
42
44
  MEMBER_KEY_MAP = {
@@ -52,7 +54,9 @@ module Lutaml
52
54
  constraints: :constraint,
53
55
  values: :value,
54
56
  mappings: :mapping,
55
- member_fields: :member_field
57
+ member_fields: :member_field,
58
+ namespaces: :namespace,
59
+ serialization_mappings: :serialization_mapping
56
60
  }.freeze
57
61
 
58
62
  FACTORY_KEYS.each do |key|
@@ -102,9 +106,11 @@ module Lutaml
102
106
  end
103
107
 
104
108
  def add_members(model, hash)
105
- expand_mapping_section(model, hash)
106
- expand_class_declarations(model, hash)
107
- expand_enum_member_fields(model, hash)
109
+ expand_declared_members(model, hash)
110
+ normalize_namespace_declaration(hash)
111
+ normalize_serialization_mapping(hash)
112
+ expand_instances_collection_name(hash)
113
+ expand_nested_members(model, hash)
108
114
  MEMBER_KEY_MAP.each do |plural_key, singular_key|
109
115
  data = hash.delete(plural_key)
110
116
  next if data.nil?
@@ -116,6 +122,138 @@ module Lutaml
116
122
  remap_filter_keys(hash, model)
117
123
  end
118
124
 
125
+ # RS 3001 §Enums: an enum-level `values { v1, v2 }` shorthand
126
+ # expands into plain members.
127
+ def expand_value_set_declaration(model, hash)
128
+ decl = hash.delete(:value_set_decl) or return
129
+ Array(decl[:items]).each do |item|
130
+ name = unquote(item[:item])
131
+ model.attributes << TopElementAttribute.new(name: name)
132
+ end
133
+ end
134
+
135
+ # RS 3001 §Value: the canonical `value` construct normalizes into
136
+ # the same member shape the bare-identifier shortcut produces.
137
+ def expand_value_declaration(model, hash)
138
+ decl = hash.delete(:value_decl) or return
139
+ apply_attribute(model, :attributes, decl.fetch(:attributes))
140
+ end
141
+
142
+ # Namespace declaration bodies arrive as keyword-line members
143
+ # ({uri: …}, {prefix: …}); flatten them onto the namespace hash.
144
+ def normalize_namespace_declaration(hash)
145
+ ns = hash[:namespaces] or return
146
+ Array(ns.delete(:members)).each do |pair|
147
+ pair.each { |key, value| ns[key] = unquote(value) }
148
+ end
149
+ end
150
+
151
+ # Serialization mapping rules arrive keyed by their line construct
152
+ # ({map_element: {wire:, field:}}); normalize into plain MappingRule
153
+ # hashes so the model cast lands typed rules.
154
+ def normalize_serialization_mapping(hash)
155
+ mapping = hash[:serialization_mappings] or return
156
+ rules = Array(mapping[:rules])
157
+ return if rules.any? && rules.all? { |e| e.is_a?(Hash) && e.key?(:kind) }
158
+
159
+ rules = rules.filter_map do |entry|
160
+ next nil unless entry.is_a?(Hash)
161
+
162
+ if entry[:wire] && entry[:to]
163
+ next { kind: 'map', wire: unquote(entry[:wire]), field: unquote(entry[:to]) }
164
+ end
165
+
166
+ first_key = entry.keys.first
167
+ kind = first_key.to_s
168
+ body = entry[first_key] || {}
169
+ unless body.is_a?(Hash)
170
+ kind_n = normalize_mapping_kind(kind)
171
+ next nil if body.to_s.empty?
172
+
173
+ term = unquote(body)
174
+ next { kind: kind_n, wire: term, field: term } if %w[element attribute map].include?(kind_n)
175
+ if kind_n == 'namespace'
176
+ mapping[:namespace_ref] = term
177
+ next nil
178
+ end
179
+
180
+ next { kind: kind_n, field: term }
181
+ end
182
+ if kind == 'element_name'
183
+ mapping[:element_name] = unquote(body[:element_name])
184
+ next nil
185
+ end
186
+ if kind == 'map_namespace'
187
+ mapping[:namespace_ref] = unquote(body[:namespace_ref])
188
+ next nil
189
+ end
190
+ normalized = { kind: normalize_mapping_kind(kind) }
191
+ wire = body[:wire] || body[:element_wire]
192
+ normalized[:wire] = unquote(wire) if wire
193
+ field = body[:field] || body[:to_field] || body[:element_field] ||
194
+ body[:attribute_field] || body[:to]
195
+ normalized[:field] = unquote(field) if field
196
+ normalized[:mode] = body[:mode] if body[:mode]
197
+ normalized[:namespace_ref] = unquote(body[:namespace_ref]) if body[:namespace_ref]
198
+ normalized
199
+ end
200
+ mapping[:rules] = rules
201
+ end
202
+
203
+ def normalize_mapping_kind(kind)
204
+ {
205
+ 'map_element' => 'element', 'map_attribute' => 'attribute',
206
+ 'map_content' => 'content', 'map_namespace' => 'namespace',
207
+ 'map_key' => 'key', 'map_value' => 'value', 'map' => 'map',
208
+ 'legacy_map' => 'map'
209
+ }.fetch(kind, kind)
210
+ end
211
+
212
+ # A named instances collection (`instances "Glazes" { ... }`) arrives
213
+ # with its name as a sibling of the collection body; move it inside
214
+ # so the InstanceCollection carries it.
215
+ def expand_instances_collection_name(hash)
216
+ return unless hash.key?(:instances) && hash.key?(:name)
217
+
218
+ name = unquote(hash.delete(:name))
219
+ body = hash[:instances].is_a?(Hash) ? hash[:instances] : {}
220
+ body[:name] = name unless body.key?(:name)
221
+ end
222
+
223
+ # The document root is a repetition of top-level constructs (RS 3001:
224
+ # documents interleave declarations), so models/packages blocks nest
225
+ # their members one level down; recurse into them.
226
+ def expand_nested_members(model, hash)
227
+ return unless hash.key?(:members)
228
+
229
+ members = hash.delete(:members)
230
+ members.to_a.each do |nested|
231
+ add_members(model, nested)
232
+ set_model_attributes(model, nested)
233
+ end
234
+ end
235
+
236
+ # Declaration-level member hooks: single-line definitions, canonical
237
+ # `value` members, mapping sections, class declarations, enum
238
+ # member_fields and from_table all arrive as bare member hashes.
239
+ def expand_declared_members(model, hash)
240
+ expand_definition_text(model, hash)
241
+ expand_value_declaration(model, hash)
242
+ expand_value_set_declaration(model, hash)
243
+ expand_mapping_section(model, hash)
244
+ expand_class_declarations(model, hash)
245
+ expand_enum_member_fields(model, hash)
246
+ expand_from_table(model, hash)
247
+ end
248
+
249
+ # RS 3001 §Definition single-line form (`definition "text"`) arrives
250
+ # as a quoted-string capture; unwrap it for the entity's definition.
251
+ def expand_definition_text(model, hash)
252
+ return unless hash.key?(:definition) && model.class.attributes.key?(:definition)
253
+
254
+ model.definition = unquote(hash.delete(:definition))
255
+ end
256
+
119
257
  # A mapping section arrives as a bare member hash
120
258
  # { format:, maps: [{ wire:, to: }] }; expand to one Mapping entry
121
259
  # per map line, each carrying the format, and strip the section keys
@@ -158,6 +296,23 @@ module Lutaml
158
296
  type: unquote(derived[:type]))
159
297
  end
160
298
 
299
+ # L7a: from_table declares the enum's members as an artifact table
300
+ # { from_table_name:, from_table_artifact? } — expansion happens at
301
+ # compile time (deterministic materialization, no runtime data dep)
302
+ def expand_from_table(model, hash)
303
+ decl = hash.delete(:from_table_decl)
304
+ return unless decl && model.is_a?(Enum)
305
+
306
+ # the members repetition splits the declaration across iteration
307
+ # hashes (name part, artifact part) — merge the parts
308
+ parts = decl.is_a?(Array) ? decl : [decl]
309
+ decl = parts.each_with_object({}) do |part, acc|
310
+ acc.merge!(part) { |_key, _old, new_val| new_val }
311
+ end
312
+ model.from_table_name = unquote(decl[:from_table_name])
313
+ model.from_table_artifact = unquote(decl[:artifact_table]) if decl[:artifact_table]
314
+ end
315
+
161
316
  def expand_enum_member_fields(model, hash)
162
317
  decl = hash.delete(:member_fields_decl)
163
318
  return unless decl && model.is_a?(Enum)