Module: Collapsium::PathedAccess

Extended by:
Support::Methods
Includes:
Support::PathComponents
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

READ_METHODS =

We want to wrap methods for Arrays and Hashes alike

(
  ::Collapsium::Support::HashMethods::KEYED_READ_METHODS \
  + ::Collapsium::Support::ArrayMethods::INDEXED_READ_METHODS
).uniq.freeze
WRITE_METHODS =
(
  ::Collapsium::Support::HashMethods::KEYED_WRITE_METHODS \
  + ::Collapsium::Support::ArrayMethods::INDEXED_WRITE_METHODS
).uniq.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::Methods

Support::Methods::BUILTINS, Support::Methods::WRAPPER_HASH

Constants included from Support::PathComponents

Support::PathComponents::DEFAULT_SEPARATOR

Instance Attribute Summary

Attributes included from Support::PathComponents

#separator

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support::Methods

builtins, loop_detected?, repeated, resolve_helpers, wrap_method, wrappers

Methods included from Support::PathComponents

#filter_components, #join_path, #normalize_path, #parent_path, #path_components, #path_prefix, #path_prefix=, #split_pattern

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.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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
# File 'lib/collapsium/pathed_access.rb', line 53

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)

    # Since Methods already contains loop prevention and we may want to
    # call wrapped methods, let's just find the method to call from the
    # leaf by name.
    meth = leaf.method(wrapped_method.name)

    # 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) and args[0] != components.last
      the_args = args.dup
      the_args[0] = components.last
    end

    # Array methods we're modifying here are indexed, so the first argument
    # must be an integer. Let's make it so :)
    if leaf.is_a? Array and the_args[0][0] =~ /[0-9]/
      the_args = the_args.dup
      the_args[0] = the_args[0].to_s.to_i
    end

    # Then we can continue with that method.
    result = meth.call(*the_args, &block)

    # Sadly, we can't just return the result and be done with it.
    # We need to tell the virality function (below) what we know about the
    # result's path prefix, so we enhance the result value explicitly here.
    result_path = receiver.path_components(receiver.path_prefix)
    result_path += components
    next ViralCapabilities.enhance_value(leaf, result, result_path)
  end # proc
end

.enhance(base) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/collapsium/pathed_access.rb', line 127

def enhance(base)
  # Make the capabilities of classes using PathedAccess viral.
  base.extend(ViralCapabilities)

  # Wrap all accessor functions to deal with paths
  READ_METHODS.each do |method|
    wrap_method(base, method, raise_on_missing: false, &PATHED_ACCESS_READER)
  end
  WRITE_METHODS.each do |method|
    wrap_method(base, method, raise_on_missing: false, &PATHED_ACCESS_WRITER)
  end
end

.extended(base) ⇒ Object



119
120
121
# File 'lib/collapsium/pathed_access.rb', line 119

def extended(base)
  enhance(base)
end

.included(base) ⇒ Object



115
116
117
# File 'lib/collapsium/pathed_access.rb', line 115

def included(base)
  enhance(base)
end

.prepended(base) ⇒ Object



123
124
125
# File 'lib/collapsium/pathed_access.rb', line 123

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.



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/collapsium/pathed_access.rb', line 142

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)

  # 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

#virality(value, *args) ⇒ Object

Ensure that all values have their path_prefix set.



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/collapsium/pathed_access.rb', line 176

def virality(value, *args)
  # Figure out what path prefix to set on the value, if any.
  # Candidates for the prefix are:
  explicit = args[0] || []
  from_self = path_components(path_prefix)
  from_value = path_components(value.path_prefix)

  prefix = []
  if not explicit.empty?
    # If we got explicit information, we most likely want to use that.
    prefix = explicit
  elsif not from_self.empty?
    # If we got information from self, that's the next best candidate.
    prefix = from_self
  end

  # However, if the value already has a better path prefix than either
  # of the above, we want to keep that.
  if prefix.length > from_value.length
    value.path_prefix = normalize_path(prefix)
  end

  return value
end