lutaml-lml 0.2.0 → 0.2.1

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: 8bdefb219dd99a2335669d57b9e7a79a21d822485ebc1a3748d915dee88d2901
4
- data.tar.gz: a63770177d2c8ff0b85e0d30caa6c9033529c75eee9738fbe59b25dc7c1d30eb
3
+ metadata.gz: 41662922310b1ec54e30485a3ec783c99f385f5e80eb0e4a98277b72ac5a71dc
4
+ data.tar.gz: 2b6030ded9c8bb82941ceefdf5e1df9a8531af5a61888faee4ecbc23df0ca961
5
5
  SHA512:
6
- metadata.gz: 3e12f920e7b614aa4902cd953e311fdde4dae2cd93a15bd9c5fe10b1c0ca234412a8228ca6cf2f0d22956ddb67cfd681127dedac417d43cd9b0c5b962e3e777a
7
- data.tar.gz: c7a727c6d617731e160d1ad6bebbba053cd6216a10c93c2d177f391881f0405f0304b3bd7a28230c18cd85cddb3383ff7639a348b36489f142d1e6c6e052ec76
6
+ metadata.gz: 94f5275ce72d247c2842a60b265bdc8980cf82b49ce45d1970b606a0a9329505776235efb2df3247165b40cd5aaccc977b9aadcf77eb4ad044119ec71e4ea0a7
7
+ data.tar.gz: bfe8561322f20e76736e00a6c67ab862207e344637097f270a1111c7a5edbc540f8137989858e8fe3584dbaded200fe150c35da83523a637d26523f9cc64f8e6
@@ -37,7 +37,12 @@ module Lutaml
37
37
 
38
38
  def initialize(*args)
39
39
  super
40
- @formatter = ::Lutaml::Formatter::Graphviz.new if defined?(::Lutaml::Formatter::Graphviz)
40
+ @formatter = case options[:layout]
41
+ when 'elk'
42
+ ::Lutaml::Formatter::Elk.new
43
+ else
44
+ ::Lutaml::Formatter::Graphviz.new
45
+ end
41
46
  @out_object = $stdout
42
47
  end
43
48
 
@@ -71,6 +76,8 @@ module Lutaml
71
76
  desc: 'Node attributes (key=value,key2=value2)'
72
77
  method_option :all, type: :string, aliases: '-a',
73
78
  desc: 'Set attributes for graph, edge, and node'
79
+ method_option :layout, type: :string, enum: %w[graphviz elk], default: 'graphviz',
80
+ desc: 'Layout engine (elk requires the elkrb gem)'
74
81
  def generate(*paths)
75
82
  assert_input_paths(paths)
76
83
 
@@ -72,6 +72,16 @@ module Lutaml
72
72
  def set_model(model, hash)
73
73
  hash = build_members(model, hash)
74
74
  set_model_attributes(model, hash)
75
+ expand_class_specializer(model, hash)
76
+ end
77
+
78
+ # The prefix specializer (`abstract class Foo`) expands to the
79
+ # canonical `abstract true` declaration (RS 3001 par. Declaration
80
+ # modifiers); the class-level modifier capture is not a block member.
81
+ def expand_class_specializer(model, hash)
82
+ return unless hash[:modifier].to_s == 'abstract' && model.class.attributes.key?(:is_abstract)
83
+
84
+ model.is_abstract = true
75
85
  end
76
86
 
77
87
  def set_model_attributes(model, hash)
@@ -83,12 +93,24 @@ module Lutaml
83
93
  def build_members(model, hash)
84
94
  members = hash.delete(:members)
85
95
  members.to_a.each do |member_hash|
96
+ record_model_version(model, member_hash)
86
97
  add_members(model, member_hash)
87
98
  set_model_attributes(model, member_hash)
88
99
  end
89
100
  hash
90
101
  end
91
102
 
103
+ # A `models <Name> version "x.y.z"` header travels with the block's
104
+ # member hash; the block itself flattens into the document, so the
105
+ # version is recorded on the document before the name is dropped.
106
+ def record_model_version(model, hash)
107
+ return unless model.class.attributes.key?(:model_versions)
108
+ return unless hash.key?(:version)
109
+
110
+ name = hash[:name].to_s
111
+ model.model_versions[name] = unquote(hash.delete(:version)).to_s
112
+ end
113
+
92
114
  def apply_attribute(model, key, value)
93
115
  return unless model.class.attributes.key?(key.to_sym)
94
116
 
@@ -159,9 +181,7 @@ module Lutaml
159
181
  rules = rules.filter_map do |entry|
160
182
  next nil unless entry.is_a?(Hash)
161
183
 
162
- if entry[:wire] && entry[:to]
163
- next { kind: 'map', wire: unquote(entry[:wire]), field: unquote(entry[:to]) }
164
- end
184
+ next { kind: 'map', wire: unquote(entry[:wire]), field: unquote(entry[:to]) } if entry[:wire] && entry[:to]
165
185
 
166
186
  first_key = entry.keys.first
167
187
  kind = first_key.to_s
@@ -172,6 +192,7 @@ module Lutaml
172
192
 
173
193
  term = unquote(body)
174
194
  next { kind: kind_n, wire: term, field: term } if %w[element attribute map].include?(kind_n)
195
+
175
196
  if kind_n == 'namespace'
176
197
  mapping[:namespace_ref] = term
177
198
  next nil
@@ -237,6 +258,7 @@ module Lutaml
237
258
  # `value` members, mapping sections, class declarations, enum
238
259
  # member_fields and from_table all arrive as bare member hashes.
239
260
  def expand_declared_members(model, hash)
261
+ expand_abstract_declaration(model, hash)
240
262
  expand_definition_text(model, hash)
241
263
  expand_value_declaration(model, hash)
242
264
  expand_value_set_declaration(model, hash)
@@ -244,10 +266,57 @@ module Lutaml
244
266
  expand_class_declarations(model, hash)
245
267
  expand_enum_member_fields(model, hash)
246
268
  expand_from_table(model, hash)
269
+ expand_operation_members(model, hash)
270
+ end
271
+
272
+ # Operation members arrive with grammar capture names
273
+ # (`visibility_modifier`, `param_default`) and per-iteration
274
+ # stereotype hashes; normalize to the model's field names.
275
+ def expand_operation_members(model, hash)
276
+ ops = hash.delete(:operations)
277
+ return unless ops && model.class.attributes.key?(:operations)
278
+
279
+ Array(ops.is_a?(Hash) ? [ops] : ops).each do |op|
280
+ model.operations << build(:operation, normalize_operation_hash(op))
281
+ end
282
+ end
283
+
284
+ def normalize_operation_hash(op)
285
+ op = op.dup
286
+ if (vis = op.delete(:visibility_modifier))
287
+ op[:visibility] = Transform::VISIBILITY_MAP.fetch(vis.to_s, 'public')
288
+ end
289
+ stereo = op[:stereotype]
290
+ if stereo
291
+ items = stereo.is_a?(Array) ? stereo : [stereo]
292
+ op[:stereotype] = items.map { |x| x.is_a?(Hash) ? x[:stereotype].to_s : x.to_s }
293
+ end
294
+ params = op[:owned_parameter]
295
+ if params
296
+ list = params.is_a?(Array) ? params : [params]
297
+ op[:owned_parameter] = list.map { |param| normalize_operation_param(param) }
298
+ end
299
+ op
300
+ end
301
+
302
+ def normalize_operation_param(param)
303
+ param = param.dup
304
+ if (default = param.delete(:param_default))
305
+ param[:default] = default.is_a?(Hash) ? default[:string].to_s : default.to_s
306
+ end
307
+ param
247
308
  end
248
309
 
249
310
  # RS 3001 §Definition single-line form (`definition "text"`) arrives
250
311
  # as a quoted-string capture; unwrap it for the entity's definition.
312
+ # RS 3001 declaration modifiers: `abstract true` maps onto the
313
+ # is_abstract field (the visibility declaration flows through the
314
+ # generic attribute path).
315
+ def expand_abstract_declaration(model, hash)
316
+ value = hash.delete(:abstract)
317
+ model.is_abstract = true if value.to_s == 'true' && model.class.attributes.key?(:is_abstract)
318
+ end
319
+
251
320
  def expand_definition_text(model, hash)
252
321
  return unless hash.key?(:definition) && model.class.attributes.key?(:definition)
253
322
 
@@ -70,6 +70,8 @@ module Lutaml
70
70
  end
71
71
 
72
72
  def format_value(prefix, key, value, indent)
73
+ return "#{prefix}#{key} = ref:(#{value[:reference]})" if reference_value?(value)
74
+
73
75
  case value
74
76
  when Array
75
77
  format_array(prefix, key, value, indent)
@@ -84,11 +86,15 @@ module Lutaml
84
86
 
85
87
  def format_array(prefix, key, items, indent)
86
88
  elements = items.map do |item|
87
- case item
88
- when Hash
89
- format_nested_instance(item, indent + 1)
89
+ if reference_value?(item)
90
+ " ref:(#{item[:reference]})"
90
91
  else
91
- " #{quote_value(item)}"
92
+ case item
93
+ when Hash
94
+ format_nested_instance(item, indent + 1)
95
+ else
96
+ " #{quote_value(item)}"
97
+ end
92
98
  end
93
99
  end
94
100
 
@@ -111,6 +117,13 @@ module Lutaml
111
117
  "#{prefix}instance#{type_clause} {\n#{inner}\n#{prefix}}"
112
118
  end
113
119
 
120
+ # RS 3001 par. Value type: a `reference` map is the wire form of a
121
+ # reference literal; it emits as `ref:(path)`, never as a nested
122
+ # instance.
123
+ def reference_value?(value)
124
+ value.is_a?(Hash) && value.keys == [:reference]
125
+ end
126
+
114
127
  def quote_value(val)
115
128
  return val.to_s if val.is_a?(Numeric) || val.is_a?(TrueClass) || val.is_a?(FalseClass)
116
129
  str = val.to_s
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Formatter
5
+ # ELK-layout diagram formatter: builds an Elkrb::Graph from the
6
+ # document, lays it out in-process (no external tools) and emits SVG,
7
+ # or converts the SVG to PS/EPS/EMF through vectory.
8
+ class Elk
9
+ VALID_TYPES = %i[svg ps eps emf].freeze
10
+
11
+ attr_reader :type
12
+
13
+ def initialize(_attributes = {})
14
+ @type = :svg
15
+ end
16
+
17
+ def type=(value)
18
+ @type = value.to_sym
19
+ return if VALID_TYPES.include?(@type)
20
+
21
+ raise ArgumentError,
22
+ "unsupported output type #{@type.inspect} (valid: #{VALID_TYPES.join(', ')})"
23
+ end
24
+
25
+ def format(document)
26
+ graph = Lutaml::Layout::ElkGraphBuilder.new.build(document)
27
+ svg = Lutaml::Layout::ElkEngine.new(input: graph).render(:svg)
28
+ @type == :svg ? svg : convert(svg)
29
+ end
30
+
31
+ private
32
+
33
+ def convert(svg)
34
+ require 'vectory'
35
+ Vectory::Svg.new(svg).public_send(:"to_#{@type}").content
36
+ rescue LoadError
37
+ raise Error, 'The vectory gem is required for PS/EPS/EMF output: ' \
38
+ 'add `gem "vectory"` to your Gemfile'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -7,7 +7,8 @@ module Lutaml
7
7
  ACCESS_SYMBOLS = {
8
8
  'public' => '+',
9
9
  'protected' => '#',
10
- 'private' => '-'
10
+ 'private' => '-',
11
+ 'package' => '~'
11
12
  }.freeze
12
13
 
13
14
  def format_attribute(node)
@@ -4,5 +4,6 @@ module Lutaml
4
4
  module Formatter
5
5
  autoload :Base, "lutaml/lml/formatter/base"
6
6
  autoload :Graphviz, "lutaml/lml/formatter/graphviz"
7
+ autoload :Elk, "lutaml/lml/formatter/elk"
7
8
  end
8
9
  end