ruby_to_uml 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/ruby_to_uml +7 -0
- data/lib/ruby_to_uml.rb +9 -0
- data/lib/ruby_to_uml/diagram.rb +78 -0
- data/lib/ruby_to_uml/parser_sexp.rb +148 -0
- data/lib/ruby_to_uml/runner.rb +79 -0
- data/lib/ruby_to_uml/uml_class.rb +56 -0
- data/lib/ruby_to_uml/version.rb +5 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3445cfab16eaa84745cd0b367155f28c86beb1a79e93c072aaf6dfe8b599154b
|
|
4
|
+
data.tar.gz: 76841acf154c49b453862ffae8525bcae1846a864a0e332cd534a5278e386465
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f7120950f0bf81be765c5758f7d94785c606f8b77cffc8ced2995d80f1aad96bc071bad8334c232c486ee64bc90d85eaaa57524fae0c6683575f29aef1acc549
|
|
7
|
+
data.tar.gz: 9cc132f121042b7e817b95b58f84187b4bb796712cb580f827407b3ec8999d51677e53a4410c8a0fd08a3dcc6e934b642cbe7d69f7716379a6250eb9a7c08412
|
data/bin/ruby_to_uml
ADDED
data/lib/ruby_to_uml.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyToUML
|
|
4
|
+
# Creates and store a yUML api string for generating diagram
|
|
5
|
+
# * type of @statements: 1..* String
|
|
6
|
+
class Diagram
|
|
7
|
+
attr_accessor :statements
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@statements = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create(&blk)
|
|
14
|
+
instance_eval(&blk)
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Adds the given statement to the @diagram array
|
|
19
|
+
# Statement can either be a String or an UmlClass
|
|
20
|
+
def add(statement)
|
|
21
|
+
# TODO: Add some sort of validation
|
|
22
|
+
|
|
23
|
+
@statements << statement if statement.is_a? String
|
|
24
|
+
if statement.is_a? UmlClass
|
|
25
|
+
|
|
26
|
+
@statements << statement.to_s
|
|
27
|
+
|
|
28
|
+
statement.children&.each do |child|
|
|
29
|
+
@statements << "[#{statement.name}]^[#{child.name}]"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
unless statement.associations.empty?
|
|
33
|
+
statement.associations.each do |name, type|
|
|
34
|
+
next if name =~ /-/
|
|
35
|
+
|
|
36
|
+
cardinality = (" #{statement.associations["#{name}-n"]}" if statement.associations["#{name}-n"])
|
|
37
|
+
@statements << "[#{statement.name}]-#{name}#{cardinality}>[#{type}]"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Sorts the statements array so that
|
|
45
|
+
# 1. Class definitions
|
|
46
|
+
# 2. Inheritance
|
|
47
|
+
# 3. Associations
|
|
48
|
+
# Otherwise, strange behavior can happen in the downloaded graph
|
|
49
|
+
def compute!
|
|
50
|
+
class_def = /^\[[\w;?|=!]*?\]$/
|
|
51
|
+
inheritance = /\[(.*?)\]\^\[(.*?)\]/
|
|
52
|
+
association = /\[.*\]-.*>\[.*\]/
|
|
53
|
+
|
|
54
|
+
@statements.sort! do |x, y|
|
|
55
|
+
if x =~ class_def && y =~ inheritance
|
|
56
|
+
-1
|
|
57
|
+
elsif x =~ class_def && y =~ association
|
|
58
|
+
-1
|
|
59
|
+
elsif x =~ inheritance && y =~ association
|
|
60
|
+
-1
|
|
61
|
+
elsif x =~ class_def && y =~ class_def
|
|
62
|
+
0
|
|
63
|
+
elsif x =~ inheritance && y =~ inheritance
|
|
64
|
+
0
|
|
65
|
+
elsif x =~ association && y =~ association
|
|
66
|
+
0
|
|
67
|
+
else
|
|
68
|
+
1
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# returns just the DSL text for diagram
|
|
74
|
+
def get_dsl
|
|
75
|
+
@statements.join(', ')
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_parser'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module RubyToUML
|
|
7
|
+
# Parses files using S-Expressions given by the RubyParser gem
|
|
8
|
+
#
|
|
9
|
+
# * type of @classes: 0..* UmlClass
|
|
10
|
+
#
|
|
11
|
+
class ParserSexp
|
|
12
|
+
def initialize(path)
|
|
13
|
+
@path_to_folder_or_file = path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Parses the source code of the files in @files
|
|
17
|
+
# to build uml classes. Returns an array containing all the
|
|
18
|
+
# parsed classes or nil if no ruby file were found in the
|
|
19
|
+
# @files array.
|
|
20
|
+
def parse_sources!
|
|
21
|
+
source_files = nil
|
|
22
|
+
if path_to_folder_or_file.match /.rb/
|
|
23
|
+
source_files = [path_to_folder_or_file]
|
|
24
|
+
else
|
|
25
|
+
files = list_child_files_paths(Pathname.new(path_to_folder_or_file))
|
|
26
|
+
source_files = files.select { |f| f.match(/\.rb/) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return nil if source_files.empty?
|
|
30
|
+
|
|
31
|
+
all_uml_classes = []
|
|
32
|
+
source_files.each do |file|
|
|
33
|
+
file_content = File.read(file)
|
|
34
|
+
parse_file(file_content).each do |uml_class|
|
|
35
|
+
all_uml_classes << uml_class
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Removes duplicates between variables and associations in the class
|
|
40
|
+
all_uml_classes.each do |uml_class|
|
|
41
|
+
uml_class.finalize_uml_class_info(all_uml_classes)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parse the given string, and return the parsed classes
|
|
46
|
+
def parse_file(file_content)
|
|
47
|
+
uml_classes = []
|
|
48
|
+
|
|
49
|
+
s_exp = RubyParser.new.parse(file_content)
|
|
50
|
+
|
|
51
|
+
if sexp_contains_one_class?(s_exp)
|
|
52
|
+
uml_classes << parse_class(s_exp)
|
|
53
|
+
else
|
|
54
|
+
s_exp.each_of_type :class do |a_class|
|
|
55
|
+
uml_classes << parse_class(a_class)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
uml_classes
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
attr_reader :path_to_folder_or_file
|
|
65
|
+
|
|
66
|
+
def list_child_files_paths(path)
|
|
67
|
+
path.children.collect do |child|
|
|
68
|
+
if child.file?
|
|
69
|
+
child
|
|
70
|
+
elsif child.directory?
|
|
71
|
+
list_child_files_paths(child) + [child]
|
|
72
|
+
end
|
|
73
|
+
end.select { |x| x }.flatten(1).map(&:to_s)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def sexp_contains_one_class?(s_exp)
|
|
77
|
+
s_exp[0] == :class
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Creates a UmlClass from a class s-expression
|
|
81
|
+
def parse_class(class_s_exp)
|
|
82
|
+
# Checks if the class is in a module
|
|
83
|
+
uml_class = is_module?(class_s_exp)
|
|
84
|
+
|
|
85
|
+
# Let's start by building the associations of the class
|
|
86
|
+
each_association_for class_s_exp do |variable, type, cardinality|
|
|
87
|
+
uml_class.associations[variable] = type
|
|
88
|
+
uml_class.associations["#{variable}-n"] = cardinality if cardinality
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Searching for a s(:const, :Const) right after the class name, which
|
|
92
|
+
# means the class inherits from a parents class, :Const
|
|
93
|
+
if class_s_exp[2] && (class_s_exp[2][0] == :const)
|
|
94
|
+
classname = recursive_class_name_find class_s_exp[2]
|
|
95
|
+
uml_class.parent = classname unless classname.nil?
|
|
96
|
+
elsif class_s_exp[2] && (class_s_exp[2][0] == :colon2)
|
|
97
|
+
# If the parent class belongs to a module
|
|
98
|
+
classname = recursive_class_name_find class_s_exp[2]
|
|
99
|
+
uml_class.parent = classname unless classname.nil?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Looks-up for instance methods
|
|
103
|
+
class_s_exp.each_of_type :defn do |instance_method|
|
|
104
|
+
# Handle question marks in method names
|
|
105
|
+
uml_class.methods << instance_method[1].to_s.gsub(/\?/, '?')
|
|
106
|
+
|
|
107
|
+
# Now looking for @variables, inside instance methods
|
|
108
|
+
# I'm looking at assignments such as @var = x
|
|
109
|
+
instance_method.each_of_type :iasgn do |assignment|
|
|
110
|
+
if assignment[1].instance_of?(Symbol) && assignment[1].to_s =~ /@/
|
|
111
|
+
variable = assignment[1].to_s.gsub('@', '')
|
|
112
|
+
uml_class.variables << variable unless uml_class.variables.include? variable
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
uml_class
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def is_module?(class_s_exp)
|
|
120
|
+
if class_s_exp[1].instance_of?(Symbol)
|
|
121
|
+
UmlClass.new class_s_exp[1].to_s
|
|
122
|
+
else
|
|
123
|
+
classname = recursive_class_name_find class_s_exp[1]
|
|
124
|
+
UmlClass.new classname unless classname.nil?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Yields the variable, the type and the cardinality for each associations
|
|
129
|
+
def each_association_for(a_class)
|
|
130
|
+
if comments = a_class.comments
|
|
131
|
+
comments.split(/\n/).each do |line|
|
|
132
|
+
line.match(/type of @(\w*): ([0-9.*n]* )?([:|\w]*)\b/) do |m|
|
|
133
|
+
yield m[1], m[3], m[2]&.chop
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def recursive_class_name_find(sexp)
|
|
140
|
+
return nil if sexp.nil?
|
|
141
|
+
return sexp[1].to_s if sexp[0] == :const || sexp[0] == :colon3
|
|
142
|
+
|
|
143
|
+
classname = recursive_class_name_find sexp[1]
|
|
144
|
+
classname = '' if classname.nil?
|
|
145
|
+
"#{classname}::#{sexp[2]}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'erb'
|
|
6
|
+
|
|
7
|
+
module RubyToUML
|
|
8
|
+
class Runner
|
|
9
|
+
def initialize(args)
|
|
10
|
+
abort('Usage: ruby_to_uml [source directory]') if args.empty?
|
|
11
|
+
|
|
12
|
+
@args = args
|
|
13
|
+
@smart_mode = false
|
|
14
|
+
@link_mode = false
|
|
15
|
+
|
|
16
|
+
parse_options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
classes = parse_s_expressions
|
|
21
|
+
abort('No ruby files in the directory.') unless classes
|
|
22
|
+
classes.each { |c| c.infer_types! classes } if smart_mode
|
|
23
|
+
|
|
24
|
+
diagram = create_diagram(classes)
|
|
25
|
+
|
|
26
|
+
if link_mode
|
|
27
|
+
uri = yuml_uri(diagram)
|
|
28
|
+
puts "Link to yUML Diagram: #{uri}"
|
|
29
|
+
else
|
|
30
|
+
png = download_diagram(yuml_uri(diagram, type: '.png'))
|
|
31
|
+
save_file(png, type: '.png')
|
|
32
|
+
puts 'Diagram saved in uml.png'
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :args
|
|
39
|
+
attr_accessor :smart_mode, :link_mode
|
|
40
|
+
|
|
41
|
+
def parse_options
|
|
42
|
+
OptionParser.new do |opts|
|
|
43
|
+
opts.on('-s', '--smart') { self.smart_mode = true }
|
|
44
|
+
opts.on('-l', '--link') { self.link_mode = true }
|
|
45
|
+
opts.on('-v', '--version') { puts VERSION }
|
|
46
|
+
end.parse!(args)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_s_expressions
|
|
50
|
+
path = args[0]
|
|
51
|
+
ParserSexp.new(path).parse_sources!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def create_diagram(classes)
|
|
55
|
+
diagram = Diagram.new
|
|
56
|
+
diagram.create do
|
|
57
|
+
classes.each { |c| add c }
|
|
58
|
+
end.compute!
|
|
59
|
+
diagram
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def yuml_uri(diagram, type: '')
|
|
63
|
+
scheme = 'https://'
|
|
64
|
+
host = 'yuml.me'
|
|
65
|
+
path = "/diagram/boring/class/#{ERB::Util.url_encode(diagram.get_dsl)}"
|
|
66
|
+
uri = URI(scheme + host + path + type)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def download_diagram(uri)
|
|
70
|
+
Net::HTTP.get_response(uri).body
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def save_file(data, type: '')
|
|
74
|
+
File.open("uml#{type}", 'w') do |file|
|
|
75
|
+
file << data
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/inflector' # To uses String#classify
|
|
4
|
+
module RubyToUML
|
|
5
|
+
# Represents a parsed uml class
|
|
6
|
+
class UmlClass
|
|
7
|
+
attr_accessor :name, :variables, :methods, :associations, :parent, :children
|
|
8
|
+
|
|
9
|
+
def initialize(name)
|
|
10
|
+
@name = name
|
|
11
|
+
@variables = []
|
|
12
|
+
@methods = []
|
|
13
|
+
@associations = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def finalize_uml_class_info(classes)
|
|
17
|
+
remove_duplicate_var_and_add_associations(classes)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_s
|
|
21
|
+
'[' + @name + '|' +
|
|
22
|
+
@variables.collect { |var| var }.join(';') + '|' +
|
|
23
|
+
@methods.collect { |met| met }.join(';') + ']'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Tries to create an association with the attributes in @variables.
|
|
27
|
+
def infer_types!(classes)
|
|
28
|
+
class_names = classes.collect(&:name)
|
|
29
|
+
@variables.each do |attribute|
|
|
30
|
+
next unless class_names.include? attribute.classify
|
|
31
|
+
|
|
32
|
+
# A type has match with the attribute's name
|
|
33
|
+
@associations[attribute] = attribute.classify
|
|
34
|
+
|
|
35
|
+
# If it's a plural, adds a cardinality
|
|
36
|
+
@associations["#{attribute}-n"] = '*' if attribute == attribute.pluralize
|
|
37
|
+
end
|
|
38
|
+
finalize_uml_class_info(classes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Deletes variables from the @variables array if they appear
|
|
44
|
+
# in an association.
|
|
45
|
+
# Sets the @children variable
|
|
46
|
+
def remove_duplicate_var_and_add_associations(classes)
|
|
47
|
+
@variables -= @associations.keys unless @associations.nil?
|
|
48
|
+
@children = classes.select do |c|
|
|
49
|
+
if c.parent == @name
|
|
50
|
+
c.parent = nil
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_to_uml
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Iuliu Pop
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-02-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: ruby_parser
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
description:
|
|
42
|
+
email:
|
|
43
|
+
- iuliu.laurentiu.pop@protonmail.com
|
|
44
|
+
executables:
|
|
45
|
+
- ruby_to_uml
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- bin/ruby_to_uml
|
|
50
|
+
- lib/ruby_to_uml.rb
|
|
51
|
+
- lib/ruby_to_uml/diagram.rb
|
|
52
|
+
- lib/ruby_to_uml/parser_sexp.rb
|
|
53
|
+
- lib/ruby_to_uml/runner.rb
|
|
54
|
+
- lib/ruby_to_uml/uml_class.rb
|
|
55
|
+
- lib/ruby_to_uml/version.rb
|
|
56
|
+
homepage: https://github.com/iulspop/ruby_to_uml
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
source_code_uri: https://github.com/iulspop/ruby_to_uml
|
|
61
|
+
post_install_message:
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 3.0.0
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 3.2.3
|
|
77
|
+
signing_key:
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: ruby_to_uml is a tool that creates class diagrams from Ruby code.
|
|
80
|
+
test_files: []
|