ruby_to_uml 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'ruby_to_uml'
5
+
6
+ runner = RubyToUML::Runner.new ARGV
7
+ runner.run
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ruby_to_uml/version'
4
+ require 'ruby_to_uml/runner'
5
+ require 'ruby_to_uml/parser_sexp'
6
+ require 'ruby_to_uml/uml_class'
7
+ require 'ruby_to_uml/diagram'
8
+
9
+ RubyToUml = RubyToUML # to create alias for module
@@ -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(/\?/, '&#63;')
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyToUML
4
+ VERSION = '2.0.0'
5
+ 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: []