rumbly 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rumbly/diagram/base.rb +47 -0
- data/lib/rumbly/diagram/debug.rb +37 -0
- data/lib/rumbly/diagram/graphviz.rb +44 -0
- data/lib/rumbly/model/abstract.rb +24 -0
- data/lib/rumbly/model/active_record/application.rb +42 -0
- data/lib/rumbly/model/active_record/attribute.rb +157 -0
- data/lib/rumbly/model/active_record/klass.rb +99 -0
- data/lib/rumbly/model/active_record/relationship.rb +166 -0
- data/lib/rumbly/model/application.rb +71 -0
- data/lib/rumbly/model/attribute.rb +63 -0
- data/lib/rumbly/model/klass.rb +38 -0
- data/lib/rumbly/model/operation.rb +25 -0
- data/lib/rumbly/model/parameter.rb +25 -0
- data/lib/rumbly/model/relationship.rb +58 -0
- data/lib/rumbly/model/simple.rb +25 -0
- data/lib/rumbly/options_hash.rb +101 -0
- data/lib/rumbly/railtie.rb +9 -0
- data/lib/rumbly/tasks.rake +50 -0
- data/lib/rumbly.rb +24 -0
- metadata +85 -0
@@ -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: []
|