scorpion-ioc 0.3.1 → 0.4.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rspec +2 -1
  4. data/README.md +111 -44
  5. data/lib/scorpion/attribute.rb +0 -1
  6. data/lib/scorpion/attribute_set.rb +15 -7
  7. data/lib/scorpion/dependency/argument_dependency.rb +25 -0
  8. data/lib/scorpion/{prey/builder_prey.rb → dependency/builder_dependency.rb} +8 -8
  9. data/lib/scorpion/dependency/captured_dependency.rb +44 -0
  10. data/lib/scorpion/dependency/class_dependency.rb +25 -0
  11. data/lib/scorpion/dependency/module_dependency.rb +14 -0
  12. data/lib/scorpion/dependency.rb +137 -0
  13. data/lib/scorpion/dependency_map.rb +135 -0
  14. data/lib/scorpion/hunt.rb +158 -0
  15. data/lib/scorpion/hunter.rb +21 -20
  16. data/lib/scorpion/locale/en.yml +5 -1
  17. data/lib/scorpion/{king.rb → object.rb} +72 -53
  18. data/lib/scorpion/object_constructor.rb +55 -0
  19. data/lib/scorpion/rails/active_record/association.rb +65 -0
  20. data/lib/scorpion/rails/active_record/model.rb +28 -0
  21. data/lib/scorpion/rails/active_record/relation.rb +66 -0
  22. data/lib/scorpion/rails/active_record.rb +21 -0
  23. data/lib/scorpion/rails/controller.rb +22 -62
  24. data/lib/scorpion/rails/job.rb +30 -0
  25. data/lib/scorpion/rails/nest.rb +86 -0
  26. data/lib/scorpion/rails/railtie.rb +16 -0
  27. data/lib/scorpion/rails.rb +4 -0
  28. data/lib/scorpion/rspec/helper.rb +25 -0
  29. data/lib/scorpion/rspec.rb +17 -0
  30. data/lib/scorpion/stinger.rb +69 -0
  31. data/lib/scorpion/version.rb +1 -1
  32. data/lib/scorpion.rb +91 -44
  33. data/scorpion.gemspec +1 -1
  34. data/spec/internal/app/models/author.rb +17 -0
  35. data/spec/internal/app/models/todo.rb +14 -0
  36. data/spec/internal/db/schema.rb +12 -1
  37. data/spec/lib/scorpion/dependency/argument_dependency_spec.rb +18 -0
  38. data/spec/lib/scorpion/dependency/builder_dependency_spec.rb +41 -0
  39. data/spec/lib/scorpion/dependency/module_dependency_spec.rb +16 -0
  40. data/spec/lib/scorpion/dependency_map_spec.rb +108 -0
  41. data/spec/lib/scorpion/dependency_spec.rb +131 -0
  42. data/spec/lib/scorpion/hunt_spec.rb +93 -0
  43. data/spec/lib/scorpion/hunter_spec.rb +53 -14
  44. data/spec/lib/scorpion/object_constructor_spec.rb +49 -0
  45. data/spec/lib/scorpion/object_spec.rb +214 -0
  46. data/spec/lib/scorpion/rails/active_record/association_spec.rb +26 -0
  47. data/spec/lib/scorpion/rails/active_record/model_spec.rb +33 -0
  48. data/spec/lib/scorpion/rails/active_record/relation_spec.rb +72 -0
  49. data/spec/lib/scorpion/rails/controller_spec.rb +9 -9
  50. data/spec/lib/scorpion/rails/job_spec.rb +34 -0
  51. data/spec/lib/scorpion/rspec/helper_spec.rb +44 -0
  52. data/spec/lib/scorpion_spec.rb +0 -35
  53. data/spec/spec_helper.rb +1 -0
  54. metadata +54 -26
  55. data/lib/scorpion/hunting_map.rb +0 -139
  56. data/lib/scorpion/prey/captured_prey.rb +0 -44
  57. data/lib/scorpion/prey/class_prey.rb +0 -13
  58. data/lib/scorpion/prey/hunted_prey.rb +0 -14
  59. data/lib/scorpion/prey/module_prey.rb +0 -14
  60. data/lib/scorpion/prey.rb +0 -94
  61. data/spec/internal/db/combustion_test.sqlite +0 -0
  62. data/spec/lib/scorpion/hunting_map_spec.rb +0 -126
  63. data/spec/lib/scorpion/instance_spec.rb +0 -5
  64. data/spec/lib/scorpion/king_spec.rb +0 -198
  65. data/spec/lib/scorpion/prey/builder_prey_spec.rb +0 -42
  66. data/spec/lib/scorpion/prey/module_prey_spec.rb +0 -16
  67. data/spec/lib/scorpion/prey_spec.rb +0 -76
@@ -0,0 +1,137 @@
1
+ module Scorpion
2
+ # Dependency that can be injected into a {Scorpion::Object} by a {Scorpion}.
3
+ class Dependency
4
+
5
+ require 'scorpion/dependency/captured_dependency'
6
+ require 'scorpion/dependency/class_dependency'
7
+ require 'scorpion/dependency/module_dependency'
8
+ require 'scorpion/dependency/builder_dependency'
9
+ require 'scorpion/dependency/argument_dependency'
10
+
11
+ # ============================================================================
12
+ # @!group Attributes
13
+ #
14
+
15
+ # @!attribute
16
+ # @return [Class,Module,Symbol] contract describing the desired behavior of the dependency.
17
+ attr_reader :contract
18
+
19
+ # @!attribute
20
+ # @return [Array<Symbol>] the traits available on the dependency.
21
+ attr_reader :traits
22
+
23
+ #
24
+ # @!endgroup Attributes
25
+
26
+ def initialize( contract, traits = nil )
27
+ @contract = contract
28
+ @traits = Set.new( Array( traits ) )
29
+ end
30
+
31
+ # @return [Boolean] if the dependency satisfies the required contract and traits.
32
+ def satisfies?( contract, traits = nil )
33
+ satisfies_contract?( contract ) && satisfies_traits?( traits )
34
+ end
35
+
36
+ # Fetch an instance of the dependency.
37
+ # @param [Hunt] the hunting context.
38
+ # @return [Object] the hunted dependency.
39
+ def fetch( hunt )
40
+ fail "Not Implemented"
41
+ end
42
+
43
+ # Release the dependency, freeing up any long held resources.
44
+ def release
45
+ end
46
+
47
+ # Replicate the Dependency.
48
+ # @return [Dependency] a replication of the dependency.
49
+ def replicate
50
+ dup
51
+ end
52
+
53
+ def ==( other )
54
+ return unless other
55
+ self.class == other.class &&
56
+ contract == other.contract &&
57
+ traits == other.traits
58
+ end
59
+ alias_method :eql?, :==
60
+
61
+ def hash
62
+ self.class.hash ^
63
+ contract.hash ^
64
+ traits.hash
65
+ end
66
+
67
+ private
68
+
69
+ # @return [Boolean] true if the pray satisfies the given contract.
70
+ def satisfies_contract?( contract )
71
+ if self.contract.is_a? Symbol
72
+ self.contract == contract
73
+ else
74
+ self.contract <= contract
75
+ end
76
+ end
77
+
78
+ # @return [Boolean] true if the pray satisfies the given contract.
79
+ def satisfies_traits?( traits )
80
+ return true if traits.blank?
81
+
82
+ Array( traits ).all? do |trait|
83
+ case trait
84
+ when Symbol then self.traits.include? trait
85
+ when Module then self.contract <= trait
86
+ else fail ArgumentError, "Unsupported trait"
87
+ end
88
+ end
89
+ end
90
+
91
+ class << self
92
+
93
+ # Define dependency based on the desired contract and traits.
94
+ # @return [Dependency] the defined dependency.
95
+ def define( contract, traits = nil , &builder )
96
+ options, traits = extract_options!( traits )
97
+
98
+ if options.key?( :return )
99
+ Scorpion::Dependency::BuilderDependency.new( contract, traits ) do
100
+ options[:return]
101
+ end
102
+ elsif with = options[:with]
103
+ Scorpion::Dependency::BuilderDependency.new( contract, traits, with )
104
+ elsif block_given?
105
+ Scorpion::Dependency::BuilderDependency.new( contract, traits, builder )
106
+ elsif contract.respond_to?( :create )
107
+ Scorpion::Dependency::BuilderDependency.new( contract, traits ) do |scorpion,*args,&block|
108
+ contract.create scorpion, *args, &block
109
+ end
110
+ else
111
+ dependency_class( contract ).new( contract, traits, &builder )
112
+ end
113
+ end
114
+
115
+ private
116
+ def extract_options!( traits )
117
+ case traits
118
+ when Hash then return [ traits, nil ]
119
+ when Array then
120
+ if traits.last.is_a? Hash
121
+ return [ traits.pop, traits ]
122
+ end
123
+ end
124
+
125
+ [ {}, traits]
126
+ end
127
+
128
+ def dependency_class( contract, &builder )
129
+ return Scorpion::Dependency::ClassDependency if contract.is_a? Class
130
+ return Scorpion::Dependency::ModuleDependency if contract.is_a? Module
131
+
132
+ raise Scorpion::BuilderRequiredError
133
+ end
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,135 @@
1
+ module Scorpion
2
+ # {#chart} available {Dependency} and {#find} them based on desired
3
+ # {Scorpion::Attribute attributes}.
4
+ class DependencyMap
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ # ============================================================================
9
+ # @!group Attributes
10
+ #
11
+
12
+ # @return [Scorpion] the scorpion that created the map.
13
+ attr_reader :scorpion
14
+
15
+ # @return [Set] the set of dependency charted on this map.
16
+ attr_reader :dependency_set
17
+ private :dependency_set
18
+
19
+ # @return [Set] the set of dependencies charted on this map that is shared
20
+ # with all child dependencies.
21
+ attr_reader :shared_dependency_set
22
+ private :shared_dependency_set
23
+
24
+ # @return [Set] the active dependency set either {#dependency_set} or {#shared_dependency_set}
25
+ attr_reader :active_dependency_set
26
+ private :active_dependency_set
27
+
28
+ #
29
+ # @!endgroup Attributes
30
+
31
+ def initialize( scorpion )
32
+ @scorpion = scorpion
33
+ @dependency_set = @active_dependency_set = []
34
+ @shared_dependency_set = []
35
+ end
36
+
37
+ # Find {Dependency} that matches the requested `contract` and `traits`.
38
+ # @param [Class,Module,Symbol] contract describing the desired behavior of the dependency.
39
+ # @param [Array<Symbol>] traits found on the {Dependency}.
40
+ # @return [Dependency] the dependency matching the attribute.
41
+ def find( contract, traits = nil )
42
+ dependency_set.find{ |p| p.satisfies?( contract, traits ) } ||
43
+ shared_dependency_set.find{ |p| p.satisfies?( contract, traits ) }
44
+ end
45
+
46
+ # Chart the {Dependency} that this hunting map can {#find}.
47
+ #
48
+ # The block is executed in the context of DependencyMap if the block does not
49
+ # accept any arguments so that {#hunt_for}, {#capture} and {#share} can be
50
+ # called as methods.
51
+ #
52
+ # @example
53
+ #
54
+ # cache = {}
55
+ # chart do
56
+ # self #=> DependencyMap
57
+ # hunt_for Repository
58
+ # capture Cache, return: cache # => NoMethodError
59
+ # end
60
+ #
61
+ # chart do |map|
62
+ # map.hunt_for Repository
63
+ # map.capture Cache, return: cache # => No problem
64
+ # end
65
+ #
66
+ # @return [self]
67
+ def chart( &block )
68
+ return unless block_given?
69
+
70
+ if block.arity == 1
71
+ yield self
72
+ else
73
+ instance_eval &block
74
+ end
75
+
76
+ self
77
+ end
78
+
79
+ # Define {Dependency} that can be found on this map by `contract` and `traits`.
80
+ #
81
+ # If a block is given, it will be used build the actual instances of the
82
+ # dependency for the {Scorpion}.
83
+ #
84
+ # @param [Class,Module,Symbol] contract describing the desired behavior of the dependency.
85
+ # @param [Array<Symbol>] traits found on the {Dependency}.
86
+ # @return [Dependency] the dependency to be hunted for.
87
+ def hunt_for( contract, traits = nil, &builder )
88
+ active_dependency_set.unshift define_dependency( contract, traits, &builder )
89
+ end
90
+
91
+ # Captures a single dependency and returns the same instance fore each request
92
+ # for the resource.
93
+ # @see #hunt_for
94
+ # @return [Dependency] the dependency to be hunted for.
95
+ def capture( contract, traits = nil, &builder )
96
+ active_dependency_set.unshift Dependency::CapturedDependency.new( define_dependency( contract, traits, &builder ) )
97
+ end
98
+ alias_method :singleton, :capture
99
+
100
+ # Share dependencies defined within the block with all child scorpions.
101
+ # @return [Dependency] the dependency to be hunted for.
102
+ def share( &block )
103
+ old_set = active_dependency_set
104
+ @active_dependency_set = shared_dependency_set
105
+ yield
106
+ ensure
107
+ @active_dependency_set = old_set
108
+ end
109
+
110
+ # @visibility private
111
+ def each( &block )
112
+ dependency_set.each &block
113
+ end
114
+ delegate [ :empty?, :blank?, :present? ] => :dependency_set
115
+
116
+ # Replicates the dependency in `other_map` into this map.
117
+ # @param [Scorpion::DependencyMap] other_map to replicate from.
118
+ # @return [self]
119
+ def replicate_from( other_map )
120
+ other_map.each do |dependency|
121
+ if replica = dependency.replicate
122
+ dependency_set << replica
123
+ end
124
+ end
125
+
126
+ self
127
+ end
128
+
129
+ private
130
+
131
+ def define_dependency( contract, traits, &builder )
132
+ Dependency.define contract, traits, &builder
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,158 @@
1
+ module Scorpion
2
+ # Captures state for a specific hunt so that constructor arguments can be
3
+ # shared with child dependencies.
4
+ #
5
+ # @example
6
+ #
7
+ # class Service
8
+ # depend_on do
9
+ # options UserOptions
10
+ # end
11
+ #
12
+ # def initialize( user )
13
+ # end
14
+ # end
15
+ #
16
+ # class UserOptions
17
+ #
18
+ # depend_on do
19
+ # user User
20
+ # end
21
+ # end
22
+ #
23
+ # user = User.find 123
24
+ # service = scorpion.fetch Service, user
25
+ # service.options.user # => user
26
+ class Hunt
27
+ extend Forwardable
28
+
29
+ # ============================================================================
30
+ # @!group Attributes
31
+ #
32
+
33
+ # @!attribute
34
+ # @return [Scorpion] scorpion used to fetch uncaptured dependency.
35
+ attr_reader :scorpion
36
+
37
+ # @!attribute
38
+ # @return [Array<Array>] the stack of trips conducted by the hunt to help
39
+ # resolve child dependencies.
40
+ attr_reader :trips
41
+ private :trips
42
+
43
+ # @!attribute
44
+ # @return [Trip] the current hunting trip.
45
+ attr_reader :trip
46
+ private :trip
47
+
48
+ delegate [:contract, :traits, :arguments, :block] => :trip
49
+
50
+ # @!attribute contract
51
+ # @return [Class,Module,Symbol] contract being hunted for.
52
+
53
+ # @!attribute traits
54
+ # @return [Array<Symbol>] traits being hunted for.
55
+
56
+ # @!attribute [r] arguments
57
+ # @return [Array<Dependency>] dependency to pass to initializer of contract when found.
58
+
59
+ # @!attribute block
60
+ # @return [#call] block to pass to constructor of contract when found.
61
+
62
+ #
63
+ # @!endgroup Attributes
64
+
65
+ def initialize( scorpion, contract, traits, *arguments, &block )
66
+ @scorpion = scorpion
67
+ @trips = []
68
+ @trip = Trip.new contract, traits, arguments, block
69
+ end
70
+
71
+ # Hunt for additional dependency to satisfy the main hunt's contract and traits.
72
+ # @see Scorpion#hunt
73
+ def fetch( contract, *arguments, &block )
74
+ fetch_by_traits( contract, nil, *arguments, &block )
75
+ end
76
+
77
+ # Hunt for additional dependency to satisfy the main hunt's contract and traits.
78
+ # @see Scorpion#hunt
79
+ def fetch_by_traits( contract, traits, *arguments, &block )
80
+ push contract, traits, arguments, block
81
+ execute
82
+ ensure
83
+ pop
84
+ end
85
+
86
+ # Inject given `object` with its expected dependencies.
87
+ # @param [Scorpion::Object] object to be injected.
88
+ # @return [Scorpion::Object] the injected object.
89
+ def inject( object )
90
+ trip.object = object
91
+ object.injected_attributes.each do |attr|
92
+ next if object.send "#{ attr.name }?"
93
+ next if attr.lazy?
94
+
95
+ object.send :inject, attr, fetch_by_traits( attr.contract, attr.traits )
96
+ end
97
+
98
+ object
99
+ end
100
+
101
+ # Allow the hunt to spawn objects.
102
+ # @see Scorpion#spawn
103
+ def spawn( klass, *arguments, &block )
104
+ scorpion.spawn( self, klass, *arguments, &block )
105
+ end
106
+
107
+ private
108
+
109
+ def execute
110
+ execute_from_trips || execute_from_scorpion
111
+ end
112
+
113
+ def execute_from_trips
114
+ return if arguments.any?
115
+
116
+ trips.each do |trip|
117
+ return trip.object if contract === trip.object
118
+ trip.arguments.each do |arg|
119
+ return arg if contract === arg
120
+ end
121
+ end
122
+
123
+ nil
124
+ end
125
+
126
+ def execute_from_scorpion
127
+ scorpion.execute self
128
+ end
129
+
130
+ def push( contract, traits, arguments, block )
131
+ trips.push trip
132
+
133
+ @trip = Trip.new contract, traits, arguments, block
134
+ end
135
+
136
+ def pop
137
+ @trip = trips.pop
138
+ end
139
+
140
+ class Trip
141
+ attr_reader :contract
142
+ attr_reader :traits
143
+ attr_reader :arguments
144
+ attr_reader :block
145
+
146
+ attr_accessor :object
147
+
148
+ def initialize( contract, traits, arguments, block )
149
+ @contract = contract
150
+ @traits = traits
151
+ @arguments = arguments
152
+ @block = block
153
+ end
154
+ end
155
+
156
+ class InitializerTrip < Trip; end
157
+ end
158
+ end
@@ -1,5 +1,5 @@
1
1
  module Scorpion
2
- # A concrete implementation of a Scorpion used to hunt down food for a {Scorpion::King}.
2
+ # A concrete implementation of a Scorpion used to hunt down food for a {Scorpion::Object}.
3
3
  # @see Scorpion
4
4
  class Hunter
5
5
  include Scorpion
@@ -8,11 +8,11 @@ module Scorpion
8
8
  # @!group Attributes
9
9
  #
10
10
 
11
- # @return [Scorpion::HuntingMap] map of {Prey} and how to create instances.
12
- attr_reader :hunting_map
13
- protected :hunting_map
11
+ # @return [Scorpion::DependencyMap] map of {Dependency} and how to create instances.
12
+ attr_reader :dependency_map
13
+ protected :dependency_map
14
14
 
15
- # @return [Scorpion] parent scorpion to deferr hunting to on missing prey.
15
+ # @return [Scorpion] parent scorpion to deferr hunting to on missing dependency.
16
16
  attr_reader :parent
17
17
  private :parent
18
18
 
@@ -21,34 +21,35 @@ module Scorpion
21
21
 
22
22
  def initialize( parent = nil, &block )
23
23
  @parent = parent
24
- @hunting_map = Scorpion::HuntingMap.new( self )
24
+ @dependency_map = Scorpion::DependencyMap.new( self )
25
25
 
26
26
  prepare &block if block_given?
27
27
  end
28
28
 
29
29
  # Prepare the scorpion for hunting.
30
- # @see HuntingMap#chart
30
+ # @see DependencyMap#chart
31
31
  def prepare( &block )
32
- hunting_map.chart &block
33
- end
34
-
35
- # @see Scorpion#hunt
36
- def hunt_by_traits( contract, traits = nil, *args, &block )
37
- unless prey = hunting_map.find( contract, traits )
38
- return parent.hunt_by_traits contract, traits if parent
39
-
40
- prey = Scorpion::Prey::ClassPrey.new( contract, nil ) if contract.is_a?( Class ) && traits.blank?
41
- unsuccessful_hunt( contract, traits ) unless prey
42
- end
43
- prey.fetch self, *args, &block
32
+ dependency_map.chart &block
44
33
  end
45
34
 
46
35
  # @see Scorpion#replicate
47
36
  def replicate
48
37
  replica = self.class.new self
49
- replica.hunting_map.replicate_from( hunting_map )
38
+ replica.dependency_map.replicate_from( dependency_map )
50
39
  replica
51
40
  end
52
41
 
42
+ # @see Scorpion#hunt
43
+ def execute( hunt )
44
+ dependency = dependency_map.find( hunt.contract, hunt.traits )
45
+ dependency ||= parent.dependency_map.find( hunt.contract, hunt.traits ) if parent
46
+ dependency ||= Dependency.define( hunt.contract ) if hunt.traits.blank?
47
+
48
+ unsuccessful_hunt( hunt.contract, hunt.traits ) unless dependency
49
+
50
+ dependency.fetch hunt
51
+ end
52
+
53
+
53
54
  end
54
55
  end
@@ -2,4 +2,8 @@ en:
2
2
  scorpion:
3
3
  errors:
4
4
  messages:
5
- unsuccessful_hunt: "Couldn't find a %{contract} builder with traits '%{traits}'"
5
+ unsuccessful_hunt: "Couldn't find a %{contract} builder with traits '%{traits}'"
6
+ builder_required: A custom builder must be provided to resolve this dependency
7
+ warnings:
8
+ messages:
9
+ mixed_scorpions: A scorpion has already been assigned. Mixing scorpions can result in unexpected results.