boil 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +117 -0
- data/lib/boil/composer/builder.rb +20 -0
- data/lib/boil/composer/dependency_walker.rb +43 -0
- data/lib/boil/composer/reflection_helpers.rb +29 -0
- data/lib/boil/composer/target_class_decorator.rb +68 -0
- data/spec/boil/composer/builder_spec.rb +143 -0
- metadata +68 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Martin Bilski
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
Automatic building of composed objects using Ruby reflection and meta-programming.
|
2
|
+
|
3
|
+
---
|
4
|
+
|
5
|
+
TODO
|
6
|
+
----
|
7
|
+
|
8
|
+
- Global namespace.
|
9
|
+
- Same module.
|
10
|
+
- Different modules.
|
11
|
+
|
12
|
+
---
|
13
|
+
|
14
|
+
Quick start
|
15
|
+
-----------
|
16
|
+
|
17
|
+
> gem install boil
|
18
|
+
|
19
|
+
An example:
|
20
|
+
|
21
|
+
ReviewSubmissionPresenter
|
22
|
+
ReviewSubmissionView
|
23
|
+
ReviewSubmissionModel
|
24
|
+
ReviewStore
|
25
|
+
ReviewValidationPolicy
|
26
|
+
ReviewResubmissionPolicy
|
27
|
+
|
28
|
+
(TODO: insert image of dependency diagram)
|
29
|
+
|
30
|
+
With classes in the global namespace or in the same module, it uses reasonable defaults:
|
31
|
+
|
32
|
+
class ReviewSubmissionPresenter
|
33
|
+
def initialize(review_submission_view, review_submission_model)
|
34
|
+
# ...
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ReviewSubmissionModel
|
39
|
+
def initialize(review_workflow_policy)
|
40
|
+
# ...
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def ReviewSubmissionView
|
45
|
+
# ...
|
46
|
+
end
|
47
|
+
|
48
|
+
class ReviewWorkflowPolicy
|
49
|
+
def initialize(review_store, review_validation_policy, review_resubmission_policy)
|
50
|
+
# ...
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
**TBD** With different module, you need to explicitly compose:
|
56
|
+
|
57
|
+
module Presenters
|
58
|
+
class ReviewSubmissionPresenter
|
59
|
+
include Boil::Composer
|
60
|
+
compose view: Views::ReviewSubmissionView
|
61
|
+
compose model: Models::ReviewSubmissionModel
|
62
|
+
|
63
|
+
def initialize(view, model)
|
64
|
+
# ...
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module Models
|
69
|
+
|
70
|
+
class ReviewSubmissionModel
|
71
|
+
include Boil::Composer
|
72
|
+
compose review_workflow_policy: Policies::ReviewWorkflowPolicy
|
73
|
+
|
74
|
+
def initialize(review_workflow_policy)
|
75
|
+
# ...
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module Views
|
81
|
+
def ReviewSubmissionView
|
82
|
+
# ...
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
module Policies
|
88
|
+
class ReviewWorkflowPolicy
|
89
|
+
include Boil::Composer
|
90
|
+
|
91
|
+
compose
|
92
|
+
review_store: Stores::ReviewStore,
|
93
|
+
review_validation_policy: Policies::ReviewValidationPolicy,
|
94
|
+
review_resubmission_policy: Policies::ReviewResubmissionPolicy
|
95
|
+
|
96
|
+
|
97
|
+
def initialize(review_store, review_validation_policy, review_resubmission_policy)
|
98
|
+
# ...
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class ReviewValidationPolicy
|
103
|
+
end
|
104
|
+
|
105
|
+
class ReviewResubmissionPolicy
|
106
|
+
include Boil::Composer
|
107
|
+
compose review_store: DataStores::ReviewStore
|
108
|
+
|
109
|
+
def initialize(review_store)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
module DataStores
|
115
|
+
class ReviewStore
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'active_support/inflector' # String.camelize, String.constantize, String.underscore
|
2
|
+
require_relative './reflection_helpers'
|
3
|
+
require_relative './dependency_walker'
|
4
|
+
require_relative './target_class_decorator'
|
5
|
+
|
6
|
+
module Boil
|
7
|
+
module Composer
|
8
|
+
module Builder
|
9
|
+
def create_factory_method_for(*classes)
|
10
|
+
classes.each do |cls|
|
11
|
+
DependencyWalker.new(TargetClassDecorator.new(self)).create_factory_method_for_class(cls)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def factory_methods
|
16
|
+
TargetClassDecorator.new(self).factory_methods
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/core_ext/module/introspection'
|
2
|
+
|
3
|
+
module Boil
|
4
|
+
module Composer
|
5
|
+
class DependencyWalker
|
6
|
+
include ReflectionHelpers
|
7
|
+
|
8
|
+
def initialize(target_class)
|
9
|
+
@target_class = target_class
|
10
|
+
end
|
11
|
+
|
12
|
+
def create_factory_method_for_class(cls)
|
13
|
+
begin
|
14
|
+
create_factory_methods_for_dependencies(constantize_class(cls))
|
15
|
+
@target_class.define_factory_method(cls)
|
16
|
+
rescue SystemStackError
|
17
|
+
raise "Stack too deep when trying to define factory method for #{cls} -- circular reference in initialize parameters?"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_factory_methods_for_dependencies(cls)
|
22
|
+
constructor_parameters(cls).each do |param|
|
23
|
+
begin
|
24
|
+
if class_name?(class_from_constructor_param(cls, param))
|
25
|
+
create_factory_method_for_class(class_from_constructor_param(cls, param))
|
26
|
+
else
|
27
|
+
# If it's already defined, we don't care that there is no corresponding class.
|
28
|
+
raise ClassConstNotFound unless @target_class.method_defined?(param)
|
29
|
+
end
|
30
|
+
rescue ClassConstNotFound
|
31
|
+
class_name = class_from_constructor_param(cls, param)
|
32
|
+
raise "#{cls} has constructor argument '#{param}' without a corresponding class (#{class_name})"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def class_from_constructor_param(parent_class, constructor_param)
|
38
|
+
parent_module_name = parent_class.parent.to_s
|
39
|
+
class_name = "#{parent_module_name}::#{constructor_param.to_s.camelize}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Boil
|
2
|
+
module Composer
|
3
|
+
module ReflectionHelpers
|
4
|
+
ClassConstNotFound = Class.new(RuntimeError)
|
5
|
+
|
6
|
+
def constructor_parameters(cls)
|
7
|
+
parameters = cls.instance_method(:initialize).parameters
|
8
|
+
return [] if parameters == [[:rest]]
|
9
|
+
parameters.map {|req, name| name || req }
|
10
|
+
end
|
11
|
+
|
12
|
+
def class_name?(name)
|
13
|
+
begin
|
14
|
+
name.constantize.is_a?(Class)
|
15
|
+
rescue NameError
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def constantize_class(class_or_name)
|
21
|
+
begin
|
22
|
+
class_or_name.is_a?(String) ? class_or_name.constantize : class_or_name
|
23
|
+
rescue NameError
|
24
|
+
raise ClassConstNotFound
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Boil
|
2
|
+
module Composer
|
3
|
+
class TargetClassDecorator
|
4
|
+
include ReflectionHelpers
|
5
|
+
|
6
|
+
def initialize(target_class)
|
7
|
+
@target_class = target_class
|
8
|
+
end
|
9
|
+
|
10
|
+
def define_factory_method(class_or_name)
|
11
|
+
factory_method_name = factory_method_name(class_or_name)
|
12
|
+
prevent_old_method_override(factory_method_name)
|
13
|
+
unless @target_class.method_defined?(factory_method_name)
|
14
|
+
cls = constantize_class(class_or_name)
|
15
|
+
constructor_params = constructor_parameters(cls)
|
16
|
+
@target_class.send(:define_method, factory_method_name) do
|
17
|
+
instance_variable_name = "@#{factory_method_name.to_s}".to_sym
|
18
|
+
instance_variable = instance_variable_get(instance_variable_name)
|
19
|
+
if instance_variable
|
20
|
+
instance_variable
|
21
|
+
else
|
22
|
+
instance_variable_set(instance_variable_name, cls.new(*constructor_params.map {|p| send(p)}))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
register_factory_method(factory_method_name)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_defined?(meth)
|
30
|
+
@target_class.method_defined?(meth)
|
31
|
+
end
|
32
|
+
|
33
|
+
def factory_method?(meth)
|
34
|
+
factory_methods.include?(meth)
|
35
|
+
end
|
36
|
+
|
37
|
+
def factory_methods
|
38
|
+
if @target_class.class_variable_defined?(FACTORY_METHODS_VAR_NAME)
|
39
|
+
@target_class.class_variable_get(FACTORY_METHODS_VAR_NAME)
|
40
|
+
else
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def prevent_old_method_override(meth)
|
48
|
+
raise "Factory method #{meth} conflicts with an existing method." if method_defined?(meth) && !factory_method?(meth)
|
49
|
+
end
|
50
|
+
|
51
|
+
FACTORY_METHODS_VAR_NAME = "@@__factory_methods"
|
52
|
+
|
53
|
+
def register_factory_method(meth)
|
54
|
+
@target_class.class_variable_set(FACTORY_METHODS_VAR_NAME, []) unless @target_class.class_variable_defined?(FACTORY_METHODS_VAR_NAME)
|
55
|
+
factory_methods = @target_class.class_variable_get(FACTORY_METHODS_VAR_NAME)
|
56
|
+
factory_methods << meth
|
57
|
+
end
|
58
|
+
|
59
|
+
def factory_method_name(class_or_name)
|
60
|
+
# Looks odd but it's there to use only the class name if it's in a module.
|
61
|
+
# "Boil::Composer::Independent".underscore => "boil/composer/independent"
|
62
|
+
# File.basename takes the last component.
|
63
|
+
# This will create clashes for classes with identical names (in different modules).
|
64
|
+
File.basename("#{class_or_name.to_s.underscore}").to_sym
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require_relative '../../../lib/boil/composer/builder'
|
2
|
+
|
3
|
+
|
4
|
+
describe Boil::Composer::Builder do
|
5
|
+
|
6
|
+
|
7
|
+
describe "given classes in the same namespace" do
|
8
|
+
|
9
|
+
module CommonModule
|
10
|
+
class Independent
|
11
|
+
def initialize
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Dependent
|
16
|
+
def initialize(independent)
|
17
|
+
@independent = independent
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :independent
|
21
|
+
end
|
22
|
+
|
23
|
+
class VeryDependent
|
24
|
+
def initialize(independent, dependent)
|
25
|
+
@independent = independent
|
26
|
+
@dependent = dependent
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :independent
|
30
|
+
attr_reader :dependent
|
31
|
+
end
|
32
|
+
|
33
|
+
class DependentOnPredefined
|
34
|
+
def initialize(predefined)
|
35
|
+
@predefined = predefined
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :predefined
|
39
|
+
end
|
40
|
+
|
41
|
+
class DependentOnNonExisting
|
42
|
+
def initialize(non_existing)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Circular1
|
47
|
+
def initialize(circular2)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Circular2
|
52
|
+
def initialize(circular1)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class DependentOnConflicting
|
57
|
+
def initialize(conflicting)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Conflicting
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
let(:target_class) do
|
67
|
+
Class.new do
|
68
|
+
extend Boil::Composer::Builder
|
69
|
+
|
70
|
+
def predefined
|
71
|
+
"predefined"
|
72
|
+
end
|
73
|
+
|
74
|
+
def conflicting
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
before do
|
80
|
+
::Module.module_eval do
|
81
|
+
include CommonModule
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should create factory method for class with parameter-less constructor" do
|
86
|
+
target_class.create_factory_method_for(CommonModule::Independent)
|
87
|
+
target_class.new.should respond_to(:independent)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should create only one instance with multiple calls to factory method" do
|
91
|
+
target_class.create_factory_method_for(CommonModule::Independent)
|
92
|
+
target = target_class.new
|
93
|
+
target.independent.should_not be_nil
|
94
|
+
target.independent.should == target.independent
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should create factory method depending on another class" do
|
98
|
+
target_class.create_factory_method_for(CommonModule::Dependent)
|
99
|
+
target = target_class.new
|
100
|
+
target.should respond_to(:independent)
|
101
|
+
target.should respond_to(:dependent)
|
102
|
+
target.independent.should == target.dependent.independent
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should support multiple root classes" do
|
106
|
+
target_class.create_factory_method_for(CommonModule::Dependent, CommonModule::VeryDependent)
|
107
|
+
target = target_class.new
|
108
|
+
target.should respond_to(:independent)
|
109
|
+
target.should respond_to(:dependent)
|
110
|
+
target.should respond_to(:very_dependent)
|
111
|
+
target.very_dependent.dependent == target.dependent
|
112
|
+
target.very_dependent.dependent.independent == target.dependent.independent
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should support pre-defined factory method" do
|
116
|
+
target_class.create_factory_method_for(CommonModule::DependentOnPredefined)
|
117
|
+
target = target_class.new
|
118
|
+
target.should respond_to(:dependent_on_predefined)
|
119
|
+
target.dependent_on_predefined.predefined.should == target.predefined
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should raise error if no factory method and no matching class" do
|
123
|
+
lambda { target_class.create_factory_method_for(CommonModule::DependentOnNonExisting) }.should raise_error
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should raise error for circular dependencies" do
|
127
|
+
lambda { target_class.create_factory_method_for(CommonModule::Circular1) }.should raise_error
|
128
|
+
lambda { target_class.create_factory_method_for(CommonModule::Circular1) }.should_not raise_error(SystemStackError)
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should raise error if methods conflict" do
|
132
|
+
lambda { target_class.create_factory_method_for(CommonModule::DependentOnConflicting) }.should raise_error
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should list factory methods" do
|
136
|
+
target_class.create_factory_method_for(CommonModule::Dependent, CommonModule::VeryDependent)
|
137
|
+
target_class.factory_methods.should have(3).items
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "given classes in different namespaces" do
|
142
|
+
end
|
143
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: boil
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Martin Bilski
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: active_support
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Escape from boilerplate code hell. This version supports automatic building
|
31
|
+
of composed objects.
|
32
|
+
email: gyamtso@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- README.md
|
38
|
+
- MIT-LICENSE
|
39
|
+
- lib/boil/composer/builder.rb
|
40
|
+
- lib/boil/composer/dependency_walker.rb
|
41
|
+
- lib/boil/composer/reflection_helpers.rb
|
42
|
+
- lib/boil/composer/target_class_decorator.rb
|
43
|
+
- spec/boil/composer/builder_spec.rb
|
44
|
+
homepage: https://github.com/bilus/compser
|
45
|
+
licenses: []
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements: []
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.8.24
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Escape from boilerplate code hell.
|
68
|
+
test_files: []
|