lutaml 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,59 @@
1
+ require "lutaml/lutaml_path/document_wrapper"
2
+ require "expressir/express_exp/formatter"
3
+
4
+ module Lutaml
5
+ module Express
6
+ module LutamlPath
7
+ class DocumentWrapper < ::Lutaml::LutamlPath::DocumentWrapper
8
+ SCHEMA_ATTRIBUTES = %w[
9
+ id
10
+ constants
11
+ declarations
12
+ entities
13
+ functions
14
+ interfaces
15
+ procedures
16
+ remarks
17
+ rules
18
+ subtype_constraints
19
+ types
20
+ version
21
+ ].freeze
22
+ SOURCE_CODE_ATTRIBUTE_NAME = "sourcecode".freeze
23
+
24
+ protected
25
+
26
+ def serialize_document(repository)
27
+ repository.schemas.each_with_object({}) do |schema, res|
28
+ res["schemas"] ||= []
29
+ serialized_schema = SCHEMA_ATTRIBUTES
30
+ .each_with_object({}) do |name, nested_res|
31
+ attr_value = schema.send(name)
32
+ nested_res[name] = serialize_value(attr_value)
33
+ if name == "entities"
34
+ nested_res[name] = merge_source_code_attr(nested_res[name],
35
+ attr_value)
36
+ end
37
+ end
38
+ res[schema.id] = serialized_schema
39
+ serialized_schema = serialized_schema
40
+ .merge(SOURCE_CODE_ATTRIBUTE_NAME =>
41
+ entity_source_code(schema))
42
+ res["schemas"].push(serialized_schema)
43
+ end
44
+ end
45
+
46
+ def merge_source_code_attr(serialized_entries, entities)
47
+ serialized_entries.map do |serialized|
48
+ entity = entities.detect { |n| n.id == serialized["id"] }
49
+ serialized.merge(SOURCE_CODE_ATTRIBUTE_NAME => entity_source_code(entity))
50
+ end
51
+ end
52
+
53
+ def entity_source_code(entity)
54
+ Expressir::ExpressExp::Formatter.format(entity)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,19 @@
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
+
13
+ all.detect { |formatter_class| formatter_class.name == name }
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ require "lutaml/formatter/graphviz"
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/formatter"
4
+ require "lutaml/uml/has_attributes"
5
+
6
+ module Lutaml
7
+ module Formatter
8
+ class Base
9
+ class << self
10
+ def inherited(subclass)
11
+ Formatter.all << subclass
12
+ end
13
+
14
+ def format(node, attributes = {})
15
+ new(attributes).format(node)
16
+ end
17
+
18
+ def name
19
+ to_s.split("::").last.downcase.to_sym
20
+ end
21
+ end
22
+
23
+ include ::Lutaml::Uml::HasAttributes
24
+
25
+ # rubocop:disable Rails/ActiveRecordAliases
26
+ def initialize(attributes = {})
27
+ update_attributes(attributes)
28
+ end
29
+ # rubocop:enable Rails/ActiveRecordAliases
30
+
31
+ def name
32
+ self.class.name
33
+ end
34
+
35
+ attr_reader :type
36
+
37
+ def type=(value)
38
+ @type = value.to_s.strip.downcase.to_sym
39
+ end
40
+
41
+ def format(node)
42
+ case node
43
+ when ::Lutaml::Uml::Node::Field then format_field(node)
44
+ when ::Lutaml::Uml::Node::Method then format_method(node)
45
+ when ::Lutaml::Uml::Node::Relationship then format_relationship(node)
46
+ when ::Lutaml::Uml::Node::ClassRelationship
47
+ then format_class_relationship(node)
48
+ when ::Lutaml::Uml::Node::ClassNode then format_class(node)
49
+ when Lutaml::Uml::Document then format_document(node)
50
+ end
51
+ end
52
+
53
+ def format_field(_node); raise NotImplementedError; end
54
+
55
+ def format_method(_node); raise NotImplementedError; end
56
+
57
+ def format_relationship(_node); raise NotImplementedError; end
58
+
59
+ def format_class_relationship(_node); raise NotImplementedError; end
60
+
61
+ def format_class(_node); raise NotImplementedError; end
62
+
63
+ def format_document(_node); raise NotImplementedError; end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "lutaml/formatter/base"
5
+ require "lutaml/layout/graph_viz_engine"
6
+
7
+ module Lutaml
8
+ module Formatter
9
+ class Graphviz < Base
10
+ class Attributes < Hash
11
+ def to_s
12
+ to_a
13
+ .reject { |(_k, val)| val.nil? }
14
+ .map { |(a, b)| "#{a}=#{b.inspect}" }
15
+ .join(" ")
16
+ end
17
+ end
18
+
19
+ ACCESS_SYMBOLS = {
20
+ "public" => "+",
21
+ "protected" => "#",
22
+ "private" => "-",
23
+ }.freeze
24
+ DEFAULT_CLASS_FONT = "Helvetica".freeze
25
+
26
+ VALID_TYPES = %i[
27
+ dot
28
+ xdot
29
+ ps
30
+ pdf
31
+ svg
32
+ svgz
33
+ fig
34
+ png
35
+ gif
36
+ jpg
37
+ jpeg
38
+ json
39
+ imap
40
+ cmapx
41
+ ].freeze
42
+
43
+ def initialize(attributes = {})
44
+ super
45
+
46
+ @graph = Attributes.new
47
+ # Associations lines style, `true` gives curved lines
48
+ # https://graphviz.org/doc/info/attrs.html#d:splines
49
+ @graph["splines"] = "ortho"
50
+ # Padding between outside of picture and nodes
51
+ @graph["pad"] = 0.5
52
+ # Padding between levels
53
+ @graph["ranksep"] = "1.2.equally"
54
+ # Padding between nodes
55
+ @graph["nodesep"] = "1.2.equally"
56
+ # TODO: set rankdir
57
+ # @graph['rankdir'] = 'BT'
58
+
59
+ @edge = Attributes.new
60
+ @edge["color"] = "gray50"
61
+
62
+ @node = Attributes.new
63
+ @node["shape"] = "box"
64
+
65
+ @type = :dot
66
+ end
67
+
68
+ attr_reader :graph
69
+ attr_reader :edge
70
+ attr_reader :node
71
+
72
+ def type=(value)
73
+ super
74
+
75
+ @type = :dot unless VALID_TYPES.include?(@type)
76
+ end
77
+
78
+ def format(node)
79
+ dot = super.lines.map(&:rstrip).join("\n")
80
+
81
+ generate_from_dot(dot)
82
+ end
83
+
84
+ def escape_html_chars(text)
85
+ text
86
+ .gsub(/</, "&#60;")
87
+ .gsub(/>/, "&#62;")
88
+ .gsub(/\[/, "&#91;")
89
+ .gsub(/\]/, "&#93;")
90
+ end
91
+
92
+ def format_field(node)
93
+ symbol = ACCESS_SYMBOLS[node.visibility]
94
+ result = "#{symbol}#{node.name}"
95
+ if node.type
96
+ keyword = node.keyword ? "«#{node.keyword}»" : ""
97
+ result += " : #{keyword}#{node.type}"
98
+ end
99
+ if node.cardinality
100
+ result += "[#{node.cardinality[:min]}..#{node.cardinality[:max]}]"
101
+ end
102
+ result = escape_html_chars(result)
103
+ result = "<U>#{result}</U>" if node.static
104
+
105
+ result
106
+ end
107
+
108
+ def format_method(node)
109
+ symbol = ACCESS_SYMBOLS[node.access]
110
+ result = "#{symbol} #{node.name}"
111
+ if node.arguments
112
+ arguments = node.arguments.map do |argument|
113
+ "#{argument.name}#{" : #{argument.type}" if argument.type}"
114
+ end.join(", ")
115
+ end
116
+
117
+ result << "(#{arguments})"
118
+ result << " : #{node.type}" if node.type
119
+ result = "<U>#{result}</U>" if node.static
120
+ result = "<I>#{result}</I>" if node.abstract
121
+
122
+ result
123
+ end
124
+
125
+ def format_relationship(node)
126
+ graph_parent_name = generate_graph_name(node.owner_end)
127
+ graph_node_name = generate_graph_name(node.member_end)
128
+ attributes = generate_graph_relationship_attributes(node)
129
+ graph_attributes = " [#{attributes}]" unless attributes.empty?
130
+
131
+ %{#{graph_parent_name} -> #{graph_node_name}#{graph_attributes}}
132
+ end
133
+
134
+ def generate_graph_relationship_attributes(node)
135
+ attributes = Attributes.new
136
+ if %w[dependency realizes].include?(node.member_end_type)
137
+ attributes["style"] = "dashed"
138
+ end
139
+ attributes["dir"] = if node.owner_end_type && node.member_end_type
140
+ "both"
141
+ elsif node.owner_end_type
142
+ "back"
143
+ else
144
+ "direct"
145
+ end
146
+ attributes["label"] = node.action if node.action
147
+ if node.owner_end_attribute_name
148
+ attributes["headlabel"] = format_label(
149
+ node.owner_end_attribute_name,
150
+ node.owner_end_cardinality
151
+ )
152
+ end
153
+ if node.member_end_attribute_name
154
+ attributes["taillabel"] = format_label(
155
+ node.member_end_attribute_name,
156
+ node.member_end_cardinality
157
+ )
158
+ end
159
+
160
+ attributes["arrowtail"] = case node.owner_end_type
161
+ when "composition"
162
+ "diamond"
163
+ when "aggregation"
164
+ "odiamond"
165
+ when "direct"
166
+ "vee"
167
+ else
168
+ "onormal"
169
+ end
170
+
171
+ attributes["arrowhead"] = case node.member_end_type
172
+ when "composition"
173
+ "diamond"
174
+ when "aggregation"
175
+ "odiamond"
176
+ when "direct"
177
+ "vee"
178
+ else
179
+ "onormal"
180
+ end
181
+ # swap labels and arrows if `dir` eq to `back`
182
+ if attributes["dir"] == "back"
183
+ attributes["arrowhead"], attributes["arrowtail"] =
184
+ [attributes["arrowtail"], attributes["arrowhead"]]
185
+ attributes["headlabel"], attributes["taillabel"] =
186
+ [attributes["taillabel"], attributes["headlabel"]]
187
+ end
188
+ attributes
189
+ end
190
+
191
+ def format_label(name, cardinality = {})
192
+ res = "+#{name}"
193
+ if cardinality.nil? ||
194
+ (cardinality["min"].nil? || cardinality["max"].nil?)
195
+ return res
196
+ end
197
+
198
+ "#{res} #{cardinality['min']}..#{cardinality['max']}"
199
+ end
200
+
201
+ def format_member_rows(members, hide_members)
202
+ unless !hide_members && members && members.length.positive?
203
+ return <<~HEREDOC.chomp
204
+ <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
205
+ <TR><TD ALIGN="LEFT"></TD></TR>
206
+ </TABLE>
207
+ HEREDOC
208
+ end
209
+
210
+ field_rows = members.map do |field|
211
+ %{<TR><TD ALIGN="LEFT">#{format_field(field)}</TD></TR>}
212
+ end
213
+ field_table = <<~HEREDOC.chomp
214
+ <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
215
+ #{field_rows.map { |row| ' ' * 10 + row }.join("\n")}
216
+ </TABLE>
217
+ HEREDOC
218
+ field_table << "\n" << " " * 6
219
+ field_table
220
+ end
221
+
222
+ def format_class(node, hide_members)
223
+ name = ["<B>#{node.name}</B>"]
224
+ name.unshift("«#{node.keyword}»") if node.keyword
225
+ name_html = <<~HEREDOC
226
+ <TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
227
+ #{name.map { |n| %(<TR><TD ALIGN="CENTER">#{n}</TD></TR>) }.join('\n')}
228
+ </TABLE>
229
+ HEREDOC
230
+
231
+ field_table = format_member_rows(node.attributes, hide_members)
232
+ method_table = format_member_rows(node.methods, hide_members)
233
+ table_body = [name_html, field_table, method_table].map do |type|
234
+ next if type.nil?
235
+
236
+ <<~TEXT
237
+ <TR>
238
+ <TD>#{type}</TD>
239
+ </TR>
240
+ TEXT
241
+ end
242
+
243
+ <<~HEREDOC.chomp
244
+ <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="10">
245
+ #{table_body.compact.join("\n")}
246
+ </TABLE>
247
+ HEREDOC
248
+ end
249
+
250
+ def format_document(node)
251
+ @fontname = node.fontname || DEFAULT_CLASS_FONT
252
+ @node["fontname"] = "#{@fontname}-bold"
253
+
254
+ if node.fidelity
255
+ hide_members = node.fidelity["hideMembers"]
256
+ hide_other_classes = node.fidelity["hideOtherClasses"]
257
+ end
258
+ classes = (node.classes +
259
+ node.enums +
260
+ node.data_types +
261
+ node.primitives).map do |class_node|
262
+ graph_node_name = generate_graph_name(class_node.name)
263
+
264
+ <<~HEREDOC
265
+ #{graph_node_name} [
266
+ shape="plain"
267
+ fontname="#{@fontname || DEFAULT_CLASS_FONT}"
268
+ label=<#{format_class(class_node, hide_members)}>]
269
+ HEREDOC
270
+ end.join("\n")
271
+ associations = node.classes.map(&:associations).compact.flatten +
272
+ node.associations
273
+ if node.groups
274
+ associations = sort_by_document_groupping(node.groups,
275
+ associations)
276
+ end
277
+ classes_names = node.classes.map(&:name)
278
+ associations = associations.map do |assoc_node|
279
+ if hide_other_classes &&
280
+ !classes_names.include?(assoc_node.member_end)
281
+ next
282
+ end
283
+
284
+ format_relationship(assoc_node)
285
+ end.join("\n")
286
+
287
+ classes = classes.lines.map { |line| " #{line}" }.join.chomp
288
+ associations = associations
289
+ .lines.map { |line| " #{line}" }.join.chomp
290
+
291
+ <<~HEREDOC
292
+ digraph G {
293
+ graph [#{@graph}]
294
+ edge [#{@edge}]
295
+ node [#{@node}]
296
+
297
+ #{classes}
298
+
299
+ #{associations}
300
+ }
301
+ HEREDOC
302
+ end
303
+
304
+ protected
305
+
306
+ def sort_by_document_groupping(groups, associations)
307
+ result = []
308
+ groups.each do |batch|
309
+ batch.each do |group_name|
310
+ associations
311
+ .select { |assc| assc.owner_end == group_name }
312
+ .each do |association|
313
+ result.push(association) unless result.include?(association)
314
+ end
315
+ end
316
+ end
317
+ associations.each do |association|
318
+ result.push(association) unless result.include?(association)
319
+ end
320
+ result
321
+ end
322
+
323
+ def generate_from_dot(input)
324
+ Lutaml::Layout::GraphVizEngine.new(input: input).render(@type)
325
+ end
326
+
327
+ def generate_graph_name(name)
328
+ name.gsub(/[^0-9a-zA-Z]/i, "")
329
+ end
330
+ end
331
+ end
332
+ end