rumbly 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.
@@ -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: []