key_tree 0.5.3 → 0.6.0

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 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