Class: Accessory::Accessor

Inherits:
Object
  • Object
show all
Defined in:
lib/accessory/accessor.rb

Overview

The parent class for accessors. Contains some shared behavior all accessors can rely on.

It doesn’t make sense to instantiate this class directly. Instantiate specific Accessor subclasses instead.

Implementing an Accessor

To implement an Accessor subclass, you must define at minimum these two methods (see the method docs of these methods for details):

You may also implement these two methods (again, see method docs for more info):

Helpers collapse

Callbacks collapse

Instance Method Summary collapse

Instance Method Details

#ensure_valid(traversal_result) ⇒ Object

Ensures that the predecessor accessor’s traversal result is one this accessor can operate on; called by #traverse_or_default.

This callback should validate that the traversal_result is one that can be traversed by the traversal-method of this accessor. If it can, the traversal_result should be returned unchanged. If it cannot, an object that can be traversed should be returned instead.

For example, if your accessor operates on Enumerable values (like Accessory::Accessors::AllAccessor), then this method should validate that the traversal_result is Enumerable; and, if it isn’t, return something that is — e.g. an empty Array.

This logic is used to replace invalid intermediate values (e.g. ‘nil`s and scalars) with containers during Lens#put_in et al.

Returns:

  • (Object)

    a now-valid traversal result



207
208
209
210
211
# File 'lib/accessory/accessor.rb', line 207

def ensure_valid(traversal_result)
  lambda do
    raise NotImplementedError, "Accessor subclass #{self.class} must implement #ensure_valid to allow chain-predecessor to use #traverse_or_default"
  end
end

#get(data, &succ) ⇒ Object

Traverses data in some way, and feeds the result of the traversal to the next step in the accessor chain by yielding the traversal-result to the passed-in block succ. The result from the yield is the result coming from the end of the accessor chain. Usually, it should be returned as-is.

succ can be yielded to multiple times, to run the rest of the accessor chain against multiple parts of data. In this case, the yield results should be gathered up into some container object to be returned together.

The successor accessor will receive the yielded element as its data.

After returning, the predecessor accessor will receive the result returned from #get as the result of its own yield.

Parameters:

  • data (Enumerable)

    the data yielded by the predecessor accessor.

  • succ (Proc)

    a thunk to the successor accessor. When #get is called by a Lens, this is passed implicitly.

Returns:

  • (Object)

    the data to pass back to the predecessor accessor as a yield result.

Raises:

  • (NotImplementedError)


119
120
121
# File 'lib/accessory/accessor.rb', line 119

def get(data, &succ)
  raise NotImplementedError, "Accessor subclass #{self.class} must implement #get"
end

#get_and_update(data, &succ) ⇒ Object

Traverses data in some way, and feeds the result of the traversal to the next step in the accessor chain by yielding the traversal-result to the passed-in block succ.

The result of the yield will be a “modification command”, one of these two:

  • an *update command* [get_result, new_value]

  • the symbol :pop

In the *update command* case:

  • the get_result should be returned as-is (or gathered together into a container object if this accessor yields multiple times.) The data flow of the get_results should replicate the data flow of #get.

  • the new_value should be used to replace or overwrite the result of this accessor’s traversal within data. For example, in Accessory::Accessors::SubscriptAccessor, data[key] = new_value is executed.

In the :pop command case:

  • the result of the traversal (before the yield) should be returned. This implies that any #get_and_update implementation must capture its traversal-results before feeding them into yield, in order to return them here.

  • the traversal-result should be removed from data. For example, in Accessory::Accessors::SubscriptAccessor, data.delete(key) is executed.

The successor in the accessor chain will receive the yielded traversal-results as its own data.

After returning, the predecessor accessor will receive the result returned from this method as the result of its own yield. This implies that this method should almost always be implemented to return an update command, looking like one of the following:

# given [get_result, new_value]
[get_result, data_after_update]

# given :pop
[traversal_result, data_after_removal]

Parameters:

  • data (Enumerable)

    the data yielded by the predecessor accessor.

  • succ (Proc)

    a thunk to the successor accessor. When #get is called by a Lens, this is passed implicitly.

Returns:

  • (Object)

    the modification-command to pass back to the predecessor accessor as a yield result.

Raises:

  • (NotImplementedError)


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

def get_and_update(data, &succ)
  raise NotImplementedError, "Accessor subclass #{self.class} must implement #get_and_update"
end

#traverse(data) ⇒ Object

Traverses data; called by #traverse_or_default.

This method should traverse data however your accessor does that, producing either one traversal-result or a container-object of gathered traversal-results.

This method can assume that data is a valid receiver for the traversal it performs. #traverse_or_default takes care of feeding in a default data in the case where the predecessor passed invalid data.

Parameters:

  • data (Object)

    the object to be traversed

Returns:

  • (Object)

    the result of traversal

Raises:

  • (NotImplementedError)


185
186
187
# File 'lib/accessory/accessor.rb', line 185

def traverse(data)
  raise NotImplementedError, "Accessor subclass #{self.class} must implement #traverse to use #traverse_or_default"
end

#traverse_or_default(data) ⇒ Object

Safely traverses data, with useful defaults; simplifies implementations of #get and #get_and_update.

Rather than writing redundant traversal logic into both methods, you can implement the callback method #traverse to define your traversal, and then call traverse_or_default(data) within your implementation to safely get a traversal-result to operate on.

The value returned by your #traverse callback will be validated by your calling the #ensure_valid callback of the _successor accessor_ in the Lens path. #ensure_valid will replace any value it considers invalid with a reasonable default. This means you don’t need to worry about what will happen if your traversal doesn’t find a value and so returns nil. The successor’s #ensure_valid will replace that nil with a value that works for it.



88
89
90
91
92
93
94
95
96
# File 'lib/accessory/accessor.rb', line 88

def traverse_or_default(data)
  traversal_result = traverse(data) || @default_value

  if @successor
    @successor.ensure_valid(traversal_result)
  else
    traversal_result
  end
end