leap 0.4.6 → 0.5.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.
@@ -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