scorpion-ioc 0.0.1 → 0.1.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.
- checksums.yaml +4 -4
- data/Gemfile +14 -10
- data/README.md +414 -26
- data/circle.yml +5 -0
- data/config.ru +7 -0
- data/lib/scorpion.rb +87 -0
- data/lib/scorpion/attribute.rb +71 -0
- data/lib/scorpion/attribute_set.rb +96 -0
- data/lib/scorpion/error.rb +29 -0
- data/lib/scorpion/hunter.rb +54 -0
- data/lib/scorpion/hunting_map.rb +115 -0
- data/lib/scorpion/king.rb +150 -0
- data/lib/scorpion/locale/en.yml +5 -0
- data/lib/scorpion/nest.rb +37 -0
- data/lib/scorpion/prey.rb +94 -0
- data/lib/scorpion/prey/builder_prey.rb +32 -0
- data/lib/scorpion/prey/captured_prey.rb +44 -0
- data/lib/scorpion/prey/class_prey.rb +13 -0
- data/lib/scorpion/prey/hunted_prey.rb +14 -0
- data/lib/scorpion/prey/module_prey.rb +14 -0
- data/lib/scorpion/rails.rb +1 -1
- data/lib/scorpion/rails/controller.rb +89 -0
- data/lib/scorpion/version.rb +1 -1
- data/scorpion.gemspec +2 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/combustion_test.sqlite +0 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/lib/scorpion/attribute_set_spec.rb +89 -0
- data/spec/lib/scorpion/attribute_spec.rb +8 -0
- data/spec/lib/scorpion/error_spec.rb +8 -0
- data/spec/lib/scorpion/hunter_spec.rb +69 -0
- data/spec/lib/scorpion/hunting_map_spec.rb +118 -0
- data/spec/lib/scorpion/{scorpion.rb → instance_spec.rb} +0 -0
- data/spec/lib/scorpion/king_spec.rb +129 -0
- data/spec/lib/scorpion/prey/module_prey_spec.rb +16 -0
- data/spec/lib/scorpion/prey_spec.rb +76 -0
- data/spec/lib/scorpion/rails/controller_spec.rb +111 -0
- data/spec/lib/scorpion_spec.rb +0 -1
- data/spec/spec_helper.rb +6 -0
- metadata +78 -6
- data/Procfile +0 -1
- data/lib/scorpion/rails/railtie.rb +0 -15
data/circle.yml
ADDED
data/config.ru
ADDED
data/lib/scorpion.rb
CHANGED
@@ -1,4 +1,91 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
I18n.load_path += Dir[ File.expand_path( '../scorpion/locale/*.yml', __FILE__ ) ]
|
4
|
+
|
1
5
|
module Scorpion
|
2
6
|
require 'scorpion/version'
|
7
|
+
require 'scorpion/error'
|
8
|
+
require 'scorpion/king'
|
9
|
+
require 'scorpion/attribute_set'
|
10
|
+
require 'scorpion/hunter'
|
11
|
+
require 'scorpion/hunting_map'
|
12
|
+
require 'scorpion/prey'
|
13
|
+
require 'scorpion/nest'
|
3
14
|
require 'scorpion/rails'
|
15
|
+
|
16
|
+
# @return [Scorpion] main scorpion for the app.
|
17
|
+
def self.instance
|
18
|
+
@instance
|
19
|
+
end
|
20
|
+
@instance = Scorpion::Hunter.new
|
21
|
+
|
22
|
+
# Prepare the {#instance} for hunting.
|
23
|
+
def self.prepare( &block )
|
24
|
+
instance.prepare &block
|
25
|
+
end
|
26
|
+
|
27
|
+
# Hunts for an object that satisfies the requested `contract` and `traits`.
|
28
|
+
# @param [Class,Module,Symbol] contract describing the desired behavior of the prey.
|
29
|
+
# @param [Array<Symbol>] traits required of the prey
|
30
|
+
# @return [Object] an object that matches the requirements defined in `attribute`.
|
31
|
+
# @raise [UnsuccessfulHunt] if a matching object cannot be found.
|
32
|
+
def hunt_by_traits!( contract, traits, *args, &block )
|
33
|
+
fail "Not implemented"
|
34
|
+
end
|
35
|
+
alias_method :fetch_by_traits!, :hunt_by_traits!
|
36
|
+
|
37
|
+
# Hunts for an object that satisfies the requested `contract` regardless of
|
38
|
+
# traits.
|
39
|
+
# @see #hunt_by_traits!
|
40
|
+
def hunt!( contract, *args, &block )
|
41
|
+
hunt_by_traits!( contract, nil, *args, &block )
|
42
|
+
end
|
43
|
+
alias_method :fetch!, :hunt!
|
44
|
+
|
45
|
+
# Populate given `king` with its expected attributes.
|
46
|
+
# @param [Scorpion::King] king to be fed.
|
47
|
+
# @return [Scorpion::King] the populated king.
|
48
|
+
def feed!( king )
|
49
|
+
king.injected_attributes.each do |attr|
|
50
|
+
king.send :feed, attr, hunt_by_traits!( attr.contract, attr.traits )
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates a new king and feeds it it's dependencies.
|
55
|
+
# @param [Class] king_class a class that includes {Scorpion::King}.
|
56
|
+
# @param [Array<Object>] args to pass to the constructor.
|
57
|
+
# @param [#call] block to pass to the constructor.
|
58
|
+
# @return [Scorpion::King] the spawned king.
|
59
|
+
def spawn( king_class, *args, &block )
|
60
|
+
if king_class < Scorpion::King
|
61
|
+
king_class.spawn self, *args, &block
|
62
|
+
else
|
63
|
+
king_class.new *args, &block
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Creates a new {Scorpion} copying the current configuration any any currently
|
68
|
+
# captured prey.
|
69
|
+
# @return [Scorpion] the replicated scorpion.
|
70
|
+
def replicate( &block )
|
71
|
+
fail "Not implemented"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Free up any captured prey and release any long-held resources.
|
75
|
+
def destroy
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Scorpion::Nest] a nest that uses this scorpion as the mother.
|
79
|
+
def build_nest
|
80
|
+
Scorpion::Nest.new( self )
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Used by concrete scorpions to notify the caller that the hunt was
|
86
|
+
# unsuccessful.
|
87
|
+
def unsuccessful_hunt!( contract, traits )
|
88
|
+
fail UnsuccessfulHunt.new contract, traits
|
89
|
+
end
|
90
|
+
|
4
91
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Scorpion
|
2
|
+
# An injected attribute and it's configuration.
|
3
|
+
class Attribute
|
4
|
+
|
5
|
+
# ============================================================================
|
6
|
+
# @!group Attributes
|
7
|
+
#
|
8
|
+
|
9
|
+
# @!attribute
|
10
|
+
# @return [Symbol] the name of the attribute.
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# @!attribute
|
14
|
+
# @return [Class,Module,Symbol] contract that describes the desired behavior
|
15
|
+
# of the injected object.
|
16
|
+
attr_reader :contract
|
17
|
+
|
18
|
+
# @!attribute
|
19
|
+
# @return [Array<Symbol>] traits that must match on instances of the {#contract}
|
20
|
+
attr_reader :traits
|
21
|
+
|
22
|
+
# @!attribute
|
23
|
+
# @return [Boolean] true if the attribute is not immediately required and
|
24
|
+
# will be hunted down on first use.
|
25
|
+
def lazy?; @lazy end
|
26
|
+
|
27
|
+
# @!attribute
|
28
|
+
# @return [Boolean] true if the attribute should have a public writer.
|
29
|
+
def public?; @public end
|
30
|
+
|
31
|
+
# @!attribute
|
32
|
+
# @return [Boolean] true if the attribute should have a public writer.
|
33
|
+
def private?; @private end
|
34
|
+
|
35
|
+
|
36
|
+
#
|
37
|
+
# @!endgroup Attributes
|
38
|
+
|
39
|
+
|
40
|
+
def initialize( name, contract, traits = nil, options = {} )
|
41
|
+
@name = name.to_sym
|
42
|
+
@contract = contract
|
43
|
+
@traits = Array( traits ).flatten.freeze
|
44
|
+
@trait_set = Set.new( @traits.map{ |t| :"#{t}?" } )
|
45
|
+
@lazy = options.fetch( :lazy, false )
|
46
|
+
@public = options.fetch( :public, false )
|
47
|
+
@private = options.fetch( :private, false )
|
48
|
+
end
|
49
|
+
|
50
|
+
def respond_to?( name, include_all = false )
|
51
|
+
super || trait_set.include?( name )
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
# @return [Set] the set of traits associated with the attribute pre-processed
|
56
|
+
# to include the trait names with a '?' suffix.
|
57
|
+
attr_reader :trait_set
|
58
|
+
|
59
|
+
def method_missing( name, *args )
|
60
|
+
if is_trait_method?( name )
|
61
|
+
trait_set.include? name
|
62
|
+
else
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_trait_method?( name )
|
68
|
+
name[-1] == '?'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'scorpion/attribute'
|
2
|
+
|
3
|
+
module Scorpion
|
4
|
+
class AttributeSet
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize( attributes = {} )
|
8
|
+
@attributes = attributes
|
9
|
+
end
|
10
|
+
|
11
|
+
def []( key )
|
12
|
+
attributes.fetch( key )
|
13
|
+
end
|
14
|
+
|
15
|
+
def each( &block )
|
16
|
+
attributes.each do |k,v|
|
17
|
+
yield v
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Merge two sets and create another.
|
22
|
+
def merge( other )
|
23
|
+
AttributeSet.new attributes.merge( other.attributes )
|
24
|
+
end
|
25
|
+
alias_method :|, :merge
|
26
|
+
|
27
|
+
# Inherit attribute definitions from another set.
|
28
|
+
def inherit!( other )
|
29
|
+
other.each do |attr|
|
30
|
+
attributes[attr.name] ||= attr
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def key?( name )
|
35
|
+
attributes.key? name
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines the food that {Scorpion::King} will feed on. A food is defined by
|
39
|
+
# invoking a method with the desired name passing the contract and traits
|
40
|
+
# desired. AttributeSet uses method_missing to dynamically define
|
41
|
+
# attributes.
|
42
|
+
#
|
43
|
+
# If the block takes an argument, AttributeSet will yield to the block
|
44
|
+
# passing itself. If no argument is provided, yield will use the
|
45
|
+
# AttributeSet itself as the calling context.
|
46
|
+
#
|
47
|
+
# @example With Argument
|
48
|
+
#
|
49
|
+
# define do |set|
|
50
|
+
# set.logger Rails::Logger
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# @example Without Argument
|
54
|
+
#
|
55
|
+
# define do
|
56
|
+
# logger Rails::Logger, :color
|
57
|
+
# end
|
58
|
+
def define( &block )
|
59
|
+
return unless block_given?
|
60
|
+
|
61
|
+
@defining_attributes = true
|
62
|
+
if block.arity == 1
|
63
|
+
yield self
|
64
|
+
else
|
65
|
+
instance_eval &block
|
66
|
+
end
|
67
|
+
|
68
|
+
self
|
69
|
+
ensure
|
70
|
+
@defining_attributes = false
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
attr_reader :attributes
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def define_attribute( name, contract, *traits )
|
80
|
+
options = traits.pop if traits.last.is_a? Hash
|
81
|
+
options ||= {}
|
82
|
+
attributes[name.to_sym] = Attribute.new name, contract, traits, options
|
83
|
+
end
|
84
|
+
|
85
|
+
def method_missing( name, *args )
|
86
|
+
return super unless @defining_attributes
|
87
|
+
|
88
|
+
if args.length >= 1
|
89
|
+
define_attribute name, *args
|
90
|
+
else
|
91
|
+
super
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
module Scorpion
|
4
|
+
class Error < StandardError
|
5
|
+
|
6
|
+
private
|
7
|
+
def translate( key, args = {} )
|
8
|
+
I18n.translate key, args.merge( scope: [:scorpion,:errors,:messages] )
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class UnsuccessfulHunt < Error
|
13
|
+
attr_reader :contract
|
14
|
+
attr_reader :traits
|
15
|
+
|
16
|
+
def initialize( contract, traits = nil )
|
17
|
+
@contract = contract
|
18
|
+
@traits = traits
|
19
|
+
|
20
|
+
super translate( :unsuccessful_hunt, contract: contract, traits: traits )
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class BuilderRequiredError < Error
|
25
|
+
def initialize( message = nil )
|
26
|
+
super ( message || translate( :builder_required ) )
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Scorpion
|
2
|
+
# A concrete implementation of a Scorpion used to hunt down food for a {Scorpion::King}.
|
3
|
+
# @see Scorpion
|
4
|
+
class Hunter
|
5
|
+
include Scorpion
|
6
|
+
|
7
|
+
# ============================================================================
|
8
|
+
# @!group Attributes
|
9
|
+
#
|
10
|
+
|
11
|
+
# @return [Scorpion::HuntingMap] map of {Prey} and how to create instances.
|
12
|
+
attr_reader :hunting_map
|
13
|
+
protected :hunting_map
|
14
|
+
|
15
|
+
# @return [Scorpion] parent scorpion to deferr hunting to on missing prey.
|
16
|
+
attr_reader :parent
|
17
|
+
private :parent
|
18
|
+
|
19
|
+
#
|
20
|
+
# @!endgroup Attributes
|
21
|
+
|
22
|
+
def initialize( parent = nil, &block )
|
23
|
+
@parent = parent
|
24
|
+
@hunting_map = Scorpion::HuntingMap.new( self )
|
25
|
+
|
26
|
+
prepare &block if block_given?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Prepare the scorpion for hunting.
|
30
|
+
# @see HuntingMap#chart
|
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.nil?
|
41
|
+
unsuccessful_hunt!( contract, traits ) unless prey
|
42
|
+
end
|
43
|
+
prey.fetch self, *args, &block
|
44
|
+
end
|
45
|
+
|
46
|
+
# @see Scorpion#replicate
|
47
|
+
def replicate
|
48
|
+
replica = self.class.new self
|
49
|
+
replica.hunting_map.replicate_from( hunting_map )
|
50
|
+
replica
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Scorpion
|
2
|
+
# {#chart} available {Prey} and {#find} them based on desired
|
3
|
+
# {#{Scorpion::Attribute attributes}.
|
4
|
+
class HuntingMap
|
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 prey charted on this map.
|
16
|
+
attr_reader :prey_set
|
17
|
+
private :prey_set
|
18
|
+
|
19
|
+
# @return [Set] the set of prey charted on this map that is shared with all
|
20
|
+
# child prey.
|
21
|
+
attr_reader :shared_prey_set
|
22
|
+
private :shared_prey_set
|
23
|
+
|
24
|
+
|
25
|
+
# @return [Set] the active prey set either {#prey_set} or {#shared_prey_set}
|
26
|
+
attr_reader :active_prey_set
|
27
|
+
private :active_prey_set
|
28
|
+
|
29
|
+
#
|
30
|
+
# @!endgroup Attributes
|
31
|
+
|
32
|
+
def initialize( scorpion )
|
33
|
+
@scorpion = scorpion
|
34
|
+
@prey_set = @active_prey_set = Set.new
|
35
|
+
@shared_prey_set = Set.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# Find {Prey} that matches the requested `contract` and `traits`.
|
39
|
+
# @param [Class,Module,Symbol] contract describing the desired behavior of the prey.
|
40
|
+
# @param [Array<Symbol>] traits found on the {Prey}.
|
41
|
+
# @return [Prey] the prey matching the attribute.
|
42
|
+
def find( contract, traits = nil )
|
43
|
+
prey_set.find{ |p| p.satisfies?( contract, traits ) } ||
|
44
|
+
shared_prey_set.find{ |p| p.satisfies?( contract, traits ) }
|
45
|
+
end
|
46
|
+
|
47
|
+
# Chart the {Prey} that this hunting map can {#find}.
|
48
|
+
def chart( &block )
|
49
|
+
return unless block_given?
|
50
|
+
|
51
|
+
if block.arity == 1
|
52
|
+
yield self
|
53
|
+
else
|
54
|
+
instance_eval &block
|
55
|
+
end
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# Define {Prey} that can be found on this map by `contract` and `traits`.
|
61
|
+
#
|
62
|
+
# If a block is given, it will be used build the actual instances of the
|
63
|
+
# prey for the {Scorpion}.
|
64
|
+
#
|
65
|
+
# @param [Class,Module,Symbol] contract describing the desired behavior of the prey.
|
66
|
+
# @param [Array<Symbol>] traits found on the {Prey}.
|
67
|
+
# @return [Scorpion::Prey] the prey to be hunted for.
|
68
|
+
def hunt_for( contract, traits = nil, &builder )
|
69
|
+
active_prey_set << prey_class( contract, &builder ).new( contract, traits, &builder )
|
70
|
+
end
|
71
|
+
alias_method :offer, :hunt_for
|
72
|
+
|
73
|
+
# Captures a single prey and returns the same instance fore each request
|
74
|
+
# for the resource.
|
75
|
+
# @see #hunt_for
|
76
|
+
def capture( contract, traits = nil, &builder )
|
77
|
+
active_prey_set << Scorpion::Prey::CapturedPrey.new( prey_class( contract, &builder ).new( contract, traits, &builder ) )
|
78
|
+
end
|
79
|
+
alias_method :singleton, :capture
|
80
|
+
|
81
|
+
# Share captured prey defined within the block with all child scorpions.
|
82
|
+
def share( &block )
|
83
|
+
old_set = active_prey_set
|
84
|
+
@active_prey_set = shared_prey_set
|
85
|
+
yield
|
86
|
+
ensure
|
87
|
+
@active_prey_set = old_set
|
88
|
+
end
|
89
|
+
|
90
|
+
def each( &block )
|
91
|
+
prey_set.each &block
|
92
|
+
end
|
93
|
+
delegate [ :empty?, :blank?, :present? ] => :prey_set
|
94
|
+
|
95
|
+
# Replicates the prey in `other_map` into this map.
|
96
|
+
# @param [Scorpion::HuntingMap] other_map to replicate from.
|
97
|
+
def replicate_from( other_map )
|
98
|
+
other_map.each do |prey|
|
99
|
+
if replica = prey.replicate
|
100
|
+
prey_set << replica
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
def prey_class( contract, &builder )
|
107
|
+
return Scorpion::Prey::BuilderPrey if block_given?
|
108
|
+
return Scorpion::Prey::HuntedPrey if contract.respond_to? :hunt
|
109
|
+
return Scorpion::Prey::ClassPrey if contract.is_a? Class
|
110
|
+
return Scorpion::Prey::ClassPrey if contract.is_a? Module
|
111
|
+
|
112
|
+
raise Scorpion::BuilderRequiredError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|