factree 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.4.0
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.14.6
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ lib/**/*.rb - README.md
2
+ --embed-mixin DSL
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in factree.gemspec
4
+ gemspec
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
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "factree"
5
+
6
+ include Factree::DSL
7
+
8
+ require "pry"
9
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
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
@@ -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
@@ -0,0 +1,10 @@
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Factree
2
+ VERSION = "0.2.0"
3
+ end
data/lib/factree.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "factree/version"
2
+ require "factree/decision"
3
+ require "factree/conclusion"
4
+ require "factree/path"
5
+ require "factree/aggregate"
6
+ require "factree/dsl"
7
+
8
+ module Factree
9
+ extend DSL
10
+ end
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: []