factree 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/.yardopts +2 -0
- data/Gemfile +4 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/bin/console +9 -0
- data/bin/setup +6 -0
- data/factree.gemspec +27 -0
- data/lib/factree/aggregate.rb +28 -0
- data/lib/factree/conclusion.rb +23 -0
- data/lib/factree/decision.rb +35 -0
- data/lib/factree/dsl.rb +24 -0
- data/lib/factree/node.rb +10 -0
- data/lib/factree/path.rb +43 -0
- data/lib/factree/pathfinder.rb +36 -0
- data/lib/factree/version.rb +3 -0
- data/lib/factree.rb +10 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5c35f29630bd9cb9399531725ed218c5da73e1c2
|
4
|
+
data.tar.gz: '09686d69ee9aeb3ea37f75d13af3c2f686dfa11e'
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: faf9cbfe994babe6b72d2038a8cab88f4123db61d505afdb26e0f07b57175456b49293558836ad48cd9809da8f93d78a195608910efa4eab5e71b5513c0b1bac
|
7
|
+
data.tar.gz: 2a279be3fa08b4fb588195eb6d6fd948ca759f19d8d622701478cf0694b328e6e299380aede9998c33523030720d2bac87f1974511ef6d2d93bc7c25f6ed7f80
|
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.0
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Factree
|
2
|
+
[![Build Status](https://travis-ci.org/jstrater/factree.svg?branch=master)](https://travis-ci.org/jstrater/factree)
|
3
|
+
|
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
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'factree'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install factree
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
[API documentation](http://www.rubydoc.info/github/jstrater/factree/)
|
25
|
+
|
26
|
+
## Development
|
27
|
+
|
28
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
29
|
+
|
30
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jstrater/factree.
|
35
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/factree.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'factree/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "factree"
|
8
|
+
spec.version = Factree::VERSION
|
9
|
+
spec.authors = ["Josh Strater"]
|
10
|
+
spec.email = ["jstrater@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Decision trees that request facts as needed}
|
13
|
+
spec.homepage = "https://github.com/jstrater/factree"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
25
|
+
spec.add_development_dependency "pry"
|
26
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'factree/decision'
|
2
|
+
|
3
|
+
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
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'factree/node'
|
2
|
+
|
3
|
+
class Factree::Conclusion < Factree::Node
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
def initialize(value)
|
7
|
+
@value = value
|
8
|
+
freeze
|
9
|
+
end
|
10
|
+
|
11
|
+
def conclusion?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"<Factree::Conclusion value=#{@value.inspect}>"
|
17
|
+
end
|
18
|
+
|
19
|
+
def ==(other)
|
20
|
+
other.is_a?(self.class) &&
|
21
|
+
@value == other.instance_variable_get(:@value)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,35 @@
|
|
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/dsl.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'factree/decision'
|
2
|
+
require 'factree/conclusion'
|
3
|
+
require 'factree/path'
|
4
|
+
require 'factree/aggregate'
|
5
|
+
|
6
|
+
# Readable shortcuts to common functions
|
7
|
+
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)
|
14
|
+
Factree::Conclusion.new(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def path(through:, given: {})
|
18
|
+
Factree::Path.through_tree(through, given)
|
19
|
+
end
|
20
|
+
|
21
|
+
def decision_with_alternatives(*decisions)
|
22
|
+
Factree::Aggregate.alternatives(*decisions)
|
23
|
+
end
|
24
|
+
end
|
data/lib/factree/node.rb
ADDED
data/lib/factree/path.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'factree/pathfinder'
|
2
|
+
|
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
|
20
|
+
|
21
|
+
# A path is {complete?} if it has reached a conclusion.
|
22
|
+
def complete?
|
23
|
+
@nodes.last.conclusion?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the conclusion value if this path is complete.
|
27
|
+
def conclusion
|
28
|
+
raise "Attempted to get conclusion from incomplete path" unless complete?
|
29
|
+
|
30
|
+
@nodes.last.value
|
31
|
+
end
|
32
|
+
|
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
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
other.is_a?(self.class) &&
|
41
|
+
@nodes == other.instance_variable_get(:@nodes)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Factree
|
2
|
+
CycleError = Class.new(StandardError)
|
3
|
+
InvalidDecisionError = Class.new(StandardError)
|
4
|
+
|
5
|
+
# @api private
|
6
|
+
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}"
|
13
|
+
end
|
14
|
+
|
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?
|
21
|
+
|
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])
|
26
|
+
end
|
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}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/factree.rb
ADDED
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: factree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Josh Strater
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-04-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: yard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.9'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- jstrater@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".ruby-version"
|
92
|
+
- ".travis.yml"
|
93
|
+
- ".yardopts"
|
94
|
+
- Gemfile
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/console
|
98
|
+
- bin/setup
|
99
|
+
- factree.gemspec
|
100
|
+
- lib/factree.rb
|
101
|
+
- lib/factree/aggregate.rb
|
102
|
+
- lib/factree/conclusion.rb
|
103
|
+
- lib/factree/decision.rb
|
104
|
+
- lib/factree/dsl.rb
|
105
|
+
- lib/factree/node.rb
|
106
|
+
- lib/factree/path.rb
|
107
|
+
- lib/factree/pathfinder.rb
|
108
|
+
- lib/factree/version.rb
|
109
|
+
homepage: https://github.com/jstrater/factree
|
110
|
+
licenses: []
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 2.6.8
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: Decision trees that request facts as needed
|
132
|
+
test_files: []
|