rumbly 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ module Rumbly
4
+ module Diagram
5
+
6
+ # This is an abstract class that defines the API for creating UML class diagrams.
7
+ # Implementations for specific formats (e.g. Yumly, Graphviz, text, etc.) should
8
+ # subclass this class and implement the following methods: +setup+,
9
+ # +process_klass+, +middle+, +process_relationship+, and +finish+.
10
+ class Base
11
+
12
+ class << self
13
+
14
+ # Creates a specific subclass of this base diagram class based on the diagram
15
+ # type specific in the global options, then calls its +#build+ method to create
16
+ # and save the UML class diagram.
17
+ def create (application)
18
+ diagram_type = Rumbly::options.diagram.type
19
+ require "rumbly/diagram/#{diagram_type}"
20
+ Rumbly::Diagram.const_get(diagram_type.to_s.classify).new(application).build
21
+ end
22
+
23
+ end
24
+
25
+ attr_reader :application
26
+
27
+ def initialize (application)
28
+ @application = application
29
+ end
30
+
31
+ # Builds a UML class diagram via the callbacks defined for this base class.
32
+ def build
33
+ setup
34
+ @application.klasses.each do |klass|
35
+ process_klass(klass)
36
+ end
37
+ middle
38
+ @application.relationships.each do |relationship|
39
+ process_relationship(relationship)
40
+ end
41
+ finish
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ require 'rumbly/diagram/base'
2
+
3
+ module Rumbly
4
+ module Diagram
5
+
6
+ class Debug < Base
7
+
8
+ def setup
9
+ puts "Application: #{application.name}"
10
+ puts
11
+ puts "Classes:"
12
+ puts
13
+ end
14
+
15
+ def process_klass (klass)
16
+ puts " #{klass.name}"
17
+ klass.attributes.each { |a| puts " #{a.label}" }
18
+ puts
19
+ end
20
+
21
+ def middle
22
+ puts "Relationships:"
23
+ puts
24
+ end
25
+
26
+ def process_relationship (relationship)
27
+ puts " #{relationship.label}"
28
+ end
29
+
30
+ def finish
31
+ puts
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ require 'rumbly/diagram/base'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'graphviz'
4
+
5
+ module Rumbly
6
+ module Diagram
7
+
8
+ class Graphviz < Base
9
+
10
+ attr_reader :graph
11
+
12
+ delegate :add_nodes, :add_edges, :get_node, :output, to: :graph
13
+
14
+ def setup
15
+ @graph = GraphViz.digraph(@application.name)
16
+ @graph.node[:shape] = :record
17
+ @graph.node[:fontsize] = 10
18
+ @graph.node[:fontname] = 'Arial'
19
+ end
20
+
21
+ def process_klass (k)
22
+ add_nodes(k.name)
23
+ end
24
+
25
+ def middle
26
+ end
27
+
28
+ def process_relationship (r)
29
+ add_edges(find(r.source), find(r.target))
30
+ end
31
+
32
+ def find (klass)
33
+ get_node(klass.name)
34
+ end
35
+
36
+ def finish
37
+ d = Rumbly::options.diagram
38
+ output(d.format => "#{d.file}.#{d.format}")
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ module Rumbly
2
+ module Model
3
+
4
+ # The +Abstract+ module is extended (not included) by abstract subclasses that
5
+ # declare their public attributes and wish to have stub methods for these attributes
6
+ # generated automatically. The stub methods raise an exception with the abstract
7
+ # class name so implementers know that they need to implement the given method(s) in
8
+ # their concrete subclass(es).
9
+ module Abstract
10
+
11
+ # Creates stub accesor methods for each of the given +attributes+. Each method
12
+ # raises a +RuntimeError+, since the extending class is meant to be abstract.
13
+ def stub_required_methods (cls, attributes)
14
+ attributes.keys.each do |a|
15
+ message = "Method '%s' called on abstract '#{cls.name}' class"
16
+ define_method(a) { raise (message % a) }
17
+ define_method("#{a}=") { |x| raise (message % "#{a}=") }
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ require 'rumbly/model/application'
2
+ require 'rumbly/model/active_record/klass'
3
+ require 'rumbly/model/active_record/relationship'
4
+
5
+ module Rumbly
6
+ module Model
7
+ module ActiveRecord
8
+
9
+ # This class is an +ActiveRecord+-specific implementation of the abstract
10
+ # +Rumbly::Model::Application+ class for representing model classes and
11
+ # relationships within the currently loaded environment.
12
+ class Application < Rumbly::Model::Application
13
+
14
+ attr_reader :name, :klasses, :relationships
15
+
16
+ # Returns the name of the current +ActiveRecord+ application.
17
+ def name
18
+ @name ||= Rails.application.class.parent.name
19
+ end
20
+
21
+ # Returns an array of all +Rumbly::Model::ActiveRecord::Klass+ objects for the
22
+ # current loaded +ActiveRecord+ environment.
23
+ def klasses
24
+ if @klasses.nil?
25
+ # build the klass list in two steps to avoid infinite loop in second call
26
+ @klasses = Klass.all_from_base_descendents(self)
27
+ @klasses += Klass.all_from_polymorphic_associations(self)
28
+ end
29
+ @klasses
30
+ end
31
+
32
+ # Returns an array of +Rumbly::Model::ActiveRecord::Relationship+ objects for
33
+ # the currently loaded +ActiveRecord+ environment.
34
+ def relationships
35
+ @relationships ||= Relationship.all_from_active_record(self)
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,157 @@
1
+ require 'rumbly/model/attribute'
2
+
3
+ module Rumbly
4
+ module Model
5
+ module ActiveRecord
6
+
7
+ # This class is an +ActiveRecord+-specific implementation of the abstract
8
+ # +Rumbly::Model::Attribute+ class use dto represent declared attributes (columns)
9
+ # on model classes in the currently loaded environment.
10
+ class Attribute < Rumbly::Model::Attribute
11
+
12
+ # Returns an array of +Rumbly::Model::ActiveRecord::Attribute+ objects, each
13
+ # of which wraps a field (column) from the given +ActiveRecord+ model class.
14
+ def self.all_from_klass (klass)
15
+ klass.cls.columns.map do |column|
16
+ new(klass, column)
17
+ end
18
+ end
19
+
20
+ def initialize (klass, column)
21
+ @klass = klass
22
+ @cls = klass.cls
23
+ @column = column
24
+ end
25
+
26
+ # Returns the name of this +ActiveRecord+ attribute based on the column
27
+ # definition.
28
+ def name
29
+ @name ||= @column.name
30
+ end
31
+
32
+ # Returns the type of this +ActiveRecord+ attribute based on the column
33
+ # definition.
34
+ def type
35
+ @type ||= begin
36
+ type = @column.type.to_s
37
+ unless @column.limit.nil?
38
+ type += "(#{@column.limit})"
39
+ end
40
+ unless @column.precision.nil? || @column.scale.nil?
41
+ type += "(#{@column.precision},#{@column.scale})"
42
+ end
43
+ type
44
+ end
45
+ end
46
+
47
+ # Returns +nil+ since +ActiveRecord+ doesn't declare attribute visibility.
48
+ def visibility
49
+ nil
50
+ end
51
+
52
+ # Returns +nil+ since +ActiveRecord+ doesn't allow for non-intrinsic attributes.
53
+ def multiplicity
54
+ nil
55
+ end
56
+
57
+ # Returns this attribute's default value based on +ActiveRecord+ column definition.
58
+ def default
59
+ @default ||= @column.default
60
+ end
61
+
62
+ # Returns +nil+ since +ActiveRecord+ doesn't support any of the standard UML
63
+ # attribute properties (e.g. read-only, union, composite, etc.).
64
+ def properties
65
+ []
66
+ end
67
+
68
+ # Returns an +Array+ of +String+ values representing constraints placed on this
69
+ # attribute via +ActiveModel+ validations. Only simple, declarative validations
70
+ # will be reflected as constraints (i.e. not conditional or custom
71
+ # validations). Also, some parameters or conditions on simple validations will
72
+ # not be shown, e.g. scope or case-sensitivity on a uniqueness validation.
73
+ # Currently, the following +ActiveModel+ validations are ignored: +inclusion+,
74
+ # +exclusion+, +format+, and any conditional validations.
75
+ def constraints
76
+ @constraints ||= begin
77
+ constraints = []
78
+ constraints << 'required' if required?
79
+ constraints << 'unique' if unique?
80
+ append_numeric_constraints(constraints)
81
+ append_length_constraints(constraints)
82
+ constraints
83
+ end
84
+ end
85
+
86
+ # Returns +nil+ since +ActiveRecord+ doesn't declare derived attributes.
87
+ def derived
88
+ nil
89
+ end
90
+
91
+ # Returns +nil+ since +ActiveRecord+ doesn't declare static (class) attributes.
92
+ def static
93
+ nil
94
+ end
95
+
96
+ private
97
+
98
+ def required?
99
+ @cls.validators_on(name).map(&:kind).include?(:presence)
100
+ end
101
+
102
+ def unique?
103
+ @cls.validators_on(name).map(&:kind).include?(:uniqueness)
104
+ end
105
+
106
+ NUMERIC_VALIDATORS = [
107
+ [ :integer_only, 'integer' ],
108
+ [ :odd, 'odd' ],
109
+ [ :even, 'even' ],
110
+ [ :greater_than, '> %{x}' ],
111
+ [ :greater_than_or_equal_to, '>= %{x}' ],
112
+ [ :equal_to, '= %{x}' ],
113
+ [ :less_than, '< %{x}' ],
114
+ [ :less_than_or_equal_to, '<= %{x}' ],
115
+ ]
116
+
117
+ # Appends any numeric constraints on this +ActiveRecord+ attribute via one or
118
+ # more +numericality+ validations.
119
+ def append_numeric_constraints (constraints)
120
+ validators = @cls.validators_on(name).select { |v| v.kind == :numericality }
121
+ unless validators.nil? || validators.empty?
122
+ options = validators.map { |v| v.options }.inject { |all,v| all.merge(v) }
123
+ NUMERIC_VALIDATORS.each do |validator|
124
+ key, str = validator
125
+ if options.has_key?(key)
126
+ constraints << str.gsub(/x/,key.to_s) % options
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Appends any length constraints put on this +ActiveRecord+ attribute via one
133
+ # or more +length+ validations.
134
+ def append_length_constraints (constraints)
135
+ validators = @cls.validators_on(name).select { |v| v.kind == :length }
136
+ unless validators.nil? || validators.empty?
137
+ options = validators.map { |v| v.options }.inject { |all,v| all.merge(v) }
138
+ constraints << case
139
+ when options.has_key?(:is)
140
+ "length = #{options[:is]}"
141
+ when options.has_key?(:in)
142
+ "length in (#{options[:in]})"
143
+ when options.has_key?(:minimum) && options.has_key?(:maximum)
144
+ "#{options[:minimum]} <= length <= #{options[:maximum]}"
145
+ when options.has_key?(:minimum)
146
+ "length >= #{options[:minimum]}"
147
+ when options.has_key?(:maximum)
148
+ "length <= #{options[:maximum]}"
149
+ end
150
+ end
151
+ end
152
+
153
+ end
154
+
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,99 @@
1
+ require 'active_record'
2
+ require 'rumbly/model/klass'
3
+ require 'rumbly/model/active_record/attribute'
4
+
5
+ module Rumbly
6
+ module Model
7
+ module ActiveRecord
8
+
9
+ # This class is an +ActiveRecord+-specific implementation of the abstract
10
+ # +Rumbly::Model::Klass+ class used to represent model classes within the currently
11
+ # loaded environment. All model class, both persistent and abstract, are modeled
12
+ # as +Klass+ objects. Also, "virtual" classes (more like interfaces) that are named
13
+ # as part of any polymorphic associations are also modeled as +Klass+es. These
14
+ # objects have a name but no underlying +ActiveRecord+ model class.
15
+ class Klass < Rumbly::Model::Klass
16
+
17
+ class << self
18
+
19
+ # Returns an array of +Klass+ objects representing +ActiveRecord+ model classes
20
+ # (be they persistent or abstract) in the currently loaded environment.
21
+ def all_from_base_descendents (app)
22
+ ::ActiveRecord::Base.descendants.select do
23
+ |cls| class_valid?(cls)
24
+ end.map { |cls| new(app, cls) }
25
+ end
26
+
27
+ # Returns an array of +Klass+ objects representing "virtual" classes that are
28
+ # named as part of any polymorphic associations. These virtual classes are more
29
+ # like interfaces, but we model them as +Klasses+ for the purposes of showing
30
+ # them in a UML class diagram.
31
+ def all_from_polymorphic_associations (app)
32
+ Relationship.associations_matching(app, :belongs_to, :polymorphic).map do |a|
33
+ new(app, nil, a.name)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ # A class is valid if it is abstract or concrete with a corresponding table.
40
+ def class_valid? (cls)
41
+ cls.abstract_class? || cls.table_exists?
42
+ end
43
+
44
+ end
45
+
46
+ # Initializes a new +Klass+ from the given +ActiveModel+ model class. Keeps
47
+ # a back pointer to the top-level +Application+ object. For "virtual" classes
48
+ # (see above), the +cls+ will be nil and the +name+ will be explicitly given.
49
+ def initialize (app, cls, name=nil)
50
+ @app = app
51
+ @cls = cls
52
+ @name = name
53
+ end
54
+
55
+ # Returns the +ActiveRecord+ model class associated with this +Klass+. Should
56
+ # only be used by other +Rumbly::Model::ActiveRecord+ classes (but no way in
57
+ # Ruby to enforce that). May be nil if this is a "virtual" class (see above).
58
+ def cls
59
+ @cls
60
+ end
61
+
62
+ # Returns the name of this +Rumbly::Model::ActiveRecord::Klass+.
63
+ def name
64
+ @name ||= @cls.name
65
+ end
66
+
67
+ # Returns an array of +Rumbly::Model::ActiveRecord::Attributes+, each of which
68
+ # describes an attribute of the +ActiveRecord+ class for this +Klass+. Don't
69
+ # bother to lookup attributes if this +Klass+ represents an abstract model class
70
+ # or is a "virtual" class (interface) stemming from a polymorphic association.
71
+ def attributes
72
+ @attributes ||= if @cls.nil? or self.abstract?
73
+ []
74
+ else
75
+ Attribute.all_from_klass(self)
76
+ end
77
+ end
78
+
79
+ # Returns nil, since +ActiveRecord+ models don't declare their operations.
80
+ def operations
81
+ nil
82
+ end
83
+
84
+ # Returns +true+ if this +Rumbly::Model::ActiveRecord::Klass+ is abstract.
85
+ def abstract
86
+ @abstract ||= (@cls.nil? ? false : @cls.abstract_class?)
87
+ end
88
+
89
+ # Returns +true+ if this +Rumbly::Model::ActiveRecord::Klass+ is a "virtual"
90
+ # class, i.e. one stemming from a polymorphic association (more like an interface).
91
+ def virtual
92
+ @virtual ||= @cls.nil?
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,166 @@
1
+ require 'rumbly/model/relationship'
2
+
3
+ module Rumbly
4
+ module Model
5
+ module ActiveRecord
6
+
7
+ # This class is an +ActiveRecord+-specific implementation of the abstract
8
+ # +Rumbly::Model::Relationship+ class used to represent declared relationships
9
+ # (associations) between model classes in the currently loaded environment.
10
+ class Relationship < Rumbly::Model::Relationship
11
+
12
+ # Returns an array of +Rumbly::Model::ActiveRecord::Relationship+ objects that
13
+ # represent both associations and generalizations (i.e. subclasses) in the
14
+ # currently loaded +ActiveRecord+ environment.
15
+ def self.all_from_active_record (app)
16
+ all_from_assocations(app) + all_from_generalizations(app)
17
+ end
18
+
19
+ # Returns an array of +Rumbly::Model::ActiveRecord::Relationship+ objects that
20
+ # represent declared associations between model classes.
21
+ def self.all_from_assocations (app)
22
+ all_associations(app).map { |a| new(app, a) }
23
+ end
24
+
25
+ # Returns an array of +Rumbly::Model::ActiveRecord::Relationship+ objects that
26
+ # represent all subclass relationships between model classes.
27
+ def self.all_from_generalizations (app)
28
+ app.klasses.map(&:cls).compact.reject(&:descends_from_active_record?).map do |c|
29
+ source = c.superclass.name
30
+ target = c.name
31
+ new(app, nil, :generalization, source, target)
32
+ end
33
+ end
34
+
35
+ # Returns an +Array+ of +ActiveRecord+ associations which match the given +type+
36
+ # and have the given +option+, e.g. +:belongs_to+ and +:polymorphic+.
37
+ def self.associations_matching (app, type, option)
38
+ all_associations(app).select { |a| a.macro == type }.select do |a|
39
+ a.options.keys.include?(option)
40
+ end
41
+ end
42
+
43
+ # Returns all +ActiveRecord+ associations for all model classes in the currently
44
+ # loaded environment.
45
+ def self.all_associations (app)
46
+ app.klasses.map(&:cls).compact.map(&:reflect_on_all_associations).flatten
47
+ end
48
+
49
+ # Initializes a new +Relationship+ using the given +ActiveModel+ +association+
50
+ # (in the case of non-generalizations), or the given +type+, +source+, and
51
+ # +target+ in the case of generalizations.
52
+ def initialize (app, association, type=nil, source=nil, target=nil)
53
+ @app = app
54
+ @association = association
55
+ @type = type
56
+ @source = source
57
+ @target = target
58
+ end
59
+
60
+ # Returns the UML relationship type for this +Relationship+. For a relationships
61
+ # that's a generalization (subclass), the +type+ is set upon initialization.
62
+ # Otherwise, this method examines the +ActiveRecord+ association for clues that
63
+ # point to the relationship being a simple +association+, an +aggregation+, or
64
+ # the even stronger +composition+.
65
+ def type
66
+ if @type.nil?
67
+ # relationships are simple associations by default
68
+ @type = :association
69
+ if [:has_one, :has_many].include?(@association.macro)
70
+ autosaves = @association.options[:autosave]
71
+ dependent = @association.options[:dependent]
72
+ # if this association auto-saves or nullifies, assume aggregation
73
+ if autosaves || dependent == :nullify
74
+ @type = :aggregation
75
+ end
76
+ # if this association destroys dependents, assume composition
77
+ if dependent == :destroy || dependent == :delete
78
+ @type = :composition
79
+ end
80
+ end
81
+ end
82
+ return @type
83
+ end
84
+
85
+ # Returns the source +Klass+ for this +Relationship+. Gets the +ActiveRecord+
86
+ # model class that's the source of the underlying association and looks up
87
+ # the corresponding +Klass+ object in our cache.
88
+ def source
89
+ @source ||= @app.klass_by_name(@association.active_record.name)
90
+ end
91
+
92
+ # Returns the target +Klass+ for this +Relationship+. Gets the +ActiveRecord+
93
+ # model class that's the target of the underlying association and looks up
94
+ # the corresponding +Klass+ object in our cache.
95
+ def target
96
+ @target ||= @app.klass_by_name(@association.klass.name)
97
+ end
98
+
99
+ # Returns the name of this +Relationship+, which is just the +name+ from the
100
+ # +ActiveRecord+ association (or nil if this +Relationship+ doesn't have an
101
+ # association, i.e. it's a generalization).
102
+ def name
103
+ (type == :generalization) ? nil : (@name ||= @association.name)
104
+ end
105
+
106
+ # Returns the multiplicity of this +Relationship+ based on the type of the
107
+ # +ActiveRecord+ association, e.g. +:has_one+, +:has_many+, +:belongs_to+, etc.
108
+ def multiplicity
109
+ (type == :generalization) ? nil : (@multiplicity ||= derive_multiplicity)
110
+ end
111
+
112
+ # Returns the "through" class declared
113
+ def through
114
+ (type == :generalization) ? nil : (@through ||= find_through_klass)
115
+ end
116
+
117
+ # Returns true, since +ActiveRecord+ doesn't have the concept of non-navigable
118
+ # assocations.
119
+ def navigable
120
+ true
121
+ end
122
+
123
+ private
124
+
125
+ # Returns an array of two numbers that represents the multiplicity of this
126
+ # +Relationship+ based on the type of +ActiveRecord+ association it is.
127
+ def derive_multiplicity
128
+ case @association.macro
129
+ when :has_one
130
+ # has_one associations can have zero or one associated object
131
+ [0,1]
132
+ when :has_many, :has_and_belongs_to_many
133
+ # has_many and habtm associations can have zero or more associated objects
134
+ [0,::Rumbly::Model::N]
135
+ when :belongs_to
136
+ # belongs_to associations normally have zero or one related object, but
137
+ # we check for a presence validator to see if the link is required
138
+ validators = source.cls.validators_on(@association.foreign_key.to_sym)
139
+ if validators.select { |v| v.kind == :presence }.any?
140
+ [1,1]
141
+ else
142
+ [0,1]
143
+ end
144
+ end
145
+ end
146
+
147
+ # Finds the +Klass+ object corresponding to the "through" class on the has_one
148
+ # or has_many association for this +Relationship+.
149
+ def find_through_klass
150
+ unless @through_checked
151
+ @through_checked = true
152
+ if [:has_one, :has_many].include?(@association.macro)
153
+ through = @association.options[:through]
154
+ unless through.nil?
155
+ return @app.klass_by_name(through)
156
+ end
157
+ end
158
+ end
159
+ return nil
160
+ end
161
+
162
+ end
163
+
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,71 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+ require 'rumbly/model/abstract'
3
+
4
+ module Rumbly
5
+ module Model
6
+
7
+ N = Infinity = 1.0/0
8
+
9
+ # This is an abstract class that represents the full model of a MVC application,
10
+ # including all classes and relationships. Object mapper-specific implementations
11
+ # should subclass this class and implement the following methods: +name+,
12
+ # +klasses+, and +relationships+.
13
+ class Application
14
+
15
+ # Attributes and default values of an Application
16
+ ATTRIBUTES = { name: '', klasses: [], relationships: [] }
17
+
18
+ # For each attribute, create stub accessor methods that raise an exception
19
+ extend Abstract
20
+ stub_required_methods(Application, ATTRIBUTES)
21
+
22
+ class << self
23
+
24
+ # Creates a new subclass of +Rumbly::Model::Application+ based on the options
25
+ # set in the main +Rumbly+ module (via rake, command line, etc.). If the
26
+ # model_type+ option is set to +auto+, the current object mapper library is
27
+ # auto-detected.
28
+ def create
29
+ model_type = auto_detect_model_type
30
+ require "rumbly/model/#{model_type}/application"
31
+ Rumbly::Model.const_get(model_type.to_s.classify)::Application.new
32
+ end
33
+
34
+ private
35
+
36
+ OBJECT_MAPPERS = [ :active_record, :data_mapper, :mongoid, :mongo_mapper ]
37
+
38
+ # Auto-detects the current object mapper gem/library if one isn't specified in
39
+ # the global +Rumbly::options+ hash.
40
+ def auto_detect_model_type
41
+ model_type = Rumbly::options.model.type
42
+ if model_type == :auto
43
+ model_type = OBJECT_MAPPERS.detect do |mapper|
44
+ Class.const_defined?(mapper.to_s.classify)
45
+ end
46
+ raise "Couldn't auto-detect object mapper gem/library" if model_type.nil?
47
+ end
48
+ model_type.to_s
49
+ end
50
+
51
+ end
52
+
53
+ # Returns a +Klass+ object from our cache indexed by +name+.
54
+ def klass_by_name (name)
55
+ klass_cache[name]
56
+ end
57
+
58
+ private
59
+
60
+ def klass_cache
61
+ @klass_cache ||= {}.tap do |cache|
62
+ klasses.each do |klass|
63
+ cache[klass.name] = klass
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,63 @@
1
+ require 'rumbly/model/abstract'
2
+
3
+ module Rumbly
4
+ module Model
5
+
6
+ # This is an abstract class that represents a single attribute of one class within
7
+ # an MVC application. Object mapper-specific implementations should subclass this
8
+ # class and implement the following methods: +name+, +type+, +visibility+,
9
+ # +multiplicity+, +default+, +properties+, +constraints+, +derived+, and +static+.
10
+ class Attribute
11
+
12
+ # Attributes and default values of an Attribute
13
+ ATTRIBUTES = {
14
+ name: '', type: '', visibility: '', multiplicity: '', default: '',
15
+ properties: [], constraints: [], derived: false, static: false
16
+ }
17
+
18
+ # For each attribute, create stub accessor methods that raise an exception
19
+ extend Abstract
20
+ stub_required_methods(Attribute, ATTRIBUTES)
21
+
22
+ # Simple question mark-style wrapper for the +Attribute#derived+ attribute.
23
+ def derived?
24
+ derived
25
+ end
26
+
27
+ # Simple question mark-style wrapper for the +Attribute#static+ attribute.
28
+ def static?
29
+ static
30
+ end
31
+
32
+ # Compares +Attribute+ objects using the +name+ attribute.
33
+ def <=> (other)
34
+ name <=> other.name
35
+ end
36
+
37
+ # Returns a string that fully describes this +Attribute+, including its visibility,
38
+ # name, type, multiplicity, default value, and any properties and/or constraints.
39
+ def label
40
+ label = "#{symbol_for_visibility} "
41
+ label += "/" if derived?
42
+ label += "#{name}"
43
+ label += " : #{type}" unless type.nil?
44
+ label += "[#{multiplicity}]" unless multiplicity.nil?
45
+ label += " = #{default}" unless default.nil?
46
+ label += " {#{props_and_constraints}}" unless props_and_constraints.empty?
47
+ label
48
+ end
49
+
50
+ private
51
+
52
+ VISIBILITY_SYMBOLS = { public: '+', private: '-', protected: '#', package: '~' }
53
+ def symbol_for_visibility
54
+ VISIBILITY_SYMBOLS[visibility] || '-'
55
+ end
56
+
57
+ def props_and_constraints
58
+ (properties + constraints).join(', ')
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ require 'rumbly/model/abstract'
2
+
3
+ module Rumbly
4
+ module Model
5
+
6
+ # This is an abstract class that represents a single model class within an MVC
7
+ # application. Object mapper-specific implementations should subclass this class and
8
+ # implement the following methods: +name+, +attributes+, +operations+, +abstract+,
9
+ # and +virtual+.
10
+ class Klass
11
+
12
+ # Attributes and default values of a Klass
13
+ ATTRIBUTES = {
14
+ name: '', attributes: [], operations: [], abstract: false, virtual: false
15
+ }
16
+
17
+ # For each attribute, create stub accessor methods that raise an exception
18
+ extend Abstract
19
+ stub_required_methods(Klass, ATTRIBUTES)
20
+
21
+ # Simple question mark-style wrapper for the +Klass#abstract+ attribute.
22
+ def abstract?
23
+ abstract
24
+ end
25
+
26
+ # Simple question mark-style wrapper for the +Klass#virtual+ attribute.
27
+ def virtual?
28
+ virtual
29
+ end
30
+
31
+ # Compares +Klass+ objects using the +name+ attribute.
32
+ def <=> (other)
33
+ name <=> other.name
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ require 'rumbly/model/abstract'
2
+
3
+ module Rumbly
4
+ module Model
5
+
6
+ # This is an abstract class that represents a single operation from one class within
7
+ # an MVC application. Object mapper-specific implementations should subclass this
8
+ # class and implement the following methods: +name+, +parameters+, and +type+.
9
+ class Operation
10
+
11
+ # Attributes and default values of a Operation
12
+ ATTRIBUTES = { name: '', parameters: [], type: 'void' }
13
+
14
+ # For each attribute, create stub accessor methods that raise an exception
15
+ extend Abstract
16
+ stub_required_methods(Operation, ATTRIBUTES)
17
+
18
+ # Compares +Operation+ objects using the +name+ attribute.
19
+ def <=> (other)
20
+ name <=> other.name
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require 'rumbly/model/abstract'
2
+
3
+ module Rumbly
4
+ module Model
5
+
6
+ # This is an abstract class that represents one parameter of an operation from a
7
+ # class within an MVC application. Object mapper-specific implementations should
8
+ # subclass this class and implement the following methods: +name+ and +type+.
9
+ class Parameter
10
+
11
+ # Attributes and default values of a Parameter
12
+ ATTRIBUTES = { name: '', type: '' }
13
+
14
+ # For each attribute, create stub accessor methods that raise an exception
15
+ extend Abstract
16
+ stub_required_methods(Parameter, ATTRIBUTES)
17
+
18
+ # Compares +Parameter+ objects using the +name+ attribute.
19
+ def <=> (other)
20
+ name <=> other.name
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,58 @@
1
+ require 'rumbly/model/abstract'
2
+
3
+ module Rumbly
4
+ module Model
5
+
6
+ # This is an abstract class that represents a (one-way) relationship between two
7
+ # classes within an MVC application. Object mapper-specific implementations should
8
+ # subclass this class and implement the following methods: +type+, +source+,
9
+ # +target+, +name+, +multiplicity+, +through+, and +navigable+.
10
+ #
11
+ # According to the UML spec, generalizations don't really have a +name+ or a
12
+ # +multiplicity+, so these attributes should should be +nil+ for this +Relationship+
13
+ # type; +navigable+ should always return true for generalizations, and the subclass
14
+ # should be the +source+, whereas the superclass should be the +target+.
15
+ class Relationship
16
+
17
+ # Attributes and default values of a Relationship
18
+ ATTRIBUTES = {
19
+ type: :association, source: nil, target: nil, name: '',
20
+ multiplicity: nil, through: nil, navigable: false
21
+ }
22
+
23
+ # For each attribute, create stub accessor methods that raise an exception
24
+ extend Abstract
25
+ stub_required_methods(Relationship, ATTRIBUTES)
26
+
27
+ # Valid Relationship types
28
+ RELATIONSHIP_TYPES = [
29
+ :dependency, :association, :aggregation, :composition, :generalization
30
+ ]
31
+
32
+ # Simple question mark-style wrapper for the +Relationship#navigable+ attribute.
33
+ def navigable?
34
+ navigable
35
+ end
36
+
37
+ # Compares two +Relationship+ objects by first seeing if their sources or targets
38
+ # differ. If those are the same, then use the name, then type, then through.
39
+ def <=> (other)
40
+ (source <=> other.source).nonzero? || (target <=> other.target).nonzero? ||
41
+ (name <=> other.name).nonzero? || (type <=> other.type).nonzero? ||
42
+ (through <=> other.through)
43
+ end
44
+
45
+ # Returns a string that fully describes this +Relationship+, including its type,
46
+ # name, source, target, through class, and multiplicity.
47
+ def label
48
+ label = "#{type.to_s}"
49
+ label += " '#{name}'"
50
+ label += " from #{source.name}"
51
+ label += " to #{target.name}"
52
+ label += " through #{through.name}" unless through.nil?
53
+ label += " #{multiplicity.inspect}"
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ module Rumbly
2
+ module Model
3
+ module Simple
4
+
5
+ classes = %w{ application klass attribute operation parameter relationship }
6
+ classes.each { |c| require "rumbly/model/#{c}" }
7
+
8
+ def self.define_class(classname)
9
+ parent = Rumbly::Model.const_get(classname)
10
+ cls = Class.new(parent) do
11
+ parent::ATTRIBUTES.keys.each { |a| attr_accessor a }
12
+ def initialize (attrs={})
13
+ (self.class.superclass)::ATTRIBUTES.each_pair do |a,v|
14
+ instance_variable_set("@#{a}", attrs[a] || v)
15
+ end
16
+ end
17
+ end
18
+ const_set(classname, cls)
19
+ end
20
+
21
+ classes.each { |c| define_class(c.capitalize) }
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,101 @@
1
+ module Rumbly
2
+
3
+ # An +OptionsHash+ is a subclass of +Hash+ class that adds the following functionality:
4
+ # - all keys are converted to symbols when storing or retrieving
5
+ # - values can be accessed using methods named after keys (ala Structs)
6
+ # - nested values can be accessed using chained method calls or dotted key values
7
+ # - keys are implicitly created if an unknown method is called
8
+ # - the +has_key?+ method is enhanced to test for nested key paths
9
+ class OptionsHash < ::Hash
10
+
11
+ # Converts +key+ to a +Symbol+ before calling the normal +Hash#[]+ method. If the
12
+ # key is a dotted list of keys, digs down into any nested hashes to find the value.
13
+ # Returns +nil+ if any of the sub-hashes are not present.
14
+ def [] (key)
15
+ unless key =~ /\./
16
+ super(key.to_sym)
17
+ else
18
+ k, *r = *split_key(key)
19
+ if (sub = self[k]).nil?
20
+ nil
21
+ else
22
+ self[k][join_keys(r)]
23
+ end
24
+ end
25
+ end
26
+
27
+ # Converts +key+ to a +Symbol+ before calling the normal +Hash#[]=+ method. If the
28
+ # key is a dotted list of keys, digs down into any nested hashes (creating them if
29
+ # necessary) to store the value.
30
+ def []= (key, value)
31
+ unless key =~ /\./
32
+ super(key.to_sym, value)
33
+ else
34
+ k, *r = *split_key(key)
35
+ sub = get_or_create_value(k)
36
+ sub[join_keys(r)] = value
37
+ end
38
+ end
39
+
40
+ # Returns +true+ if this +OptionsHash+ has a value stored under the given +key+.
41
+ # In the case of a compound key (multiple keys separated by dots), digs down into
42
+ # any nested hashes to find a value. Returns +false+ if any of the sub-hashes or
43
+ # values are nil.
44
+ def has_key? (key)
45
+ unless key =~ /\./
46
+ super(key.to_sym)
47
+ else
48
+ k, *r = *split_key(key)
49
+ return false if (sub = self[k]).nil?
50
+ return sub.has_key?(join_keys(r))
51
+ end
52
+ end
53
+
54
+ # Allows values to be stored and retrieved using methods named for the keys. If
55
+ # an attempt is made to access a key that doesn't exist, a nested +OptionsHash+
56
+ # will be created as the value stored for the given key. This allows for setting
57
+ # a nested option without having to explicitly create each nested hash.
58
+ def method_missing (name, *args, &blk)
59
+ unless respond_to?(name)
60
+ if reader?(name, args, blk)
61
+ get_or_create_value(name)
62
+ elsif writer?(name, args, blk)
63
+ store_value(chop_sym(name), args[0])
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def split_key (path)
71
+ path.to_s.split('.').map(&:to_sym)
72
+ end
73
+
74
+ def join_keys (a)
75
+ a.join('.')
76
+ end
77
+
78
+ def reader? (name, args, blk)
79
+ blk.nil? && args.empty?
80
+ end
81
+
82
+ def writer? (name, args, blk)
83
+ blk.nil? && args.size == 1 && name =~ /=$/
84
+ end
85
+
86
+ def chop_sym (sym)
87
+ sym.to_s.chop.to_sym
88
+ end
89
+
90
+ def get_or_create_value (key)
91
+ self[key] ||= OptionsHash.new
92
+ end
93
+
94
+ def store_value (key, value)
95
+ get_or_create_value(key)
96
+ store(key, value)
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,9 @@
1
+ module Rumbly
2
+ # If Ruby on Rails is running, adds a set of rake tasks to make it easy to generate UML
3
+ # class diagrams for whatever object mapper you're using in your Rails application.
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load 'rumbly/tasks.rake'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ require 'rumbly'
2
+ require 'rumbly/model/application'
3
+ require 'rumbly/diagram/base'
4
+
5
+ def say(message)
6
+ print message unless Rake.application.options.quiet
7
+ end
8
+
9
+ namespace :rumbly do
10
+
11
+ # Allows options given via Rake environment to override default options. Nested
12
+ # options are accessed using dot notation, e.g. "diagram.type = graphviz".
13
+ task :options do
14
+ ENV.each do |key, value|
15
+ # if option exists in defaults, do some basic conversions and override
16
+ key.downcase.gsub(/_/,'.')
17
+ if Rumbly::options.has_key?(key)
18
+ value = case value
19
+ when "true", "yes" then true
20
+ when "false", "no" then false
21
+ when /,/ then value.split(/\s*,\s*/).map(&:to_sym)
22
+ else value
23
+ end
24
+ Rumbly::options[key] = value
25
+ end
26
+ end
27
+ end
28
+
29
+ # Loads the Ruby on Rails environment and model classes.
30
+ task :load_model do
31
+ say "Loading Rails application environment..."
32
+ Rake::Task[:environment].invoke
33
+ say "done.\n"
34
+ say "Loading Rails application classes..."
35
+ Rails.application.eager_load!
36
+ say "done.\n"
37
+ end
38
+
39
+ # Generates a UML diagram based on the given options and the loaded Rails model.
40
+ task generate: [:options, :load_model] do
41
+ say "Generating UML diagram for Rails model..."
42
+ app = Rumbly::Model::Application.create
43
+ Rumbly::Diagram::Base.create(app)
44
+ say "done.\n"
45
+ end
46
+
47
+ end
48
+
49
+ desc "Generate a UML diagram based on your Rails model classes"
50
+ task rumbly: 'rumbly:generate'
data/lib/rumbly.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'rumbly/options_hash'
2
+ require 'rumbly/railtie' if defined?(Rails)
3
+
4
+ module Rumbly
5
+
6
+ class << self
7
+ attr_accessor :options
8
+ end
9
+
10
+ # setup default options
11
+ self.options = OptionsHash.new
12
+
13
+ # general options
14
+ self.options.messages = :verbose
15
+
16
+ # model options
17
+ self.options.model.type = :auto
18
+
19
+ # diagram options
20
+ self.options.diagram.type = :graphviz
21
+ self.options.diagram.file = 'classes'
22
+ self.options.diagram.format = :pdf
23
+
24
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rumbly
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dustin Frazier
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2010-02-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &70245880702880 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70245880702880
25
+ - !ruby/object:Gem::Dependency
26
+ name: ruby-graphviz
27
+ requirement: &70245880731300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70245880731300
36
+ description: More detailed description coming soon...
37
+ email: ruby@frayzhe.net
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - lib/rumbly/diagram/base.rb
43
+ - lib/rumbly/diagram/debug.rb
44
+ - lib/rumbly/diagram/graphviz.rb
45
+ - lib/rumbly/model/abstract.rb
46
+ - lib/rumbly/model/active_record/application.rb
47
+ - lib/rumbly/model/active_record/attribute.rb
48
+ - lib/rumbly/model/active_record/klass.rb
49
+ - lib/rumbly/model/active_record/relationship.rb
50
+ - lib/rumbly/model/application.rb
51
+ - lib/rumbly/model/attribute.rb
52
+ - lib/rumbly/model/klass.rb
53
+ - lib/rumbly/model/operation.rb
54
+ - lib/rumbly/model/parameter.rb
55
+ - lib/rumbly/model/relationship.rb
56
+ - lib/rumbly/model/simple.rb
57
+ - lib/rumbly/options_hash.rb
58
+ - lib/rumbly/railtie.rb
59
+ - lib/rumbly.rb
60
+ - lib/rumbly/tasks.rake
61
+ homepage: http://github.com/frayzhe/rumbly
62
+ licenses: []
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.15
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Let's get ready to rumble
85
+ test_files: []