lutaml 0.3.2 → 0.4.1.pre.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d1809b80de4e3e3d8ed081e31f19008e3fcb30559a8e95b6156a03bfca36aa6
4
- data.tar.gz: be6fd904458828f232294f03d0cdb1fec2dbc349301449ebb3f9a1a77b375195
3
+ metadata.gz: 0e2accd48e5932fbf111f54429604e7709a98418d6b9c97af40f29c8547a7238
4
+ data.tar.gz: 6561a16f4d783ddc50812af99e3da854dc2a8673be5e2cc9e7adcffa1678099f
5
5
  SHA512:
6
- metadata.gz: b58852c0e45b0ebe4bd27f7c782ee605abe4e448107c8371ddd98d5fb9f731053ec3f46565d34f43d9b6ca2363c09d7db55c6adba282db934c01a543f117b97e
7
- data.tar.gz: 47326b6b3301018542b4764161e931df57e582c4e4d22d0f94961caf4fbe9a495b76fe65337fe22dd3c1e71b2c909da52e93caa000040566c36d145ae8733a63
6
+ metadata.gz: 3006890a278d837602f7c3149c46b7810a7f5e367100862eac17ac7e54ef2bf18623b9bb8a98483b12cec3fe52bfbef1e3f3c40b7231b7d669c31b0b5e3548f1
7
+ data.tar.gz: c509f969284fc89e5a4d3293426a71ff861aa8283f33193114887d55d2173427a988998db906d18ea94e1a821db07147a213f892052241dd383c6e36556fa2b0
@@ -30,6 +30,8 @@ jobs:
30
30
  uses: actions/setup-ruby@v1
31
31
  with:
32
32
  ruby-version: ${{ matrix.ruby }}
33
+ - name: Install Grpahviz macOS
34
+ run: brew install graphviz
33
35
  - name: Update gems
34
36
  run: |
35
37
  sudo gem install bundler --force
@@ -29,6 +29,8 @@ jobs:
29
29
  uses: actions/setup-ruby@v1
30
30
  with:
31
31
  ruby-version: ${{ matrix.ruby }}
32
+ - name: Install Grpahviz Ubuntu
33
+ run: sudo apt-get install graphviz
32
34
  - name: Update gems
33
35
  run: |
34
36
  gem install bundler
@@ -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
- In order to parse files supported by lutaml extensions, use Lutaml::Parser.parse method(currently only .exp are supported):
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,262 @@
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
+ .first
92
+ result = @formatter.format(document)
93
+
94
+ if @output_path
95
+ output_path = @output_path
96
+ if output_path.directory?
97
+ output_path = output_path.join(input_path
98
+ .basename(".*").to_s +
99
+ ".#{@formatter.type}")
100
+ end
101
+
102
+ output_path.open("w+") { |file| file.write(result) }
103
+ else
104
+ @out_object.puts(result)
105
+ end
106
+ end
107
+ end
108
+
109
+ protected
110
+
111
+ def text_bold(body = nil)
112
+ text_effect(1, body)
113
+ end
114
+
115
+ def text_italic(body = nil)
116
+ text_effect(3, body)
117
+ end
118
+
119
+ def text_bold_italic(body = nil)
120
+ text_bold(text_italic(body))
121
+ end
122
+
123
+ def text_underline(body = nil)
124
+ text_effect(4, body)
125
+ end
126
+
127
+ def text_effect(num, body = nil)
128
+ result = "\e[#{num}m"
129
+ result << "#{body}#{text_reset}" unless body.nil?
130
+
131
+ result
132
+ end
133
+
134
+ def text_reset
135
+ "\e[0m"
136
+ end
137
+
138
+ def setup_parser_options
139
+ @option_parser.banner = ""
140
+ format_desc = "The output formatter (Default: '#{@formatter.name}')"
141
+ @option_parser
142
+ .on("-f",
143
+ "--formatter VALUE",
144
+ format_desc) do |value|
145
+ @formatter = value
146
+ end
147
+ @option_parser
148
+ .on("-t", "--type VALUE", "The output format type") do |value|
149
+ @type = value
150
+ end
151
+ @option_parser
152
+ .on("-o", "--output VALUE", "The output path") do |value|
153
+ @output_path = Pathname.new(value)
154
+ end
155
+ @option_parser
156
+ .on("-i", "--input-format VALUE", "The input format") do |value|
157
+ @input_format = value
158
+ end
159
+ @option_parser
160
+ .on("-h", "--help", "Prints this help") do
161
+ print_help
162
+ exit
163
+ end
164
+ @option_parser.on("-g", "--graph VALUE") do |value|
165
+ Parsers::Attribute.parse(value).each do |key, attr_value|
166
+ @formatter.graph[key] = attr_value
167
+ end
168
+ end
169
+
170
+ @option_parser.on("-e", "--edge VALUE") do |value|
171
+ Parsers::Attribute.parse(value).each do |key, attr_value|
172
+ @formatter.edge[key] = attr_value
173
+ end
174
+ end
175
+
176
+ @option_parser.on("-n", "--node VALUE") do |value|
177
+ Parsers::Attribute.parse(value).each do |key, attr_value|
178
+ @formatter.node[key] = attr_value
179
+ end
180
+ end
181
+
182
+ @option_parser.on("-a", "--all VALUE") do |value|
183
+ Parsers::Attribute.parse(value).each do |key, attr_value|
184
+ @formatter.graph[key] = attr_value
185
+ @formatter.edge[key] = attr_value
186
+ @formatter.node[key] = attr_value
187
+ end
188
+ end
189
+ end
190
+
191
+ def print_help
192
+ @out_object.puts <<~HELP
193
+ #{text_bold('Usage:')} lutaml [options] PATHS
194
+
195
+ #{text_bold('Overview:')} Generate output from Supplied language files if supported
196
+
197
+ #{text_bold('Options:')}
198
+ #{@option_parser}
199
+ #{text_bold('Paths:')}
200
+
201
+ LUTAML can accept multiple paths for parsing for easier batch processing.
202
+
203
+ The location of the output by default is standard output.
204
+
205
+ The output can be directed to a path with #{text_bold_italic('--output')}, which can be a file or a directory.
206
+ If the output path is a directory, then the filename will be the same as the input filename,
207
+ with it's file extension substituted with the #{text_bold_italic('--type')}.
208
+
209
+ #{text_underline('Examples')}
210
+
211
+ `lutaml project.lutaml`
212
+
213
+ Produces DOT notation, sent to standard output
214
+
215
+ `lutaml -o . project.lutaml`
216
+
217
+ Produces DOT notation, written to #{text_italic('./project.dot')}
218
+
219
+ `lutaml -o ./diagram.dot project.lutaml`
220
+
221
+ Produces DOT notation, written to #{text_italic('./diagram.dot')}
222
+
223
+ `lutaml -o ./diagram.png project.lutaml`
224
+
225
+ Produces PNG image, written to #{text_italic('./diagram.png')}
226
+
227
+ `lutaml -t png -o . project.lutaml`
228
+
229
+ Produces PNG image, written to #{text_italic('./project.png')}
230
+
231
+ `lutaml -t png -o . project.lutaml core_ext.lutaml`
232
+
233
+ Produces PNG images, written to #{text_italic('./project.png')} and #{text_italic('./core_ext.png')}
234
+
235
+ #{text_bold('Inputs:')}
236
+
237
+ #{text_underline('Lutaml')}
238
+
239
+ Lutaml dsl syntax files, supports diagram generation(image or dot files) with Graphviz
240
+
241
+ #{text_bold('Formatters:')}
242
+
243
+ #{text_underline('Graphviz')}
244
+
245
+ Generates DOT notation and can use the DOT notation to generate any format Graphviz can produce.
246
+
247
+ The output format is based on #{text_bold_italic('--type')}, which by default is "dot".
248
+ 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.
249
+
250
+ Valid types/extensions are: #{Lutaml::Formatter::Graphviz::VALID_TYPES.join(', ')}
251
+
252
+ #{text_bold('Options:')}
253
+
254
+ -g, --graph VALUE The graph attributes
255
+ -e, --edge VALUE The edge attributes
256
+ -n, --node VALUE The node attributes
257
+ -a, --all VALUE Set attributes for graph, edge, and node
258
+
259
+ HELP
260
+ end
261
+ end
262
+ end
@@ -1,57 +1,15 @@
1
1
  require "lutaml/lutaml_path/document_wrapper"
2
- require "expressir/express_exp/formatter"
2
+ require "expressir/express_exp/hyperlink_formatter"
3
3
 
4
4
  module Lutaml
5
5
  module Express
6
6
  module LutamlPath
7
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
8
  protected
25
9
 
26
10
  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)
11
+ repository
12
+ .to_hash(formatter: Expressir::ExpressExp::HyperlinkFormatter)['schemas']
55
13
  end
56
14
  end
57
15
  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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ class Engine
5
+ attr_accessor :input
6
+
7
+ def initialize(input:)
8
+ @input = input
9
+ end
10
+
11
+ def render(_type)
12
+ raise ArgumentError, "Implement render method"
13
+ end
14
+ end
15
+ 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
@@ -22,18 +22,24 @@ module Lutaml
22
22
  return attr_value.map(&method(:serialize_to_hash))
23
23
  end
24
24
 
25
- attr_value
25
+ serialize_to_hash(attr_value)
26
26
  end
27
27
 
28
28
  def serialize_to_hash(object)
29
- return object if [String, Integer, Float].include?(object.class)
29
+ return object if [String, Integer, Float, FalseClass, TrueClass, Symbol, NilClass].include?(object.class)
30
30
 
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 { |n| serialize_to_hash(n) }
34
+ variable.map do |n|
35
+ serialize_to_hash(n)
36
+ end
35
37
  else
36
- variable
38
+ if [String, Integer, Float, FalseClass, TrueClass, Symbol, NilClass].include?(variable.class) || var == :@parent
39
+ variable
40
+ else
41
+ serialize_to_hash(variable)
42
+ end
37
43
  end
38
44
  end
39
45
  end
data/lib/lutaml/parser.rb CHANGED
@@ -4,20 +4,54 @@ require "lutaml/uml/lutaml_path/document_wrapper"
4
4
  require "lutaml/express/lutaml_path/document_wrapper"
5
5
 
6
6
  module Lutaml
7
- module Parser
8
- module_function
7
+ class Parser
8
+ attr_reader :parse_type, :file_list
9
9
 
10
- def parse(file)
11
- case File.extname(file.path)[1..-1]
10
+ class << self
11
+ def parse(file_list, input_type = nil)
12
+ file_list = file_list.is_a?(Array) ? file_list : [file_list]
13
+ new(Array(file_list), input_type).parse
14
+ end
15
+
16
+ def parse_into_document(file_list, input_type = nil)
17
+ file_list = file_list.is_a?(Array) ? file_list : [file_list]
18
+ new(Array(file_list), input_type).parse_into_document
19
+ end
20
+ end
21
+
22
+ def initialize(file_list, input_type)
23
+ @parse_type = input_type ? input_type : File.extname(file_list.first.path)[1..-1]
24
+ @file_list = file_list
25
+ end
26
+
27
+ def parse
28
+ documents = parse_into_document
29
+ return [document_wrapper(documents)] if parse_type == "exp"
30
+
31
+ documents.map { |doc| document_wrapper(doc) }
32
+ end
33
+
34
+ def parse_into_document
35
+ case parse_type
12
36
  when "exp"
13
- Lutaml::Express::LutamlPath::DocumentWrapper
14
- .new(Lutaml::Express::Parsers::Exp.parse(file))
37
+ Expressir::ExpressExp::Parser.from_files(file_list.map(&:path))
15
38
  when "lutaml"
16
- Lutaml::Uml::LutamlPath::DocumentWrapper
17
- .new(Lutaml::Uml::Parsers::Dsl.parse(file))
39
+ file_list.map { |file| Lutaml::Uml::Parsers::Dsl.parse(file) }
40
+ when "yml"
41
+ file_list.map { |file| Lutaml::Uml::Parsers::Yaml.parse(file.path) }
18
42
  else
19
43
  raise ArgumentError, "Unsupported file format"
20
44
  end
21
45
  end
46
+
47
+ private
48
+
49
+ def document_wrapper(document)
50
+ if parse_type == "exp"
51
+ return Lutaml::Express::LutamlPath::DocumentWrapper.new(document)
52
+ end
53
+
54
+ Lutaml::Uml::LutamlPath::DocumentWrapper.new(document)
55
+ end
22
56
  end
23
57
  end
@@ -1,3 +1,3 @@
1
1
  module Lutaml
2
- VERSION = "0.3.2".freeze
2
+ VERSION = "0.4.1-alpha.2".freeze
3
3
  end
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 that have been added into git.
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`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
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.3.2
4
+ version: 0.4.1.pre.alpha.2
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-01 00:00:00.000000000 Z
11
+ date: 2021-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-express
@@ -153,7 +153,8 @@ dependencies:
153
153
  description: 'LutaML: data models in textual form'
154
154
  email:
155
155
  - open.source@ribose.com'
156
- executables: []
156
+ executables:
157
+ - lutaml
157
158
  extensions: []
158
159
  extra_rdoc_files: []
159
160
  files:
@@ -169,8 +170,15 @@ files:
169
170
  - Rakefile
170
171
  - bin/console
171
172
  - bin/setup
173
+ - exe/lutaml
172
174
  - lib/lutaml.rb
175
+ - lib/lutaml/command_line.rb
173
176
  - lib/lutaml/express/lutaml_path/document_wrapper.rb
177
+ - lib/lutaml/formatter.rb
178
+ - lib/lutaml/formatter/base.rb
179
+ - lib/lutaml/formatter/graphviz.rb
180
+ - lib/lutaml/layout/engine.rb
181
+ - lib/lutaml/layout/graph_viz_engine.rb
174
182
  - lib/lutaml/lutaml_path/document_wrapper.rb
175
183
  - lib/lutaml/parser.rb
176
184
  - lib/lutaml/uml/lutaml_path/document_wrapper.rb
@@ -194,9 +202,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
194
202
  version: '0'
195
203
  required_rubygems_version: !ruby/object:Gem::Requirement
196
204
  requirements:
197
- - - ">="
205
+ - - ">"
198
206
  - !ruby/object:Gem::Version
199
- version: '0'
207
+ version: 1.3.1
200
208
  requirements: []
201
209
  rubygems_version: 3.0.3
202
210
  signing_key: