key_tree 0.5.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9cfde9ce8a431f8ce658b0d092958dcb97cf237fd1132b459a80191b72ef002b
4
- data.tar.gz: f51c7e7ce679a1830d929e24114089820f8c1a79082c3e487ef5ac4fa23fa81f
3
+ metadata.gz: 535d58bfc5880880fe498d115be5d62a7cb4670661c17d46f4a44087f02c36a2
4
+ data.tar.gz: 42896ddb6f71accd5a23504ae63e38abf242bf6f839df4b599d7e567c48fcb4f
5
5
  SHA512:
6
- metadata.gz: a904cca7e3be01bfb902b5c144685fc8c6dd3b52227f22fbd048e8d9e98e1675473bf766cc159ed422b6ac1c35a3b94969beea50dd29bdb528e4242a149a0c77
7
- data.tar.gz: c9f17cdd2fff51099699d625a72a57bec483859369518c1b273db0478099320426aa24292a71eb07234d8f92ccabcda06875319356c9dae209b267be2657e75e
6
+ metadata.gz: a0e0c52ca0cc4766b226d8f31efcf1e97aa154a8a67f132d5f4f2c0764bd2c45ff05e8a9686f25ed3bfcc87caa144960ffd9dfdcf083084e71d490d9418d2444
7
+ data.tar.gz: 8cff399bdf835fb25fb45d0c50cf562e7f080ef9d5a88b7fd53c2a9c029b747fa1065bbf83455ffe1ef7a91eba5a91ef28b0ee2688eb06c942a7a4110a0c6eb7
data/.rubocop.yml CHANGED
@@ -1,3 +1,6 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+
1
4
  Metrics/BlockLength:
2
5
  Exclude:
3
6
  - 'spec/*_spec.rb'
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
data/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Release Notes
2
2
 
3
+ ## v0.6.0 – 2018-05-30
4
+
5
+ ### Major changes
6
+
7
+ * Updated to Ruby ~> 2.3
8
+ * Added refinements module for `to_key_*` conversions in core classes
9
+ * `KeyTree::Tree` rewritten from scratch, to use a refinements
10
+ to an internal `Hash` structure instead of subclassing `Hash`
11
+
12
+ ### New methods
13
+
14
+ * `KeyTree::Forest#key_paths`
15
+ * `KeyTree::Forest#to_key_forest`
16
+ * `KeyTree::Forest#to_key_wood`
17
+ * `KeyTree::Path#===`
18
+ * `KeyTree::Path#to_key_path`
19
+ * `KeyTree::Tree#delete(key_path)`
20
+ * `KeyTree::Tree#delete!(key_path)`
21
+ * `KeyTree::Tree#store(key_path, new_value)`
22
+ * `KeyTree::Tree#store!(key_path, new_value)`
23
+ * `KeyTree::Tree#to_key_tree`
24
+ * `KeyTree::Tree#to_key_wood`
25
+
26
+ * Using `KeyTree::Refinements`
27
+ * `Array#to_key_forest`
28
+ * `Array#to_key_path`
29
+ * `Array#to_key_wood`
30
+ * `Hash#to_key_tree`
31
+ * `Hash#to_key_wood`
32
+ * `String#to_key_path`
33
+ * `Symbol#to_key_path`
34
+
35
+ * Using `KeyTree::Refine::DeepHash`
36
+ * `Hash#deep`
37
+ * `Hash#deep_delete(key_path)`
38
+ * `Hash#deep_fetch(key_path, default, &default_proc)`
39
+ * `Hash#deep_merge(other)`
40
+ * `Hash#deep_merge!(other)`
41
+ * `Hash#deep_store(key_path, new_value)`
42
+ * `Hash#deep_transform_keys(&block)`
43
+ * `Hash#deep_transform_keys!(&block)`
44
+
45
+ ### Removed methods
46
+
47
+ * `KeyTree::Tree` no longer inherits `Hash`, but most of the
48
+ inherited methods didn't work properly anyway
49
+
3
50
  ## v0.5.3 – 2018-05-25
4
51
 
5
52
  ### Bug fixes
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/setup'
2
4
  require 'bundler/gem_tasks'
3
5
 
@@ -11,4 +13,4 @@ desc 'Run RSpec'
11
13
  require 'rspec/core/rake_task'
12
14
  RSpec::Core::RakeTask.new(:spec)
13
15
 
14
- task default: %i[rubocop spec]
16
+ task default: %i[spec rubocop]
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'key_tree'
data/key_tree.gemspec CHANGED
@@ -1,17 +1,22 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'key_tree/version'
5
+
6
+ begin
7
+ require 'git-version-bump'
8
+ GIT_VERSION = GVB.version.freeze
9
+ rescue LoadError
10
+ GIT_VERSION = '0.0.0.UNDEF'
11
+ end
5
12
 
6
13
  Gem::Specification.new do |spec|
7
14
  spec.name = 'key_tree'
8
- spec.version = KeyTree::VERSION
9
- spec.date = KeyTree::DATE
15
+ spec.version = GIT_VERSION
10
16
  spec.authors = ['Calle Englund']
11
17
  spec.email = ['calle@discord.bofh.se']
12
18
 
13
19
  spec.summary = 'Manage trees of keys'
14
- spec.description = spec.summary
15
20
  spec.homepage = 'https://github.com/notcalle/ruby-keytree'
16
21
  spec.license = 'MIT'
17
22
 
@@ -22,10 +27,13 @@ Gem::Specification.new do |spec|
22
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
28
  spec.require_paths = ['lib']
24
29
 
25
- spec.add_dependency 'git-version-bump', '~> 0.15'
30
+ spec.platform = Gem::Platform::RUBY
31
+ spec.required_ruby_version = '~> 2.3'
26
32
 
27
33
  spec.add_development_dependency 'bundler', '~> 1.16'
34
+ spec.add_development_dependency 'git-version-bump', '~> 0.15'
28
35
  spec.add_development_dependency 'rake', '~> 10.0'
29
36
  spec.add_development_dependency 'rspec', '~> 3.0'
30
37
  spec.add_development_dependency 'rubocop', '~> 0.52'
38
+ spec.add_development_dependency 'ruby-prof', '~> 0.17'
31
39
  end
@@ -1,7 +1,12 @@
1
- require 'key_tree/tree'
2
- require 'key_tree/meta_data'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'meta_data'
4
+ require_relative 'refinements'
5
+ require_relative 'tree'
6
+
7
+ module KeyTree # rubocop:disable Style/Documentation
8
+ using Refinements
3
9
 
4
- module KeyTree
5
10
  #
6
11
  # A forest is a (possibly nested) collection of trees
7
12
  #
@@ -10,10 +15,13 @@ module KeyTree
10
15
 
11
16
  def self.[](*contents)
12
17
  contents.reduce(Forest.new) do |result, content|
13
- result << KeyTree[content]
18
+ result << content.to_key_wood
14
19
  end
15
20
  end
16
21
 
22
+ alias to_key_forest itself
23
+ alias to_key_wood itself
24
+
17
25
  # For a numeric key, return the n:th tree in the forest
18
26
  #
19
27
  # For a key path convertable key, return the closest match in the forest
@@ -24,8 +32,12 @@ module KeyTree
24
32
  #
25
33
  def [](key)
26
34
  return super(key) if key.is_a?(Numeric)
27
- tree_with_default_key(key)[key]
28
- rescue KeyError
35
+
36
+ trees.lazy.each do |tree|
37
+ result = tree[key]
38
+ return result unless result.nil?
39
+ break if tree.prefix?(key)
40
+ end
29
41
  nil
30
42
  end
31
43
 
@@ -37,11 +49,27 @@ module KeyTree
37
49
  end
38
50
 
39
51
  def key?(key)
40
- trees.any? { |tree| tree.key?(key) }
52
+ trees.lazy.any? { |tree| tree.key?(key) }
41
53
  end
54
+ alias has_key? key?
42
55
 
43
56
  def prefix?(key)
44
- trees.any? { |tree_or_forest| tree_or_forest.prefix?(key) }
57
+ trees.lazy.any? { |tree| tree.prefix?(key) }
58
+ end
59
+ alias has_prefix? prefix?
60
+
61
+ def key_path?(key)
62
+ trees.lazy.any? { |tree| tree.key_path?(key) }
63
+ end
64
+ alias has_key_path? key_path?
65
+
66
+ def include?(needle)
67
+ case needle
68
+ when Tree, Forest
69
+ super(needle)
70
+ else
71
+ key_path?(needle)
72
+ end
45
73
  end
46
74
 
47
75
  # Flattening a forest produces a tree with the equivalent view of key paths
@@ -64,17 +92,15 @@ module KeyTree
64
92
  end
65
93
  end
66
94
 
67
- private
68
-
69
- def tree_with_default_key(key)
70
- result = trees.detect do |tree|
71
- tree.prefix?(key) || tree.default_key?(key)
72
- end
73
- result || raise(KeyError, %(key not found: "#{key}"))
95
+ # Return all visible key paths in the forest
96
+ def key_paths
97
+ trees.reduce(Set.new) { |result, tree| result.merge(tree.key_paths) }
74
98
  end
75
99
 
100
+ private
101
+
76
102
  def tree_with_key(key)
77
- result = trees.detect do |tree|
103
+ result = trees.lazy.detect do |tree|
78
104
  tree.prefix?(key)
79
105
  end
80
106
  result || raise(KeyError, %(key not found: "#{key}"))
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KeyTree
2
4
  module Loader
3
5
  # KeyTree loader that ignores payload and produces an empty tree
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'key_tree/loader/nil'
2
4
 
3
5
  module KeyTree
@@ -1,4 +1,4 @@
1
- require 'key_tree/tree'
1
+ # frozen_string_literal: true
2
2
 
3
3
  module KeyTree
4
4
  #
@@ -20,3 +20,5 @@ module KeyTree
20
20
  end
21
21
  end
22
22
  end
23
+
24
+ require_relative 'tree'
data/lib/key_tree/path.rb CHANGED
@@ -1,4 +1,10 @@
1
- module KeyTree
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'refinements'
4
+
5
+ module KeyTree # rubocop:disable Style/Documentation
6
+ using Refinements
7
+
2
8
  #
3
9
  # Representation of the key path to a value in a key tree
4
10
  #
@@ -8,9 +14,9 @@ module KeyTree
8
14
  #
9
15
  # Make a new key path from one or more keys or paths
10
16
  #
11
- def self.[](*keys_or_paths)
12
- keys_or_paths.reduce(Path.new) do |result, key_or_path|
13
- result << Path.new(key_or_path)
17
+ def self.[](*key_paths)
18
+ key_paths.reduce(Path.new) do |result, key_path|
19
+ result << key_path.to_key_path
14
20
  end
15
21
  end
16
22
 
@@ -24,19 +30,21 @@ module KeyTree
24
30
  # KeyTree::Path.new("a.b.c")
25
31
  # => ["a", "b", "c"]
26
32
  #
27
- def initialize(key_or_path = [])
28
- case key_or_path
29
- when String
30
- initialize(key_or_path.split('.'))
31
- when Symbol
32
- initialize(key_or_path.to_s)
33
+ def initialize(key_path = nil)
34
+ case key_path
35
+ when NilClass
36
+ nil
33
37
  when Array
34
- key_or_path.each { |key| append(key.to_sym) }
38
+ concat(key_path.map(&:to_sym))
35
39
  else
36
- raise ArgumentError, 'key path must be String, Symbol or Array of those'
40
+ initialize(key_path.to_key_path)
37
41
  end
38
42
  end
39
43
 
44
+ def to_key_path
45
+ self
46
+ end
47
+
40
48
  def to_s
41
49
  join('.')
42
50
  end
@@ -46,45 +54,34 @@ module KeyTree
46
54
  end
47
55
 
48
56
  def <<(other)
49
- case other
50
- when Path
51
- other.reduce(self) do |result, key|
52
- result.append(key)
53
- end
54
- else
55
- self << Path[other]
56
- end
57
+ concat(other.to_key_path)
57
58
  end
58
59
 
59
60
  def +(other)
60
- dup << other
61
+ dup.concat(other.to_key_path)
61
62
  end
62
63
 
63
- # drop(+prefix+)
64
- #
65
- # Returns a key path without the leading prefix
66
- #
67
- # drop(+n+)
64
+ # Returns a key path without the leading +prefix+
68
65
  #
69
- # Returns a key path without the first n elements
70
- #
71
- def drop(prefix)
72
- case prefix
73
- when Path
74
- return self unless prefix?(other)
75
- drop(other.length)
76
- else
77
- super(prefix)
78
- end
66
+ # :call-seq:
67
+ # Path - other => Path
68
+ def -(other)
69
+ other = other.to_key_path
70
+ raise KeyError unless prefix?(other)
71
+ super(other.length)
79
72
  end
80
73
 
81
74
  # Is +other+ a prefix?
82
75
  #
76
+ # :call-seq:
77
+ # prefix?(other) => boolean
83
78
  def prefix?(other)
79
+ other = other.to_key_path
84
80
  return false if other.length > length
85
81
  key_enum = each
86
82
  other.all? { |other_key| key_enum.next == other_key }
87
83
  end
84
+ alias === prefix?
88
85
 
89
86
  # Would +other+ conflict?
90
87
  #
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyTree
4
+ module Refine
5
+ # Refinements to Hash for deep_ methods, for traversing nested structures
6
+ module DeepHash
7
+ refine Hash do # rubocop:disable Metrics/BlockLength
8
+ # Return a deep enumerator for all (+key_path+, +value+) pairs in a
9
+ # nested hash structure.
10
+ #
11
+ # :call-seq:
12
+ # deep => Enumerator
13
+ def deep
14
+ Enumerator.new do |yielder|
15
+ deep_enumerator(yielder)
16
+ end
17
+ end
18
+
19
+ # Fetch a leaf value from a nested hash structure
20
+ #
21
+ # :call-seq:
22
+ # deep_fetch(key_path) => value
23
+ # deep_fetch(key_path, default) => value || default
24
+ # deep_fetch(key_path) { |key_path| block } => value || block
25
+ def deep_fetch(key_path, *args, &key_missing)
26
+ key_error = [KeyError, %(key path invalid: "#{key_path}")]
27
+ result = key_path.reduce(self) do |hash, key|
28
+ raise(*key_error) unless hash.is_a?(Hash)
29
+ hash.fetch(key, *args, &key_missing)
30
+ end
31
+ return result unless result.is_a?(Hash)
32
+ raise(*key_error)
33
+ end
34
+
35
+ # Store a new value in a nested hash structure, expanding it
36
+ # if necessary.
37
+ #
38
+ # :call-seq:
39
+ # deep_store(key_path, new_value) => new_value
40
+ #
41
+ # Raises KeyError if a prefix of the +key_path+ has a value.
42
+ def deep_store(key_path, new_value)
43
+ *prefix_path, last_key = key_path
44
+ result = prefix_path.reduce(self) do |hash, key|
45
+ result = hash.fetch(key) { hash[key] = {} }
46
+ next result if result.is_a?(Hash)
47
+ raise KeyError, %(prefix has value: "#{key_path}")
48
+ end
49
+ result[last_key] = new_value
50
+ end
51
+
52
+ # Delete a leaf value in a nested hash structure
53
+ #
54
+ # :call-seq:
55
+ # deep_delete(key_path)
56
+ #
57
+ # Raises KeyError if a prefix of the +key_path+ has a value.
58
+ def deep_delete(key_path)
59
+ *prefix_path, last_key = key_path
60
+ result = prefix_path.reduce(self) do |hash, key|
61
+ result = hash.fetch(key, nil)
62
+ next result if result.is_a?(Hash)
63
+ raise KeyError, %(prefix has value: "#{key_path}")
64
+ end
65
+ result.delete(last_key)
66
+ end
67
+
68
+ # Deeply merge nested hash structures
69
+ #
70
+ # :call-seq:
71
+ # deep_merge!(other) => self
72
+ # deep_merge!(other) { |key, lhs, rhs| } => self
73
+ def deep_merge!(other)
74
+ merge!(other) do |key, lhs, rhs|
75
+ next lhs.merge!(rhs) if lhs.is_a?(Hash) && rhs.is_a?(Hash)
76
+ next yield(key, lhs, rhs) if block_given?
77
+ rhs
78
+ end
79
+ end
80
+
81
+ # Deeply merge nested hash structures
82
+ #
83
+ # :call-seq:
84
+ # deep_merge(other) => self
85
+ # deep_merge(other) { |key, lhs, rhs| } => self
86
+ def deep_merge(other)
87
+ merge(other) do |key, lhs, rhs|
88
+ next lhs.merge(rhs) if lhs.is_a?(Hash) && rhs.is_a?(Hash)
89
+ next yield(key, lhs, rhs) if block_given?
90
+ rhs
91
+ end
92
+ end
93
+
94
+ # Transform keys in a nested hash structure
95
+ #
96
+ # :call-seq:
97
+ # deep_transform_keys { |key| block }
98
+ def deep_transform_keys(&block)
99
+ result = transform_keys(&block)
100
+ result.transform_values! do |value|
101
+ next value unless value.is_a?(Hash)
102
+ value.deep_transform_keys(&block)
103
+ end
104
+ end
105
+
106
+ # Transform keys in a nested hash structure
107
+ #
108
+ # :call-seq:
109
+ # deep_transform_keys! { |key| block }
110
+ def deep_transform_keys!(&block)
111
+ result = transform_keys!(&block)
112
+ result.transform_values! do |value|
113
+ next value unless value.is_a?(Hash)
114
+ value.deep_transform_keys!(&block)
115
+ end
116
+ end
117
+
118
+ def deep_enumerator(yielder, prefix = [])
119
+ each do |key, value|
120
+ key_path = prefix + [key]
121
+ yielder << [key_path, value]
122
+ value.deep_enumerator(yielder, key_path) if value.is_a?(Hash)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'forest'
4
+ require_relative 'path'
5
+ require_relative 'tree'
6
+
7
+ module KeyTree
8
+ # KeyTree refinements to core classes
9
+ module Refinements
10
+ refine Array do
11
+ def to_key_forest
12
+ Forest[*map(&:to_key_wood)]
13
+ end
14
+ alias_method :to_key_wood, :to_key_forest
15
+
16
+ def to_key_path
17
+ Path.new(self)
18
+ end
19
+ end
20
+
21
+ refine Hash do
22
+ def to_key_tree
23
+ Tree[self]
24
+ end
25
+ alias_method :to_key_wood, :to_key_tree
26
+ end
27
+
28
+ refine String do
29
+ def to_key_path
30
+ split('.').to_key_path
31
+ end
32
+ end
33
+
34
+ refine Symbol do
35
+ def to_key_path
36
+ to_s.to_key_path
37
+ end
38
+ end
39
+ end
40
+ end
data/lib/key_tree/tree.rb CHANGED
@@ -1,118 +1,145 @@
1
- require 'key_tree/path'
2
- require 'key_tree/meta_data'
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'meta_data'
5
+ require_relative 'path'
6
+ require_relative 'refinements'
7
+ require_relative 'refine/deep_hash'
8
+
9
+ module KeyTree # rubocop:disable Style/Documentation
10
+ using Refinements
11
+ using Refine::DeepHash
3
12
 
4
- module KeyTree
5
13
  # A tree of key-value lookup tables (hashes)
6
- class Tree < Hash
14
+ class Tree
7
15
  include MetaData
16
+ extend Forwardable
8
17
  #
9
18
  # KeyTree::Tree.new(+hash+)
10
19
  #
11
20
  # Initialize a new KeyTree from nested Hash:es
12
21
  #
13
22
  def self.[](hash = {})
14
- keytree = Tree.new
15
- hash.each do |key, value|
16
- keytree[key] = value
17
- end
18
- keytree
23
+ new(hash)
19
24
  end
20
25
 
21
- def [](key_or_path)
22
- super(Path[key_or_path])
26
+ def initialize(hash = {}, default = nil, &default_proc)
27
+ @hash = hash.to_h.deep_transform_keys(&:to_sym)
28
+ @default = default
29
+ @default_proc = default_proc
23
30
  end
24
31
 
25
- def fetch(key_or_path, *args, &missing_key)
26
- super(Path[key_or_path], *args, &missing_key)
27
- end
32
+ attr_reader :default, :default_proc
28
33
 
29
- def values_at(*keys)
30
- super(keys.map { |key_or_path| Path[key_or_path] })
31
- end
34
+ alias to_key_tree itself
35
+ alias to_key_wood itself
32
36
 
33
- def []=(key_or_path, new_value)
34
- path = Path[key_or_path]
37
+ delegate %i[empty? to_h to_json] => :@hash
35
38
 
36
- delete_if { |key, _| path.conflict?(key) }
39
+ # Convert a Tree to YAML, with string keys
40
+ #
41
+ # :call-seq:
42
+ # to_yaml => String
43
+ def to_yaml
44
+ to_h.deep_transform_keys(&:to_s).to_yaml
45
+ end
37
46
 
38
- case new_value
39
- when Hash
40
- new_value.each { |suffix, value| self[path + suffix] = value }
41
- else
42
- super(path, new_value)
47
+ def [](key_path)
48
+ fetch(key_path) do
49
+ next default_proc.call(self, key_path) unless default_proc.nil?
50
+ default
43
51
  end
52
+ rescue KeyError
53
+ default
44
54
  end
45
55
 
46
- def key?(key_or_path)
47
- super(Path[key_or_path])
56
+ def fetch(key_path, *args, &key_missing)
57
+ @hash.deep_fetch(key_path.to_key_path, *args, &key_missing)
48
58
  end
49
59
 
50
- def default_key?(key_or_path)
51
- return unless default_proc
52
- default_proc.yield(self, Path[key_or_path])
53
- true
54
- rescue KeyError
55
- false
60
+ def store(key_path, new_value)
61
+ @hash.deep_store(key_path.to_key_path, new_value)
56
62
  end
57
63
 
58
- def prefix?(key_or_path)
59
- keys.any? { |key| key.prefix?(Path[key_or_path]) }
64
+ def store!(key_path, new_value)
65
+ store(key_path, new_value)
66
+ rescue KeyError
67
+ delete!(key_path)
68
+ retry
60
69
  end
70
+ alias []= store!
61
71
 
62
- def conflict?(key_or_path)
63
- keys.any? { |key| key.conflict?(Path[key_or_path]) }
72
+ def delete(key_path)
73
+ @hash.deep_delete(key_path.to_key_path)
64
74
  end
65
75
 
66
- # The merging of trees needs some extra consideration; due to the
67
- # nature of key paths, prefix conflicts must be deleted
68
- #
69
- def merge!(other, &merger)
70
- other = Tree[other] unless other.is_a?(Tree)
71
- delete_if { |key, _| other.conflict?(key) }
72
- super
76
+ def delete!(key_path)
77
+ delete(key_path)
78
+ rescue KeyError
79
+ key_path = key_path[0..-2]
80
+ retry
73
81
  end
74
- alias << merge!
75
82
 
76
- def merge(other, &merger)
77
- dup.merge!(other, &merger)
83
+ def values_at(*key_paths)
84
+ key_paths.map { |key_path| self[key_path] }
78
85
  end
79
- alias + merge
80
86
 
81
- # Format +fmtstr+ with values from the Tree
82
- def format(fmtstr)
83
- Kernel.format(fmtstr, Hash.new { |_, key| fetch(key) })
87
+ # Return all maximal key paths in a tree
88
+ #
89
+ # :call-seq:
90
+ # keys => Array of KeyTree::Path
91
+ def keys
92
+ @hash.deep.each_with_object([]) do |(key_path, value), result|
93
+ result << key_path.to_key_path unless value.is_a?(Hash)
94
+ end
84
95
  end
96
+ alias key_paths keys
85
97
 
86
- # Convert a Tree back to nested hashes.
87
- #
88
- # to_h => Hash, with symbol keys
89
- # to_h(string_keys: true) => Hash, with string keys
90
- def to_h(**kwargs)
91
- to_hash_tree(**kwargs)
98
+ def include?(key_path)
99
+ fetch(key_path)
100
+ true
101
+ rescue KeyError
102
+ false
92
103
  end
104
+ alias key? include?
105
+ alias has_key? include?
106
+ alias key_path? include?
107
+ alias has_key_path? include?
93
108
 
94
- # Convert a Tree to JSON, with string keys
95
- def to_json
96
- to_hash_tree(string_keys: true).to_json
109
+ def prefix?(key_path)
110
+ key_path.to_key_path.reduce(@hash) do |subtree, key|
111
+ return false unless subtree.is_a?(Hash)
112
+ return false unless subtree.key?(key)
113
+ subtree[key]
114
+ end
115
+ true
97
116
  end
117
+ alias has_prefix? prefix?
98
118
 
99
- # Convert a Tree to YAML, with string keys
100
- def to_yaml
101
- to_hash_tree(string_keys: true).to_yaml
119
+ def value?(needle)
120
+ @hash.deep.lazy.any? { |(_, straw)| straw == needle }
102
121
  end
122
+ alias has_value? value?
103
123
 
104
- private
124
+ # Merge values from +other+ tree into self
125
+ #
126
+ # :call-seq:
127
+ # merge!(other) => self
128
+ # merge!(other) { |key, lhs, rhs| } => self
129
+ def merge!(other, &block)
130
+ @hash.deep_merge!(other.to_h, &block)
131
+ self
132
+ end
133
+ alias << merge!
105
134
 
106
- def to_hash_tree(key_pairs = self, string_keys: false)
107
- hash = key_pairs.group_by do |path, _|
108
- string_keys ? path.first.to_s : path.first
109
- end
110
- hash.transform_values do |next_level|
111
- next_level.map! { |path, value| [path[1..-1], value] }
112
- first_key, first_value = next_level.first
113
- next first_value if first_key.nil? || first_key.empty?
114
- to_hash_tree(next_level)
115
- end
135
+ # Return a new tree by merging values from +other+ tree
136
+ #
137
+ # :call-seq:
138
+ # merge(other) => Tree
139
+ # merge(other) { |key, lhs, rhs| } => Tree
140
+ def merge(other, &block)
141
+ @hash.deep_merge(other.to_h, &block).to_key_tree
116
142
  end
143
+ alias + merge
117
144
  end
118
145
  end
data/lib/key_tree.rb CHANGED
@@ -1,7 +1,9 @@
1
- require 'key_tree/version'
2
- require 'key_tree/tree'
1
+ # frozen_string_literal: true
2
+
3
3
  require 'key_tree/forest'
4
4
  require 'key_tree/loader'
5
+ require 'key_tree/refinements'
6
+ require 'key_tree/tree'
5
7
 
6
8
  # Manage a tree of keys
7
9
  #
@@ -13,17 +15,10 @@ require 'key_tree/loader'
13
15
  # -> 2
14
16
  #
15
17
  module KeyTree
18
+ using Refinements
19
+
16
20
  def self.[](contents = {})
17
- case contents
18
- when Tree, Forest
19
- contents
20
- when Hash
21
- Tree[contents]
22
- when Array
23
- Forest[*contents]
24
- else
25
- raise ArgumentError, "can't load #{contents.class} into a KeyTree"
26
- end
21
+ contents.to_key_wood
27
22
  end
28
23
 
29
24
  # Load a KeyTree from some external serialization
@@ -49,7 +44,7 @@ module KeyTree
49
44
  contents = loader.load(serialization)
50
45
  contents = { prefix => contents } unless prefix.nil?
51
46
 
52
- self[contents].with_meta_data do |meta_data|
47
+ contents.to_key_wood.with_meta_data do |meta_data|
53
48
  meta_data << { load: { type: type, loader: loader } }
54
49
  meta_data << { load: { prefix: prefix } } unless prefix.nil?
55
50
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: key_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Calle Englund
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-25 00:00:00.000000000 Z
11
+ date: 2018-05-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: git-version-bump
14
+ name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.15'
20
- type: :runtime
19
+ version: '1.16'
20
+ type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.15'
26
+ version: '1.16'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bundler
28
+ name: git-version-bump
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.16'
33
+ version: '0.15'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.16'
40
+ version: '0.15'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +80,21 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.52'
83
- description: Manage trees of keys
83
+ - !ruby/object:Gem::Dependency
84
+ name: ruby-prof
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.17'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.17'
97
+ description:
84
98
  email:
85
99
  - calle@discord.bofh.se
86
100
  executables: []
@@ -106,8 +120,9 @@ files:
106
120
  - lib/key_tree/loader/nil.rb
107
121
  - lib/key_tree/meta_data.rb
108
122
  - lib/key_tree/path.rb
123
+ - lib/key_tree/refine/deep_hash.rb
124
+ - lib/key_tree/refinements.rb
109
125
  - lib/key_tree/tree.rb
110
- - lib/key_tree/version.rb
111
126
  homepage: https://github.com/notcalle/ruby-keytree
112
127
  licenses:
113
128
  - MIT
@@ -118,9 +133,9 @@ require_paths:
118
133
  - lib
119
134
  required_ruby_version: !ruby/object:Gem::Requirement
120
135
  requirements:
121
- - - ">="
136
+ - - "~>"
122
137
  - !ruby/object:Gem::Version
123
- version: '0'
138
+ version: '2.3'
124
139
  required_rubygems_version: !ruby/object:Gem::Requirement
125
140
  requirements:
126
141
  - - ">="
@@ -1,6 +0,0 @@
1
- require 'git-version-bump'
2
-
3
- module KeyTree
4
- VERSION = GVB.version.freeze
5
- DATE = GVB.date.freeze
6
- end