boil 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -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: []