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