Module: Collapsium::PathedAccess
- Extended by:
- Support::HashMethods, Support::Methods
- Included in:
- UberHash
- Defined in:
- lib/collapsium/pathed_access.rb
Overview
The PathedAccess module can be used to extend Hash with pathed access on top of regular access, i.e. instead of ‘h[“second”]` you can write `h`.
The main benefit is much simpler code for accessing nested structured. For any given path, PathedAccess will return nil from ‘[]` if any of the path components do not exist.
Similarly, intermediate nodes will be created when you write a value for a path.
Constant Summary collapse
- DEFAULT_SEPARATOR =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Default path separator
'.'.freeze
- PATHED_ACCESS_READER =
Create a reader and write proc, because we only know
PathedAccess.create_proc(false).freeze
- PATHED_ACCESS_WRITER =
PathedAccess.create_proc(true).freeze
Constants included from Support::HashMethods
Support::HashMethods::KEYED_READ_METHODS, Support::HashMethods::KEYED_WRITE_METHODS, Support::HashMethods::READ_METHODS, Support::HashMethods::WRITE_METHODS
Class Method Summary collapse
-
.create_proc(write_access) ⇒ Object
Returns a proc for either read or write access.
- .enhance(base) ⇒ Object
- .extended(base) ⇒ Object
- .included(base) ⇒ Object
- .prepended(base) ⇒ Object
-
.recursive_fetch(path, data, current_path = [], options = {}) ⇒ Object
Given the path components, recursively fetch any but the last key.
Instance Method Summary collapse
-
#filter_components(components) ⇒ Object
Given path components, filters out unnecessary ones.
-
#join_path(components) ⇒ Object
Join path components with the ‘#separator`.
-
#normalize_path(path) ⇒ Object
Normalizes a String path so that there are no empty components, and it starts with a separator.
-
#path_components(path) ⇒ Object
Break path into components.
- #path_prefix ⇒ Object
-
#path_prefix=(value) ⇒ Object
Assume any pathed access has this prefix.
-
#separator ⇒ String
The separator is the character or pattern splitting paths.
-
#split_pattern ⇒ RegExp
The pattern to split paths at; based on ‘separator`.
-
#virality(value) ⇒ Object
Ensure that all values have their path_prefix set.
Methods included from Support::Methods
loop_detected?, repeated, resolve_helpers, wrap_method
Class Method Details
.create_proc(write_access) ⇒ Object
Returns a proc for either read or write access. Procs for write access will create intermediary hashes when e.g. setting a value for ‘foo.bar.baz`, and the `bar` Hash doesn’t exist yet.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/collapsium/pathed_access.rb', line 68 def create_proc(write_access) return proc do |wrapped_method, *args, &block| # If there are no arguments, there's nothing to do with paths. Just # delegate to the hash. if args.empty? next wrapped_method.call(*args, &block) end # The method's receiver is encapsulated in the wrapped_method; we'll # use it a few times so let's reduce typing. This is essentially the # equivalent of `self`. receiver = wrapped_method.receiver # With any of the dispatch methods, we know that the first argument has # to be a key. We'll try to split it by the path separator. components = receiver.path_components(args[0].to_s) # If there are no components, return the receiver itself/the root if components.empty? next receiver end # Try to find the leaf, based on the given components. leaf = recursive_fetch(components, receiver, [], create: write_access) # The tricky part is what to do with the leaf. meth = nil if receiver.object_id == leaf.object_id # a) if the leaf and the receiver are identical, then the receiver # itself was requested, and we really just need to delegate to its # wrapped_method. meth = wrapped_method else # b) if the leaf is different from the receiver, we want to delegate # to the leaf. meth = leaf.method(wrapped_method.name) end # If the first argument was a symbol key, we want to use it verbatim. # Otherwise we had pathed access, and only want to pass the last # component to whatever method we're calling. the_args = args if not args[0].is_a?(Symbol) the_args = args.dup the_args[0] = components.last end # Then we can continue with that method. next meth.call(*the_args, &block) end # proc end |
.enhance(base) ⇒ Object
136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/collapsium/pathed_access.rb', line 136 def enhance(base) # Make the capabilities of classes using PathedAccess viral. base.extend(ViralCapabilities) # Wrap all accessor functions to deal with paths KEYED_READ_METHODS.each do |method| wrap_method(base, method, &PATHED_ACCESS_READER) end KEYED_WRITE_METHODS.each do |method| wrap_method(base, method, &PATHED_ACCESS_WRITER) end end |
.extended(base) ⇒ Object
128 129 130 |
# File 'lib/collapsium/pathed_access.rb', line 128 def extended(base) enhance(base) end |
.included(base) ⇒ Object
124 125 126 |
# File 'lib/collapsium/pathed_access.rb', line 124 def included(base) enhance(base) end |
.prepended(base) ⇒ Object
132 133 134 |
# File 'lib/collapsium/pathed_access.rb', line 132 def prepended(base) enhance(base) end |
.recursive_fetch(path, data, current_path = [], options = {}) ⇒ Object
Given the path components, recursively fetch any but the last key.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/collapsium/pathed_access.rb', line 151 def recursive_fetch(path, data, current_path = [], = {}) # Split path into head and tail; for the next iteration, we'll look use # only head, and pass tail on recursively. head = path[0] current_path << head tail = path.slice(1, path.length) # We know that the data has the current path. We also know that thanks to # virality, data will respond to :path_prefix. So we might as well set the # path, as long as it is more specific than what was previously there. current_normalized = data.normalize_path(current_path) if current_normalized.length > data.path_prefix.length data.path_prefix = current_normalized end # For the leaf element, we do nothing because that's where we want to # dispatch to. if path.length == 1 return data end # If we're a write function, then we need to create intermediary objects, # i.e. what's at head if nothing is there. if data[head].nil? # If the head is nil, we can't recurse. In create mode that means we # want to create hash children, but in read mode we're done recursing. # By returning a hash here, we allow the caller to send methods on to # this temporary, making a PathedAccess Hash act like any other Hash. if not [:create] return {} end data[head] = {} end # Ok, recurse. return recursive_fetch(tail, data[head], current_path, ) end |
Instance Method Details
#filter_components(components) ⇒ Object
Given path components, filters out unnecessary ones.
201 202 203 |
# File 'lib/collapsium/pathed_access.rb', line 201 def filter_components(components) return components.select { |c| not c.nil? and not c.empty? } end |
#join_path(components) ⇒ Object
Join path components with the ‘#separator`.
207 208 209 |
# File 'lib/collapsium/pathed_access.rb', line 207 def join_path(components) return components.join(separator) end |
#normalize_path(path) ⇒ Object
Normalizes a String path so that there are no empty components, and it starts with a separator.
214 215 216 217 218 219 220 221 222 |
# File 'lib/collapsium/pathed_access.rb', line 214 def normalize_path(path) components = [] if path.respond_to?(:split) # likely a String components = path_components(path) elsif path.respond_to?(:join) # likely an Array components = filter_components(path) end return separator + join_path(components) end |
#path_components(path) ⇒ Object
Break path into components. Expects a String path separated by the ‘#separator`, and returns the path split into components (an Array of String).
195 196 197 |
# File 'lib/collapsium/pathed_access.rb', line 195 def path_components(path) return filter_components(path.split(split_pattern)) end |
#path_prefix ⇒ Object
42 43 44 45 |
# File 'lib/collapsium/pathed_access.rb', line 42 def path_prefix @path_prefix ||= '' return @path_prefix end |
#path_prefix=(value) ⇒ Object
Assume any pathed access has this prefix.
38 39 40 |
# File 'lib/collapsium/pathed_access.rb', line 38 def path_prefix=(value) @path_prefix = normalize_path(value) end |
#separator ⇒ String
Returns the separator is the character or pattern splitting paths.
31 32 33 34 |
# File 'lib/collapsium/pathed_access.rb', line 31 def separator @separator ||= DEFAULT_SEPARATOR return @separator end |
#split_pattern ⇒ RegExp
Returns the pattern to split paths at; based on ‘separator`.
53 54 55 |
# File 'lib/collapsium/pathed_access.rb', line 53 def split_pattern /(?<!\\)#{Regexp.escape(separator)}/ end |
#virality(value) ⇒ Object
Ensure that all values have their path_prefix set.
226 227 228 229 230 231 232 233 |
# File 'lib/collapsium/pathed_access.rb', line 226 def (value) # If a value was set via a nested Hash, it may not have got its # path_prefix set during storing (i.e. x[key] = { nested: some_hash } # In that case, we do always know that the value's path prefix is the same # as the receiver. value.path_prefix = path_prefix return value end |