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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +14 -10
  3. data/README.md +414 -26
  4. data/circle.yml +5 -0
  5. data/config.ru +7 -0
  6. data/lib/scorpion.rb +87 -0
  7. data/lib/scorpion/attribute.rb +71 -0
  8. data/lib/scorpion/attribute_set.rb +96 -0
  9. data/lib/scorpion/error.rb +29 -0
  10. data/lib/scorpion/hunter.rb +54 -0
  11. data/lib/scorpion/hunting_map.rb +115 -0
  12. data/lib/scorpion/king.rb +150 -0
  13. data/lib/scorpion/locale/en.yml +5 -0
  14. data/lib/scorpion/nest.rb +37 -0
  15. data/lib/scorpion/prey.rb +94 -0
  16. data/lib/scorpion/prey/builder_prey.rb +32 -0
  17. data/lib/scorpion/prey/captured_prey.rb +44 -0
  18. data/lib/scorpion/prey/class_prey.rb +13 -0
  19. data/lib/scorpion/prey/hunted_prey.rb +14 -0
  20. data/lib/scorpion/prey/module_prey.rb +14 -0
  21. data/lib/scorpion/rails.rb +1 -1
  22. data/lib/scorpion/rails/controller.rb +89 -0
  23. data/lib/scorpion/version.rb +1 -1
  24. data/scorpion.gemspec +2 -0
  25. data/spec/internal/config/database.yml +3 -0
  26. data/spec/internal/config/routes.rb +3 -0
  27. data/spec/internal/db/combustion_test.sqlite +0 -0
  28. data/spec/internal/db/schema.rb +3 -0
  29. data/spec/internal/log/.gitignore +1 -0
  30. data/spec/internal/public/favicon.ico +0 -0
  31. data/spec/lib/scorpion/attribute_set_spec.rb +89 -0
  32. data/spec/lib/scorpion/attribute_spec.rb +8 -0
  33. data/spec/lib/scorpion/error_spec.rb +8 -0
  34. data/spec/lib/scorpion/hunter_spec.rb +69 -0
  35. data/spec/lib/scorpion/hunting_map_spec.rb +118 -0
  36. data/spec/lib/scorpion/{scorpion.rb → instance_spec.rb} +0 -0
  37. data/spec/lib/scorpion/king_spec.rb +129 -0
  38. data/spec/lib/scorpion/prey/module_prey_spec.rb +16 -0
  39. data/spec/lib/scorpion/prey_spec.rb +76 -0
  40. data/spec/lib/scorpion/rails/controller_spec.rb +111 -0
  41. data/spec/lib/scorpion_spec.rb +0 -1
  42. data/spec/spec_helper.rb +6 -0
  43. metadata +78 -6
  44. data/Procfile +0 -1
  45. data/lib/scorpion/rails/railtie.rb +0 -15
data/circle.yml ADDED
@@ -0,0 +1,5 @@
1
+ database:
2
+ override:
3
+ # We don't actually use a DB but it is included in the internal rails app
4
+ # for specs. So override default behavior to do nothing
5
+ - echo "Skip DB"
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.require :default, :development
5
+
6
+ Combustion.initialize! :all
7
+ run Combustion::Application
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