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.
- checksums.yaml +7 -0
- data/lib/lutaml/lml/association_label_resolver.rb +52 -0
- data/lib/lutaml/lml/cli.rb +262 -0
- data/lib/lutaml/lml/data_processor/attribute_processing.rb +81 -0
- data/lib/lutaml/lml/data_processor/collection_processing.rb +37 -0
- data/lib/lutaml/lml/data_processor/instance_processing.rb +63 -0
- data/lib/lutaml/lml/data_processor/value_processing.rb +98 -0
- data/lib/lutaml/lml/data_processor/view_processing.rb +25 -0
- data/lib/lutaml/lml/data_processor.rb +49 -0
- data/lib/lutaml/lml/document_builder.rb +139 -0
- data/lib/lutaml/lml/executor/adapter_helpers.rb +45 -0
- data/lib/lutaml/lml/executor/condition_evaluator.rb +169 -0
- data/lib/lutaml/lml/executor/csv_adapter.rb +88 -0
- data/lib/lutaml/lml/executor/format_adapter.rb +54 -0
- data/lib/lutaml/lml/executor/xml_adapter.rb +102 -0
- data/lib/lutaml/lml/executor.rb +89 -0
- data/lib/lutaml/lml/format/adapter/document.rb +11 -0
- data/lib/lutaml/lml/format/adapter/mapping.rb +19 -0
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +127 -0
- data/lib/lutaml/lml/format/adapter/transform.rb +11 -0
- data/lib/lutaml/lml/format.rb +29 -0
- data/lib/lutaml/lml/formatter/base.rb +79 -0
- data/lib/lutaml/lml/formatter/graphviz/document_formatter.rb +89 -0
- data/lib/lutaml/lml/formatter/graphviz/html_builder.rb +72 -0
- data/lib/lutaml/lml/formatter/graphviz/node_formatter.rb +74 -0
- data/lib/lutaml/lml/formatter/graphviz/relationship_formatter.rb +130 -0
- data/lib/lutaml/lml/formatter/graphviz.rb +90 -0
- data/lib/lutaml/lml/formatter.rb +8 -0
- data/lib/lutaml/lml/grammar/concerns/associations.rb +76 -0
- data/lib/lutaml/lml/grammar/concerns/attributes.rb +126 -0
- data/lib/lutaml/lml/grammar/concerns/data_structures.rb +84 -0
- data/lib/lutaml/lml/grammar/concerns/definitions.rb +222 -0
- data/lib/lutaml/lml/grammar/concerns/instance_rules.rb +59 -0
- data/lib/lutaml/lml/grammar/concerns/primitives.rb +89 -0
- data/lib/lutaml/lml/grammar/concerns/view_rules.rb +34 -0
- data/lib/lutaml/lml/grammar/concerns.rb +17 -0
- data/lib/lutaml/lml/grammar/core.rb +71 -0
- data/lib/lutaml/lml/grammar/full.rb +12 -0
- data/lib/lutaml/lml/grammar/instances.rb +38 -0
- data/lib/lutaml/lml/grammar.rb +12 -0
- data/lib/lutaml/lml/has_attributes.rb +14 -0
- data/lib/lutaml/lml/import_resolver.rb +89 -0
- data/lib/lutaml/lml/layout/engine.rb +17 -0
- data/lib/lutaml/lml/layout/graph_viz_engine.rb +19 -0
- data/lib/lutaml/lml/layout.rb +8 -0
- data/lib/lutaml/lml/model_compiler.rb +325 -0
- data/lib/lutaml/lml/models/action.rb +10 -0
- data/lib/lutaml/lml/models/association.rb +26 -0
- data/lib/lutaml/lml/models/cardinality.rb +10 -0
- data/lib/lutaml/lml/models/collection.rb +11 -0
- data/lib/lutaml/lml/models/constraint.rb +20 -0
- data/lib/lutaml/lml/models/data_type.rb +31 -0
- data/lib/lutaml/lml/models/diagram.rb +17 -0
- data/lib/lutaml/lml/models/document.rb +45 -0
- data/lib/lutaml/lml/models/enum.rb +28 -0
- data/lib/lutaml/lml/models/fidelity.rb +10 -0
- data/lib/lutaml/lml/models/group.rb +11 -0
- data/lib/lutaml/lml/models/instance.rb +13 -0
- data/lib/lutaml/lml/models/instance_collection.rb +12 -0
- data/lib/lutaml/lml/models/instances_export.rb +10 -0
- data/lib/lutaml/lml/models/instances_import.rb +11 -0
- data/lib/lutaml/lml/models/operation.rb +23 -0
- data/lib/lutaml/lml/models/operation_parameter.rb +11 -0
- data/lib/lutaml/lml/models/package.rb +22 -0
- data/lib/lutaml/lml/models/primitive_type.rb +26 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +31 -0
- data/lib/lutaml/lml/models/uml_class.rb +84 -0
- data/lib/lutaml/lml/models/value.rb +12 -0
- data/lib/lutaml/lml/models/view_filter.rb +11 -0
- data/lib/lutaml/lml/models/view_import.rb +11 -0
- data/lib/lutaml/lml/parser.rb +22 -0
- data/lib/lutaml/lml/pipeline.rb +64 -0
- data/lib/lutaml/lml/preprocessor.rb +56 -0
- data/lib/lutaml/lml/transform.rb +20 -0
- data/lib/lutaml/lml/version.rb +7 -0
- data/lib/lutaml/lml/view_resolver.rb +48 -0
- data/lib/lutaml/lml/yaml_parser.rb +17 -0
- data/lib/lutaml/lml.rb +67 -0
- 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,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('&', '&')
|
|
16
|
+
.gsub('<', '<')
|
|
17
|
+
.gsub('>', '>')
|
|
18
|
+
.gsub('[', '[')
|
|
19
|
+
.gsub(']', ']')
|
|
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
|