modularity 0.6.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +148 -0
- data/bin/migrate-modularity1-to-modularity2 +7 -0
- data/lib/modularity.rb +1 -2
- data/lib/modularity/as_trait.rb +34 -3
- data/lib/modularity/migrator.rb +53 -0
- data/lib/modularity/version.rb +1 -1
- data/modularity.gemspec +1 -0
- data/spec/modularity/as_trait_spec.rb +210 -0
- data/spec/modularity/migrator_spec.rb +109 -0
- data/spec/spec_helper.rb +1 -0
- metadata +32 -19
- data/README.rdoc +0 -106
- data/lib/modularity/does.rb +0 -23
- data/lib/modularity/inflector.rb +0 -60
- data/spec/as_trait_spec.rb +0 -29
- data/spec/does_spec.rb +0 -93
data/README.md
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
Modularity 2 - Traits and partial classes for Ruby
|
2
|
+
==================================================
|
3
|
+
|
4
|
+
Modularity enhances Ruby's [`Module`] so it can be used traits and partial classes.
|
5
|
+
This allows very simple definition of meta-programming macros like the
|
6
|
+
`has_many` that you know from Rails.
|
7
|
+
|
8
|
+
Modularity also lets you organize large models into multiple source files
|
9
|
+
in a way that is less awkward than using modules.
|
10
|
+
|
11
|
+
Note that this is **Modularity 2**, which has a different syntax older version.
|
12
|
+
Modularity 1 users can use [a script to migrate their code](#migrating-from-modularity-1)
|
13
|
+
or use the [modularity1 branch](https://github.com/makandra/modularity/tree/modularity1).
|
14
|
+
|
15
|
+
|
16
|
+
Example 1: Easy meta-programming macros
|
17
|
+
----------------------------------------
|
18
|
+
|
19
|
+
Ruby allows you to construct classes using meta-programming macros like
|
20
|
+
`acts_as_tree` or `has_many :items`. These macros will add methods,
|
21
|
+
callbacks, etc. to the calling class. However, right now Ruby (and Rails) makes it awkward to define
|
22
|
+
such macros in your project as part of your application domain.
|
23
|
+
|
24
|
+
Modularity allows you to extract common behaviour into reusable macros by defining traits with parameters.
|
25
|
+
Your macros can live in your application, allowing you to express your application domain in both classes
|
26
|
+
and macros.
|
27
|
+
|
28
|
+
Here is an example of a `strip_field` macro, which created setter methods that remove leading and trailing whitespace from newly assigned values:
|
29
|
+
|
30
|
+
# app/models/article.rb
|
31
|
+
class Article
|
32
|
+
include DoesStripFields[:name, :brand]
|
33
|
+
end
|
34
|
+
|
35
|
+
# app/models/shared/does_strip_fields.rb
|
36
|
+
module DoesStripFields
|
37
|
+
as_trait do |*fields|
|
38
|
+
fields.each do |field|
|
39
|
+
define_method("#{field}=") do |value|
|
40
|
+
self[field] = value.strip
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
We like to add `app/models/shared` and `app/controllers/shared` to the load paths of our Rails projects.
|
47
|
+
These are great places to store macros that are re-used from multiple classes.
|
48
|
+
|
49
|
+
|
50
|
+
Example 2: Mixins with class methods
|
51
|
+
------------------------------------
|
52
|
+
|
53
|
+
Using a module to add both instance methods and class methods is
|
54
|
+
[very awkward](http://redcorundum.blogspot.com/2006/06/mixing-in-class-methods.html).
|
55
|
+
Modularity does away with the clutter and lets you say this:
|
56
|
+
|
57
|
+
# app/models/model.rb
|
58
|
+
class Model
|
59
|
+
include Mixin
|
60
|
+
end
|
61
|
+
|
62
|
+
# app/models/mixin.rb
|
63
|
+
module Mixin
|
64
|
+
as_trait do
|
65
|
+
def instance_method
|
66
|
+
# ...
|
67
|
+
end
|
68
|
+
def self.class_method
|
69
|
+
# ..
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
`private` and `protected` will also work as expected when defining a trait.
|
75
|
+
|
76
|
+
|
77
|
+
Example 3: Splitting a model into multiple source files
|
78
|
+
-------------------------------------------------------
|
79
|
+
|
80
|
+
Models are often concerned with multiple themes like "authentication", "contact info" or "permissions", each requiring
|
81
|
+
a couple of validations and callbacks here, and some method there. Modularity lets you organize your model into multiple
|
82
|
+
partial classes, so each file can deal with a single aspect of your model:
|
83
|
+
|
84
|
+
# app/models/user.rb
|
85
|
+
class User < ActiveRecord::Base
|
86
|
+
include User::DoesAuthentication
|
87
|
+
include User::DoesAddress
|
88
|
+
end
|
89
|
+
|
90
|
+
# app/models/user/does_authentication.rb
|
91
|
+
module User::DoesAuthentication
|
92
|
+
as_trait do
|
93
|
+
# methods, validations, etc. regarding usernames and passwords go here
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# app/models/user/does_permissions.rb
|
98
|
+
module User::DoesPermissions
|
99
|
+
as_trait do
|
100
|
+
# methods, validations, etc. regarding contact information go here
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
Some criticism has been raised for splitting large models into files like this.
|
105
|
+
Essentially, even though have an easier time navigating your code, you will still
|
106
|
+
have one giant model with many side effects.
|
107
|
+
|
108
|
+
There are [many better ways](http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/)
|
109
|
+
to decompose a huge Ruby class.
|
110
|
+
|
111
|
+
|
112
|
+
Installation
|
113
|
+
------------
|
114
|
+
|
115
|
+
Add the following to your `Gemfile`:
|
116
|
+
|
117
|
+
gem 'modularity', '>=2'
|
118
|
+
|
119
|
+
Now run `bundle install`.
|
120
|
+
|
121
|
+
|
122
|
+
Migrating from Modularity 1
|
123
|
+
---------------------------
|
124
|
+
|
125
|
+
If you have been using Modularity 1 with the `does` syntax, we provide a script to migrate your Ruby project
|
126
|
+
automatically.
|
127
|
+
|
128
|
+
1. Make sure your project has tests and you have a backup of your files (or pushed your commits to Git)
|
129
|
+
|
130
|
+
2. From your project directory, do this:
|
131
|
+
|
132
|
+
find . -name "*.rb" | migrate-modularity1-to-modularity2
|
133
|
+
|
134
|
+
3. The script will rename your files and change your code. It will also syntax-check your files after conversion
|
135
|
+
(since the script is not perfect).
|
136
|
+
|
137
|
+
4. Check the diff to see what the script has done.
|
138
|
+
|
139
|
+
5. Run tests to see if everything still works.
|
140
|
+
|
141
|
+
|
142
|
+
|
143
|
+
|
144
|
+
Credits
|
145
|
+
-------
|
146
|
+
|
147
|
+
Henning Koch from [makandra.com](http://makandra.com/)
|
148
|
+
|
data/lib/modularity.rb
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
require 'modularity/
|
2
|
-
|
1
|
+
require 'modularity/as_trait'
|
data/lib/modularity/as_trait.rb
CHANGED
@@ -1,9 +1,40 @@
|
|
1
1
|
module Modularity
|
2
|
+
|
3
|
+
class ParametrizedTrait < Module
|
4
|
+
|
5
|
+
def initialize(blank_trait, args)
|
6
|
+
@args = args
|
7
|
+
@macro = blank_trait.instance_variable_get(:@modularity_macro)
|
8
|
+
include(blank_trait)
|
9
|
+
end
|
10
|
+
|
11
|
+
def included(base)
|
12
|
+
base.class_exec(*@args, &@macro)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
2
17
|
module AsTrait
|
3
|
-
|
4
|
-
|
18
|
+
|
19
|
+
def as_trait(¯o)
|
20
|
+
|
21
|
+
@modularity_macro = macro
|
22
|
+
|
23
|
+
def self.included(base)
|
24
|
+
unless base.is_a?(ParametrizedTrait)
|
25
|
+
base.class_exec(&@modularity_macro)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.[](*args)
|
31
|
+
blank_trait = self
|
32
|
+
ParametrizedTrait.new(blank_trait, args)
|
33
|
+
end
|
34
|
+
|
5
35
|
end
|
36
|
+
|
6
37
|
end
|
7
38
|
end
|
8
39
|
|
9
|
-
|
40
|
+
Module.send(:include, Modularity::AsTrait)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
gem 'activesupport'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Modularity
|
6
|
+
class Migrator
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def migrate(old_path)
|
10
|
+
old_path = File.expand_path(old_path)
|
11
|
+
new_path = fix_filename(old_path)
|
12
|
+
rename(old_path, new_path) unless old_path == new_path
|
13
|
+
old_code = File.read(new_path)
|
14
|
+
new_code = fix_code(old_code)
|
15
|
+
rewrite_file(new_path, new_code) unless old_code == new_code
|
16
|
+
puts "Migrated #{old_path}"
|
17
|
+
`ruby -c #{new_path}`
|
18
|
+
new_code
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def fix_filename(path)
|
24
|
+
path = File.expand_path(path)
|
25
|
+
new_path = path.sub(/\/([^\/]+)_trait\.rb$/, '/does_\\1.rb')
|
26
|
+
new_path
|
27
|
+
end
|
28
|
+
|
29
|
+
def fix_code(code)
|
30
|
+
code = code.gsub(/module (.*?)([A-Za-z0-9_]+)Trait\b/, 'module \\1Does\\2')
|
31
|
+
code = code.gsub(/does ['":]([A-Za-z0-9\_\/]+)(?:'|"|$)(?:,\s*(.*)$)?/) do
|
32
|
+
trait_path = $1
|
33
|
+
parameters = $2
|
34
|
+
trait_path = trait_path.sub(/([A-Za-z0-9\_]+)$/, 'does_\\1')
|
35
|
+
trait_class = trait_path.camelize # don't use classify, it removes plurals!
|
36
|
+
substituted = "include #{trait_class}"
|
37
|
+
substituted << "[#{parameters}]" if parameters
|
38
|
+
substituted
|
39
|
+
end
|
40
|
+
code
|
41
|
+
end
|
42
|
+
|
43
|
+
def rename(old, new)
|
44
|
+
FileUtils.mv(old, new)
|
45
|
+
end
|
46
|
+
|
47
|
+
def rewrite_file(path, content)
|
48
|
+
File.open(path, "w") { |file| file.write(content) }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/modularity/version.rb
CHANGED
data/modularity.gemspec
CHANGED
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Modularity::AsTrait do
|
4
|
+
|
5
|
+
describe '.included' do
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
@doing_class = Class.new
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'without parameters' do
|
12
|
+
|
13
|
+
it "applies the trait macro of the given module" do
|
14
|
+
|
15
|
+
module DoesSome
|
16
|
+
as_trait do
|
17
|
+
some_trait_included
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
@doing_class.should_receive(:some_trait_included)
|
22
|
+
|
23
|
+
@doing_class.class_eval do
|
24
|
+
include DoesSome
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
it "applies the trait macro of the given namespaced module" do
|
30
|
+
|
31
|
+
module Some
|
32
|
+
module DoesChild
|
33
|
+
as_trait do
|
34
|
+
some_child_trait_included
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
@doing_class.should_receive(:some_child_trait_included)
|
40
|
+
|
41
|
+
@doing_class.class_eval do
|
42
|
+
include Some::DoesChild
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
it "lets a trait define methods with different visibility" do
|
48
|
+
|
49
|
+
module DoesVisibility
|
50
|
+
as_trait do
|
51
|
+
def public_method_from_trait
|
52
|
+
end
|
53
|
+
protected
|
54
|
+
def protected_method_from_trait
|
55
|
+
end
|
56
|
+
private
|
57
|
+
def private_method_from_trait
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
@doing_class.class_eval do
|
63
|
+
include DoesVisibility
|
64
|
+
end
|
65
|
+
|
66
|
+
instance = @doing_class.new
|
67
|
+
|
68
|
+
instance.public_methods.collect(&:to_s).should include("public_method_from_trait")
|
69
|
+
instance.protected_methods.collect(&:to_s).should include("protected_method_from_trait")
|
70
|
+
instance.private_methods.collect(&:to_s).should include("private_method_from_trait")
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'appends methods outside the trait macro' do
|
75
|
+
|
76
|
+
module HybridModule
|
77
|
+
|
78
|
+
as_trait do
|
79
|
+
define_method :trait_method do
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def vanilla_method
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
@doing_class.class_eval do
|
89
|
+
include HybridModule
|
90
|
+
end
|
91
|
+
|
92
|
+
instance = @doing_class.new
|
93
|
+
|
94
|
+
instance.should respond_to(:trait_method)
|
95
|
+
instance.should respond_to(:vanilla_method)
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'applies multiple trait macros' do
|
100
|
+
|
101
|
+
module FirstTrait
|
102
|
+
as_trait do
|
103
|
+
define_method :first do
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module SecondTrait
|
109
|
+
as_trait do
|
110
|
+
define_method :second do
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
@doing_class.class_eval do
|
116
|
+
include FirstTrait
|
117
|
+
include SecondTrait
|
118
|
+
end
|
119
|
+
|
120
|
+
instance = @doing_class.new
|
121
|
+
|
122
|
+
instance.should respond_to(:first)
|
123
|
+
instance.should respond_to(:second)
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "with parameters" do
|
130
|
+
|
131
|
+
it "it applies a trait macro with parameters" do
|
132
|
+
|
133
|
+
module DoesCallMethod
|
134
|
+
as_trait do |field|
|
135
|
+
send(field)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
@doing_class.should_receive(:foo)
|
140
|
+
@doing_class.class_eval do
|
141
|
+
include DoesCallMethod[:foo]
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
it "facilitates metaprogramming acrobatics" do
|
147
|
+
|
148
|
+
module DoesDefineConstantMethod
|
149
|
+
as_trait do |name, return_value|
|
150
|
+
define_method name do
|
151
|
+
return_value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
@doing_class.class_eval do
|
157
|
+
include DoesDefineConstantMethod["some_method", "some_return_value"]
|
158
|
+
end
|
159
|
+
|
160
|
+
instance = @doing_class.new
|
161
|
+
instance.should respond_to(:some_method)
|
162
|
+
instance.some_method.should == "some_return_value"
|
163
|
+
end
|
164
|
+
|
165
|
+
it "allies to call an unparametrized trait macro with an empty parameter list" do
|
166
|
+
|
167
|
+
module DoesSome
|
168
|
+
as_trait do
|
169
|
+
some_trait_included
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
@doing_class.should_receive(:some_trait_included)
|
174
|
+
|
175
|
+
@doing_class.class_eval do
|
176
|
+
include DoesSome[]
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'appends methods outside the trait macro' do
|
182
|
+
|
183
|
+
module HybridModuleWithParameters
|
184
|
+
|
185
|
+
as_trait do |name|
|
186
|
+
define_method name do
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def vanilla_method
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
@doing_class.class_eval do
|
196
|
+
include HybridModuleWithParameters[:trait_method]
|
197
|
+
end
|
198
|
+
|
199
|
+
instance = @doing_class.new
|
200
|
+
|
201
|
+
instance.should respond_to(:trait_method)
|
202
|
+
instance.should respond_to(:vanilla_method)
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'modularity/migrator' # this is an optional file and thus not loaded automatically
|
4
|
+
|
5
|
+
describe Modularity::Migrator do
|
6
|
+
|
7
|
+
subject do
|
8
|
+
Modularity::Migrator
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '.fix_filename' do
|
12
|
+
|
13
|
+
it 'should replace the suffix _trait with a prefix does' do
|
14
|
+
subject.send(:fix_filename, '/path/to/search_trait.rb').should == '/path/to/does_search.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should not rename files that don't end in _trait.rb" do
|
18
|
+
subject.send(:fix_filename, '/path/to/search.rb').should == '/path/to/search.rb'
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '.fix_code' do
|
24
|
+
|
25
|
+
it 'renames a module FooBarTrait to DoesFooBar' do
|
26
|
+
|
27
|
+
old_code = <<-RUBY
|
28
|
+
module Namespace::FooBarTrait
|
29
|
+
as_trait do
|
30
|
+
define_method :foo do
|
31
|
+
end
|
32
|
+
define_method :bar do
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
RUBY
|
37
|
+
|
38
|
+
new_code = <<-RUBY
|
39
|
+
module Namespace::DoesFooBar
|
40
|
+
as_trait do
|
41
|
+
define_method :foo do
|
42
|
+
end
|
43
|
+
define_method :bar do
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
RUBY
|
48
|
+
|
49
|
+
subject.send(:fix_code, old_code).should == new_code
|
50
|
+
end
|
51
|
+
|
52
|
+
it "does not rename modules that aren't traits" do
|
53
|
+
|
54
|
+
old_code = <<-RUBY
|
55
|
+
module Namespace::FooBar
|
56
|
+
def foo
|
57
|
+
end
|
58
|
+
def bar
|
59
|
+
end
|
60
|
+
end
|
61
|
+
RUBY
|
62
|
+
|
63
|
+
subject.send(:fix_code, old_code).should == old_code
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'replaces does calls with include' do
|
67
|
+
|
68
|
+
old_code = <<-RUBY
|
69
|
+
class User < ActiveRecord::Base
|
70
|
+
does 'user/search'
|
71
|
+
does 'user/account_settings'
|
72
|
+
does 'trashable'
|
73
|
+
end
|
74
|
+
RUBY
|
75
|
+
|
76
|
+
new_code = <<-RUBY
|
77
|
+
class User < ActiveRecord::Base
|
78
|
+
include User::DoesSearch
|
79
|
+
include User::DoesAccountSettings
|
80
|
+
include DoesTrashable
|
81
|
+
end
|
82
|
+
RUBY
|
83
|
+
|
84
|
+
subject.send(:fix_code, old_code).should == new_code
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'puts does parameters into square brackets' do
|
88
|
+
|
89
|
+
old_code = <<-RUBY
|
90
|
+
class User < ActiveRecord::Base
|
91
|
+
does 'flag', :active, :default => true
|
92
|
+
does 'record/search', :field => :email
|
93
|
+
end
|
94
|
+
RUBY
|
95
|
+
|
96
|
+
new_code = <<-RUBY
|
97
|
+
class User < ActiveRecord::Base
|
98
|
+
include DoesFlag[:active, :default => true]
|
99
|
+
include Record::DoesSearch[:field => :email]
|
100
|
+
end
|
101
|
+
RUBY
|
102
|
+
|
103
|
+
subject.send(:fix_code, old_code).should == new_code
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modularity
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 2.0.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,12 +9,12 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2014-01-08 00:00:00.000000000 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rake
|
17
|
-
requirement: &
|
17
|
+
requirement: &22585700 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ! '>='
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: '0'
|
23
23
|
type: :development
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *22585700
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: rspec
|
28
|
-
requirement: &
|
28
|
+
requirement: &22585200 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - <
|
@@ -33,26 +33,38 @@ dependencies:
|
|
33
33
|
version: '2'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *22585200
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rspec_candy
|
39
|
+
requirement: &22584740 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *22584740
|
37
48
|
description: Traits and partial classes for Ruby
|
38
49
|
email: github@makandra.de
|
39
|
-
executables:
|
50
|
+
executables:
|
51
|
+
- migrate-modularity1-to-modularity2
|
40
52
|
extensions: []
|
41
53
|
extra_rdoc_files: []
|
42
54
|
files:
|
43
55
|
- .gitignore
|
44
56
|
- Gemfile
|
45
57
|
- MIT-LICENSE
|
46
|
-
- README.
|
58
|
+
- README.md
|
47
59
|
- Rakefile
|
60
|
+
- bin/migrate-modularity1-to-modularity2
|
48
61
|
- lib/modularity.rb
|
49
62
|
- lib/modularity/as_trait.rb
|
50
|
-
- lib/modularity/
|
51
|
-
- lib/modularity/inflector.rb
|
63
|
+
- lib/modularity/migrator.rb
|
52
64
|
- lib/modularity/version.rb
|
53
65
|
- modularity.gemspec
|
54
|
-
- spec/as_trait_spec.rb
|
55
|
-
- spec/
|
66
|
+
- spec/modularity/as_trait_spec.rb
|
67
|
+
- spec/modularity/migrator_spec.rb
|
56
68
|
- spec/rcov.opts
|
57
69
|
- spec/spec.opts
|
58
70
|
- spec/spec_helper.rb
|
@@ -69,21 +81,22 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
81
|
- - ! '>='
|
70
82
|
- !ruby/object:Gem::Version
|
71
83
|
version: '0'
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
hash: 2498377129858303393
|
72
87
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
88
|
none: false
|
74
89
|
requirements:
|
75
90
|
- - ! '>='
|
76
91
|
- !ruby/object:Gem::Version
|
77
92
|
version: '0'
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
hash: 2498377129858303393
|
78
96
|
requirements: []
|
79
97
|
rubyforge_project:
|
80
|
-
rubygems_version: 1.
|
98
|
+
rubygems_version: 1.3.9.5
|
81
99
|
signing_key:
|
82
100
|
specification_version: 3
|
83
101
|
summary: Traits and partial classes for Ruby
|
84
|
-
test_files:
|
85
|
-
- spec/as_trait_spec.rb
|
86
|
-
- spec/does_spec.rb
|
87
|
-
- spec/rcov.opts
|
88
|
-
- spec/spec.opts
|
89
|
-
- spec/spec_helper.rb
|
102
|
+
test_files: []
|
data/README.rdoc
DELETED
@@ -1,106 +0,0 @@
|
|
1
|
-
= modularity - Traits and partial classes for Ruby
|
2
|
-
|
3
|
-
Modularity provides traits and partial classes for Ruby.
|
4
|
-
This lets you organize large models into multiple source files.
|
5
|
-
It also allows very simple definition of meta-programming macros,
|
6
|
-
as you might now from <tt>acts_as_something</tt> type of plugins,
|
7
|
-
or the macros Rails provides for your models.
|
8
|
-
|
9
|
-
Modularity traits are to your models what partials are for your Rails views.
|
10
|
-
|
11
|
-
== Example 1: Splitting a model into multiple source files
|
12
|
-
|
13
|
-
Models are often concerned with multiple themes like "authentication", "contact info" or "permissions", each requiring
|
14
|
-
a couple of validations and callbacks here, and some method there. Modularity lets you organize your model into multiple
|
15
|
-
partial classes, so each file can deal with a single aspect of your model:
|
16
|
-
|
17
|
-
# app/models/user.rb
|
18
|
-
class User < ActiveRecord::Base
|
19
|
-
does "user/authentication"
|
20
|
-
does "user/address"
|
21
|
-
end
|
22
|
-
|
23
|
-
# app/models/user/authentication_trait.rb
|
24
|
-
module User::AuthenticationTrait
|
25
|
-
as_trait do
|
26
|
-
# methods, validations, etc. regarding usernames and passwords go here
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
# app/models/user/permissions_trait.rb
|
31
|
-
module User::PermissionsTrait
|
32
|
-
as_trait do
|
33
|
-
# methods, validations, etc. regarding contact information go here
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
== Example 2: Easy meta-programming macros
|
38
|
-
|
39
|
-
Ruby allows you to construct classes using meta-programming macros like <tt>acts_as_tree</tt> or <tt>has_many :items</tt>.
|
40
|
-
These macros will add methods, callbacks, etc. to the calling class. Hoever, right now Ruby (and Rails) makes it awkward to define
|
41
|
-
such macros in your project as part of your application domain.
|
42
|
-
|
43
|
-
Modularity allows you to extract common behaviour into reusable macros by defining traits with parameters. Your macros can live in your
|
44
|
-
application, allowing you to express your application domain in both classes and macros.
|
45
|
-
|
46
|
-
Here is an example of a <tt>strip_field</tt> macro, which created setter methods that remove leading and trailing whitespace from newly assigned values:
|
47
|
-
|
48
|
-
# app/models/article.rb
|
49
|
-
class Article
|
50
|
-
does "strip_fields", :name, :brand
|
51
|
-
end
|
52
|
-
|
53
|
-
# app/models/shared/strip_fields_trait.rb
|
54
|
-
module StripFieldsTrait
|
55
|
-
as_trait do |*fields|
|
56
|
-
fields.each do |field|
|
57
|
-
define_method("#{field}=") do |value|
|
58
|
-
self[field] = value.strip
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
We like to add <tt>app/models/shared</tt> and <tt>app/controllers/shared</tt> to the load paths of our Rails projects. These are great places to store macros
|
65
|
-
that are re-used from multiple classes.
|
66
|
-
|
67
|
-
== Example 3: Mixins with class methods
|
68
|
-
|
69
|
-
Using a module to add both instance methods and class methods is {very awkward}[http://redcorundum.blogspot.com/2006/06/mixing-in-class-methods.html].
|
70
|
-
Modularity does away with the clutter and lets you say this:
|
71
|
-
|
72
|
-
# app/models/model.rb
|
73
|
-
class Model
|
74
|
-
does "mixin"
|
75
|
-
end
|
76
|
-
|
77
|
-
# app/models/mixin_trait.rb
|
78
|
-
module MixinTrait
|
79
|
-
as_trait do
|
80
|
-
def instance_method
|
81
|
-
# ...
|
82
|
-
end
|
83
|
-
def self.class_method
|
84
|
-
# ..
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
<tt>private</tt> and <tt>protected</tt> will also work as expected when defining a trait.
|
89
|
-
|
90
|
-
== Installation
|
91
|
-
|
92
|
-
sudo gem install modularity
|
93
|
-
|
94
|
-
|
95
|
-
== Note if you're still on Ruby 1.8.6
|
96
|
-
|
97
|
-
Modularity requires Ruby 1.8.7. Earlier versions are missing <tt>class_exec</tt>. You might be able to hack in <tt>class_exec</tt>
|
98
|
-
using {this}[http://github.com/brynary/rspec/blob/f80d61a399b34f58084a378c85a43a95ff484619/lib/spec/extensions/instance_exec.rb] as a guide, but it's not pretty.
|
99
|
-
|
100
|
-
== Credits
|
101
|
-
|
102
|
-
Henning Koch
|
103
|
-
|
104
|
-
{makandra.com}[http://makandra.com/]
|
105
|
-
|
106
|
-
{gem-session.com}[http://gem-session.com/]
|
data/lib/modularity/does.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
require 'modularity/inflector'
|
2
|
-
require 'modularity/as_trait'
|
3
|
-
|
4
|
-
module Modularity
|
5
|
-
module Does
|
6
|
-
|
7
|
-
def self.included(base)
|
8
|
-
base.extend ClassMethods
|
9
|
-
end
|
10
|
-
|
11
|
-
module ClassMethods
|
12
|
-
def does(trait_name, *args)
|
13
|
-
trait_name = "#{Modularity::Inflector.camelize(trait_name.to_s)}Trait"
|
14
|
-
trait = Modularity::Inflector.constantize(trait_name)
|
15
|
-
macro = trait.instance_variable_get("@trait_macro") or raise "Missing trait directive in #{trait_name}"
|
16
|
-
class_exec(*args, ¯o)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
Object.send :include, Modularity::Does
|
data/lib/modularity/inflector.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
# These methods are backported from Rails so modularity works with plain Ruby.
|
2
|
-
|
3
|
-
module Modularity
|
4
|
-
class Inflector
|
5
|
-
class << self
|
6
|
-
|
7
|
-
# File activesupport/lib/active_support/inflector.rb, line 178
|
8
|
-
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
9
|
-
if first_letter_in_uppercase
|
10
|
-
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
11
|
-
else
|
12
|
-
lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
|
17
|
-
if Module.method(:const_get).arity == 1
|
18
|
-
# Tries to find a constant with the name specified in the argument string:
|
19
|
-
#
|
20
|
-
# "Module".constantize # => Module
|
21
|
-
# "Test::Unit".constantize # => Test::Unit
|
22
|
-
#
|
23
|
-
# The name is assumed to be the one of a top-level constant, no matter whether
|
24
|
-
# it starts with "::" or not. No lexical context is taken into account:
|
25
|
-
#
|
26
|
-
# C = 'outside'
|
27
|
-
# module M
|
28
|
-
# C = 'inside'
|
29
|
-
# C # => 'inside'
|
30
|
-
# "C".constantize # => 'outside', same as ::C
|
31
|
-
# end
|
32
|
-
#
|
33
|
-
# NameError is raised when the name is not in CamelCase or the constant is
|
34
|
-
# unknown.
|
35
|
-
def constantize(camel_cased_word)
|
36
|
-
names = camel_cased_word.split('::')
|
37
|
-
names.shift if names.empty? || names.first.empty?
|
38
|
-
|
39
|
-
constant = Object
|
40
|
-
names.each do |name|
|
41
|
-
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
42
|
-
end
|
43
|
-
constant
|
44
|
-
end
|
45
|
-
else
|
46
|
-
def constantize(camel_cased_word) #:nodoc:
|
47
|
-
names = camel_cased_word.split('::')
|
48
|
-
names.shift if names.empty? || names.first.empty?
|
49
|
-
|
50
|
-
constant = Object
|
51
|
-
names.each do |name|
|
52
|
-
constant = constant.const_defined?(name, false) ? constant.const_get(name) : constant.const_missing(name)
|
53
|
-
end
|
54
|
-
constant
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
data/spec/as_trait_spec.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Trait
|
4
|
-
as_trait do
|
5
|
-
"hi world"
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
module ParametrizedTrait
|
10
|
-
as_trait do |name|
|
11
|
-
"hi, #{name}"
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
describe Modularity::AsTrait do
|
16
|
-
|
17
|
-
describe 'as_trait' do
|
18
|
-
|
19
|
-
it "should let modules save a proc upon loading" do
|
20
|
-
Trait.instance_variable_get("@trait_macro").call.should == "hi world"
|
21
|
-
end
|
22
|
-
|
23
|
-
it "should let modules save a proc with parameters upon loading" do
|
24
|
-
ParametrizedTrait.instance_variable_get("@trait_macro").call("jean").should == "hi, jean"
|
25
|
-
end
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
end
|
data/spec/does_spec.rb
DELETED
@@ -1,93 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module SomeTrait
|
4
|
-
as_trait do
|
5
|
-
some_trait_included
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
module Some
|
10
|
-
module ChildTrait
|
11
|
-
as_trait do
|
12
|
-
some_child_trait_included
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
module CallMethodTrait
|
18
|
-
as_trait do |field|
|
19
|
-
send(field)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
module VisibilityTrait
|
24
|
-
as_trait do
|
25
|
-
def public_method_from_trait
|
26
|
-
end
|
27
|
-
protected
|
28
|
-
def protected_method_from_trait
|
29
|
-
end
|
30
|
-
private
|
31
|
-
def private_method_from_trait
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
module DefineConstantMethodTrait
|
37
|
-
as_trait do |name, return_value|
|
38
|
-
define_method name do
|
39
|
-
return_value
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
class Doer
|
45
|
-
end
|
46
|
-
|
47
|
-
describe Modularity::AsTrait do
|
48
|
-
|
49
|
-
describe 'does' do
|
50
|
-
|
51
|
-
it "should apply the named module" do
|
52
|
-
Doer.should_receive(:some_trait_included)
|
53
|
-
Doer.class_eval do
|
54
|
-
does "some"
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
it "should apply a namespaced module, using slash-notation like require" do
|
59
|
-
Doer.should_receive(:some_child_trait_included)
|
60
|
-
Doer.class_eval do
|
61
|
-
does "some/child"
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
it "should class_eval the as_trait proc on the doer" do
|
66
|
-
Doer.should_receive(:foo)
|
67
|
-
Doer.class_eval do
|
68
|
-
does "call_method", :foo
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
it "should allow the trait to define methods with different visibility" do
|
73
|
-
Doer.class_eval do
|
74
|
-
does "visibility"
|
75
|
-
end
|
76
|
-
instance = Doer.new
|
77
|
-
instance.public_methods.collect(&:to_s).should include("public_method_from_trait")
|
78
|
-
instance.protected_methods.collect(&:to_s).should include("protected_method_from_trait")
|
79
|
-
instance.private_methods.collect(&:to_s).should include("private_method_from_trait")
|
80
|
-
end
|
81
|
-
|
82
|
-
it "should allow the trait to perform metaprogramming acrobatics" do
|
83
|
-
Doer.class_eval do
|
84
|
-
does "define_constant_method", "some_method", "some_return_value"
|
85
|
-
end
|
86
|
-
instance = Doer.new
|
87
|
-
instance.should respond_to(:some_method)
|
88
|
-
instance.some_method.should == "some_return_value"
|
89
|
-
end
|
90
|
-
|
91
|
-
end
|
92
|
-
|
93
|
-
end
|