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

Instance Method Summary collapse

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 = [], options = {})
  # 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 options[:create]
      return {}
    end

    data[head] = {}
  end

  # Ok, recurse.
  return recursive_fetch(tail, data[head], current_path, options)
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_prefixObject



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

#separatorString

Returns the separator is the character or pattern splitting paths.

Returns:

  • (String)

    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_patternRegExp

Returns the pattern to split paths at; based on ‘separator`.

Returns:

  • (RegExp)

    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 virality(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