xumlidot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/bin/xumlidot +39 -0
  3. data/lib/xumlidot.rb +10 -0
  4. data/lib/xumlidot/diagram.rb +18 -0
  5. data/lib/xumlidot/diagram/dot.rb +55 -0
  6. data/lib/xumlidot/diagram/dot/klass.rb +75 -0
  7. data/lib/xumlidot/diagram/dot/module.rb +11 -0
  8. data/lib/xumlidot/diagram/shared/naming.rb +19 -0
  9. data/lib/xumlidot/diagram/xmi.rb +190 -0
  10. data/lib/xumlidot/diagram/xmi/argument.rb +34 -0
  11. data/lib/xumlidot/diagram/xmi/attribute.rb +24 -0
  12. data/lib/xumlidot/diagram/xmi/constant.rb +16 -0
  13. data/lib/xumlidot/diagram/xmi/id.rb +39 -0
  14. data/lib/xumlidot/diagram/xmi/klass.rb +133 -0
  15. data/lib/xumlidot/diagram/xmi/method.rb +38 -0
  16. data/lib/xumlidot/diagram/xmi/superklass.rb +18 -0
  17. data/lib/xumlidot/directory_tree.rb +24 -0
  18. data/lib/xumlidot/options.rb +104 -0
  19. data/lib/xumlidot/parsers.rb +14 -0
  20. data/lib/xumlidot/parsers/args.rb +159 -0
  21. data/lib/xumlidot/parsers/call.rb +92 -0
  22. data/lib/xumlidot/parsers/file.rb +15 -0
  23. data/lib/xumlidot/parsers/generic.rb +106 -0
  24. data/lib/xumlidot/parsers/klass_definition.rb +76 -0
  25. data/lib/xumlidot/parsers/method_signature.rb +95 -0
  26. data/lib/xumlidot/parsers/module_definition.rb +55 -0
  27. data/lib/xumlidot/parsers/scope.rb +54 -0
  28. data/lib/xumlidot/parsers/stack.rb +96 -0
  29. data/lib/xumlidot/types.rb +18 -0
  30. data/lib/xumlidot/types/argument.rb +61 -0
  31. data/lib/xumlidot/types/arguments.rb +14 -0
  32. data/lib/xumlidot/types/attribute.rb +34 -0
  33. data/lib/xumlidot/types/attributes.rb +11 -0
  34. data/lib/xumlidot/types/constant.rb +49 -0
  35. data/lib/xumlidot/types/constants.rb +57 -0
  36. data/lib/xumlidot/types/inherited_module.rb +24 -0
  37. data/lib/xumlidot/types/instance_methods.rb +10 -0
  38. data/lib/xumlidot/types/klass.rb +60 -0
  39. data/lib/xumlidot/types/klass_definition.rb +61 -0
  40. data/lib/xumlidot/types/klass_methods.rb +10 -0
  41. data/lib/xumlidot/types/method.rb +10 -0
  42. data/lib/xumlidot/types/method_signature.rb +64 -0
  43. data/lib/xumlidot/types/methods.rb +10 -0
  44. data/lib/xumlidot/types/module.rb +11 -0
  45. data/lib/xumlidot/types/module_definition.rb +14 -0
  46. data/lib/xumlidot/types/superklass.rb +32 -0
  47. data/spec/spec_helper.rb +2 -0
  48. metadata +175 -0
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'id'
4
+
5
+ module Xumlidot
6
+ class Diagram
7
+ class Xmi
8
+ module Attribute
9
+ include ::Xumlidot::Diagram::Xmi::ID
10
+
11
+ # TODO - public/private visibility on attributes
12
+ def draw
13
+ attribute_xmi = "<ownedAttribute aggregation=\"none\" isDerived=\"false\" isDerivedUnion=\"false\" isID=\"false\" isLeaf=\"false\" isReadOnly=\"false\" isStatic=\"false\" name=\"#{name_to_xmi}\" visibility=\"public\" xmi:id=\"#{id}\" xmi:type=\"uml:Property\">"
14
+ attribute_xmi += "</ownedAttribute>"
15
+ end
16
+
17
+ def name_to_xmi
18
+ name.encode(:xml => :text) if name
19
+ end
20
+
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../types'
4
+ require_relative 'id'
5
+
6
+ module Xumlidot
7
+ class Diagram
8
+ class Xmi
9
+ module Constant
10
+ def to_xmi
11
+ "#{formatted_namespace}::#{@name}"
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Xumlidot
6
+ class Diagram
7
+ class Xmi
8
+ # Helper - everything needs an id and these ids need to be used in the
9
+ # Element section
10
+ module ID
11
+ def id
12
+ @_id ||= new_id
13
+ end
14
+
15
+ def force_id(id)
16
+ @_id = id
17
+ end
18
+
19
+ def gen_id
20
+ @gen_id ||= "#{new_id[0..5]}.#{new_id[0..5]}".upcase
21
+ end
22
+
23
+ def association_id
24
+ @association_id ||= "#{new_id[0..5]}.#{new_id[0..5]}".upcase
25
+ end
26
+
27
+ def association_end_id
28
+ @association_end_id ||= "#{new_id[0..5]}.#{new_id[0..5]}".upcase
29
+ end
30
+
31
+ private
32
+
33
+ def new_id
34
+ SecureRandom.hex
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../types'
4
+ require_relative 'id'
5
+ require_relative 'constant'
6
+ require_relative 'superklass'
7
+ require_relative '../shared/naming'
8
+
9
+ module Xumlidot
10
+ class Diagram
11
+ class Xmi
12
+ # Draw the klass
13
+ module Klass
14
+ include ::Xumlidot::Diagram::Xmi::ID
15
+ include ::Xumlidot::Diagram::Shared::Naming
16
+
17
+ module Name
18
+ def to_xmi
19
+ map do |constant|
20
+ constant.extend(::Xumlidot::Diagram::Xmi::Constant) unless constant.respond_to?(:to_xmi)
21
+ constant.to_xmi
22
+ end.join
23
+ end
24
+ end
25
+
26
+ # TODO: Split this into model and diagram classes
27
+ class Model
28
+ end
29
+
30
+ class Diagram
31
+ end
32
+
33
+ def draw_klass(options)
34
+ definition.name.extend(Name)
35
+ xmi = "<ownedMember isAbstract=\"false\" isActive=\"false\" isLeaf=\"false\" name=\"#{definition.name.to_xmi}\" visibility=\"public\" xmi:id=\"#{id}\" xmi:type=\"uml:Class\">"
36
+ xmi += draw_model_inheritance if options.inheritance
37
+ xmi += extend_and_draw(attributes)
38
+ xmi += extend_and_draw(class_methods)
39
+ xmi += extend_and_draw(instance_methods)
40
+ xmi += "</ownedMember>"
41
+ end
42
+
43
+ # Draws a diagram element i.e. the part which is rendered
44
+ def draw_diagram(options)
45
+ xml = %(<uml:DiagramElement preferredShapeType="Class" subject="#{id}" xmi:id="#{id}de">
46
+ </uml:DiagramElement>)
47
+
48
+ return xml if @definition.superklass.empty? && @definition.inherited_modules.empty?
49
+ return xml unless options.inheritance
50
+
51
+ xml += draw_diagram_generalisation
52
+ end
53
+
54
+ def draw_diagram_generalisation
55
+ xml = ''
56
+
57
+ if ! @definition.superklass.empty?
58
+ xml += %(<uml:DiagramElement fromDiagramElement="#{@definition.superklass.id}de" preferredShapeType="Generalization" subject="#{gen_id}" toDiagramElement="#{id}de">
59
+ </uml:DiagramElement>)
60
+ end
61
+
62
+ return xml if @definition.inherited_modules.empty?
63
+
64
+ @definition.inherited_modules.each do |m|
65
+ next if m.empty?
66
+
67
+ xml += %(<uml:DiagramElement fromDiagramElement="#{m.id}de" preferredShapeType="Generalization" subject="#{gen_id}" toDiagramElement="#{id}de">
68
+ </uml:DiagramElement>)
69
+ end
70
+
71
+ xml
72
+ end
73
+
74
+ # Inheritance has to be drawn both as part of the model
75
+ # and as a part of the diagram
76
+ #
77
+ # general =
78
+ # id = IMPORTANT; will be used to draw the lines in the diagram
79
+ #
80
+ def draw_model_inheritance
81
+ return '' if @definition.superklass.empty? && @definition.inherited_modules.empty?
82
+
83
+ xml = ''
84
+
85
+ if ! @definition.superklass.empty?
86
+ xml += %(<generalization general="#{@definition.superklass.id}" xmi:id="#{gen_id}" xmi:type="uml:Generalization">
87
+ </generalization>)
88
+ end
89
+
90
+ return xml if @definition.inherited_modules.empty?
91
+
92
+ @definition.inherited_modules.each do |m|
93
+ next if m.empty?
94
+
95
+ xml += %(<generalization general="#{m.id}" xmi:id="#{gen_id}" xmi:type="uml:Generalization">
96
+ </generalization>)
97
+ end
98
+ xml
99
+ end
100
+
101
+ def draw_model_composition(composee)
102
+ %(<ownedMember isAbstract="false" isDerived="false" isLeaf="false" xmi:id="#{association_id}" xmi:type="uml:Association">
103
+ <memberEnd xmi:idref="#{association_end_id}"/>
104
+ <ownedEnd aggregation="none" association="#{association_id}" isDerived="false" isDerivedUnion="false" isLeaf="false" isNavigable="true" isReadOnly="false" isStatic="false" type="#{id}" xmi:id="9JMZlYaD.AACASCI" xmi:type="uml:Property">
105
+ </ownedEnd>
106
+ <memberEnd xmi:idref="#{composee.association_end_id}"/>
107
+ <ownedEnd aggregation="composite" association="#{association_id}" isDerived="false" isDerivedUnion="false" isLeaf="false" isNavigable="true" isReadOnly="false" isStatic="false" type="#{composee.id}" xmi:id="9JMZlYaD.AACASCK" xmi:type="uml:Property">
108
+ </ownedEnd>
109
+ </ownedMember>)
110
+ end
111
+
112
+ def draw_diagram_composition(composee)
113
+ %(<uml:DiagramElement fromDiagramElement="#{id}de" preferredShapeType="Association" subject="#{association_id}" toDiagramElement="#{composee.id}de">
114
+ </uml:DiagramElement>)
115
+ end
116
+
117
+ # Im not happy with this - xmi should not have to
118
+ # know about types and it should be a method
119
+ def extend_and_draw(collection)
120
+ collection.map do |member|
121
+ case member
122
+ when ::Xumlidot::Types::MethodSignature
123
+ member.extend(::Xumlidot::Diagram::Xmi::MethodSignature)
124
+ when ::Xumlidot::Types::Attribute
125
+ member.extend(::Xumlidot::Diagram::Xmi::Attribute)
126
+ end
127
+ member.draw
128
+ end.join(' ')
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../types'
4
+ require_relative 'id'
5
+
6
+ module Xumlidot
7
+ class Diagram
8
+ class Xmi
9
+ module MethodSignature
10
+ include ::Xumlidot::Diagram::Xmi::ID
11
+
12
+ # Ugh
13
+ def name_to_xmi
14
+ return '&lt;&lt;' if name == :<<
15
+ return '&gt;&gt;' if name == :>>
16
+ return '&lt;=&gt;' if name == :<=>
17
+ name
18
+ end
19
+
20
+ def draw
21
+ xmi = "<ownedOperation isAbstract=\"false\" isLeaf=\"false\" isOrdered=\"false\" isQuery=\"false\" isStatic=\"#{superclass_method}\" isUnique=\"true\" name=\"#{name_to_xmi}\" visibility=\"#{visibility}\" xmi:id=\"#{id}\" xmi:type=\"uml:Operation\">"
22
+ xmi += "<ownedParameter kind=\"return\" xmi:id=\"#{return_id}\" xmi:type=\"uml:Parameter\"/>"
23
+ args.each do |argument|
24
+ argument.extend(::Xumlidot::Diagram::Xmi::Argument)
25
+ xmi += argument.draw
26
+ end
27
+ xmi += "</ownedOperation>"
28
+ end
29
+
30
+ private
31
+
32
+ def return_id
33
+ @_return_id ||= new_id
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../types'
4
+ require_relative 'id'
5
+
6
+ module Xumlidot
7
+ class Diagram
8
+ class Xmi
9
+ module Superklass
10
+ include ::Xumlidot::Diagram::Xmi::ID
11
+
12
+ def draw_identifier
13
+ [name, namespace.reverse].reverse.flatten.join('::')
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ require 'find'
2
+
3
+ module Xumlidot
4
+ # Recurse down a directory tree
5
+ class DirectoryTree
6
+ def initialize(directories, options)
7
+ @directories = directories
8
+ @options = options
9
+ @excluded = Regexp.new(@options.exclude)
10
+ end
11
+
12
+ def find_all_rb_files(&block)
13
+ @directories.each do |directory|
14
+ Find.find(directory) do |path|
15
+ next if path =~ @exluded
16
+
17
+ next unless path.end_with? '.rb'
18
+
19
+ yield path if block_given?
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,104 @@
1
+ require 'optparse'
2
+ require 'optparse/time'
3
+ require 'ostruct'
4
+
5
+ module Xumlidot
6
+ OptionsError = Class.new(StandardError)
7
+
8
+ class Options
9
+ def self.method_missing(m, *args, &block)
10
+ if @options.respond_to?(m)
11
+ @options.send(m)
12
+ else
13
+ raise OptionsError.new("Unknown Option #{m}")
14
+ end
15
+ end
16
+
17
+ def self.parse(args)
18
+ @options = OpenStruct.new
19
+
20
+ @options.title = 'Class Diagram'
21
+ @options.model_name = 'My Model'
22
+ @options.diagram_type = :dot
23
+ @options.rails = false
24
+ @options.debug = false
25
+ @options.inheritance = true
26
+ @options.composition = true
27
+ @options.usage = true
28
+ @options.split = 1
29
+ @options.sequence = ''
30
+ @options.exclude = ''
31
+
32
+ ENV.delete("XUMLIDOT_DEBUG")
33
+
34
+ opt_parser = OptionParser.new do |opts|
35
+ opts.banner = "Usage: xumlidot.rb [options]"
36
+
37
+ opts.separator ""
38
+ opts.separator "Specific options:"
39
+
40
+ opts.on("-t", "--title [TEXT]", "Title for the diagram") do |v|
41
+ @options.title = v
42
+ end
43
+
44
+ opts.on("-m", "--model [TEXT]", "Model Name for the diagram") do |v|
45
+ @options.model_name = v
46
+ end
47
+
48
+ opts.on("-d", "--dot", "Output diagram using dot (default)") do |v|
49
+ @options.diagram_type = :dot
50
+ end
51
+
52
+ opts.on("-x", "--xmi", "Output diagram using xmi") do |v|
53
+ @options.diagram_type = :xmi
54
+ end
55
+
56
+ opts.on("-d", "--debug", "Output debug information") do |v|
57
+ @options.debug = true
58
+ ENV["XUMLIDOT_DEBUG"] = '1'
59
+ end
60
+
61
+ opts.on("-i", "--no-inheritance", "Output inheritence links on the diagram") do |v|
62
+ @options.inheritance = false
63
+ end
64
+
65
+ opts.on("-c", "--no-composition", "Output composition links on the diagram") do |v|
66
+ @options.composition = false
67
+ end
68
+
69
+ opts.on("-r", "--rails", "Expect a Rails application") do |v|
70
+ @options.rails = v
71
+ end
72
+
73
+
74
+ opts.on("-e", "--exclude [TEXT[", "Output usage links on the diagram") do |v|
75
+ @options.exclude = v
76
+ end
77
+
78
+ opts.on("-u", "--[no-]usage", "Output usage links on the diagram") do |v|
79
+ @options.usage = v
80
+ end
81
+ opts.separator ""
82
+
83
+ opts.separator "Common options:"
84
+
85
+ # No argument, shows at tail. This will print an options summary.
86
+ # Try it and see!
87
+ opts.on_tail("-h", "--help", "Show this message") do
88
+ puts opts
89
+ exit
90
+ end
91
+
92
+ # Another typical switch to print the version.
93
+ opts.on_tail("--version", "Show version") do
94
+ puts ::Version.join('.')
95
+ exit
96
+ end
97
+ end
98
+
99
+ opt_parser.parse!(args)
100
+ @options
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,14 @@
1
+ require 'ruby_parser'
2
+ require 'sexp_processor'
3
+ require 'ostruct'
4
+ require 'pry'
5
+
6
+ require_relative 'parsers/generic'
7
+ require_relative 'parsers/args'
8
+ require_relative 'parsers/call'
9
+ require_relative 'parsers/file'
10
+ require_relative 'parsers/klass_definition'
11
+ require_relative 'parsers/method_signature'
12
+ require_relative 'parsers/module_definition'
13
+ require_relative 'parsers/scope'
14
+ require_relative 'parsers/stack'
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../parsers'
4
+ require_relative '../types'
5
+
6
+ module Xumlidot
7
+ module Parsers
8
+ # Parser for the arguments to a method
9
+ #
10
+ # e.g. formats def method(a, b = nil)
11
+ # to a string 'a, b = nil'
12
+ #
13
+ class Args < MethodBasedSexpProcessor
14
+
15
+ def initialize(exp)
16
+ super()
17
+
18
+ @arguments = ::Xumlidot::Types::Arguments.new
19
+
20
+ process(exp)
21
+ rescue => e
22
+ STDERR.puts " ** bug: unable to process args #{exp} "
23
+ end
24
+
25
+ def to_s
26
+ @arguments.to_s
27
+ end
28
+
29
+ def definition
30
+ @arguments
31
+ end
32
+
33
+ # Note - special case since a value of nil for default
34
+ # means we shouldn't display it and so we use the :nil
35
+ # symbol to represent an *actual assignment of nil* to
36
+ # a variable.
37
+ def process_nil(exp)
38
+ @argument.default = :nil
39
+ s()
40
+ end
41
+
42
+ def process_str(exp)
43
+ @argument.default = exp.value
44
+ s()
45
+ end
46
+
47
+ def process_hash(exp)
48
+ @argument.default = {}
49
+ s()
50
+ end
51
+
52
+ # const means that we have a constant assignment such as (a = Foo)
53
+ def process_const(exp)
54
+ @argument.default = exp.value.to_s
55
+ s()
56
+ end
57
+
58
+ # Colon2 means that we have a constant assignment such as (a = Foo::Bar)
59
+ def process_colon2(exp)
60
+ name = exp.flatten
61
+ name.delete :const
62
+ name.delete :colon2
63
+
64
+ leader = ''
65
+ if name.first == :colon3
66
+ leader = '::'
67
+ name.delete :colon3
68
+ end
69
+
70
+ # I'm not sure how best to proceed here.
71
+ #
72
+ # I can use const_set to start creating the constants heirachy
73
+ # but this is complex since it needs to be inserted into the right
74
+ # place and for that I need the namespace...which suggests this ISNT
75
+ # the place to do that. I possibly need a fake class ...
76
+ @argument.default = leader + name.map do |v|
77
+ v.to_s
78
+ end.to_a.join('::')
79
+
80
+ s()
81
+ end
82
+
83
+ # Colon2 means that we have a constant assignment such as (a = ::Foo::Bar)
84
+ # again see the note in colon2 about how to proceed
85
+ def process_colon3(exp)
86
+ process_colon2(exp)
87
+ @argument.default = "::#{argument.default}"
88
+ end
89
+
90
+ def process_array(exp)
91
+ @argument.default = []
92
+
93
+ exp.shift # remove :array
94
+ exp.each { |element| process(element) }
95
+ s()
96
+ end
97
+
98
+ def process_lasgn(exp)
99
+ exp.shift # remove :lasgn
100
+
101
+ @argument.name = exp.shift.to_s
102
+
103
+ value = exp.shift
104
+ process(value)
105
+ s()
106
+ end
107
+
108
+ def process_lit(exp)
109
+ exp.shift # remove :lit
110
+
111
+ case @argument.default
112
+ when Array
113
+ @argument.default << exp.value
114
+ when nil
115
+ @argument.default = exp.value
116
+ when Symbol
117
+ @argument.default = exp.value.to_s
118
+ when String
119
+ @argument.default = exp.value.to_s
120
+ when Sexp
121
+ # binding.pry
122
+ when Hash
123
+ # binding.pry
124
+ else
125
+ # binding.pry
126
+ end
127
+ exp.shift
128
+ s()
129
+ end
130
+
131
+ def process_kwarg(exp)
132
+ exp.shift # remove :kwarg
133
+ @argument.name = "#{exp[0]}:"
134
+ process(exp)
135
+ s()
136
+ rescue => e
137
+ STDERR.puts " ** bug: unable to process kwarg #{exp}; failure to parse default value? "
138
+ s()
139
+ end
140
+
141
+ def process_args(exp)
142
+ exp.shift # remove :args
143
+ exp.each do |arg|
144
+ @argument = ::Xumlidot::Types::Argument.new
145
+ if arg.is_a? Sexp
146
+ @argument.assign = '='
147
+ process(arg)
148
+ else
149
+ @argument.name = arg.to_s
150
+ end
151
+
152
+ @arguments << @argument
153
+ end
154
+ rescue => e
155
+ STDERR.puts " ** bug: unable to process args #{exp}; failure to parse default value? "
156
+ end
157
+ end
158
+ end
159
+ end