Class: Accessory::Lens

Inherits:
Object
  • Object
show all
Includes:
Access::FluentHelpers
Defined in:
lib/accessory/lens.rb,
lib/accessory/access.rb,
lib/accessory/bound_lens.rb

Overview

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.

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Access::FluentHelpers

#[], #after_last, #all, #attr, #before_first, #between_each, #betwixt, #filter, #first, #ivar, #last, #subscript

Class Method Details

.[](*accessors) ⇒ Lens

Returns a Accessory::Lens containing the specified accessors.

Returns:

  • (Lens)

    a Lens containing the specified accessors.



39
40
41
# File 'lib/accessory/lens.rb', line 39

def self.[](*accessors)
  new(accessors).freeze
end

.emptyLens

Returns the empty (identity) Lens.

Returns:

  • (Lens)

    the empty (identity) Lens.



33
34
35
# File 'lib/accessory/lens.rb', line 33

def self.empty
  @empty_lens ||= (new([]).freeze)
end

Instance Method Details

#+(other) ⇒ Lens Also known as: /

Returns a new Accessory::Lens resulting from concatenating other to the end of the receiver.

Parameters:

  • other (Object)

    an accessor, an Array of accessors, or another Lens

Returns:

  • (Lens)

    the new joined Lens



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# 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

#get_and_update_in(subject, &mutator_fn) ⇒ Array

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:

    1. the value to surface as the “get” value of the traversal

    2. 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: Kernel.get_and_update_in/3

Parameters:

  • subject (Object)

    the data-structure to traverse

  • mutator_fn (Proc)

    a block taking the original value derived from traversing subject, and returning a modification operation.

Returns:

  • (Array)

    a two-element Array, consisting of

    1. the old value(s) found after all traversals, and

    2. the updated subject



146
147
148
149
150
151
152
# 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

#get_in(subject) ⇒ Object

Traverses subject using the chain of accessors held in this Lens, returning the discovered value.

Equivalent in Elixir: Kernel.get_in/2

Returns:

  • (Object)

    the value found after all traversals.



118
119
120
121
122
123
124
# File 'lib/accessory/lens.rb', line 118

def get_in(subject)
  if @parts.empty?
    subject
  else
    get_in_step(subject, @parts)
  end
end

#on(subject) ⇒ BoundLens

Returns a new BoundLens wrapping this Lens, bound to the specified subject.

Parameters:

  • subject (Object)

    the data-structure to traverse

Returns:

  • (BoundLens)

    a new BoundLens that will traverse subject using this Lens



136
137
138
# File 'lib/accessory/bound_lens.rb', line 136

def on(subject)
  Accessory::BoundLens.on(subject, self)
end

#pop_in(subject) ⇒ Object

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: Kernel.pop_in/2

Parameters:

  • subject (Object)

    the data-structure to traverse

Returns:

  • (Object)

    the updated subject



193
194
195
# File 'lib/accessory/lens.rb', line 193

def pop_in(subject)
  self.get_and_update_in(subject){ :pop }
end

#put_in(subject, new_value) ⇒ Object

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: Kernel.put_in/3

Parameters:

  • subject (Object)

    the data-structure to traverse

  • new_value (Object)

    a replacement value at the traversal position.

Returns:

  • (Object)

    the updated subject



180
181
182
183
# File 'lib/accessory/lens.rb', line 180

def put_in(subject, new_value)
  _, new_data = self.get_and_update_in(subject){ [nil, new_value] }
  new_data
end

#then(accessor) ⇒ Lens

Returns a new Accessory::Lens resulting from appending accessor to the receiver.

Parameters:

  • accessor (Object)

    the accessor to append

Returns:

  • (Lens)

    the new joined Lens



77
78
79
80
81
82
83
84
# 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

#update_in(subject, &new_value_fn) ⇒ Array

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: Kernel.update_in/3

Parameters:

  • subject (Object)

    the data-structure to traverse

  • new_value_fn (Proc)

    a block taking the original value derived from traversing subject, and returning a replacement value.

Returns:

  • (Array)

    a two-element Array, consisting of

    1. the old value(s) found after all traversals, and

    2. the updated subject



166
167
168
169
# File 'lib/accessory/lens.rb', line 166

def update_in(subject, &new_value_fn)
  _, new_data = self.get_and_update_in(subject){ |v| [nil, new_value_fn.call(v)] }
  new_data
end