hash_tree 0.1.0 → 0.1.1

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