key_tree 0.5.2 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|