leap 0.4.6 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,18 +1,36 @@
1
1
  module Leap
2
+ # Encapsulates a set of strategies to determine a potentially non-obvious attribute of a Leap subject.
2
3
  class Decision
3
- attr_reader :goal, :signature_method, :committees, :minutes
4
+ # The goal of the decision, as defined by the first parameter of <tt>Leap::Subject#decide</tt>.
5
+ attr_reader :goal
4
6
 
7
+ # The method name used to retrieve a curated hash of the subject instance's attributes for use during deliberation. Initially defined by the <tt>:with</tt> option on <tt>Leap::Subject#decide</tt>.
8
+ attr_reader :signature_method
9
+
10
+ # Returns the array of committees defined within the decision block.
11
+ attr_reader :committees
12
+
13
+ # Creates a <tt>Leap::Decision</tt> object with a given goal.
14
+ #
15
+ # Generally you won't initialize a <tt>Leap::Decision</tt> directly; <tt>Leap::Subject#decide</tt> does it for you.
16
+ #
17
+ # @param [Symbol] goal The ultimate goal of the decision--what are we trying to decide about the subject?
18
+ # @param [Hash] options
19
+ # @option [optional, Symbol] with The name of an instance method on the subject that will, when called, provide a Hash of curated attributes about itself. <a href="http://github.com/brighterplanet/charisma">Charisma</a> is great for this. If this option is not provided, Leap will use a low-budget alternative (see <tt>Leap::ImplicitAttributes</tt>)
5
20
  def initialize(goal, options)
6
21
  @goal = goal
7
22
  @signature_method = options[:with] || :_leap_implicit_attributes
8
23
  @committees = []
9
24
  end
10
25
 
11
- def make(subject, *considerations)
12
- characteristics = subject.send(subject.class.decisions[goal].signature_method)
26
+ # Make the decision.
27
+ #
28
+ # General you won't call this directly, but rather use the dynamically-created method with this decision's goal as its name on the subject instance.
29
+ # @see Leap::GoalMethodsDocumentation
30
+ def make(characteristics, *considerations)
13
31
  options = considerations.extract_options!
14
32
  committees.reject { |c| characteristics.keys.include? c.name }.reverse.inject(Deliberation.new(characteristics)) do |deliberation, committee|
15
- if report = committee.report(subject, deliberation.characteristics, considerations, options)
33
+ if report = committee.report(deliberation.characteristics, considerations, options)
16
34
  deliberation.reports.unshift report
17
35
  deliberation.characteristics[committee.name] = report.conclusion
18
36
  end
@@ -21,6 +39,11 @@ module Leap
21
39
  end
22
40
 
23
41
  include ::Blockenspiel::DSL
42
+
43
+ # Define a committee within the decision.
44
+ #
45
+ # Used within a <tt>Leap::Subject#decide</tt> block to define a new committee. See <tt>Leap::Committee</tt> for details.
46
+ # @param [Symbol] name The name of the attribute that the committee will return.
24
47
  def committee(name, &blk)
25
48
  committee = ::Leap::Committee.new(name)
26
49
  @committees << committee
@@ -1,17 +1,27 @@
1
1
  module Leap
2
+ # Encapsulates a single computation of a Leap subject's decision, as performed on an instance of that subject class.
2
3
  class Deliberation
4
+ # Accumulates knowledge about the Leap subject instance, beginning with the instance's explicit attributes, and later augmented by committee proceedings.
3
5
  attr_accessor :characteristics
6
+
7
+ # Accumulates proceedings of the deliberation, including conclusions and methodology.
4
8
  attr_accessor :reports
5
9
 
10
+ # Create a new deliberation, to be made in light of the provided characteristics of the Leap subject instance.
11
+ # @param [Hash] characteristics The initial set of characteristics made available to committees during deliberation.
6
12
  def initialize(characteristics)
7
13
  self.characteristics = characteristics
8
14
  self.reports = []
9
15
  end
10
16
 
17
+ # Convenience method to access values within the deliberation's characteristics hash.
18
+ # @param [Symbol] characteristic
11
19
  def [](characteristic)
12
20
  characteristics[characteristic]
13
21
  end
14
22
 
23
+ # Report which named protocols the deliberation incidentally complied with.
24
+ # @return [Array]
15
25
  def compliance
16
26
  reports.map(&:quorum).map(&:compliance).inject do |memo, c|
17
27
  next c unless memo
@@ -0,0 +1,16 @@
1
+ module Leap
2
+ # Provides an intelligent accessor method for a subject instance's deliberations.
3
+ # @see Leap::Subject
4
+ module DeliberationsAccessor
5
+ # Returns a special hash of deliberations that will make necessary decisions if they have not yet been made.
6
+ def deliberations
7
+ @deliberations ||= Hash.new do |h, k|
8
+ return nil unless respond_to? k
9
+ send k
10
+ h[k]
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+
@@ -0,0 +1,20 @@
1
+ module Leap
2
+ # Used strictly for documenting the dynamic methods created by <tt>Leap::Subject#decide</tt>
3
+ #
4
+ # @note This module is provided due to limitations in the YARD documentation system.
5
+ # @see Leap::Subject#decide
6
+ module GoalMethodsDocumentation
7
+
8
+ # @overload your_goal_name(*considerations = [], options = {})
9
+ # Computes a previously-defined Leap decision named <tt>your_goal_name</tt>.
10
+ #
11
+ # @param [optional, Array] considerations An ordered array of additional details, immutable during the course of deliberation, that should be made available to each committee for provision, upon request, to quorums.
12
+ # @param [optional, Hash] options Additional options
13
+ # @option comply Force the ensuing deliberation to comply with one or more "protocols" by only respecting quorums that comply with this (these) protocol(s). Protocols can be anything--a Fixnum, a String, whatever, but by tradition a Symbol. If compliance is required with multiple protocols, they should be passed in an Array.
14
+ # @return The value of the newly-decided goal.
15
+ # @raise [Leap::NoSolutionError] Leap could not compute the decision's goal on this subject instance given its characteristics and compliance constraint.
16
+ def method_missing(*args, &blk)
17
+ super
18
+ end
19
+ end
20
+ end
@@ -1,7 +1,11 @@
1
1
  module Leap
2
+ # Ideally Leap subjects will provide methods that return a curated hash of attributes suitable for Leap decisions and indicate them with the <tt>:with</tt> option on <tt>Leap::Subject#decide</tt>.
3
+ # If this type of method is not available, or if it is not properly indicated, Leap will fall back to using the stopgap in this module.
2
4
  module ImplicitAttributes
5
+ # Provides an articifial attributes hash constructed from the object's instance variables.
6
+ # @return [Hash]
3
7
  def _leap_implicit_attributes
4
- Hash[*instance_variables.map { |variable| variable.to_s.delete('@').to_sym }.zip(instance_variables.map { |variable| instance_variable_get variable }).flatten]
8
+ Hash[*instance_variables.map { |variable| variable.to_s.delete('@').to_sym }.zip(instance_variables.map { |variable| instance_variable_get variable }).flatten].except(:deliberations)
5
9
  end
6
10
  end
7
11
  end
@@ -0,0 +1,40 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Leap
4
+ # Raised when a Leap solution cannot be found.
5
+ #
6
+ # If this is raised unexpectedly, try removing compliance constraints or double-checking that you have enough quorums within mainline committees to provide conclusions given any combination of input data.
7
+ class NoSolutionError < ArgumentError;
8
+ # Create the excpetion
9
+ def initialize(options = {})
10
+ @goal = options[:goal]
11
+ @deliberation = options[:deliberation]
12
+
13
+ if @goal
14
+ help = "No solution was found for \"#{@goal}\"."
15
+ else
16
+ help = "No solution was found."
17
+ end
18
+
19
+ if @deliberation
20
+ help << " (#{deliberation_report})"
21
+ end
22
+
23
+ super help
24
+ end
25
+
26
+ private
27
+
28
+ # A report on the deliberation proceedings, for debugging purposes.
29
+ def deliberation_report
30
+ @deliberation.characteristics.keys.sort_by(&:to_s).map do |characteristic|
31
+ statement = "#{characteristic}: "
32
+ if report = @deliberation.reports.find { |r| r.committee.name == characteristic }
33
+ statement << report.quorum.name.humanize.downcase
34
+ else
35
+ statement << 'provided as input'
36
+ end
37
+ end.join ', '
38
+ end
39
+ end
40
+ end
@@ -1,8 +1,25 @@
1
1
  module Leap
2
+ # A basic building block of a Leap decision.
3
+ #
4
+ # A quorum encapsulates a specific methodology for drawing a conclusion based on a set of input information.
2
5
  class Quorum
3
- include XmlSerializer
4
-
5
- attr_reader :name, :requirements, :supplements, :process, :compliance
6
+
7
+ # The name of the quorum
8
+ attr_reader :name
9
+
10
+ # Quorum attribute requirements, as defined by the <tt>:needs</tt> option
11
+ attr_reader :requirements
12
+
13
+ # Useful attributes, as defined by the <tt>:wants</tt> option
14
+ attr_reader :supplements
15
+
16
+ # The quorum's methodology, as a Ruby closure.
17
+ attr_reader :process
18
+
19
+ # Protocols with which this quorum complies.
20
+ attr_reader :compliance
21
+
22
+ # (see Leap::Committee#quorum)
6
23
  def initialize(name, options, blk)
7
24
  @name = name
8
25
  @requirements = Array.wrap options[:needs]
@@ -11,41 +28,31 @@ module Leap
11
28
  @process = blk
12
29
  end
13
30
 
31
+ # Do the provided characteristics satisfy the quorum's requirements?
32
+ # @param [Hash] characteristics
33
+ # @return [TrueClass, NilClass]
14
34
  def satisfied_by?(characteristics)
15
35
  (requirements - characteristics.keys).empty?
16
36
  end
17
37
 
38
+ # Does the quorum comply with the given set of protocols?
39
+ # @param [Array] guideines The list of protocols we're for which we're checking compliance.
40
+ # @return [TrueClass, NilClass]
18
41
  def complies_with?(guidelines)
19
42
  (guidelines - compliance).empty?
20
43
  end
21
44
 
45
+ # Perform the quorum's methodology using the given characteristics.
46
+ # @param [Hash] characteristics
47
+ # @return The methodology's result
22
48
  def acknowledge(characteristics, considerations)
23
49
  considerations.unshift characteristics
24
50
  process.call(*considerations[0...process.arity])
25
51
  end
26
52
 
53
+ # All characteristics either needed or wanted by the quorum.
27
54
  def characteristics
28
55
  requirements + supplements
29
56
  end
30
-
31
- def as_json(*)
32
- {
33
- 'name' => name.to_s,
34
- 'requirements' => requirements.map(&:to_s),
35
- 'appreciates' => supplements.map(&:to_s),
36
- 'complies' => compliance.map(&:to_s)
37
- }
38
- end
39
-
40
- def to_xml(options = {})
41
- super options do |xml|
42
- xml.quorum do |quorum_block|
43
- quorum_block.name name.to_s, :type => 'string'
44
- array_to_xml(quorum_block, :requirements, requirements)
45
- array_to_xml(quorum_block, :appreciates, supplements, 'supplement')
46
- array_to_xml(quorum_block, :complies, compliance, 'compliance')
47
- end
48
- end
49
- end
50
57
  end
51
58
  end
@@ -1,41 +1,27 @@
1
1
  module Leap
2
+ # Encapsulates a committee's report; that is, the conclusion of its selected quorum, along with administrative details.
2
3
  class Report
3
- include XmlSerializer
4
-
5
- attr_reader :subject, :committee, :conclusion, :quorum
6
4
 
7
- def initialize(subject, committee, report)
5
+ # The committee that produced the report
6
+ attr_reader :committee
7
+
8
+ # The committee's conclusion
9
+ attr_reader :conclusion
10
+
11
+ # The quorum whose methodology provided the conclusion.
12
+ attr_reader :quorum
13
+
14
+ # Create a new report.
15
+ #
16
+ # This is generally called in the midst of <tt>Leap::Committee#report</tt>
17
+ # @param [Leap::Committee] The committee that produced the report.
18
+ # @param [Hash] report A single-pair hash containing the responsible quorum and its conclusion.
19
+ # @raise [ArgumentError] Raised for anonymous reports, or if the report is not made properly.
20
+ def initialize(committee, report)
8
21
  raise ArgumentError, 'Reports must identify themselves' unless committee.is_a?(::Leap::Committee)
9
- @subject = subject
10
22
  @committee = committee
11
23
  raise ArgumentError, 'Please report with quorum => conclusion' unless report.is_a?(Hash) and report.length == 1
12
24
  @quorum, @conclusion = report.first
13
25
  end
14
-
15
- def formatted_conclusion
16
- return @formatted_conclusion unless @formatted_conclusion.nil?
17
- if characteristic = subject.class.characteristics[committee.name]
18
- @formatted_conclusion = characteristic.display committee.name => conclusion
19
- end
20
- @formatted_conclusion ||= conclusion
21
- end
22
-
23
- def as_json(*)
24
- {
25
- 'committee' => committee.as_json,
26
- 'conclusion' => formatted_conclusion,
27
- 'quorum' => quorum.as_json
28
- }
29
- end
30
-
31
- def to_xml(options = {})
32
- super options do |xml|
33
- xml.report do |report_block|
34
- committee.to_xml(options.merge :skip_instruct => true, :builder => report_block)
35
- report_block.conclusion formatted_conclusion, :type => formatted_conclusion.class.to_s.downcase
36
- quorum.to_xml(options.merge :skip_instruct => true, :builder => report_block)
37
- end
38
- end
39
- end
40
26
  end
41
27
  end
@@ -1,19 +1,66 @@
1
1
  module Leap
2
+ # In Leap lingo, Subject refers to the host class, instances of which we're trying to arrive at conclusions about.
3
+ #
4
+ # It is within Subjects that we establish decision-making strategies using <tt>#decide</tt>
5
+ # @see #decide
2
6
  module Subject
7
+ # Establishes the <tt>@decisions</tt> class instance variable for accumulating Leap decisions, along with an accessor for their deliberations.
8
+ # Also injects <tt>Leap::ImplicitAttributes</tt>, which provides a low-budget attribute curation method.
9
+ # @see Leap::Decision
10
+ # @see Leap::Deliberation
11
+ # @see Leap::DeliberationsAccessor
12
+ # @see Leap::ImplicitAttributes
3
13
  def self.extended(base)
4
14
  base.instance_variable_set :@decisions, {}
5
- base.send :attr_reader, :deliberations
6
15
  base.send :include, ::Leap::ImplicitAttributes
16
+ base.send :include, ::Leap::DeliberationsAccessor
17
+ base.send :include, ::Leap::GoalMethodsDocumentation
7
18
  end
19
+
20
+ # Accumulates <tt>Leap::Decision</tt> objects, having been defined with <tt>#decide</tt>.
21
+ # @see #decide
8
22
  attr_reader :decisions
23
+
24
+ # Defines a new <tt>Leap::Decision</tt> on the subject, using a DSL within its block.
25
+ #
26
+ # The DSL within the block is primarily composed of <tt>Leap::Decision#committee</tt>.
27
+ # Using <tt>#decide</tt> will define a new method on instances of the subject class, named after the decision's <tt>goal</tt>,
28
+ # that <i>computes</i> the decision through a process called deliberation (see <tt>Leap::GoalMethodsDocumentation</tt>).
29
+ #
30
+ # @example Defining a Leap decision
31
+ # class Person
32
+ # include Leap
33
+ # decide :lucky_number do
34
+ # # Decision definition elided (see Leap::Decision#committee)
35
+ # end
36
+ # end
37
+ #
38
+ # Person.new.lucky_number # => 42
39
+ #
40
+ # @param [Symbol] goal The goal of the decision--what is it that we're trying to decide about the subject?
41
+ # @param [optional, Hash] options
42
+ # @option [optional, Symbol] with The name of an instance method on the subject that will, when called, provide a Hash of curated attributes about itself. <a href="http://github.com/brighterplanet/charisma">Charisma</a> is great for this. If this option is not provided, Leap will use a low-budget alternative (see <tt>Leap::ImplicitAttributes</tt>)
43
+ #
44
+ # @see Leap::Decision
45
+ # @see Leap::Decision#committee
46
+ # @see Leap::Deliberation
47
+ # @see Leap::GoalMethodsDocumentation
48
+ # @see Leap::ImplicitAttributes
9
49
  def decide(goal, options = {}, &blk)
10
50
  decisions[goal] = ::Leap::Decision.new goal, options
11
51
  Blockenspiel.invoke(blk, decisions[goal])
12
52
  define_method goal do |*considerations|
13
53
  options = considerations.extract_options!
14
54
  @deliberations ||= {}
15
- @deliberations[goal] = self.class.decisions[goal].make(self, *considerations.push(options))
16
- @deliberations[goal][goal] or raise ::Leap::NoSolutionError
55
+ decision = self.class.decisions[goal]
56
+ characteristics = send(self.class.decisions[goal].signature_method)
57
+ considerations.push(options)
58
+ @deliberations[goal] = decision.make(characteristics, *considerations)
59
+ if @deliberations[goal][goal].nil?
60
+ raise ::Leap::NoSolutionError, :goal => goal,
61
+ :deliberation => @deliberations[goal]
62
+ end
63
+ @deliberations[goal][goal]
17
64
  end
18
65
  end
19
66
  end
@@ -1,3 +1,3 @@
1
- module Leap
2
- VERSION = '0.4.6'
3
- end
1
+ module Leap
2
+ VERSION = "0.5.0"
3
+ end
@@ -3,7 +3,13 @@ require 'bundler'
3
3
  Bundler.setup
4
4
  require 'test/unit'
5
5
  require 'shoulda'
6
- require 'characterizable'
6
+ require 'charisma'
7
+ require 'active_support/version'
8
+ %w{
9
+ active_support/core_ext/hash/except
10
+ }.each do |active_support_3_requirement|
11
+ require active_support_3_requirement
12
+ end if ActiveSupport::VERSION::MAJOR == 3
7
13
 
8
14
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
15
  $LOAD_PATH.unshift(File.dirname(__FILE__))
@@ -25,7 +31,7 @@ class Person
25
31
  @magic_integer = options[:magic_integer] if options[:magic_integer] && options[:magic_integer].is_a?(Integer)
26
32
  end
27
33
 
28
- include Characterizable
34
+ include Charisma
29
35
 
30
36
  characterize do
31
37
  has :name
@@ -61,7 +67,7 @@ class Person
61
67
  end
62
68
 
63
69
  committee :magic_float do
64
- quorum 'ancient recipe', :needs => :name do |characteristics|
70
+ quorum 'ancient recipe', :needs => :name, :complies => :zeus do |characteristics|
65
71
  ('A'..'Z').to_a.index(characteristics[:name].chars.to_a.first).to_f / ('A'..'Z').to_a.index(characteristics[:name].chars.to_a.last).to_f
66
72
  end
67
73
  end
@@ -102,3 +108,14 @@ class Thing
102
108
  committee(:anything) {}
103
109
  end
104
110
  end
111
+
112
+ class Seamus
113
+ include Leap
114
+ decide :can_i_commit_to_that_date do
115
+ committee :can_i_commit_to_that_date do
116
+ quorum 'my first instinct', :complies => :bent do |characteristics|
117
+ :maybe
118
+ end
119
+ end
120
+ end
121
+ end
@@ -11,7 +11,6 @@ class TestLeap < Test::Unit::TestCase
11
11
  end
12
12
 
13
13
  should 'naturally receive an International Psychics Association-compliant lucky number' do
14
- @person.lucky_number
15
14
  assert_equal [:ipa], @person.deliberations[:lucky_number].compliance
16
15
  end
17
16
  end
@@ -26,22 +25,15 @@ class TestLeap < Test::Unit::TestCase
26
25
  end
27
26
 
28
27
  should 'nevertheless remember how his lucky number was determined' do
29
- @person.lucky_number # make the decision
30
- assert_equal({ :magic_integer => 6, :lucky_number => 36, :age => 5, :litmus => {}}, @person.deliberations[:lucky_number].characteristics)
28
+ assert_equal(@person.deliberations[:lucky_number].characteristics, { :magic_integer => 6, :lucky_number => 36, :age => 5, :litmus => {}})
31
29
  assert_equal 'ninja style', @person.deliberations[:lucky_number].reports.find{ |r| r.committee.name == :magic_integer }.quorum.name
32
30
  end
33
31
 
34
- should 'but only as long as it had actually been determined' do
35
- assert_nil @person.deliberations
36
- end
37
-
38
32
  should 'only give quorums what they ask for' do
39
- @person.lucky_number # make the decision
40
33
  assert_equal({}, @person.deliberations[:lucky_number].reports.find{ |r| r.committee.name == :litmus }.conclusion)
41
34
  end
42
35
 
43
36
  should 'not receive an International Psychics Association-compliant lucky number unless he asks for it' do
44
- @person.lucky_number
45
37
  assert_equal [], @person.deliberations[:lucky_number].compliance
46
38
  end
47
39
  end
@@ -90,15 +82,63 @@ class TestLeap < Test::Unit::TestCase
90
82
  end
91
83
  end
92
84
 
85
+ context "A lazy subject" do
86
+ setup do
87
+ @thing = Thing.new
88
+ @thing.anything rescue nil
89
+ end
90
+
91
+ should 'have proper implicit characteristics' do
92
+ assert_equal Hash.new, @thing.deliberations[:anything].characteristics
93
+ end
94
+ end
95
+
93
96
  context "An impossible decision" do
94
97
  setup do
95
98
  @thing = Thing.new
96
99
  end
97
100
 
98
101
  should 'be impossible to make' do
99
- assert_raise ::Leap::NoSolutionError do
102
+ exception = assert_raise ::Leap::NoSolutionError do
100
103
  @thing.anything
101
104
  end
105
+
106
+ assert_match(/No solution was found for "anything"/, exception.message)
107
+ end
108
+ end
109
+
110
+ context "A difficult decision" do
111
+ setup do
112
+ @person = Person.new :name => 'Bozo'
113
+ end
114
+
115
+ should 'provide details about its apparent impossibility' do
116
+ exception = assert_raise ::Leap::NoSolutionError do
117
+ @person.lucky_number :comply => :zeus
118
+ end
119
+
120
+ assert_match(/No solution was found for "lucky_number"/, exception.message)
121
+ assert_match(/magic_float: ancient recipe, name: provided as input/, exception.message)
122
+ end
123
+ end
124
+
125
+ context 'Seamus deciding about whether he can commit to a date' do
126
+ setup do
127
+ @seamus = Seamus.new
128
+ end
129
+
130
+ should 'work for most people' do
131
+ assert_equal :maybe, @seamus.can_i_commit_to_that_date
132
+ end
133
+
134
+ should 'work for BenT, who is easygoing' do
135
+ assert_equal :maybe, @seamus.can_i_commit_to_that_date(:comply => :bent)
136
+ end
137
+
138
+ should 'never work for andy' do
139
+ assert_raise ::Leap::NoSolutionError do
140
+ @seamus.can_i_commit_to_that_date(:comply => :andy)
141
+ end
102
142
  end
103
143
  end
104
144
  end