hash_tree 0.1.0 → 0.1.1

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: 1b474a91f66ef7115da2bd73b98c436c6409301e332de18ed2439d4262e3cda6
4
- data.tar.gz: 3314e15480623ee8e9a984b0054835bea8c1d77d48cc885eedf653dfa4346ba6
3
+ metadata.gz: 6e76908b94b4a03beeb674da5e4e4b810094198e216d5be3b43f4acbd672f8e3
4
+ data.tar.gz: 3e83394d0f76fb228895a15b5953bff081ae5e853e648d6ca0b0cd18ae5a95a0
5
5
  SHA512:
6
- metadata.gz: d92a9ec50b0032e3b8a675336d70ec3a5e238905ce71e990750997691bf776104f80d083be7cd59b32c1f59c08c2333f6f4f7ddd2970b9e8e03fb1af16c1965b
7
- data.tar.gz: e2bfbd59bcbede4045e7aaae6b68a837cec99395ef00562732af9819581b981d9fde25c3b7c3d3e777c5f86fac931754760fa74e5d2dc7c0d8a90dd674b5b331
6
+ metadata.gz: d90748a40a23b0d8d1648649b41124a3ad8ae55896dfe73ff95173648c9913142844d0b20aff1a8709cdeae39d1483da648183c40e13d56f8ae709ae321dc0cd
7
+ data.tar.gz: 5a52fb194d2de6d47298a2fa36391c82f945e255412d5175cdd04c3a76990d0872421cb0e3e5d4ef04208b373eef71078bceaa633c39757f8af48c150b212127
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-2.6.6
1
+ ruby-2.7.1
data/hash_tree.gemspec CHANGED
@@ -36,8 +36,17 @@ Gem::Specification.new do |spec|
36
36
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
37
  spec.require_paths = ["lib"]
38
38
 
39
- spec.add_development_dependency "bundler", "~> 1.17"
39
+ spec.add_development_dependency "bundler", "~> 2.2.10"
40
40
  spec.add_development_dependency "rake", "~> 12.3.3"
41
41
  spec.add_development_dependency "rspec", "~> 3.0"
42
42
  spec.add_development_dependency "indented_io"
43
+
44
+ # In development mode override load paths for gems whose source are located
45
+ # as siblings of this project directory
46
+ if File.directory?("#{__dir__}/.git")
47
+ local_projects = Dir["../*"].select { |path|
48
+ File.directory?(path) && File.exist?("#{path}/Gemfile")
49
+ }.map { |relpath| "#{File.absolute_path(relpath)}/lib" }
50
+ $LOAD_PATH.unshift *local_projects
51
+ end
43
52
  end
@@ -1,3 +1,3 @@
1
1
  module HashTree
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/hash_tree.rb CHANGED
@@ -4,17 +4,34 @@ require "indented_io"
4
4
  # Simple tree of hashes
5
5
  #
6
6
  # TODO
7
- # o each
8
- # o traverse
9
- # o #ancestors and #parents as enumerators
10
- # o HashTree::Set, HashTree::Map, HashTree::Array
7
+ # o Different interfaces:
8
+ # Stable (the default - attributes are cached recursively)
9
+ # Dynamic (for trees with non-trivial edits - attributes are cached dynamically)
10
+ # Dot (for dot functionality
11
+ # Algorithms
12
+ #
13
+ # o XPath?!
14
+ # o enumerators: descendants, ancestors, preorder, etc. etc.
15
+ # o HashTree::Array
11
16
  # o Semantics of <=>: strictly partial so it returns nil on no common path?
12
17
  # o Drop <=> ?
13
18
  # o Implement #common to return common path of two elements
14
- # o modularize?
19
+ # o Make is possible to include HashTree as a module instead of deriving from class
20
+ # o []=
21
+ # o #traverse filter?
22
+ # o more elaborate selectors - emit, skip
23
+ # :include => emit node and continue to children
24
+ # :fetch => emit node but skip children
25
+ # :exclude => skip node and continue to children
26
+ # :prune => skip node and children
15
27
  #
28
+ # + each
29
+ # + traverse
30
+ # + HashTree::Set, HashTree::Map
31
+
16
32
  module HashTree
17
33
  class Error < StandardError; end
34
+ class KeyNotFound < Error; end
18
35
 
19
36
  # The base Node type for HashTree implementations. It is not supposed to be called
20
37
  # from user-code
@@ -25,9 +42,13 @@ module HashTree
25
42
  # Hash from key to child node
26
43
  attr_reader :children
27
44
 
28
- def initialize(parent, key)
45
+ def initialize(parent, key, attach: true)
29
46
  @children = {}
30
- parent&.do_attach(key, self)
47
+ if attach
48
+ parent&.do_attach(key, self)
49
+ else
50
+ @parent = parent
51
+ end
31
52
  end
32
53
 
33
54
  # Attach a child to self
@@ -52,8 +73,14 @@ module HashTree
52
73
  # Returns true iff key is included in children
53
74
  def key?(key) @children.key?(key) end
54
75
 
76
+ # List of keys
77
+ def keys() @children.keys end
78
+
79
+ # List of values
80
+ def values() @children.values end
81
+
55
82
  # The root object or self if parent is nil
56
- def root() @root ||= parents.last || self end
83
+ def root() @root ||= (parent&.root || self) end
57
84
 
58
85
  # List of parents up to the root element. If include_self is true, also
59
86
  # include self as the first element
@@ -67,17 +94,73 @@ module HashTree
67
94
  (@ancestors ||= parents(false).reverse) + (include_self ? [self] : [])
68
95
  end
69
96
 
70
- # Recursively lookup object by dot-separated list of keys
97
+ # Recursively lookup object by dot-separated list of keys. Note that for
98
+ # this to work, key names must not contain dot characters ('.')
71
99
  #
72
- # Note that for this to work, keys may not contain a dots ('.')
73
- def dot(path)
74
- path.split(".").inject(self) { |a,e|
75
- a[e] or raise Error, "Can't lookup '#{e}' in #{a.path.inspect}"
76
- }
100
+ # The reverse method, #path, is only defined for HashTree::Set because a
101
+ # HashTree::Map node doesn't know its own key
102
+ def dot(path_or_keys, raise_on_not_found: true)
103
+ keys = path_or_keys.is_a?(String) ? path_or_keys.split(".") : path_or_keys
104
+ key = keys.shift or return self
105
+ if child = dot_lookup(key)
106
+ child.send(:dot, keys, raise_on_not_found: raise_on_not_found)
107
+ elsif raise_on_not_found
108
+ raise KeyNotFound, "Can't lookup '#{key}' in #{self.path.inspect}"
109
+ else
110
+ nil
111
+ end
112
+ end
113
+
114
+ # True if path_or_keys address a node
115
+ def dot?(path_or_keys)
116
+ !dot(path_or_keys, raise_on_not_found: false).nil?
117
+ end
118
+
119
+ # List of [key, child] tuples
120
+ def each(&block)
121
+ if block_given?
122
+ children.each { |key, child| yield(key, child) }
123
+ else
124
+ children.each
125
+ end
126
+ end
127
+
128
+ # Return list of nodes in preorder
129
+ def preorder
130
+ [self] + values.inject([]) { |a,e| a + e.preorder }
131
+ end
132
+
133
+ # Return list of nodes in postorder
134
+ def postorder
135
+ values.inject([]) { |a,e| a + e.postorder } + [self]
136
+ end
137
+
138
+ # EXPERIMENTAL
139
+ #
140
+ # Process nodes in preorder. The block is called with the current node as
141
+ # argument and can return true to process its child nodes or false or nil
142
+ # to skip them. #traverse doesn't accumulate a result, this is the
143
+ # responsibily of the block
144
+ def traverse(&block)
145
+ values.each { |child| child.traverse(&block) } if yield(self)
146
+ end
147
+
148
+ # EXPERIMENTAL
149
+ #
150
+ # Process nodes in postorder (bottom-up). The block is called with the
151
+ # current node and a list of aggregated children. The optional filter
152
+ # argument is a lambda. The lambda is called with the current node as
153
+ # argument and the node if skipped if the lambda returns falsy
154
+ def aggregate(filter = lambda { |node| true }, &block)
155
+ if filter.nil? || filter.call(self)
156
+ yield(self, values.map { |child| child.aggregate(filter, &block) }.compact)
157
+ else
158
+ nil
159
+ end
77
160
  end
78
161
 
79
162
  protected
80
- # Attach a child node to self
163
+ # Attach a child node
81
164
  def do_attach(key, child)
82
165
  !@children.key?(key) or raise Error, "Duplicate child key: #{key.inspect}"
83
166
  !child.parent or raise Error, "Child is already attached"
@@ -86,6 +169,13 @@ module HashTree
86
169
  child.send(:clear_cached_properties)
87
170
  end
88
171
 
172
+ # Lookup key in current object. This method is used by #dot to lookup a
173
+ # child element and can be overridden to make #dot cross branches in the
174
+ # hierarchy. This is useful when you use HashTree to implement a graph
175
+ def dot_lookup(key)
176
+ self[key]
177
+ end
178
+
89
179
  private
90
180
  # Recursively clear cached properties like @parents in each node in the
91
181
  # tree. Should be called whenever the node is attached or detached from a
@@ -96,7 +186,7 @@ module HashTree
96
186
  # themselves constructed recursively so that iff a node has a cached
97
187
  # property, then all its parents will also cache it
98
188
  def clear_cached_properties()
99
- if@root || @parents || @ancestors || @path
189
+ if @root || @parents || @ancestors || @path
100
190
  @root = nil
101
191
  @parents = nil
102
192
  @ancestors = nil
@@ -109,15 +199,21 @@ module HashTree
109
199
  class Set < Node
110
200
  alias node_attach attach
111
201
 
112
- # Key of this node
202
+ # Key of this node.
203
+ # TODO: Make it possible/required to alias this method to provide an internal key
113
204
  attr_reader :key
114
205
 
115
- def initialize(parent, key)
116
- super(parent, @key = key)
206
+ def initialize(parent, key, attach: true)
207
+ super(parent, @key = key, attach: attach)
117
208
  end
118
209
 
119
210
  def attach(child) do_attach(child.key, child) end
120
211
 
212
+ def retach(node)
213
+ node.parent.detach(node.key)
214
+ attach(node)
215
+ end
216
+
121
217
  # Unique dot-separated list of keys leading from the root object to
122
218
  # self. Note that the root object is not included in the path so that
123
219
  #
@@ -126,7 +222,7 @@ module HashTree
126
222
  # is always true
127
223
  #
128
224
  # Note that for this to work, keys may not contain a dots ('.')
129
- def path() @path ||= ancestors(true)[1..-1].join(".") end
225
+ def path() @path ||= ancestors(true)[1..-1].map(&:key).join(".") end
130
226
 
131
227
  # A set node is rendered as its key
132
228
  def to_s() key.to_s end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hash_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Claus Rasmussen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-10 00:00:00.000000000 Z
11
+ date: 2022-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.17'
19
+ version: 2.2.10
20
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: '1.17'
26
+ version: 2.2.10
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -104,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
104
  - !ruby/object:Gem::Version
105
105
  version: '0'
106
106
  requirements: []
107
- rubygems_version: 3.0.8
107
+ rubygems_version: 3.2.26
108
108
  signing_key:
109
109
  specification_version: 4
110
110
  summary: A tree implemented as nested hashes