forester 0.0.1
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.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +41 -0
- data/README.md +47 -0
- data/Rakefile +8 -0
- data/forester.gemspec +24 -0
- data/lib/forester/aggregators.rb +42 -0
- data/lib/forester/hash_with_indifferent_access.rb +28 -0
- data/lib/forester/node_content.rb +23 -0
- data/lib/forester/node_content_factory.rb +19 -0
- data/lib/forester/tree_factory.rb +47 -0
- data/lib/forester/tree_node.rb +38 -0
- data/lib/forester/version.rb +18 -0
- data/lib/forester.rb +11 -0
- data/test/test_treenode.rb +25 -0
- data/test/trees/simple_tree.yml +45 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 617105460b435e325d7fdc13dfe7b9361fc2bddc
|
4
|
+
data.tar.gz: 6adf0a77fa3d486dd08b60f38ca4cce64c98de5b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 018d6aef999f11fe3b815957a8a34d67661b5824b703caea5905c27b964203e939766a759fd7f5d75352c1b4b85b4a830026606df2122de38491dd73f6780858
|
7
|
+
data.tar.gz: f7716b917ce1657d4ad50b13a68cc105ee9efc6319873c3f99d0dabe15777c7d0729473caf64025a18511b7796b588275ff0db1fbd49a6c9a2f00cba549fe31e
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
forester (0.0.1)
|
5
|
+
enzymator (~> 0.0)
|
6
|
+
rubytree (~> 0.9)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: http://rubygems.org/
|
10
|
+
specs:
|
11
|
+
byebug (9.0.5)
|
12
|
+
coderay (1.1.1)
|
13
|
+
enzymator (0.0.1)
|
14
|
+
json (1.8.3)
|
15
|
+
method_source (0.8.2)
|
16
|
+
minitest (5.9.0)
|
17
|
+
pry (0.10.4)
|
18
|
+
coderay (~> 1.1.0)
|
19
|
+
method_source (~> 0.8.1)
|
20
|
+
slop (~> 3.4)
|
21
|
+
pry-byebug (3.4.0)
|
22
|
+
byebug (~> 9.0)
|
23
|
+
pry (~> 0.10)
|
24
|
+
rake (11.2.2)
|
25
|
+
rubytree (0.9.7)
|
26
|
+
json (~> 1.8)
|
27
|
+
structured_warnings (~> 0.2)
|
28
|
+
slop (3.6.0)
|
29
|
+
structured_warnings (0.2.0)
|
30
|
+
|
31
|
+
PLATFORMS
|
32
|
+
ruby
|
33
|
+
|
34
|
+
DEPENDENCIES
|
35
|
+
forester!
|
36
|
+
minitest (~> 5.9)
|
37
|
+
pry-byebug
|
38
|
+
rake (~> 11.2)
|
39
|
+
|
40
|
+
BUNDLED WITH
|
41
|
+
1.12.5
|
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# Forester
|
2
|
+
|
3
|
+
Based on *rubytree* and *enzymator*, this gem lets you build trees and run queries against them.
|
4
|
+
|
5
|
+
## FAQ
|
6
|
+
|
7
|
+
- What's the difference between forester and rubytree?
|
8
|
+
|
9
|
+
The main class provided by the *rubytree* gem is **Tree::TreeNode**. In the case of forester, it is **Forester::TreeNode**, which is nothing more than a subclass of the former.
|
10
|
+
|
11
|
+
- Why is this a separate gem and not just a pull request in rubytree?
|
12
|
+
|
13
|
+
Because I needed to develop a certain feature on top of TreeNode in a time-sensitive manner. Rubytree devs should feel free to take anything they like from this project.
|
14
|
+
|
15
|
+
- Why is forester not a fork of rubytree?
|
16
|
+
|
17
|
+
Because I didn't feel the need to copy the whole codebase. All I needed was to extend the functionality of a class.
|
18
|
+
|
19
|
+
- What can I do with forester?
|
20
|
+
|
21
|
+
Everything you can do with rubytree, possibly in a more intention-revealing way, plus arbitrary, configurable aggregations on trees. Read the specs for more details.
|
22
|
+
|
23
|
+
## Installation
|
24
|
+
|
25
|
+
Add this line to your application's Gemfile:
|
26
|
+
|
27
|
+
gem 'forester'
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install forester
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
Build your tree with any of the factory methods in TreeFactory, and then start messaging the resulting instance of TreeNode.
|
40
|
+
|
41
|
+
## Contributing
|
42
|
+
|
43
|
+
1. Fork it
|
44
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
45
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
46
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
47
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/forester.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'forester/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'forester'
|
8
|
+
s.version = '0.0.1'
|
9
|
+
s.date = '2016-07-17'
|
10
|
+
s.summary = "A gem to represent and interact with tree data structures"
|
11
|
+
s.description = "Based on rubytree and enzymator, this gem lets you build trees and run queries against them."
|
12
|
+
s.authors = ["Eugenio Bruno"]
|
13
|
+
s.email = 'eugeniobruno@gmail.com'
|
14
|
+
s.homepage = 'http://rubygems.org/gems/forester'
|
15
|
+
s.license = 'MIT'
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split($/)
|
18
|
+
s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
19
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
20
|
+
s.require_paths = ['lib']
|
21
|
+
|
22
|
+
s.add_runtime_dependency 'rubytree', ['~> 0.9']
|
23
|
+
s.add_runtime_dependency 'enzymator', ['~> 0.0']
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Forester
|
2
|
+
module Aggregators
|
3
|
+
|
4
|
+
def aggregate(config)
|
5
|
+
Enzymator::Aggregation.new(config).run_on(self)
|
6
|
+
end
|
7
|
+
|
8
|
+
def values_by_subtree_of_level(options = {})
|
9
|
+
default_options = {
|
10
|
+
level: 1,
|
11
|
+
group_field: 'name',
|
12
|
+
aggregation_field: 'value',
|
13
|
+
if_field_missing: lambda { |c| [] },
|
14
|
+
include_ancestry_in_keys: false, # if false, with_root is ignored
|
15
|
+
with_root: false,
|
16
|
+
}
|
17
|
+
|
18
|
+
options = default_options.merge(options)
|
19
|
+
|
20
|
+
Enzymator::Aggregation.new({
|
21
|
+
null_result: lambda { Hash.new },
|
22
|
+
initial_clusters: lambda { |tree| tree.nodes_of_level(options[:level]) },
|
23
|
+
map: lambda do |node|
|
24
|
+
key_nodes = if options[:include_ancestry_in_keys]
|
25
|
+
node.ancestry(options[:with_root], true)
|
26
|
+
else
|
27
|
+
[node]
|
28
|
+
end
|
29
|
+
key = key_nodes.map { |kn| kn.get(options[:group_field]) { |n| n.object_id } }
|
30
|
+
key = key.first if key.one?
|
31
|
+
key
|
32
|
+
end,
|
33
|
+
enumerator: lambda { |level| level.each_node },
|
34
|
+
map_each: lambda { |node| node.get(options[:aggregation_field], &options[:if_field_missing]) },
|
35
|
+
reduce_each: lambda { |acum, value| Array(acum).concat(Array(value)) },
|
36
|
+
reduce: lambda { |prev, group, result| prev.merge( { group => result } ) },
|
37
|
+
|
38
|
+
}).run_on(self, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Forester
|
2
|
+
class HashWithIndifferentAccess < SimpleDelegator
|
3
|
+
|
4
|
+
def has_key?(key)
|
5
|
+
equivs(key).any? { |k| super(k) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch(key, default = nil, &block)
|
9
|
+
maybe_key = equivs(key).select { |k| __getobj__.has_key?(k) }
|
10
|
+
if maybe_key.empty?
|
11
|
+
super(key)
|
12
|
+
else
|
13
|
+
super(maybe_key.first)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
equivs(key).map { |k| super(k) }.compact.first || super
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def equivs(key)
|
24
|
+
[key.to_sym, key.to_s]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Forester
|
2
|
+
class NodeContent
|
3
|
+
|
4
|
+
def initialize(hash)
|
5
|
+
@hash = HashWithIndifferentAccess.new(hash)
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_missing(name, *args, &block)
|
9
|
+
if @hash.has_key?(name)
|
10
|
+
@hash.fetch(name)
|
11
|
+
elsif block_given?
|
12
|
+
yield self
|
13
|
+
else
|
14
|
+
raise ArgumentError.new("the node \"#{self.name}\" does not have any \"#{name}\"")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def respond_to_missing?(name, include_private = false)
|
19
|
+
@hash.has_key?(name) || super
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Forester
|
2
|
+
class NodeContentFactory
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def from_hash(hash, children_key)
|
7
|
+
NodeContent.new(HashWithIndifferentAccess.new(without_key(hash, children_key)))
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def without_key(hash, key)
|
13
|
+
hash.reject { |k, _| k == key }
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Forester
|
2
|
+
class TreeFactory
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def from_yaml_file(file)
|
7
|
+
from_hash_with_root_key(YAML.load_file(file))
|
8
|
+
end
|
9
|
+
|
10
|
+
def from_root_hash(hash, children_key = :children)
|
11
|
+
from_hash_with_root_key({ root: hash }, children_key)
|
12
|
+
end
|
13
|
+
|
14
|
+
def from_hash_with_root_key(hash, children_key = :children, root_key = :root)
|
15
|
+
dummy_root = TreeNode.new('<TEMP>')
|
16
|
+
real_root = fetch_indifferently(hash, root_key)
|
17
|
+
|
18
|
+
tree = with_children(dummy_root, [real_root], children_key).first_child
|
19
|
+
tree.detached_subtree_copy
|
20
|
+
end
|
21
|
+
|
22
|
+
def from_hash(hash, children_key, uid = SecureRandom.uuid)
|
23
|
+
name = uid
|
24
|
+
content = NodeContentFactory.from_hash(hash, children_key)
|
25
|
+
TreeNode.new(name, content)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def fetch_indifferently(hash, key, default = nil)
|
31
|
+
[key.to_sym, key.to_s].map { |k| hash[k] }.compact.first || default || hash.fetch(root_key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_children(tree_node, children, children_key)
|
35
|
+
children.each do |child|
|
36
|
+
child_node = TreeFactory.from_hash(child, children_key)
|
37
|
+
child_children = fetch_indifferently(child, children_key, []) # nth level
|
38
|
+
|
39
|
+
tree_node << with_children(child_node, child_children, children_key)
|
40
|
+
end
|
41
|
+
tree_node
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Forester
|
2
|
+
class TreeNode < Tree::TreeNode
|
3
|
+
|
4
|
+
include Aggregators
|
5
|
+
|
6
|
+
def ancestry(include_root = true, include_self = false, descending = true)
|
7
|
+
ancestors = self.parentage || []
|
8
|
+
ancestors = ancestors[0...-1] unless include_root
|
9
|
+
ancestors = ancestors.unshift(self) if include_self
|
10
|
+
if descending then ancestors.reverse else ancestors end
|
11
|
+
end
|
12
|
+
|
13
|
+
def nodes_of_level(l)
|
14
|
+
if l.between?(0, max_level) then each_level.take(l + 1).last else [] end
|
15
|
+
end
|
16
|
+
|
17
|
+
def max_level
|
18
|
+
node_height
|
19
|
+
end
|
20
|
+
|
21
|
+
def each_level
|
22
|
+
breadth_each.slice_when { |prev_node, next_node| prev_node.level < next_node.level }
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(field, &block)
|
26
|
+
content.public_send(field, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def contents
|
30
|
+
each_node.map(&:content)
|
31
|
+
end
|
32
|
+
|
33
|
+
def each_node
|
34
|
+
breadth_each
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/forester.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'tree'
|
2
|
+
require 'enzymator'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'forester/aggregators'
|
7
|
+
require 'forester/tree_node'
|
8
|
+
require 'forester/tree_factory'
|
9
|
+
require 'forester/node_content'
|
10
|
+
require 'forester/node_content_factory'
|
11
|
+
require 'forester/hash_with_indifferent_access'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'forester'
|
3
|
+
|
4
|
+
class TreeNodeTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_tree_node
|
7
|
+
|
8
|
+
path_to_trees = "#{File.dirname(__FILE__)}/trees"
|
9
|
+
tree = Forester::TreeFactory.from_yaml_file("#{path_to_trees}/simple_tree.yml")
|
10
|
+
|
11
|
+
assert_equal 10, tree.size
|
12
|
+
|
13
|
+
assert_equal (0..9).reduce(:+), tree.reduce(0) { |acum, node| acum + node.get('value') }
|
14
|
+
|
15
|
+
expected = {
|
16
|
+
["First node of level 1", "First node of level 2"] => ["Already in level 2", "I want to be the very best", "like no one ever was"],
|
17
|
+
["First node of level 1", "Second node of level 2"] => ["I have a sibling to my left", "She wants to catch them all"],
|
18
|
+
["Second node of level 1", "Third node of level 2"] => ["Reached level 3", "It's dark", "A hidden secret lies in the deepest leaves...", "Just kidding.", "Could forester handle trees with hundreds of levels?", "Maybe."]}
|
19
|
+
|
20
|
+
aggregation_result = tree.values_by_subtree_of_level(level: 2, aggregation_field: 'strings', include_ancestry_in_keys: true)
|
21
|
+
|
22
|
+
assert_equal expected, aggregation_result
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
---
|
2
|
+
root:
|
3
|
+
name: root
|
4
|
+
value: 0
|
5
|
+
strings:
|
6
|
+
- This is the root
|
7
|
+
children:
|
8
|
+
- name: First node of level 1
|
9
|
+
value: 1
|
10
|
+
children:
|
11
|
+
- name: First node of level 2
|
12
|
+
value: 2
|
13
|
+
strings:
|
14
|
+
- Already in level 2
|
15
|
+
- I want to be the very best
|
16
|
+
- like no one ever was
|
17
|
+
- name: Second node of level 2
|
18
|
+
value: 3
|
19
|
+
strings:
|
20
|
+
- I have a sibling to my left
|
21
|
+
- She wants to catch them all
|
22
|
+
- name: Second node of level 1
|
23
|
+
value: 4
|
24
|
+
children:
|
25
|
+
- name: Third node of level 2
|
26
|
+
value: 5
|
27
|
+
children:
|
28
|
+
- name: First node of level 3
|
29
|
+
value: 6
|
30
|
+
strings:
|
31
|
+
- Reached level 3
|
32
|
+
- It's dark
|
33
|
+
- name: Second node of level 3
|
34
|
+
value: 7
|
35
|
+
strings:
|
36
|
+
- A hidden secret lies in the deepest leaves...
|
37
|
+
- Just kidding.
|
38
|
+
children:
|
39
|
+
- name: First node of level 4
|
40
|
+
value: 8
|
41
|
+
- name: Second child of level 3
|
42
|
+
value: 9
|
43
|
+
strings:
|
44
|
+
- Could forester handle trees with hundreds of levels?
|
45
|
+
- Maybe.
|
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: forester
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eugenio Bruno
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rubytree
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.9'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: enzymator
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.0'
|
41
|
+
description: Based on rubytree and enzymator, this gem lets you build trees and run
|
42
|
+
queries against them.
|
43
|
+
email: eugeniobruno@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- Gemfile
|
49
|
+
- Gemfile.lock
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- forester.gemspec
|
53
|
+
- lib/forester.rb
|
54
|
+
- lib/forester/aggregators.rb
|
55
|
+
- lib/forester/hash_with_indifferent_access.rb
|
56
|
+
- lib/forester/node_content.rb
|
57
|
+
- lib/forester/node_content_factory.rb
|
58
|
+
- lib/forester/tree_factory.rb
|
59
|
+
- lib/forester/tree_node.rb
|
60
|
+
- lib/forester/version.rb
|
61
|
+
- test/test_treenode.rb
|
62
|
+
- test/trees/simple_tree.yml
|
63
|
+
homepage: http://rubygems.org/gems/forester
|
64
|
+
licenses:
|
65
|
+
- MIT
|
66
|
+
metadata: {}
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 2.5.1
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: A gem to represent and interact with tree data structures
|
87
|
+
test_files:
|
88
|
+
- test/test_treenode.rb
|
89
|
+
- test/trees/simple_tree.yml
|