xumlidot 0.1.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.
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