key_tree 0.5.2 → 0.8.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 +4 -4
- data/.github/workflows/ruby.yml +37 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +15 -4
- data/CHANGELOG.md +222 -0
- data/Gemfile +4 -8
- data/README.md +6 -1
- data/Rakefile +2 -0
- data/bin/console +5 -4
- data/bin/setup +0 -1
- data/key_tree.gemspec +20 -7
- data/lib/key_tree.rb +70 -70
- data/lib/key_tree/forest.rb +58 -26
- data/lib/key_tree/loader.rb +19 -16
- data/lib/key_tree/loader/nil.rb +2 -0
- data/lib/key_tree/meta_data.rb +3 -1
- data/lib/key_tree/path.rb +32 -40
- data/lib/key_tree/refine/deep_hash.rb +157 -0
- data/lib/key_tree/refinements.rb +40 -0
- data/lib/key_tree/tree.rb +101 -74
- data/lib/key_tree/version.rb +8 -4
- data/ruby-keytree.sublime-project +8 -0
- metadata +110 -21
- data/.travis.yml +0 -5
- data/RELEASE_NOTES.md +0 -97
data/lib/key_tree/forest.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
-
|
2
|
-
|
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 <<
|
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
|
@@ -22,41 +30,59 @@ module KeyTree
|
|
22
30
|
# key path matches in trees further away, returning nil. This preserves
|
23
31
|
# the constraints that only leaves may contain a value.
|
24
32
|
#
|
25
|
-
def [](key
|
33
|
+
def [](key)
|
26
34
|
return super(key) if key.is_a?(Numeric)
|
27
|
-
|
28
|
-
|
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
|
|
32
|
-
|
33
|
-
|
44
|
+
# Fetch a value from a forest
|
45
|
+
#
|
46
|
+
# :call-seq:
|
47
|
+
# fetch(key) => value
|
48
|
+
# fetch(key, default) => value
|
49
|
+
# fetch(key) { |key| } => value
|
50
|
+
#
|
51
|
+
# The first form raises a +KeyError+ unless +key+ has a value.
|
52
|
+
def fetch(key, *default)
|
53
|
+
trees.lazy.each do |tree|
|
54
|
+
catch do |ball|
|
55
|
+
return tree.fetch(key) { throw ball }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
return yield(key) if block_given?
|
59
|
+
return default.first unless default.empty?
|
34
60
|
|
35
|
-
|
36
|
-
values.reverse.reduce { |left, right| yield(key, left, right) }
|
61
|
+
raise KeyError, %(key not found: "#{key}")
|
37
62
|
end
|
38
63
|
|
39
|
-
def
|
40
|
-
|
41
|
-
tree.prefix?(key) || tree.default_key?(key)
|
42
|
-
end
|
43
|
-
result || raise(KeyError, "key not found: #{key}")
|
64
|
+
def key?(key)
|
65
|
+
trees.lazy.any? { |tree| tree.key?(key) }
|
44
66
|
end
|
67
|
+
alias has_key? key?
|
45
68
|
|
46
|
-
def
|
47
|
-
|
48
|
-
tree.prefix?(key) || tree.default_key?(key)
|
49
|
-
end
|
50
|
-
raise(KeyError, "key not found: #{key}") if result.empty?
|
51
|
-
result
|
69
|
+
def prefix?(key)
|
70
|
+
trees.lazy.any? { |tree| tree.prefix?(key) }
|
52
71
|
end
|
72
|
+
alias has_prefix? prefix?
|
53
73
|
|
54
|
-
def
|
55
|
-
trees.any? { |tree| tree.
|
74
|
+
def key_path?(key)
|
75
|
+
trees.lazy.any? { |tree| tree.key_path?(key) }
|
56
76
|
end
|
77
|
+
alias has_key_path? key_path?
|
57
78
|
|
58
|
-
def
|
59
|
-
|
79
|
+
def include?(needle)
|
80
|
+
case needle
|
81
|
+
when Tree, Forest
|
82
|
+
super(needle)
|
83
|
+
else
|
84
|
+
key_path?(needle)
|
85
|
+
end
|
60
86
|
end
|
61
87
|
|
62
88
|
# Flattening a forest produces a tree with the equivalent view of key paths
|
@@ -74,9 +100,15 @@ module KeyTree
|
|
74
100
|
remaining = [self]
|
75
101
|
remaining.each do |woods|
|
76
102
|
next yielder << woods if woods.is_a?(Tree)
|
103
|
+
|
77
104
|
woods.each { |wood| remaining << wood }
|
78
105
|
end
|
79
106
|
end
|
80
107
|
end
|
108
|
+
|
109
|
+
# Return all visible key paths in the forest
|
110
|
+
def key_paths
|
111
|
+
trees.reduce(Set.new) { |result, tree| result.merge(tree.key_paths) }
|
112
|
+
end
|
81
113
|
end
|
82
114
|
end
|
data/lib/key_tree/loader.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'key_tree/loader/nil'
|
2
4
|
|
3
5
|
module KeyTree
|
@@ -8,26 +10,27 @@ module KeyTree
|
|
8
10
|
yaml: 'YAML', yml: 'YAML'
|
9
11
|
}.freeze
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
class << self
|
14
|
+
def [](type)
|
15
|
+
type = type.to_sym if type.respond_to?(:to_sym)
|
16
|
+
loaders[type] || @fallback
|
17
|
+
end
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
19
|
+
def []=(type, loader_class)
|
20
|
+
type = type.to_sym if type.respond_to?(:to_sym)
|
21
|
+
loaders[type] = loader_class
|
22
|
+
end
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
end
|
24
|
+
attr_writer :fallback, :loaders
|
25
|
+
alias fallback fallback=
|
24
26
|
|
25
|
-
|
27
|
+
private
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
def loaders
|
30
|
+
@loaders ||= BUILTIN_LOADERS.each_with_object({}) do |pair, result|
|
31
|
+
type, name = pair
|
32
|
+
result[type] = const_get(name) if const_defined?(name)
|
33
|
+
end
|
31
34
|
end
|
32
35
|
end
|
33
36
|
end
|
data/lib/key_tree/loader/nil.rb
CHANGED
data/lib/key_tree/meta_data.rb
CHANGED
data/lib/key_tree/path.rb
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
|
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.[](*
|
12
|
-
|
13
|
-
result << Path.new(
|
17
|
+
def self.[](*key_paths)
|
18
|
+
key_paths.reduce(Path.new) do |result, key_path|
|
19
|
+
result << Path.new(key_path)
|
14
20
|
end
|
15
21
|
end
|
16
22
|
|
@@ -22,21 +28,16 @@ module KeyTree
|
|
22
28
|
#
|
23
29
|
# Example:
|
24
30
|
# KeyTree::Path.new("a.b.c")
|
25
|
-
# => [
|
31
|
+
# => [:a, :b, :c]
|
26
32
|
#
|
27
|
-
def initialize(
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
when Symbol
|
32
|
-
initialize(key_or_path.to_s)
|
33
|
-
when Array
|
34
|
-
key_or_path.each { |key| append(key.to_sym) }
|
35
|
-
else
|
36
|
-
raise ArgumentError, 'key path must be String, Symbol or Array of those'
|
37
|
-
end
|
33
|
+
def initialize(key_path = [])
|
34
|
+
key_path = key_path.to_key_path unless key_path.is_a? Array
|
35
|
+
|
36
|
+
super(key_path.map(&:to_sym))
|
38
37
|
end
|
39
38
|
|
39
|
+
alias to_key_path itself
|
40
|
+
|
40
41
|
def to_s
|
41
42
|
join('.')
|
42
43
|
end
|
@@ -46,45 +47,36 @@ module KeyTree
|
|
46
47
|
end
|
47
48
|
|
48
49
|
def <<(other)
|
49
|
-
|
50
|
-
when Path
|
51
|
-
other.reduce(self) do |result, key|
|
52
|
-
result.append(key)
|
53
|
-
end
|
54
|
-
else
|
55
|
-
self << Path[other]
|
56
|
-
end
|
50
|
+
concat(other.to_key_path)
|
57
51
|
end
|
58
52
|
|
59
53
|
def +(other)
|
60
|
-
dup
|
54
|
+
dup.concat(other.to_key_path)
|
61
55
|
end
|
62
56
|
|
63
|
-
#
|
57
|
+
# Returns a key path without the leading +prefix+
|
64
58
|
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
case prefix
|
73
|
-
when Path
|
74
|
-
return self unless prefix?(other)
|
75
|
-
drop(other.length)
|
76
|
-
else
|
77
|
-
super(prefix)
|
78
|
-
end
|
59
|
+
# :call-seq:
|
60
|
+
# drop(other) => Path
|
61
|
+
def drop(other)
|
62
|
+
other = other.to_key_path
|
63
|
+
raise KeyError unless prefix?(other)
|
64
|
+
|
65
|
+
super(other.length)
|
79
66
|
end
|
80
67
|
|
81
68
|
# Is +other+ a prefix?
|
82
69
|
#
|
70
|
+
# :call-seq:
|
71
|
+
# prefix?(other) => boolean
|
83
72
|
def prefix?(other)
|
73
|
+
other = other.to_key_path
|
84
74
|
return false if other.length > length
|
75
|
+
|
85
76
|
key_enum = each
|
86
77
|
other.all? { |other_key| key_enum.next == other_key }
|
87
78
|
end
|
79
|
+
alias === prefix?
|
88
80
|
|
89
81
|
# Would +other+ conflict?
|
90
82
|
#
|
@@ -0,0 +1,157 @@
|
|
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
|
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, *default)
|
26
|
+
catch do |ball|
|
27
|
+
result = key_path.reduce(self) do |hash, key|
|
28
|
+
throw ball unless hash.is_a?(Hash)
|
29
|
+
hash.fetch(key) { throw ball }
|
30
|
+
end
|
31
|
+
return result unless result.is_a?(Hash)
|
32
|
+
end
|
33
|
+
return yield(key_path) if block_given?
|
34
|
+
return default.first unless default.empty?
|
35
|
+
|
36
|
+
raise KeyError, %(key path invalid: "#{key_path}")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Store a new value in a nested hash structure, expanding it
|
40
|
+
# if necessary.
|
41
|
+
#
|
42
|
+
# :call-seq:
|
43
|
+
# deep_store(key_path, new_value) => new_value
|
44
|
+
#
|
45
|
+
# Raises KeyError if a prefix of the +key_path+ has a value.
|
46
|
+
def deep_store(key_path, new_value)
|
47
|
+
*prefix_path, last_key = key_path
|
48
|
+
result = prefix_path.reduce(self) do |hash, key|
|
49
|
+
result = hash.fetch(key) { hash[key] = {} }
|
50
|
+
next result if result.is_a?(Hash)
|
51
|
+
|
52
|
+
raise KeyError, %(prefix has value: "#{key_path}")
|
53
|
+
end
|
54
|
+
result[last_key] = new_value
|
55
|
+
end
|
56
|
+
|
57
|
+
# Delete a leaf value in a nested hash structure
|
58
|
+
#
|
59
|
+
# :call-seq:
|
60
|
+
# deep_delete(key_path)
|
61
|
+
#
|
62
|
+
# Raises KeyError if a prefix of the +key_path+ has a value.
|
63
|
+
def deep_delete(key_path)
|
64
|
+
*prefix_path, last_key = key_path
|
65
|
+
result = prefix_path.reduce(self) do |hash, key|
|
66
|
+
result = hash.fetch(key, nil)
|
67
|
+
next result if result.is_a?(Hash)
|
68
|
+
|
69
|
+
raise KeyError, %(prefix has value: "#{key_path}")
|
70
|
+
end
|
71
|
+
result.delete(last_key)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Deeply merge nested hash structures
|
75
|
+
#
|
76
|
+
# :call-seq:
|
77
|
+
# deep_merge!(other) => self
|
78
|
+
# deep_merge!(other) { |key_path, lhs, rhs| } => self
|
79
|
+
def deep_merge!(other, prefix = [], &block)
|
80
|
+
merge!(other) do |key, lhs, rhs|
|
81
|
+
key_path = prefix + [key]
|
82
|
+
both_are_hashes = lhs.is_a?(Hash) && rhs.is_a?(Hash)
|
83
|
+
next lhs.deep_merge!(rhs, key_path, &block) if both_are_hashes
|
84
|
+
next yield(key_path, lhs, rhs) unless block.nil?
|
85
|
+
|
86
|
+
rhs
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Deeply merge nested hash structures
|
91
|
+
#
|
92
|
+
# :call-seq:
|
93
|
+
# deep_merge(other) => self
|
94
|
+
# deep_merge(other) { |key_path, lhs, rhs| } => self
|
95
|
+
def deep_merge(other, prefix = [], &block)
|
96
|
+
merge(other) do |key, lhs, rhs|
|
97
|
+
key_path = prefix + [key]
|
98
|
+
both_are_hashes = lhs.is_a?(Hash) && rhs.is_a?(Hash)
|
99
|
+
next lhs.deep_merge(rhs, key_path, &block) if both_are_hashes
|
100
|
+
next yield(key_path, lhs, rhs) unless block.nil?
|
101
|
+
|
102
|
+
rhs
|
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
|
+
|
115
|
+
value.deep_transform_keys(&block)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Transform keys in a nested hash structure
|
120
|
+
#
|
121
|
+
# :call-seq:
|
122
|
+
# deep_transform_keys! { |key| block }
|
123
|
+
def deep_transform_keys!(&block)
|
124
|
+
result = transform_keys!(&block)
|
125
|
+
result.transform_values! do |value|
|
126
|
+
next value unless value.is_a?(Hash)
|
127
|
+
|
128
|
+
value.deep_transform_keys!(&block)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Comvert any keys containing a +.+ in a hash structure
|
133
|
+
# to nested hashes.
|
134
|
+
#
|
135
|
+
# :call-seq:
|
136
|
+
# deep_key_pathify => Hash
|
137
|
+
def deep_key_pathify
|
138
|
+
each_with_object({}) do |(key, value), result|
|
139
|
+
key_path = Path[key]
|
140
|
+
value = value.deep_key_pathify if value.is_a?(Hash)
|
141
|
+
result.deep_store(key_path, value)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def deep_enumerator(yielder, prefix = [])
|
146
|
+
each do |key, value|
|
147
|
+
key_path = prefix + [key]
|
148
|
+
yielder << [key_path, value]
|
149
|
+
value.deep_enumerator(yielder, key_path) if value.is_a?(Hash)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
require_relative '../path'
|