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 +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +2 -1
- data/LICENSE.txt +21 -0
- data/README.md +5 -1
- data/factree.gemspec +1 -0
- data/lib/factree/aggregate.rb +8 -24
- data/lib/factree/conclusion.rb +2 -12
- data/lib/factree/dsl.rb +33 -13
- data/lib/factree/facts.rb +66 -0
- data/lib/factree/facts_spy.rb +22 -0
- data/lib/factree/path.rb +34 -35
- data/lib/factree/pathfinder.rb +23 -23
- data/lib/factree/version.rb +1 -1
- data/lib/factree.rb +0 -1
- metadata +8 -6
- data/lib/factree/decision.rb +0 -35
- data/lib/factree/node.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1acd242c9352248bdd755e245c55e026d16cdc0
|
4
|
+
data.tar.gz: b888a69e8cd08b85e1d3177e2b38b5c3f8cda78c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf47060ea361430911941b26da4cef20bb527847c9f0efd522f3bac3b32d7fe94dc56d750dc91f1947598d6e6aa07bcb8573d6a462a350af2c9c17f42782795a
|
7
|
+
data.tar.gz: 42455c60d1d5b9ddb10a0d50cd6a7c2d4847e855453a60c9f1058bce56327dc1469684e5358c243d36db5513779ef33d4a0476ab852cec54c021038f8a4d96f2
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.1.5
|
data/.travis.yml
CHANGED
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
|
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)/})
|
data/lib/factree/aggregate.rb
CHANGED
@@ -1,28 +1,12 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# @api private
|
3
2
|
module Factree::Aggregate
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
data/lib/factree/conclusion.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
|
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
|
-
|
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/
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
1
|
+
module Factree
|
2
|
+
# Raised when attempting to get a conclusion from an incomplete path
|
3
|
+
NoConclusionError = Class.new(StandardError)
|
2
4
|
|
3
|
-
#
|
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
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
18
|
+
# A path is {complete?} if it has reached a conclusion.
|
19
|
+
def complete?
|
20
|
+
!@conclusion.nil?
|
21
|
+
end
|
25
22
|
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
28
|
+
@conclusion.value
|
29
|
+
end
|
32
30
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
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
|
data/lib/factree/pathfinder.rb
CHANGED
@@ -1,35 +1,35 @@
|
|
1
|
+
require 'factree/path'
|
2
|
+
require 'factree/facts'
|
3
|
+
require 'factree/facts_spy'
|
4
|
+
|
1
5
|
module Factree
|
2
|
-
|
3
|
-
|
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
|
-
#
|
8
|
-
def self.
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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.
|
29
|
-
unless
|
30
|
-
raise Factree::
|
31
|
-
"Expected #{
|
32
|
-
"
|
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
|
data/lib/factree/version.rb
CHANGED
data/lib/factree.rb
CHANGED
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.
|
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-
|
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/
|
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.
|
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
|
data/lib/factree/decision.rb
DELETED
@@ -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
|