class Accessory::Lens
A Lens
is a “free-floating” lens (i.e. not bound to a subject document.) It serves as a container for {Accessor} instances, and represents the traversal path one would take to get from a hypothetical subject document, to a data value nested somewhere within it.
A Lens
can be used directly to traverse documents, using {get_in}, {put_in}, {pop_in}, etc. These methods take an explicit subject document to traverse, rather than requiring that the Lens
be bound to a document first.
As such, a Lens
is reusable. A common use of a Lens
is to access the same deeply-nested traversal position within a large collection of subject documents, e.g.:
foo_bar_baz = Lens[:foo, :bar, :baz] docs.map{ |doc| foo_bar_baz.get_in(doc) }
A Lens
can also be bound to a specific subject document to create a {BoundLens}. See {BoundLens.on}.
Lenses are created frozen. Methods that “extend” a Lens
actually create and return new derived Lenses.
Public Class Methods
Returns a {Lens} containing the specified accessors
. @return [Lens] a Lens
containing the specified accessors
.
# File lib/accessory/lens.rb, line 39 def self.[](*accessors) new(accessors).freeze end
@!visibility private
# File lib/accessory/lens.rb, line 48 def initialize(initial_parts) @parts = [] for part in initial_parts append_accessor!(part) end end
Public Instance Methods
Returns a new {Lens} resulting from concatenating other
to the end of the receiver. @param other [Object] an accessor, an Array
of accessors, or another Lens
@return [Lens] the new joined Lens
# File lib/accessory/lens.rb, line 90 def +(other) parts = case other when Accessory::Lens other.to_a when Array other else [other] end d = self.dup d.instance_eval do for part in parts append_accessor!(part) end end d.freeze end
Traverses subject
using the chain of accessors held in this Lens
, modifying the final value at the end of the traversal chain using the passed mutator_fn
, and returning the original targeted value(s) pre-modification.
mutator_fn
must return one of two data “shapes”:
-
a two-element
Array
, representing:-
the value to surface as the “get” value of the traversal
-
the new value to replace at the traversal-position
-
-
the Symbol
:pop
— which will remove the value from its parent, and return it as-is.
Equivalent in Elixir: {hexdocs.pm/elixir/Kernel.html#get_and_update_in/3 Kernel.get_and_update_in/3
}
@param subject [Object] the data-structure to traverse @param mutator_fn [Proc] a block taking the original value derived from
traversing +subject+, and returning a modification operation.
@return [Array] a two-element Array
, consisting of
1. the _old_ value(s) found after all traversals, and 2. the updated +subject+
# File lib/accessory/lens.rb, line 146 def get_and_update_in(subject, &mutator_fn) if @parts.empty? subject else get_and_update_in_step(subject, @parts, mutator_fn) end end
Traverses subject
using the chain of accessors held in this Lens
, returning the discovered value.
Equivalent in Elixir: {hexdocs.pm/elixir/Kernel.html#get_in/2 Kernel.get_in/2
}
@return [Object] the value found after all traversals.
# File lib/accessory/lens.rb, line 118 def get_in(subject) if @parts.empty? subject else get_in_step(subject, @parts) end end
@!visibility private
# File lib/accessory/lens.rb, line 62 def inspect(format: :long) parts_desc = @parts.map{ |part| part.inspect(format: :short) }.join(', ') parts_desc = "[#{parts_desc}]" case format when :long "#Lens#{parts_desc}" when :short parts_desc end end
Returns a new {BoundLens} wrapping this Lens
, bound to the specified subject
. @param subject [Object] the data-structure to traverse @return [BoundLens] a new BoundLens that will traverse subject
using
this Lens
# File lib/accessory/bound_lens.rb, line 136 def on(subject) Accessory::BoundLens.on(subject, self) end
Traverses subject
using the chain of accessors held in this Lens
, removing the final value at the end of the traversal chain from its position within its parent container.
Equivalent in Elixir: {hexdocs.pm/elixir/Kernel.html#pop_in/2 Kernel.pop_in/2
}
@param subject [Object] the data-structure to traverse @return [Object] the updated subject
# File lib/accessory/lens.rb, line 203 def pop_in(subject) _, popped_item, new_data = self.get_and_update_in(subject){ :pop } [popped_item, new_data] end
Traverses subject
using the chain of accessors held in this Lens
, replacing the final value at the end of the traversal chain with new_value
.
Equivalent in Elixir: {hexdocs.pm/elixir/Kernel.html#put_in/3 Kernel.put_in/3
}
@param subject [Object] the data-structure to traverse @param new_value [Object] a replacement value at the traversal position. @return [Object] the updated subject
# File lib/accessory/lens.rb, line 190 def put_in(subject, new_value) _, _, new_data = self.get_and_update_in(subject){ [:dirty, nil, new_value] } new_data end
Returns a new {Lens} resulting from appending accessor
to the receiver. @param accessor [Object] the accessor to append @return [Lens] the new joined Lens
# File lib/accessory/lens.rb, line 77 def then(accessor) d = self.dup d.instance_eval do @parts = @parts.dup append_accessor!(accessor) end d.freeze end
@!visibility private
# File lib/accessory/lens.rb, line 57 def to_a @parts end
Traverses subject
using the chain of accessors held in this Lens
, replacing the final value at the end of the traversal chain with the result from the passed new_value_fn
.
Equivalent in Elixir: {hexdocs.pm/elixir/Kernel.html#update_in/3 Kernel.update_in/3
}
@param subject [Object] the data-structure to traverse @param new_value_fn [Proc] a block taking the original value derived from
traversing +subject+, and returning a replacement value.
@return [Array] a two-element Array
, consisting of
1. the _old_ value(s) found after all traversals, and 2. the updated +subject+
# File lib/accessory/lens.rb, line 166 def update_in(subject, &new_value_fn) _, _, new_data = self.get_and_update_in(subject) do |v| case new_value_fn.call(v) in [:set, new_value] [:dirty, nil, new_value] in :keep [:clean, nil, v] in :pop :pop end end new_data end
Private Instance Methods
# File lib/accessory/lens.rb, line 208 def append_accessor!(part) accessor = case part when Accessory::Accessor part when Array Accessory::Accessors::SubscriptAccessor.new(part[0], default: part[1]) else Accessory::Accessors::SubscriptAccessor.new(part) end unless @parts.empty? @parts.last.successor = accessor end @parts.push(accessor) end
# File lib/accessory/lens.rb, line 239 def get_and_update_in_step(data, path, mutator_fn) step_accessor = path.first rest_of_path = path[1..-1] if rest_of_path.empty? step_accessor.get_and_update(data, &mutator_fn) else step_accessor.get_and_update(data){ |v| get_and_update_in_step(v, rest_of_path, mutator_fn) } end end
# File lib/accessory/lens.rb, line 227 def get_in_step(data, path) step_accessor = path.first rest_of_path = path[1..-1] if rest_of_path.empty? step_accessor.get(data) else step_accessor.get(data){ |v| get_in_step(v, rest_of_path) } end end