lutaml-lml 0.1.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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/lib/lutaml/lml/association_label_resolver.rb +52 -0
  3. data/lib/lutaml/lml/cli.rb +262 -0
  4. data/lib/lutaml/lml/data_processor/attribute_processing.rb +81 -0
  5. data/lib/lutaml/lml/data_processor/collection_processing.rb +37 -0
  6. data/lib/lutaml/lml/data_processor/instance_processing.rb +63 -0
  7. data/lib/lutaml/lml/data_processor/value_processing.rb +98 -0
  8. data/lib/lutaml/lml/data_processor/view_processing.rb +25 -0
  9. data/lib/lutaml/lml/data_processor.rb +49 -0
  10. data/lib/lutaml/lml/document_builder.rb +139 -0
  11. data/lib/lutaml/lml/executor/adapter_helpers.rb +45 -0
  12. data/lib/lutaml/lml/executor/condition_evaluator.rb +169 -0
  13. data/lib/lutaml/lml/executor/csv_adapter.rb +88 -0
  14. data/lib/lutaml/lml/executor/format_adapter.rb +54 -0
  15. data/lib/lutaml/lml/executor/xml_adapter.rb +102 -0
  16. data/lib/lutaml/lml/executor.rb +89 -0
  17. data/lib/lutaml/lml/format/adapter/document.rb +11 -0
  18. data/lib/lutaml/lml/format/adapter/mapping.rb +19 -0
  19. data/lib/lutaml/lml/format/adapter/standard_adapter.rb +127 -0
  20. data/lib/lutaml/lml/format/adapter/transform.rb +11 -0
  21. data/lib/lutaml/lml/format.rb +29 -0
  22. data/lib/lutaml/lml/formatter/base.rb +79 -0
  23. data/lib/lutaml/lml/formatter/graphviz/document_formatter.rb +89 -0
  24. data/lib/lutaml/lml/formatter/graphviz/html_builder.rb +72 -0
  25. data/lib/lutaml/lml/formatter/graphviz/node_formatter.rb +74 -0
  26. data/lib/lutaml/lml/formatter/graphviz/relationship_formatter.rb +130 -0
  27. data/lib/lutaml/lml/formatter/graphviz.rb +90 -0
  28. data/lib/lutaml/lml/formatter.rb +8 -0
  29. data/lib/lutaml/lml/grammar/concerns/associations.rb +76 -0
  30. data/lib/lutaml/lml/grammar/concerns/attributes.rb +126 -0
  31. data/lib/lutaml/lml/grammar/concerns/data_structures.rb +84 -0
  32. data/lib/lutaml/lml/grammar/concerns/definitions.rb +222 -0
  33. data/lib/lutaml/lml/grammar/concerns/instance_rules.rb +59 -0
  34. data/lib/lutaml/lml/grammar/concerns/primitives.rb +89 -0
  35. data/lib/lutaml/lml/grammar/concerns/view_rules.rb +34 -0
  36. data/lib/lutaml/lml/grammar/concerns.rb +17 -0
  37. data/lib/lutaml/lml/grammar/core.rb +71 -0
  38. data/lib/lutaml/lml/grammar/full.rb +12 -0
  39. data/lib/lutaml/lml/grammar/instances.rb +38 -0
  40. data/lib/lutaml/lml/grammar.rb +12 -0
  41. data/lib/lutaml/lml/has_attributes.rb +14 -0
  42. data/lib/lutaml/lml/import_resolver.rb +89 -0
  43. data/lib/lutaml/lml/layout/engine.rb +17 -0
  44. data/lib/lutaml/lml/layout/graph_viz_engine.rb +19 -0
  45. data/lib/lutaml/lml/layout.rb +8 -0
  46. data/lib/lutaml/lml/model_compiler.rb +325 -0
  47. data/lib/lutaml/lml/models/action.rb +10 -0
  48. data/lib/lutaml/lml/models/association.rb +26 -0
  49. data/lib/lutaml/lml/models/cardinality.rb +10 -0
  50. data/lib/lutaml/lml/models/collection.rb +11 -0
  51. data/lib/lutaml/lml/models/constraint.rb +20 -0
  52. data/lib/lutaml/lml/models/data_type.rb +31 -0
  53. data/lib/lutaml/lml/models/diagram.rb +17 -0
  54. data/lib/lutaml/lml/models/document.rb +45 -0
  55. data/lib/lutaml/lml/models/enum.rb +28 -0
  56. data/lib/lutaml/lml/models/fidelity.rb +10 -0
  57. data/lib/lutaml/lml/models/group.rb +11 -0
  58. data/lib/lutaml/lml/models/instance.rb +13 -0
  59. data/lib/lutaml/lml/models/instance_collection.rb +12 -0
  60. data/lib/lutaml/lml/models/instances_export.rb +10 -0
  61. data/lib/lutaml/lml/models/instances_import.rb +11 -0
  62. data/lib/lutaml/lml/models/operation.rb +23 -0
  63. data/lib/lutaml/lml/models/operation_parameter.rb +11 -0
  64. data/lib/lutaml/lml/models/package.rb +22 -0
  65. data/lib/lutaml/lml/models/primitive_type.rb +26 -0
  66. data/lib/lutaml/lml/models/top_element_attribute.rb +31 -0
  67. data/lib/lutaml/lml/models/uml_class.rb +84 -0
  68. data/lib/lutaml/lml/models/value.rb +12 -0
  69. data/lib/lutaml/lml/models/view_filter.rb +11 -0
  70. data/lib/lutaml/lml/models/view_import.rb +11 -0
  71. data/lib/lutaml/lml/parser.rb +22 -0
  72. data/lib/lutaml/lml/pipeline.rb +64 -0
  73. data/lib/lutaml/lml/preprocessor.rb +56 -0
  74. data/lib/lutaml/lml/transform.rb +20 -0
  75. data/lib/lutaml/lml/version.rb +7 -0
  76. data/lib/lutaml/lml/view_resolver.rb +48 -0
  77. data/lib/lutaml/lml/yaml_parser.rb +17 -0
  78. data/lib/lutaml/lml.rb +67 -0
  79. metadata +178 -0
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ module Format
6
+ module Adapter
7
+ class Mapping < Lutaml::KeyValue::Mapping
8
+ def initialize
9
+ super(:lml)
10
+ end
11
+
12
+ def dup_instance
13
+ self.class.new
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ module Format
6
+ module Adapter
7
+ class StandardAdapter < Document
8
+ TYPE_KEY = "__type__"
9
+
10
+ def self.parse(data, _options = {})
11
+ return data if data.is_a?(Hash)
12
+
13
+ input = data.is_a?(IO) ? data : StringIO.new(data.to_s)
14
+ doc = Lutaml::Lml::Pipeline.call(input, resolve: false)
15
+ instance_to_hash(doc.instance)
16
+ end
17
+
18
+ def initialize(attributes = {}, **options)
19
+ super
20
+ end
21
+
22
+ def to_lml(_options = {})
23
+ attrs = @attributes.dup
24
+ type_name = attrs.delete(TYPE_KEY) || "Data"
25
+ body = hash_to_lml_body(attrs)
26
+ "instance #{type_name} {\n#{body}\n}"
27
+ end
28
+
29
+ private
30
+
31
+ def self.instance_to_hash(instance)
32
+ return nil unless instance
33
+
34
+ hash = {}
35
+ hash[TYPE_KEY] = instance.type if instance.type
36
+
37
+ Array(instance.attributes).each do |attr|
38
+ if attr.instances.any?
39
+ hashes = attr.instances.map { |i| instance_to_hash(i) }
40
+ hash[attr.name] = attr.instances.one? ? hashes.first : hashes
41
+ elsif attr.value.is_a?(Array)
42
+ hash[attr.name] = attr.value.map { |v| primitive_value(v) }
43
+ elsif !attr.value.nil?
44
+ hash[attr.name] = primitive_value(attr.value)
45
+ end
46
+ end
47
+
48
+ if instance.instance
49
+ hash.merge!(instance_to_hash(instance.instance))
50
+ end
51
+
52
+ hash
53
+ end
54
+
55
+ def self.primitive_value(val)
56
+ case val
57
+ when TrueClass, FalseClass then val
58
+ when Integer, Float then val
59
+ else val.to_s
60
+ end
61
+ end
62
+
63
+ def hash_to_lml_body(hash, indent = 1)
64
+ lines = []
65
+ prefix = " " * indent
66
+
67
+ hash.each do |key, value|
68
+ next if key == TYPE_KEY
69
+
70
+ lines << format_value(prefix, key, value, indent)
71
+ end
72
+
73
+ lines.join("\n")
74
+ end
75
+
76
+ def format_value(prefix, key, value, indent)
77
+ case value
78
+ when Array
79
+ format_array(prefix, key, value, indent)
80
+ when Hash
81
+ format_nested(prefix, key, value, indent)
82
+ when TrueClass, FalseClass, Integer, Float
83
+ "#{prefix}#{key} = #{value}"
84
+ else
85
+ "#{prefix}#{key} = #{quote_value(value)}"
86
+ end
87
+ end
88
+
89
+ def format_array(prefix, key, items, indent)
90
+ elements = items.map do |item|
91
+ case item
92
+ when Hash
93
+ format_nested_instance(item, indent + 1)
94
+ else
95
+ " #{quote_value(item)}"
96
+ end
97
+ end
98
+
99
+ inner = elements.join(",\n")
100
+ "#{prefix}#{key} = [\n#{inner}\n#{prefix}]"
101
+ end
102
+
103
+ def format_nested(prefix, key, hash, indent)
104
+ type_name = hash.fetch(TYPE_KEY, "")
105
+ inner = hash_to_lml_body(hash, indent + 1)
106
+ type_clause = type_name.empty? ? "" : " #{type_name}"
107
+ "#{prefix}#{key} = instance#{type_clause} {\n#{inner}\n#{prefix}}"
108
+ end
109
+
110
+ def format_nested_instance(hash, indent)
111
+ prefix = " " * indent
112
+ type_name = hash.fetch(TYPE_KEY, "")
113
+ inner = hash_to_lml_body(hash, indent + 1)
114
+ type_clause = type_name.empty? ? "" : " #{type_name}"
115
+ "#{prefix}instance#{type_clause} {\n#{inner}\n#{prefix}}"
116
+ end
117
+
118
+ def quote_value(val)
119
+ return val.to_s if val.is_a?(Numeric) || val.is_a?(TrueClass) || val.is_a?(FalseClass)
120
+ str = val.to_s
121
+ str.match?(/^[\w-]+$/) ? str : "\"#{str}\""
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ module Format
6
+ module Adapter
7
+ class Transform < Lutaml::KeyValue::Transform; end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ module Format
6
+ module Adapter
7
+ autoload :Document, "lutaml/lml/format/adapter/document"
8
+ autoload :Mapping, "lutaml/lml/format/adapter/mapping"
9
+ autoload :Transform, "lutaml/lml/format/adapter/transform"
10
+ autoload :StandardAdapter, "lutaml/lml/format/adapter/standard_adapter"
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ # ExportTransformer is defined at Lutaml::Model::ExportTransformer but
17
+ # referenced as bare ExportTransformer from KeyValue::Transform. Bridge the gap
18
+ # so constant resolution succeeds for the :lml format's to_lml path.
19
+ unless Lutaml::KeyValue.const_defined?(:ExportTransformer)
20
+ Lutaml::KeyValue::ExportTransformer = Lutaml::Model::ExportTransformer
21
+ end
22
+
23
+ Lutaml::Model::FormatRegistry.register(
24
+ :lml,
25
+ mapping_class: Lutaml::Lml::Format::Adapter::Mapping,
26
+ adapter_class: Lutaml::Lml::Format::Adapter::StandardAdapter,
27
+ transformer: Lutaml::Lml::Format::Adapter::Transform,
28
+ key_value: true,
29
+ )
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Formatter
5
+ class << self
6
+ def all
7
+ @all ||= []
8
+ end
9
+
10
+ def find_by_name(name)
11
+ name = name.to_sym
12
+ all.detect { |formatter_class| formatter_class.name == name }
13
+ end
14
+ end
15
+
16
+ class Base
17
+ FORMAT_HANDLERS = {
18
+ Lml::TopElementAttribute => :format_attribute,
19
+ Lml::Operation => :format_operation,
20
+ Lml::Association => :format_relationship,
21
+ Lml::Document => :format_document,
22
+ Lml::DataType => :format_class,
23
+ Lml::UmlClass => :format_class,
24
+ Lml::Enum => :format_class
25
+ }.freeze
26
+
27
+ class << self
28
+ def inherited(subclass)
29
+ super
30
+ Formatter.all << subclass
31
+ end
32
+
33
+ def format(node, attributes = {})
34
+ new(attributes).format(node)
35
+ end
36
+
37
+ def name
38
+ to_s.split('::').last.downcase.to_sym
39
+ end
40
+ end
41
+
42
+ include Lml::HasAttributes
43
+
44
+ def initialize(attributes = {})
45
+ update_attributes(attributes)
46
+ end
47
+
48
+ def name
49
+ self.class.name
50
+ end
51
+
52
+ attr_reader :type
53
+
54
+ def type=(value)
55
+ @type = value.to_s.strip.downcase.to_sym
56
+ end
57
+
58
+ def format(node)
59
+ result = dispatch_format(node)
60
+ return unless result
61
+
62
+ result
63
+ end
64
+
65
+ def dispatch_format(node)
66
+ handler = FORMAT_HANDLERS.find { |type, _| node.is_a?(type) }&.last
67
+ return unless handler
68
+
69
+ public_send(handler, node)
70
+ end
71
+
72
+ def format_attribute(_node) = raise(NotImplementedError)
73
+ def format_operation(_node) = raise(NotImplementedError)
74
+ def format_relationship(_node) = raise(NotImplementedError)
75
+ def format_class(_node) = raise(NotImplementedError)
76
+ def format_document(_node) = raise(NotImplementedError)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Formatter
5
+ class Graphviz < Base
6
+ module DocumentFormatter
7
+ def format_document(node)
8
+ @fontname = node.fontname || DEFAULT_CLASS_FONT
9
+ @node['fontname'] = "#{@fontname}-bold"
10
+
11
+ hide_members, hide_other_classes = extract_fidelity_options(node)
12
+ classes = format_all_classes(node, hide_members)
13
+ associations = build_associations(node, hide_other_classes)
14
+
15
+ build_digraph(classes, associations)
16
+ end
17
+
18
+ def extract_fidelity_options(node)
19
+ if node.fidelity
20
+ [node.fidelity.hideMembers, node.fidelity.hideOtherClasses]
21
+ else
22
+ [nil, nil]
23
+ end
24
+ end
25
+
26
+ def format_all_classes(node, hide_members)
27
+ node.all_classes.map do |class_node|
28
+ graph_node_name = generate_graph_name(class_node.name)
29
+ <<~HEREDOC
30
+ #{graph_node_name} [
31
+ shape="plain"
32
+ fontname="#{@fontname || DEFAULT_CLASS_FONT}"
33
+ label=<#{format_class(class_node, hide_members)}>]
34
+ HEREDOC
35
+ end.join("\n")
36
+ end
37
+
38
+ def build_associations(node, hide_other_classes)
39
+ associations = collect_all_associations(node)
40
+ associations = sort_by_document_grouping(node.groups, associations) if node.groups
41
+ classes_names = node.classes.map(&:name)
42
+ format_filtered_associations(associations, classes_names, hide_other_classes)
43
+ end
44
+
45
+ def collect_all_associations(node)
46
+ class_level = node.classifiable_classes
47
+ seen = Set.new
48
+ all = class_level.filter_map(&:associations).flatten + node.associations
49
+ all.uniq { |a| association_key(a) }
50
+ end
51
+
52
+ def format_filtered_associations(associations, classes_names, hide_other_classes)
53
+ associations.filter_map do |assoc_node|
54
+ next if hide_other_classes && !classes_names.include?(assoc_node.member_end)
55
+
56
+ format_relationship(assoc_node)
57
+ end.join("\n")
58
+ end
59
+
60
+ def build_digraph(classes, associations)
61
+ indented_classes = indent_lines(classes)
62
+ indented_assocs = indent_lines(associations)
63
+
64
+ <<~HEREDOC
65
+ digraph G {
66
+ graph [#{@graph}]
67
+ edge [#{@edge}]
68
+ node [#{@node}]
69
+
70
+ #{indented_classes}
71
+
72
+ #{indented_assocs}
73
+ }
74
+ HEREDOC
75
+ end
76
+
77
+ def indent_lines(text)
78
+ text.lines.map { |line| " #{line}" }.join.chomp
79
+ end
80
+
81
+ private
82
+
83
+ def association_key(assoc)
84
+ [assoc.owner_end.to_s, assoc.member_end.to_s, assoc.name.to_s]
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Formatter
5
+ class Graphviz < Base
6
+ module HtmlBuilder
7
+ EMPTY_MEMBER_TABLE = <<~HEREDOC.chomp
8
+ <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
9
+ <TR><TD ALIGN="LEFT"></TD></TR>
10
+ </TABLE>
11
+ HEREDOC
12
+
13
+ def escape_html_chars(text)
14
+ text
15
+ .gsub('&', '&amp;')
16
+ .gsub('<', '&#60;')
17
+ .gsub('>', '&#62;')
18
+ .gsub('[', '&#91;')
19
+ .gsub(']', '&#93;')
20
+ end
21
+
22
+ def format_member_rows(members, hide_members, &formatter)
23
+ return EMPTY_MEMBER_TABLE if hide_members || !members&.any?
24
+
25
+ formatter ||= method(:dispatch_format)
26
+ field_rows = members.map do |member|
27
+ %(<TR><TD ALIGN="LEFT">#{formatter.call(member)}</TD></TR>)
28
+ end
29
+ build_member_table(field_rows)
30
+ end
31
+
32
+ def format_enum_member(member)
33
+ enum_literal?(member) ? format_enum_literal(member) : dispatch_format(member)
34
+ end
35
+
36
+ def enum_literal?(node)
37
+ node.is_a?(Lml::TopElementAttribute) &&
38
+ node.type.nil? && node.cardinality.nil?
39
+ end
40
+
41
+ def build_member_table(field_rows)
42
+ indented = field_rows.map { |row| (' ' * 10) + row }.join("\n")
43
+ <<~HEREDOC.chomp
44
+ <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
45
+ #{indented}
46
+ </TABLE>
47
+ HEREDOC
48
+ .concat("\n")
49
+ .concat(' ' * 6)
50
+ end
51
+
52
+ def build_name_table(name_parts)
53
+ <<~HEREDOC
54
+ <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
55
+ #{name_parts.map { |n| %(<TR><TD ALIGN="CENTER">#{n}</TD></TR>) }.join("\n")}
56
+ </TABLE>
57
+ HEREDOC
58
+ end
59
+
60
+ def build_table_body(name_html, field_table, method_table)
61
+ [name_html, field_table, method_table].compact.filter_map do |type|
62
+ <<~TEXT
63
+ <TR>
64
+ <TD>#{type}</TD>
65
+ </TR>
66
+ TEXT
67
+ end.join("\n")
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Formatter
5
+ class Graphviz < Base
6
+ module NodeFormatter
7
+ ACCESS_SYMBOLS = {
8
+ 'public' => '+',
9
+ 'protected' => '#',
10
+ 'private' => '-'
11
+ }.freeze
12
+
13
+ def format_attribute(node)
14
+ symbol = ACCESS_SYMBOLS[node.visibility]
15
+ result = "#{symbol}#{node.name}"
16
+ if node.type
17
+ keyword = node.keyword ? "«#{node.keyword}»" : ''
18
+ result += " : #{keyword}#{node.type}"
19
+ end
20
+ result += format_cardinality_bounds(node.cardinality) if node.cardinality
21
+ result = escape_html_chars(result)
22
+ result = "<U>#{result}</U>" if node.static
23
+ result
24
+ end
25
+
26
+ def format_cardinality_bounds(cardinality)
27
+ min = cardinality.min || '*'
28
+ max = cardinality.max || '*'
29
+ "[#{min}..#{max}]"
30
+ end
31
+
32
+ def format_operation(node)
33
+ symbol = ACCESS_SYMBOLS[node.visibility]
34
+ params = format_operation_params(node.owned_parameter)
35
+ result = "#{symbol} #{node.name}(#{params})"
36
+ result << " : #{node.return_type}" if node.return_type
37
+ result = escape_html_chars(result)
38
+ result = "<U>#{result}</U>" if node.is_static
39
+ result = "<I>#{result}</I>" if node.is_abstract
40
+ result
41
+ end
42
+
43
+ def format_operation_params(params)
44
+ return '' unless params&.any?
45
+
46
+ params.map do |param|
47
+ param.type ? "#{param.name} : #{param.type}" : param.name.to_s
48
+ end.join(', ')
49
+ end
50
+
51
+ def format_class(node, hide_members = nil)
52
+ name = ["<B>#{escape_html_chars(node.name)}</B>"]
53
+ name.unshift("«#{escape_html_chars(node.keyword)}»") if node.keyword
54
+ name_html = build_name_table(name)
55
+
56
+ member_formatter = node.is_a?(Lml::Enum) ? method(:format_enum_member) : nil
57
+ field_table = format_member_rows(node.attributes, hide_members, &member_formatter)
58
+ method_table = format_member_rows(node.operations, hide_members) if node.operations&.any?
59
+ table_body = build_table_body(name_html, field_table, method_table)
60
+
61
+ <<~HEREDOC.chomp
62
+ <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="10">
63
+ #{table_body}
64
+ </TABLE>
65
+ HEREDOC
66
+ end
67
+
68
+ def format_enum_literal(node)
69
+ "<I>#{escape_html_chars(node.name.to_s)}</I>"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Formatter
5
+ class Graphviz < Base
6
+ module RelationshipFormatter
7
+ ARROW_TYPES = {
8
+ 'composition' => 'diamond',
9
+ 'aggregation' => 'odiamond',
10
+ 'direct' => 'vee'
11
+ }.freeze
12
+
13
+ DASHED_TYPES = %w[dependency realizes].freeze
14
+
15
+ DIRECTION_LABELS = {
16
+ 'target' => '%s ▶',
17
+ 'source' => '◀ %s'
18
+ }.freeze
19
+
20
+ def format_relationship(node)
21
+ graph_parent_name = generate_graph_name(node.owner_end)
22
+ graph_node_name = generate_graph_name(node.member_end)
23
+ attributes = build_edge_attributes(node)
24
+ graph_attributes = " [#{attributes}]" unless attributes.empty?
25
+
26
+ "#{graph_parent_name} -> #{graph_node_name}#{graph_attributes}"
27
+ end
28
+
29
+ def build_edge_attributes(node)
30
+ attrs = Attributes.new
31
+
32
+ apply_edge_style(attrs, node)
33
+ apply_edge_direction(attrs, node)
34
+ apply_edge_labels(attrs, node)
35
+ apply_arrow_types(attrs, node)
36
+ maybe_swap_labels(attrs)
37
+
38
+ attrs
39
+ end
40
+
41
+ def format_label(name, cardinality = nil)
42
+ res = "+#{name}"
43
+ return res if cardinality.nil? || cardinality.min.nil? || cardinality.max.nil?
44
+
45
+ "#{res} #{cardinality.min}..#{cardinality.max}"
46
+ end
47
+
48
+ private
49
+
50
+ def sort_by_document_grouping(groups, associations)
51
+ result = []
52
+ seen = Set.new
53
+
54
+ groups.each do |group|
55
+ group.values.each do |group_name|
56
+ matches = associations.select { |a| a.owner_end == group_name }
57
+ matches.each { |a| add_unseen(a, result, seen) }
58
+ end
59
+ end
60
+ associations.each { |a| add_unseen(a, result, seen) }
61
+ result
62
+ end
63
+
64
+ def add_unseen(association, result, seen)
65
+ return if seen.include?(association)
66
+
67
+ result.push(association)
68
+ seen.add(association)
69
+ end
70
+
71
+ def apply_edge_style(attrs, node)
72
+ attrs['style'] = 'dashed' if DASHED_TYPES.include?(node.member_end_type)
73
+ end
74
+
75
+ def apply_edge_direction(attrs, node)
76
+ attrs['dir'] = if node.owner_end_type && node.member_end_type
77
+ 'both'
78
+ elsif node.owner_end_type
79
+ 'back'
80
+ else
81
+ 'direct'
82
+ end
83
+ end
84
+
85
+ def apply_edge_labels(attrs, node)
86
+ apply_verb_label(attrs, node)
87
+ apply_direction_label(attrs, node)
88
+ apply_endpoint_labels(attrs, node)
89
+ end
90
+
91
+ def apply_verb_label(attrs, node)
92
+ attrs['label'] = node.action.verb if node&.action&.verb
93
+ end
94
+
95
+ def apply_direction_label(attrs, node)
96
+ return unless node&.action&.direction
97
+
98
+ template = DIRECTION_LABELS[node.action.direction]
99
+ attrs['label'] = template % attrs['label'] if template
100
+ end
101
+
102
+ def apply_endpoint_labels(attrs, node)
103
+ if node.owner_end_attribute_name
104
+ attrs['headlabel'] = format_label(
105
+ node.owner_end_attribute_name, node.owner_end_cardinality
106
+ )
107
+ end
108
+
109
+ return unless node.member_end_attribute_name
110
+
111
+ attrs['taillabel'] = format_label(
112
+ node.member_end_attribute_name, node.member_end_cardinality
113
+ )
114
+ end
115
+
116
+ def apply_arrow_types(attrs, node)
117
+ attrs['arrowtail'] = ARROW_TYPES.fetch(node.owner_end_type, 'onormal')
118
+ attrs['arrowhead'] = ARROW_TYPES.fetch(node.member_end_type, 'onormal')
119
+ end
120
+
121
+ def maybe_swap_labels(attrs)
122
+ return unless attrs['dir'] == 'back' && attrs['arrowtail'] != 'vee'
123
+
124
+ attrs['arrowhead'], attrs['arrowtail'] = [attrs['arrowtail'], attrs['arrowhead']]
125
+ attrs['headlabel'], attrs['taillabel'] = [attrs['taillabel'], attrs['headlabel']]
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end