factree 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c35f29630bd9cb9399531725ed218c5da73e1c2
4
- data.tar.gz: '09686d69ee9aeb3ea37f75d13af3c2f686dfa11e'
3
+ metadata.gz: c1acd242c9352248bdd755e245c55e026d16cdc0
4
+ data.tar.gz: b888a69e8cd08b85e1d3177e2b38b5c3f8cda78c
5
5
  SHA512:
6
- metadata.gz: faf9cbfe994babe6b72d2038a8cab88f4123db61d505afdb26e0f07b57175456b49293558836ad48cd9809da8f93d78a195608910efa4eab5e71b5513c0b1bac
7
- data.tar.gz: 2a279be3fa08b4fb588195eb6d6fd948ca759f19d8d622701478cf0694b328e6e299380aede9998c33523030720d2bac87f1974511ef6d2d93bc7c25f6ed7f80
6
+ metadata.gz: cf47060ea361430911941b26da4cef20bb527847c9f0efd522f3bac3b32d7fe94dc56d750dc91f1947598d6e6aa07bcb8573d6a462a350af2c9c17f42782795a
7
+ data.tar.gz: 42455c60d1d5b9ddb10a0d50cd6a7c2d4847e855453a60c9f1058bce56327dc1469684e5358c243d36db5513779ef33d4a0476ab852cec54c021038f8a4d96f2
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.4.0
1
+ 2.1.5
data/.travis.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.4.0
4
+ - 2.1.5
5
+ - 2.4.1
5
6
  before_install: gem install bundler -v 1.14.6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Grand Rounds
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Factree
2
+ [![Gem Version](https://badge.fury.io/rb/factree.svg)](https://rubygems.org/gems/factree)
2
3
  [![Build Status](https://travis-ci.org/jstrater/factree.svg?branch=master)](https://travis-ci.org/jstrater/factree)
3
4
 
4
- Factree provides a way to make choices based on a set of facts that are not yet known. It breaks the process down into individual decisions, each of which can lead to a number of other decisions, until finally a conclusion is reached -- something like a choose your own adventure novel. As each decision is made, the facts necessary to make the next decision are identified.
5
+ Factree provides tools for making choices based on a set of facts that are not yet known. You write a decision function that takes a set facts and returns a conclusion. Factree will run your function and make sure it has all of the facts it needs to complete. If it doesn't, then Factree will tell you what's needed to continue.
5
6
 
6
7
  ## Installation
7
8
 
@@ -33,3 +34,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
33
34
 
34
35
  Bug reports and pull requests are welcome on GitHub at https://github.com/jstrater/factree.
35
36
 
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/factree.gemspec CHANGED
@@ -11,6 +11,7 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = %q{Decision trees that request facts as needed}
13
13
  spec.homepage = "https://github.com/jstrater/factree"
14
+ spec.license = "MIT"
14
15
 
15
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
17
  f.match(%r{^(test|spec|features)/})
@@ -1,28 +1,12 @@
1
- require 'factree/decision'
2
-
1
+ # @api private
3
2
  module Factree::Aggregate
4
- # Returns a new decision tree. The root of the tree calls the first Decision's decide function. If that returns nil, the next alternative is tried, and the next, and so on, until a valid Conclusion is returned.
5
- #
6
- # @param [Array<Decision>] decisions A decision followed by alternative decisions.
7
- # @return [Decision] A decision tree that checks the alternatives in order to reach a conclusion.
8
- def self.alternatives(*decisions)
9
- if decisions.empty?
10
- # Base case: no more decisions
11
- return Factree::Decision.new{ nil }
12
- else
13
- # Recursive case
14
- first, *rest = decisions
15
- remaining_alternatives = alternatives(*rest)
16
-
17
- return Factree::Decision.new first.required_facts do |facts|
18
- first_result = first.decide(facts)
19
-
20
- if first_result.nil?
21
- remaining_alternatives
22
- else
23
- first_result
24
- end
25
- end
3
+ # @see DSL.decide_between_alternatives
4
+ def self.alternatives(facts, *decide_procs)
5
+ conclusion = nil
6
+ decide_procs.each do |decide|
7
+ conclusion = decide.call(facts)
8
+ break unless conclusion.nil?
26
9
  end
10
+ conclusion
27
11
  end
28
12
  end
@@ -1,6 +1,4 @@
1
- require 'factree/node'
2
-
3
- class Factree::Conclusion < Factree::Node
1
+ class Factree::Conclusion
4
2
  attr_reader :value
5
3
 
6
4
  def initialize(value)
@@ -8,16 +6,8 @@ class Factree::Conclusion < Factree::Node
8
6
  freeze
9
7
  end
10
8
 
11
- def conclusion?
12
- true
13
- end
14
-
15
- def to_s
16
- "<Factree::Conclusion value=#{@value.inspect}>"
17
- end
18
-
19
9
  def ==(other)
20
- other.is_a?(self.class) &&
10
+ self.class == other.class &&
21
11
  @value == other.instance_variable_get(:@value)
22
12
  end
23
13
  end
data/lib/factree/dsl.rb CHANGED
@@ -1,24 +1,44 @@
1
- require 'factree/decision'
2
1
  require 'factree/conclusion'
3
- require 'factree/path'
2
+ require 'factree/pathfinder'
4
3
  require 'factree/aggregate'
5
4
 
6
- # Readable shortcuts to common functions
5
+ # Readable shortcuts to common functions.
7
6
  module Factree::DSL
8
- def decision(requires: [], &decide)
9
- required_facts = [requires].flatten
10
- Factree::Decision.new(required_facts, &decide)
11
- end
12
-
13
- def conclusion(value)
7
+ # Creates a {Conclusion} to return from a decision proc.
8
+ #
9
+ # The conclusion has an associated value (can be anything.)
10
+ #
11
+ # @param [Object] value Any value, including nil
12
+ # @return [Conclusion]
13
+ def conclusion(value=nil)
14
14
  Factree::Conclusion.new(value)
15
15
  end
16
16
 
17
- def path(through:, given: {})
18
- Factree::Path.through_tree(through, given)
17
+ # Navigates as far as possible through a decision tree with the given set of facts.
18
+ #
19
+ # The path will stop when either
20
+ # - a conclusion is returned, or
21
+ # - there aren't enough facts to make the decision.
22
+ #
23
+ # == Errors
24
+ #
25
+ # If a decision function fails to return a {Conclusion}, an {InvalidConclusionError} will be raised.
26
+ #
27
+ # @param [Hash] facts The set of facts used to make decisions
28
+ # @param [Proc] decide The decision proc. Takes a set of {Facts} and returns a {Conclusion}.
29
+ # @return [Path] Information about the path followed through the tree
30
+ def find_path(**facts, &decide)
31
+ Factree::Pathfinder.find(facts, &decide)
19
32
  end
20
33
 
21
- def decision_with_alternatives(*decisions)
22
- Factree::Aggregate.alternatives(*decisions)
34
+ # A tool for composing lists of decision procs to be tried one after another until a conclusion is reached. When a proc returns nil instead of a {Conclusion}, the next proc in decide_procs is used instead.
35
+ #
36
+ # If the last decision proc is reached and it returns nil, then this method returns nil.
37
+ #
38
+ # @param [Facts] facts The facts to pass through to the decision procs.
39
+ # @param [Array<#call>] decide_procs A decision proc followed by alternative procs.
40
+ # @return [Decision] A decision tree that checks the alternatives in order to reach a conclusion.
41
+ def decide_between_alternatives(facts, *decide_procs)
42
+ Factree::Aggregate.alternatives(facts, *decide_procs)
23
43
  end
24
44
  end
@@ -0,0 +1,66 @@
1
+ require 'forwardable'
2
+
3
+ class Factree::Facts
4
+ extend Forwardable
5
+ def_delegators :@hash,
6
+ :[],
7
+ :to_h,
8
+ :keys
9
+
10
+ def self.coerce(source)
11
+ return source if source.is_a? self
12
+ new(**source)
13
+ end
14
+
15
+ def initialize(**hash)
16
+ @hash = hash.freeze
17
+ freeze
18
+ end
19
+
20
+ # Checks to see if a fact is present.
21
+ #
22
+ # @param [Symbol] fact_name
23
+ # @return [Boolean]
24
+ def known?(fact_name)
25
+ @hash.has_key? fact_name
26
+ end
27
+
28
+ # Requires that certain facts are present in order to proceed with the decision. If any of the facts are missing, the path will stop here.
29
+ #
30
+ # @param [Array<Symbol>] fact_names Names of facts to require
31
+ # @return [void]
32
+ def require(*fact_names)
33
+ self.class.throw_missing_facts unless fact_names.all? { |name| known? name }
34
+ end
35
+
36
+ # Gets the value of a fact. This also {#require}s the fact.
37
+ #
38
+ # @param [Symbol] fact_name
39
+ # @return [Object]
40
+ def [](fact_name)
41
+ self.require(fact_name)
42
+ @hash[fact_name]
43
+ end
44
+
45
+ def ==(other)
46
+ self.to_h == other.to_h
47
+ end
48
+
49
+ # @api private
50
+ def self.throw_missing_facts
51
+ throw MISSING_FACTS
52
+ end
53
+
54
+ # @api private
55
+ def self.catch_missing_facts
56
+ catch(MISSING_FACTS) do
57
+ yield
58
+ end
59
+ end
60
+
61
+ # Kernel#catch uses object ID to match thrown values. This gives us a unique
62
+ # ID and a readable message in case it's thrown somewhere it's not expected.
63
+ #
64
+ # @api private
65
+ MISSING_FACTS = "Attempted to read missing facts from a Factree::Facts instance"
66
+ end
@@ -0,0 +1,22 @@
1
+ require 'delegate'
2
+
3
+ # {Facts} decorator to spy on calls to {Facts#require}
4
+ # @api private
5
+ class Factree::FactsSpy < SimpleDelegator
6
+ def initialize(facts, &before_require)
7
+ @facts = facts
8
+ @before_require = before_require
9
+ super(facts)
10
+ freeze
11
+ end
12
+
13
+ def require(*fact_names)
14
+ @before_require.call(*fact_names)
15
+ super
16
+ end
17
+
18
+ def [](fact_name)
19
+ @before_require.call(fact_name)
20
+ super
21
+ end
22
+ end
data/lib/factree/path.rb CHANGED
@@ -1,43 +1,42 @@
1
- require 'factree/pathfinder'
1
+ module Factree
2
+ # Raised when attempting to get a conclusion from an incomplete path
3
+ NoConclusionError = Class.new(StandardError)
2
4
 
3
- # Paths follow a sequence of nodes from the root of a decision tree toward the leaves. Use {Path.through_tree} to find a path through a tree.
4
- #
5
- # A path may or may not actually reach a conclusion. If it does, it will be {complete?} and return the {conclusion} value. If it doesn't, then you can get the names of all of the facts needed to get past the next decision from {required_facts}.
6
- class Factree::Path
7
- # Uses the given set of facts to find a path through the tree. The path will go as far as possible, and it will only stop when a conclusion is reached or when it doesn't have all the facts needed to make a decision.
8
- # @param [Factree::Decision, Factree::Conclusion] root The root node of the tree
9
- # @param [Hash] facts The set of facts used to make decisions
10
- # @return [Factree::Path]
11
- def self.through_tree(root, facts, finder: Factree::Pathfinder)
12
- new(finder.find_node_sequence(root, facts))
13
- end
14
-
15
- # Want to create a path? Use {.through_tree} instead.
16
- def initialize(node_sequence)
17
- @nodes = node_sequence.to_a.freeze
18
- freeze
19
- end
5
+ # {Path}s record useful information about an attempt to reach a {Conclusion} for a decision proc.
6
+ #
7
+ # A path may or may not actually reach a conclusion. If it does, it will be {complete?} and return the {conclusion} value. If it doesn't, then you can get the names of all of the facts needed to get past the next decision step from {required_facts}.
8
+ class Path
9
+ # Want to create a path? Use {DSL.find_path} instead.
10
+ #
11
+ # @api private
12
+ def initialize(required_facts=[], conclusion=nil)
13
+ @required_facts = required_facts.to_a.uniq
14
+ @conclusion = conclusion
15
+ freeze
16
+ end
20
17
 
21
- # A path is {complete?} if it has reached a conclusion.
22
- def complete?
23
- @nodes.last.conclusion?
24
- end
18
+ # A path is {complete?} if it has reached a conclusion.
19
+ def complete?
20
+ !@conclusion.nil?
21
+ end
25
22
 
26
- # Returns the conclusion value if this path is complete.
27
- def conclusion
28
- raise "Attempted to get conclusion from incomplete path" unless complete?
23
+ # Returns the conclusion value if this path is complete.
24
+ def conclusion
25
+ # We don't want to return nil to indicate a missing conclusion, since that could confused for a nil conclusion with a nil value.
26
+ raise Factree::NoConclusionError, "Attempted to get conclusion from incomplete path" unless complete?
29
27
 
30
- @nodes.last.value
31
- end
28
+ @conclusion.value
29
+ end
32
30
 
33
- # A list of the facts required to make all of the decisions along the path, including the last one. If the path is not complete, then the facts in this list are sufficient to progress to the next node.
34
- # @return [Symbol] A list of fact names in the order they're required in the tree
35
- def required_facts
36
- [].concat(*@nodes.map(&:required_facts)).uniq
37
- end
31
+ # A list of the facts that were required to get this far, plus any facts needed to make further progress. If the path is not complete, then the facts in this list are sufficient to progress past this point.
32
+ #
33
+ # @return [Array<Symbol>] A list of fact names in the order they're required in the tree
34
+ attr_reader :required_facts
38
35
 
39
- def ==(other)
40
- other.is_a?(self.class) &&
41
- @nodes == other.instance_variable_get(:@nodes)
36
+ def ==(other)
37
+ self.class == other.class &&
38
+ @required_facts == other.instance_variable_get(:@required_facts) &&
39
+ @conclusion == other.instance_variable_get(:@conclusion)
40
+ end
42
41
  end
43
42
  end
@@ -1,35 +1,35 @@
1
+ require 'factree/path'
2
+ require 'factree/facts'
3
+ require 'factree/facts_spy'
4
+
1
5
  module Factree
2
- CycleError = Class.new(StandardError)
3
- InvalidDecisionError = Class.new(StandardError)
6
+ # Raised when a decision proc fails to return a Conclusion as expected
7
+ InvalidConclusionError = Class.new(StandardError)
4
8
 
5
9
  # @api private
6
10
  module Pathfinder
7
- # Returns the sequence of nodes for the furthest path possible from the given node with the given set of facts.
8
- def self.find_node_sequence(node, facts, visited=Set.new)
9
- # No cycles allowed
10
- if visited.include? node
11
- raise Factree::CycleError,
12
- "Cycle detected in decision tree. Node appeared twice: #{node}"
11
+ # @see DSL.find_path
12
+ def self.find(raw_facts, &decide)
13
+ facts_without_spy = Factree::Facts.coerce(raw_facts)
14
+ required_facts = []
15
+ facts = Factree::FactsSpy.new(facts_without_spy) do |*fact_names|
16
+ required_facts += fact_names
13
17
  end
14
18
 
15
- # Base case: leaf node (conclusion)
16
- return [node] if node.conclusion?
17
-
18
- # Base case: not enough facts to call node.decide
19
- missing_facts = node.required_facts - facts.keys
20
- return [node] unless missing_facts.empty?
19
+ conclusion = Factree::Facts.catch_missing_facts do
20
+ conclusion = decide.call(facts)
21
+ type_check_conclusion conclusion, &decide
22
+ conclusion
23
+ end
21
24
 
22
- # Recursive case: return full path by prepending this node to the rest
23
- next_node = node.decide(facts)
24
- type_check_next_node(node, next_node)
25
- return [node] + find_node_sequence(next_node, facts, visited + [node])
25
+ Factree::Path.new(required_facts.uniq, conclusion)
26
26
  end
27
27
 
28
- private_class_method def self.type_check_next_node(source_node, next_node)
29
- unless next_node.is_a? Factree::Node
30
- raise Factree::InvalidDecisionError,
31
- "Expected #{source_node} to return a Factree::Node" +
32
- "from #decide. Got: #{next_node.inspect}"
28
+ private_class_method def self.type_check_conclusion(conclusion, &decide)
29
+ unless conclusion.is_a? Factree::Conclusion
30
+ raise Factree::InvalidConclusionError,
31
+ "Expected #{decide.inspect} to return a Factree::Conclusion. " +
32
+ "Got #{conclusion.inspect}"
33
33
  end
34
34
  end
35
35
  end
@@ -1,3 +1,3 @@
1
1
  module Factree
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.1"
3
3
  end
data/lib/factree.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "factree/version"
2
- require "factree/decision"
3
2
  require "factree/conclusion"
4
3
  require "factree/path"
5
4
  require "factree/aggregate"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Strater
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-04-22 00:00:00.000000000 Z
11
+ date: 2017-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -92,6 +92,7 @@ files:
92
92
  - ".travis.yml"
93
93
  - ".yardopts"
94
94
  - Gemfile
95
+ - LICENSE.txt
95
96
  - README.md
96
97
  - Rakefile
97
98
  - bin/console
@@ -100,14 +101,15 @@ files:
100
101
  - lib/factree.rb
101
102
  - lib/factree/aggregate.rb
102
103
  - lib/factree/conclusion.rb
103
- - lib/factree/decision.rb
104
104
  - lib/factree/dsl.rb
105
- - lib/factree/node.rb
105
+ - lib/factree/facts.rb
106
+ - lib/factree/facts_spy.rb
106
107
  - lib/factree/path.rb
107
108
  - lib/factree/pathfinder.rb
108
109
  - lib/factree/version.rb
109
110
  homepage: https://github.com/jstrater/factree
110
- licenses: []
111
+ licenses:
112
+ - MIT
111
113
  metadata: {}
112
114
  post_install_message:
113
115
  rdoc_options: []
@@ -125,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
127
  version: '0'
126
128
  requirements: []
127
129
  rubyforge_project:
128
- rubygems_version: 2.6.8
130
+ rubygems_version: 2.4.8
129
131
  signing_key:
130
132
  specification_version: 4
131
133
  summary: Decision trees that request facts as needed
@@ -1,35 +0,0 @@
1
- require 'factree/node'
2
-
3
- # A Decision is a single node in a decision tree. It's responsible for choosing the next node based on the given set of facts.
4
- class Factree::Decision < Factree::Node
5
- # The names of the facts required to make this decision
6
- # @return [Array<Symbol>]
7
- attr_reader :required_facts
8
-
9
- # Create a new decision tree node. The block is used to decide on the next node for a given set of facts when navigating through the tree.
10
- #
11
- # The block will be called with a hash containing all of the available facts. It must return another {Decision} (the next decision in the path through the tree) or a {Conclusion}.
12
- #
13
- # @param [Array<Symbol>] required_facts The names of facts required to decide on the next node.
14
- def initialize(required_facts=[], &decide)
15
- @decide = decide
16
- @required_facts = required_facts.uniq.freeze
17
- freeze
18
- end
19
-
20
- # Use the provided facts to decide on the next step.
21
- # @return [Decision, Conclusion] The next node
22
- def decide(facts)
23
- @decide.call(facts)
24
- end
25
-
26
- def to_s
27
- "<Factree::Decision decide=#{@decide} required_facts=[#{required_facts.join(", ")}]>"
28
- end
29
-
30
- def ==(other)
31
- other.is_a?(self.class) &&
32
- @required_facts == other.required_facts &&
33
- @decide == other.instance_variable_get(:@decide)
34
- end
35
- end
data/lib/factree/node.rb DELETED
@@ -1,10 +0,0 @@
1
- # This is the base class for nodes in a decision tree.
2
- class Factree::Node
3
- def conclusion?
4
- false
5
- end
6
-
7
- def required_facts
8
- []
9
- end
10
- end