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