lutaml 0.3.3 → 0.4.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 +4 -4
- data/.github/workflows/macos.yml +2 -0
- data/.github/workflows/ubuntu.yml +2 -0
- data/.github/workflows/windows.yml +7 -0
- data/README.adoc +19 -1
- data/exe/lutaml +22 -0
- data/lib/lutaml/command_line.rb +261 -0
- data/lib/lutaml/formatter.rb +19 -0
- data/lib/lutaml/formatter/base.rb +66 -0
- data/lib/lutaml/formatter/graphviz.rb +332 -0
- data/lib/lutaml/layout/engine.rb +15 -0
- data/lib/lutaml/layout/graph_viz_engine.rb +18 -0
- data/lib/lutaml/lutaml_path/document_wrapper.rb +3 -1
- data/lib/lutaml/parser.rb +38 -8
- data/lib/lutaml/version.rb +1 -1
- data/lutaml.gemspec +5 -2
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84bb179e752bc157f0de4a59ea8cf88e9bd1fe4430726dcefcf63269d12c86fb
|
4
|
+
data.tar.gz: a5f8917e80a33814b9cc6a9e3556b335da72fe05fd73e4d54dc294d1c4b6a27f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64b11704d20aff55c53cb6b1d857d346a6e9016b84b091581372c38b100a3d21d499685550e8d5a8b132e4cc7de5e8da37c3efd14ae325d119a207a76fc0feea
|
7
|
+
data.tar.gz: f9800bc7e40536370807a3e4a3429d60dfa1f2fd728054adf6c9a8e73ba813b20bad80e22c85323309089eb41e1455806381513c139de1a99cc804ebb37378f2
|
data/.github/workflows/macos.yml
CHANGED
@@ -30,6 +30,13 @@ jobs:
|
|
30
30
|
uses: actions/setup-ruby@v1
|
31
31
|
with:
|
32
32
|
ruby-version: ${{ matrix.ruby }}
|
33
|
+
- name: Install graphviz
|
34
|
+
uses: nick-invision/retry@v1
|
35
|
+
with:
|
36
|
+
polling_interval_seconds: 5
|
37
|
+
timeout_minutes: 5
|
38
|
+
max_attempts: 3
|
39
|
+
command: choco install --no-progress graphviz --version 2.38.0.20190211
|
33
40
|
- name: Update gems
|
34
41
|
shell: pwsh
|
35
42
|
run: |
|
data/README.adoc
CHANGED
@@ -30,7 +30,9 @@ $ gem install lutaml
|
|
30
30
|
|
31
31
|
=== Usage
|
32
32
|
|
33
|
-
|
33
|
+
== From ruby
|
34
|
+
|
35
|
+
In order to parse files supported by lutaml extensions, use Lutaml::Parser.parse method.
|
34
36
|
|
35
37
|
[source,ruby]
|
36
38
|
----
|
@@ -38,6 +40,22 @@ In order to parse files supported by lutaml extensions, use Lutaml::Parser.parse
|
|
38
40
|
Lutaml::Parser.parse(File.new("example.exp")) # will produce Lutaml::LutamlPath::DocumentWrapper object with serialized express repository
|
39
41
|
----
|
40
42
|
|
43
|
+
== With cli tool
|
44
|
+
|
45
|
+
There is a cli tool available for parsing lutaml/exp files(also yaml datastruct files are supported).
|
46
|
+
|
47
|
+
[source,bash]
|
48
|
+
----
|
49
|
+
# Will generate `test.dot` file in the current directory
|
50
|
+
$: lutaml -o . test.lutaml
|
51
|
+
|
52
|
+
# Will generate `test.png` file in the `assets` directory
|
53
|
+
$: lutaml -o assets -t png test.lutaml
|
54
|
+
----
|
55
|
+
|
56
|
+
For additional info refer to `lutaml --help output`
|
57
|
+
|
58
|
+
|
41
59
|
== Development
|
42
60
|
|
43
61
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/exe/lutaml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
# resolve bin path, ignoring symlinks
|
6
|
+
require "pathname"
|
7
|
+
bin_file = Pathname.new(__FILE__).realpath
|
8
|
+
|
9
|
+
# add self to libpath
|
10
|
+
$:.unshift File.expand_path("../../lib", bin_file)
|
11
|
+
|
12
|
+
# Fixes https://github.com/rubygems/rubygems/issues/1420
|
13
|
+
require "rubygems/specification"
|
14
|
+
|
15
|
+
class Gem::Specification
|
16
|
+
def this; self; end
|
17
|
+
end
|
18
|
+
|
19
|
+
require "lutaml"
|
20
|
+
require "lutaml/command_line"
|
21
|
+
|
22
|
+
Lutaml::CommandLine.run(ARGV.dup, STDOUT)
|
@@ -0,0 +1,261 @@
|
|
1
|
+
require "optparse"
|
2
|
+
require "pathname"
|
3
|
+
require "lutaml/formatter"
|
4
|
+
require "lutaml/uml/has_attributes"
|
5
|
+
require "lutaml/uml/parsers/attribute"
|
6
|
+
require "lutaml/uml/parsers/dsl"
|
7
|
+
require "lutaml/uml/parsers/yaml"
|
8
|
+
|
9
|
+
module Lutaml
|
10
|
+
class CommandLine
|
11
|
+
class Error < StandardError; end
|
12
|
+
class FileError < Error; end
|
13
|
+
class NotSupportedInputFormat < Error; end
|
14
|
+
|
15
|
+
include ::Lutaml::Uml::HasAttributes
|
16
|
+
|
17
|
+
SUPPORTED_FORMATS = %w[yaml lutaml exp].freeze
|
18
|
+
DEFAULT_INPUT_FORMAT = "lutaml".freeze
|
19
|
+
|
20
|
+
def self.run(args, out_object, attributes = {})
|
21
|
+
new(attributes, out_object).run(args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(attributes = {}, out_object = STDOUT)
|
25
|
+
@formatter = ::Lutaml::Formatter::Graphviz.new
|
26
|
+
@verbose = false
|
27
|
+
@option_parser = OptionParser.new
|
28
|
+
@out_object = out_object
|
29
|
+
|
30
|
+
setup_parser_options
|
31
|
+
|
32
|
+
# rubocop:disable Rails/ActiveRecordAliases
|
33
|
+
update_attributes(attributes)
|
34
|
+
# rubocop:enable Rails/ActiveRecordAliases
|
35
|
+
end
|
36
|
+
|
37
|
+
def output_path=(value)
|
38
|
+
@output_path = determine_output_path_value(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def determine_output_path_value(value)
|
42
|
+
unless value.nil? || @output_path = value.is_a?(Pathname)
|
43
|
+
return Pathname.new(value.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
value
|
47
|
+
end
|
48
|
+
|
49
|
+
def paths=(values)
|
50
|
+
@paths = values.to_a.map { |path| Pathname.new(path) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def formatter=(value)
|
54
|
+
value = value.to_s.strip.downcase.to_sym
|
55
|
+
value = Lutaml::Uml::Formatter.find_by(name: value)
|
56
|
+
raise Error, "Formatter not found: #{value}" if value.nil?
|
57
|
+
|
58
|
+
@formatter = value
|
59
|
+
end
|
60
|
+
|
61
|
+
def input_format=(value)
|
62
|
+
if value.nil?
|
63
|
+
@input_format = DEFAULT_INPUT_FORMAT
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
@input_format = SUPPORTED_FORMATS.detect { |n| n == value }
|
68
|
+
raise(NotSupportedInputFormat, value) if @input_format.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
def run(original_args)
|
72
|
+
args = original_args.dup
|
73
|
+
@option_parser.parse!(args) rescue nil
|
74
|
+
@paths = args
|
75
|
+
@formatter.type = @type
|
76
|
+
|
77
|
+
if @output_path&.file? && @paths.length > 1
|
78
|
+
raise Error,
|
79
|
+
'Output path must be a directory \
|
80
|
+
if multiple input files are given'
|
81
|
+
end
|
82
|
+
|
83
|
+
@paths.each do |input_path_string|
|
84
|
+
input_path = Pathname.new(input_path_string)
|
85
|
+
unless input_path.exist?
|
86
|
+
raise FileError, "File does not exist: #{input_path}"
|
87
|
+
end
|
88
|
+
|
89
|
+
document = Lutaml::Parser
|
90
|
+
.parse_into_document(File.new(input_path), @input_format)
|
91
|
+
result = @formatter.format(document)
|
92
|
+
|
93
|
+
if @output_path
|
94
|
+
output_path = @output_path
|
95
|
+
if output_path.directory?
|
96
|
+
output_path = output_path.join(input_path
|
97
|
+
.basename(".*").to_s +
|
98
|
+
".#{@formatter.type}")
|
99
|
+
end
|
100
|
+
|
101
|
+
output_path.open("w+") { |file| file.write(result) }
|
102
|
+
else
|
103
|
+
@out_object.puts(result)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
protected
|
109
|
+
|
110
|
+
def text_bold(body = nil)
|
111
|
+
text_effect(1, body)
|
112
|
+
end
|
113
|
+
|
114
|
+
def text_italic(body = nil)
|
115
|
+
text_effect(3, body)
|
116
|
+
end
|
117
|
+
|
118
|
+
def text_bold_italic(body = nil)
|
119
|
+
text_bold(text_italic(body))
|
120
|
+
end
|
121
|
+
|
122
|
+
def text_underline(body = nil)
|
123
|
+
text_effect(4, body)
|
124
|
+
end
|
125
|
+
|
126
|
+
def text_effect(num, body = nil)
|
127
|
+
result = "\e[#{num}m"
|
128
|
+
result << "#{body}#{text_reset}" unless body.nil?
|
129
|
+
|
130
|
+
result
|
131
|
+
end
|
132
|
+
|
133
|
+
def text_reset
|
134
|
+
"\e[0m"
|
135
|
+
end
|
136
|
+
|
137
|
+
def setup_parser_options
|
138
|
+
@option_parser.banner = ""
|
139
|
+
format_desc = "The output formatter (Default: '#{@formatter.name}')"
|
140
|
+
@option_parser
|
141
|
+
.on("-f",
|
142
|
+
"--formatter VALUE",
|
143
|
+
format_desc) do |value|
|
144
|
+
@formatter = value
|
145
|
+
end
|
146
|
+
@option_parser
|
147
|
+
.on("-t", "--type VALUE", "The output format type") do |value|
|
148
|
+
@type = value
|
149
|
+
end
|
150
|
+
@option_parser
|
151
|
+
.on("-o", "--output VALUE", "The output path") do |value|
|
152
|
+
@output_path = Pathname.new(value)
|
153
|
+
end
|
154
|
+
@option_parser
|
155
|
+
.on("-i", "--input-format VALUE", "The input format") do |value|
|
156
|
+
@input_format = value
|
157
|
+
end
|
158
|
+
@option_parser
|
159
|
+
.on("-h", "--help", "Prints this help") do
|
160
|
+
print_help
|
161
|
+
exit
|
162
|
+
end
|
163
|
+
@option_parser.on("-g", "--graph VALUE") do |value|
|
164
|
+
Parsers::Attribute.parse(value).each do |key, attr_value|
|
165
|
+
@formatter.graph[key] = attr_value
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
@option_parser.on("-e", "--edge VALUE") do |value|
|
170
|
+
Parsers::Attribute.parse(value).each do |key, attr_value|
|
171
|
+
@formatter.edge[key] = attr_value
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
@option_parser.on("-n", "--node VALUE") do |value|
|
176
|
+
Parsers::Attribute.parse(value).each do |key, attr_value|
|
177
|
+
@formatter.node[key] = attr_value
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
@option_parser.on("-a", "--all VALUE") do |value|
|
182
|
+
Parsers::Attribute.parse(value).each do |key, attr_value|
|
183
|
+
@formatter.graph[key] = attr_value
|
184
|
+
@formatter.edge[key] = attr_value
|
185
|
+
@formatter.node[key] = attr_value
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def print_help
|
191
|
+
@out_object.puts <<~HELP
|
192
|
+
#{text_bold('Usage:')} lutaml [options] PATHS
|
193
|
+
|
194
|
+
#{text_bold('Overview:')} Generate output from Supplied language files if supported
|
195
|
+
|
196
|
+
#{text_bold('Options:')}
|
197
|
+
#{@option_parser}
|
198
|
+
#{text_bold('Paths:')}
|
199
|
+
|
200
|
+
LUTAML can accept multiple paths for parsing for easier batch processing.
|
201
|
+
|
202
|
+
The location of the output by default is standard output.
|
203
|
+
|
204
|
+
The output can be directed to a path with #{text_bold_italic('--output')}, which can be a file or a directory.
|
205
|
+
If the output path is a directory, then the filename will be the same as the input filename,
|
206
|
+
with it's file extension substituted with the #{text_bold_italic('--type')}.
|
207
|
+
|
208
|
+
#{text_underline('Examples')}
|
209
|
+
|
210
|
+
`lutaml project.lutaml`
|
211
|
+
|
212
|
+
Produces DOT notation, sent to standard output
|
213
|
+
|
214
|
+
`lutaml -o . project.lutaml`
|
215
|
+
|
216
|
+
Produces DOT notation, written to #{text_italic('./project.dot')}
|
217
|
+
|
218
|
+
`lutaml -o ./diagram.dot project.lutaml`
|
219
|
+
|
220
|
+
Produces DOT notation, written to #{text_italic('./diagram.dot')}
|
221
|
+
|
222
|
+
`lutaml -o ./diagram.png project.lutaml`
|
223
|
+
|
224
|
+
Produces PNG image, written to #{text_italic('./diagram.png')}
|
225
|
+
|
226
|
+
`lutaml -t png -o . project.lutaml`
|
227
|
+
|
228
|
+
Produces PNG image, written to #{text_italic('./project.png')}
|
229
|
+
|
230
|
+
`lutaml -t png -o . project.lutaml core_ext.lutaml`
|
231
|
+
|
232
|
+
Produces PNG images, written to #{text_italic('./project.png')} and #{text_italic('./core_ext.png')}
|
233
|
+
|
234
|
+
#{text_bold('Inputs:')}
|
235
|
+
|
236
|
+
#{text_underline('Lutaml')}
|
237
|
+
|
238
|
+
Lutaml dsl syntax files, supports diagram generation(image or dot files) with Graphviz
|
239
|
+
|
240
|
+
#{text_bold('Formatters:')}
|
241
|
+
|
242
|
+
#{text_underline('Graphviz')}
|
243
|
+
|
244
|
+
Generates DOT notation and can use the DOT notation to generate any format Graphviz can produce.
|
245
|
+
|
246
|
+
The output format is based on #{text_bold_italic('--type')}, which by default is "dot".
|
247
|
+
If #{text_bold_italic('--type')} is not given and #{text_bold_italic('--output')} is, the file extension of the #{text_bold_italic('--output')} path will be used.
|
248
|
+
|
249
|
+
Valid types/extensions are: #{Lutaml::Formatter::Graphviz::VALID_TYPES.join(', ')}
|
250
|
+
|
251
|
+
#{text_bold('Options:')}
|
252
|
+
|
253
|
+
-g, --graph VALUE The graph attributes
|
254
|
+
-e, --edge VALUE The edge attributes
|
255
|
+
-n, --node VALUE The node attributes
|
256
|
+
-a, --all VALUE Set attributes for graph, edge, and node
|
257
|
+
|
258
|
+
HELP
|
259
|
+
end
|
260
|
+
end
|
261
|
+
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(/</, "<")
|
87
|
+
.gsub(/>/, ">")
|
88
|
+
.gsub(/\[/, "[")
|
89
|
+
.gsub(/\]/, "]")
|
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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ruby-graphviz"
|
4
|
+
require "lutaml/layout/engine"
|
5
|
+
|
6
|
+
module Lutaml
|
7
|
+
module Layout
|
8
|
+
class GraphVizEngine < Engine
|
9
|
+
def render(type)
|
10
|
+
Open3.popen3("dot -T#{type}") do |stdin, stdout, _stderr, _wait|
|
11
|
+
stdin.puts(input)
|
12
|
+
stdin.close
|
13
|
+
stdout.read
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -31,7 +31,9 @@ module Lutaml
|
|
31
31
|
object.instance_variables.each_with_object({}) do |var, res|
|
32
32
|
variable = object.instance_variable_get(var)
|
33
33
|
res[var.to_s.gsub("@", "")] = if variable.is_a?(Array)
|
34
|
-
variable.map
|
34
|
+
variable.map do |n|
|
35
|
+
serialize_to_hash(n)
|
36
|
+
end
|
35
37
|
else
|
36
38
|
variable
|
37
39
|
end
|
data/lib/lutaml/parser.rb
CHANGED
@@ -4,20 +4,50 @@ require "lutaml/uml/lutaml_path/document_wrapper"
|
|
4
4
|
# require "lutaml/express/lutaml_path/document_wrapper"
|
5
5
|
|
6
6
|
module Lutaml
|
7
|
-
|
8
|
-
|
7
|
+
class Parser
|
8
|
+
attr_reader :parse_type, :file
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
class << self
|
11
|
+
def parse(file, input_type = nil)
|
12
|
+
new(file, input_type).parse
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_into_document(file, input_type = nil)
|
16
|
+
new(file, input_type).parse_into_document
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(file, input_type)
|
21
|
+
@parse_type = input_type ? input_type : File.extname(file.path)[1..-1]
|
22
|
+
@file = file
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse
|
26
|
+
document = parse_into_document
|
27
|
+
document_wrapper(document)
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_into_document
|
31
|
+
case parse_type
|
12
32
|
# when "exp"
|
13
|
-
# Lutaml::Express::
|
14
|
-
# .new(Lutaml::Express::Parsers::Exp.parse(file))
|
33
|
+
# Lutaml::Express::Parsers::Exp.parse(file)
|
15
34
|
when "lutaml"
|
16
|
-
Lutaml::Uml::
|
17
|
-
|
35
|
+
Lutaml::Uml::Parsers::Dsl.parse(file)
|
36
|
+
when "yml"
|
37
|
+
Lutaml::Uml::Parsers::Yaml.parse(file.path)
|
18
38
|
else
|
19
39
|
raise ArgumentError, "Unsupported file format"
|
20
40
|
end
|
21
41
|
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def document_wrapper(document)
|
46
|
+
if parse_type == "exp"
|
47
|
+
return Lutaml::Express::LutamlPath::DocumentWrapper.new(document)
|
48
|
+
end
|
49
|
+
|
50
|
+
Lutaml::Uml::LutamlPath::DocumentWrapper.new(document)
|
51
|
+
end
|
22
52
|
end
|
23
53
|
end
|
data/lib/lutaml/version.rb
CHANGED
data/lutaml.gemspec
CHANGED
@@ -16,9 +16,12 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.metadata["changelog_uri"] = "https://github.com/lutaml/lutaml/releases"
|
17
17
|
|
18
18
|
# Specify which files should be added to the gem when it is released.
|
19
|
-
# The `git ls-files -z` loads the files in the RubyGem
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem
|
20
|
+
# that have been added into git.
|
20
21
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
|
-
`git ls-files -z
|
22
|
+
`git ls-files -z`
|
23
|
+
.split("\x0")
|
24
|
+
.reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
25
|
end
|
23
26
|
spec.bindir = "exe"
|
24
27
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lutaml
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-01-
|
11
|
+
date: 2021-01-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: lutaml-uml
|
@@ -139,7 +139,8 @@ dependencies:
|
|
139
139
|
description: 'LutaML: data models in textual form'
|
140
140
|
email:
|
141
141
|
- open.source@ribose.com'
|
142
|
-
executables:
|
142
|
+
executables:
|
143
|
+
- lutaml
|
143
144
|
extensions: []
|
144
145
|
extra_rdoc_files: []
|
145
146
|
files:
|
@@ -155,8 +156,15 @@ files:
|
|
155
156
|
- Rakefile
|
156
157
|
- bin/console
|
157
158
|
- bin/setup
|
159
|
+
- exe/lutaml
|
158
160
|
- lib/lutaml.rb
|
161
|
+
- lib/lutaml/command_line.rb
|
159
162
|
- lib/lutaml/express/lutaml_path/document_wrapper.rb
|
163
|
+
- lib/lutaml/formatter.rb
|
164
|
+
- lib/lutaml/formatter/base.rb
|
165
|
+
- lib/lutaml/formatter/graphviz.rb
|
166
|
+
- lib/lutaml/layout/engine.rb
|
167
|
+
- lib/lutaml/layout/graph_viz_engine.rb
|
160
168
|
- lib/lutaml/lutaml_path/document_wrapper.rb
|
161
169
|
- lib/lutaml/parser.rb
|
162
170
|
- lib/lutaml/uml/lutaml_path/document_wrapper.rb
|