boil 0.0.5
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/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: []
|