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 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
@@ -0,0 +1,10 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in forester.gemspec
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem "rake", "~> 11.2"
8
+ gem "minitest", "~> 5.9"
9
+ gem 'pry-byebug'
10
+ end
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
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
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
@@ -0,0 +1,18 @@
1
+ module Forester
2
+ class Version
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ PATCH = 1
6
+ PRE = nil
7
+
8
+ class << self
9
+
10
+ # @return [String]
11
+ def to_s
12
+ [MAJOR, MINOR, PATCH, PRE].compact.join('.')
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ 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