lutaml 0.3.2 → 0.4.1.pre.alpha.2

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 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: