accessory 0.1.4 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e6d18076731a0f1b3a28ce6ef77c75ef35dff6b7646f4880286b9bcf64ac393
4
- data.tar.gz: '07918b78b0d0b1ae8097f22f36039a04fdefe9e59a10cae62758da84785fac4f'
3
+ metadata.gz: c03753edaa94362541af5695a93547b6e1f3565f37aa82e7536b3e81d212eb7c
4
+ data.tar.gz: a3584fbf0b4ee32555feccd642c7225fd6c6f8f85563fcb03b542624f36f7660
5
5
  SHA512:
6
- metadata.gz: ccdb07d177f3b436c91303585b921494d64aac6baa30abc32debf6ebc7ee4a6ff032b3db48f51d3c7b5d6498daeb7a298bcfd75840682bdc842e8fd70581bcf0
7
- data.tar.gz: 2183b4b3513781e43b99490b4b86617ee8f08624c5c71a1355100fb84199bae2f27865d6b17496802c29c5571d39699e239b4dfc163983e789e618c39daf898d
6
+ metadata.gz: 61015ff40a150d474e43ec454a4c50cb0fd4021660a10cf65d61cd69ec4abe3293523fa53cceda199da7996ed5bea160efc45bf826724647fbc7020f0c666b57
7
+ data.tar.gz: 3ec8ca3a2f53df27e958621997102d6d10220a15f26c48dc6cdb3f56403f387d629d531433d93f05e6ced2f30ad7d046df0ae27ad620047715b3648bf2ec20dd
@@ -1,14 +1,14 @@
1
1
  module Accessory; end
2
2
 
3
3
  require 'accessory/version'
4
- require 'accessory/lens_path'
5
4
  require 'accessory/lens'
5
+ require 'accessory/bound_lens'
6
6
  require 'accessory/access'
7
7
 
8
8
  module Accessory
9
9
  refine ::Object do
10
- def lens
11
- ::Accessory::Lens.on(self)
10
+ def lens(...)
11
+ ::Accessory::BoundLens.on(self, ...)
12
12
  end
13
13
  end
14
14
  end
@@ -10,87 +10,102 @@ require 'accessory/accessors/all_accessor'
10
10
  require 'accessory/accessors/first_accessor'
11
11
  require 'accessory/accessors/last_accessor'
12
12
 
13
+ ##
14
+ # A set of convenient module-function helpers to use with <tt>Lens[...]</tt>.
15
+ #
16
+ # These functions aren't very convenient unless you
17
+ #
18
+ # include Accessory
19
+
13
20
  module Accessory::Access
14
- # (see Accessory::SubscriptAccessor)
21
+ # (see Accessory::Accessors::SubscriptAccessor)
15
22
  def self.subscript(...)
16
- Accessory::SubscriptAccessor.new(...)
23
+ Accessory::Accessors::SubscriptAccessor.new(...)
17
24
  end
18
25
 
19
- # (see Accessory::AttributeAccessor)
26
+ # (see Accessory::Accessors::AttributeAccessor)
20
27
  def self.attr(...)
21
- Accessory::AttributeAccessor.new(...)
28
+ Accessory::Accessors::AttributeAccessor.new(...)
22
29
  end
23
30
 
24
- # (see Accessory::InstanceVariableAccessor)
31
+ # (see Accessory::Accessors::InstanceVariableAccessor)
25
32
  def self.ivar(...)
26
- Accessory::InstanceVariableAccessor.new(...)
33
+ Accessory::Accessors::InstanceVariableAccessor.new(...)
27
34
  end
28
35
 
29
- # (see Accessory::BetwixtAccessor)
36
+ # (see Accessory::Accessors::BetwixtAccessor)
30
37
  def self.betwixt(...)
31
- Accessory::BetwixtAccessor.new(...)
38
+ Accessory::Accessors::BetwixtAccessor.new(...)
32
39
  end
33
40
 
34
- # Alias for +Accessory::Access.betwixt(0)+. See {Access.betwixt}
41
+ # Alias for +Accessory::Accessors::Access.betwixt(0)+. See {Access.betwixt}
35
42
  def self.before_first
36
43
  self.betwixt(0)
37
44
  end
38
45
 
39
- # Alias for +Accessory::Access.betwixt(-1)+. See {Access.betwixt}
46
+ # Alias for +Accessory::Accessors::Access.betwixt(-1)+. See {Access.betwixt}
40
47
  def self.after_last
41
48
  self.betwixt(-1)
42
49
  end
43
50
 
44
- # (see Accessory::BetweenEachAccessor)
51
+ # (see Accessory::Accessors::BetweenEachAccessor)
45
52
  def self.between_each
46
- Accessory::BetweenEachAccessor.new
53
+ Accessory::Accessors::BetweenEachAccessor.new
47
54
  end
48
55
 
49
- # (see Accessory::AllAccessor)
56
+ # (see Accessory::Accessors::AllAccessor)
50
57
  def self.all
51
- Accessory::AllAccessor.new
58
+ Accessory::Accessors::AllAccessor.new
52
59
  end
53
60
 
54
- # (see Accessory::FirstAccessor)
61
+ # (see Accessory::Accessors::FirstAccessor)
55
62
  def self.first
56
- Accessory::FirstAccessor.new
63
+ Accessory::Accessors::FirstAccessor.new
57
64
  end
58
65
 
59
- # (see Accessory::LastAccessor)
66
+ # (see Accessory::Accessors::LastAccessor)
60
67
  def self.last
61
- Accessory::LastAccessor.new
68
+ Accessory::Accessors::LastAccessor.new
62
69
  end
63
70
 
64
- # (see Accessory::FilterAccessor)
71
+ # (see Accessory::Accessors::FilterAccessor)
65
72
  def self.filter(&pred)
66
- Accessory::FilterAccessor.new(pred)
73
+ Accessory::Accessors::FilterAccessor.new(pred)
67
74
  end
68
75
  end
69
76
 
77
+ ##
78
+ # A set of convenient "fluent API" builder methods
79
+ # that get mixed into {Lens} and {BoundLens}.
80
+ #
81
+ # These do the same thing as the {Access} helper of the same name, but
82
+ # wrap the resulting accessor in a call to <tt>#then</tt>, deriving a new
83
+ # {Lens} or {BoundLens} from the addition of the accessor.
84
+
70
85
  module Accessory::Access::FluentHelpers
71
- # (see Accessory::SubscriptAccessor)
86
+ # (see Accessory::Accessors::SubscriptAccessor)
72
87
  def subscript(...)
73
- self.then(Accessory::SubscriptAccessor.new(...))
88
+ self.then(Accessory::Accessors::SubscriptAccessor.new(...))
74
89
  end
75
90
 
76
91
  # Alias for {#subscript}
77
92
  def [](...)
78
- self.then(Accessory::SubscriptAccessor.new(...))
93
+ self.then(Accessory::Accessors::SubscriptAccessor.new(...))
79
94
  end
80
95
 
81
- # (see Accessory::AttributeAccessor)
96
+ # (see Accessory::Accessors::AttributeAccessor)
82
97
  def attr(...)
83
- self.then(Accessory::AttributeAccessor.new(...))
98
+ self.then(Accessory::Accessors::AttributeAccessor.new(...))
84
99
  end
85
100
 
86
- # (see Accessory::InstanceVariableAccessor)
101
+ # (see Accessory::Accessors::InstanceVariableAccessor)
87
102
  def ivar(...)
88
- self.then(Accessory::InstanceVariableAccessor.new(...))
103
+ self.then(Accessory::Accessors::InstanceVariableAccessor.new(...))
89
104
  end
90
105
 
91
- # (see Accessory::BetwixtAccessor)
106
+ # (see Accessory::Accessors::BetwixtAccessor)
92
107
  def betwixt(...)
93
- self.then(Accessory::BetwixtAccessor.new(...))
108
+ self.then(Accessory::Accessors::BetwixtAccessor.new(...))
94
109
  end
95
110
 
96
111
  # Alias for +#betwixt(0)+. See {#betwixt}
@@ -103,29 +118,29 @@ module Accessory::Access::FluentHelpers
103
118
  self.betwixt(-1)
104
119
  end
105
120
 
106
- # (see Accessory::BetweenEachAccessor)
121
+ # (see Accessory::Accessors::BetweenEachAccessor)
107
122
  def between_each
108
- self.then(Accessory::BetweenEachAccessor.new)
123
+ self.then(Accessory::Accessors::BetweenEachAccessor.new)
109
124
  end
110
125
 
111
- # (see Accessory::AllAccessor)
126
+ # (see Accessory::Accessors::AllAccessor)
112
127
  def all
113
- self.then(Accessory::AllAccessor.new)
128
+ self.then(Accessory::Accessors::AllAccessor.new)
114
129
  end
115
130
 
116
- # (see Accessory::FirstAccessor)
131
+ # (see Accessory::Accessors::FirstAccessor)
117
132
  def first
118
- self.then(Accessory::FirstAccessor.new)
133
+ self.then(Accessory::Accessors::FirstAccessor.new)
119
134
  end
120
135
 
121
- # (see Accessory::LastAccessor)
136
+ # (see Accessory::Accessors::LastAccessor)
122
137
  def last
123
- self.then(Accessory::LastAccessor.new)
138
+ self.then(Accessory::Accessors::LastAccessor.new)
124
139
  end
125
140
 
126
- # (see Accessory::FilterAccessor)
141
+ # (see Accessory::Accessors::FilterAccessor)
127
142
  def filter(&pred)
128
- self.then(Accessory::FilterAccessor.new(pred))
143
+ self.then(Accessory::Accessors::FilterAccessor.new(pred))
129
144
  end
130
145
  end
131
146
 
@@ -133,6 +148,6 @@ class Accessory::Lens
133
148
  include Accessory::Access::FluentHelpers
134
149
  end
135
150
 
136
- class Accessory::LensPath
151
+ class Accessory::BoundLens
137
152
  include Accessory::Access::FluentHelpers
138
153
  end
@@ -1,19 +1,33 @@
1
1
  module Accessory; end
2
2
 
3
3
  ##
4
- # @!visibility private
5
- class Accessory::Accessor
6
-
7
- # @!visibility private
8
- DEFAULT_NOT_SET_SENTINEL = :"98e47971-e708-42ca-bee7-0c62fe5e11c9"
4
+ # The parent class for accessors. Contains some shared behavior all accessors
5
+ # can rely on.
6
+ #
7
+ # It doesn't make sense to instantiate this class directly. Instantiate specific
8
+ # {Accessor} subclasses instead.
9
+ #
10
+ # == Implementing an Accessor
11
+ #
12
+ # To implement an {Accessor} subclass, you must define at minimum these two
13
+ # methods (see the method docs of these methods for details):
14
+ #
15
+ # * {Accessor#get}
16
+ # * {Accessor#get_and_update}
17
+ #
18
+ # You may also implement these two methods (again, see method docs for more
19
+ # info):
20
+ #
21
+ # * {Accessor#traverse}
22
+ # * {Accessor#ensure_valid}
9
23
 
10
- # @!visibility private
24
+ class Accessory::Accessor
11
25
  TERMINAL_DEFAULT_FN = lambda{ nil }
26
+ private_constant :TERMINAL_DEFAULT_FN
12
27
 
13
28
  # @!visibility private
14
- def initialize(default = nil)
15
- @default_value = default || DEFAULT_NOT_SET_SENTINEL
16
- @make_default_fn = TERMINAL_DEFAULT_FN
29
+ def initialize(default_value = nil)
30
+ @default_value = default_value
17
31
  end
18
32
 
19
33
  # @!visibility private
@@ -40,7 +54,10 @@ class Accessory::Accessor
40
54
  end
41
55
 
42
56
  # @!visibility private
43
- HIDDEN_IVARS = [:@default_value, :@make_default_fn]
57
+ HIDDEN_IVARS = [:@default_value, :@successor]
58
+ private_constant :HIDDEN_IVARS
59
+
60
+ # @!visibility private
44
61
  def inspect_args
45
62
  (instance_variables - HIDDEN_IVARS).map do |ivar_k|
46
63
  ivar_v = instance_variable_get(ivar_k)
@@ -49,19 +66,151 @@ class Accessory::Accessor
49
66
  end
50
67
 
51
68
  # @!visibility private
52
- attr_accessor :make_default_fn
69
+ attr_accessor :successor
53
70
 
54
- # @!visibility private
55
- def value_or_default(data)
56
- return nil if data.nil?
71
+ # @!group Helpers
57
72
 
58
- maybe_value = value_from(data)
59
- return maybe_value unless maybe_value.nil?
73
+ # Safely traverses +data+, with useful defaults; simplifies implementations of
74
+ # {get} and {get_and_update}.
75
+ #
76
+ # Rather than writing redundant traversal logic into both methods, you can
77
+ # implement the callback method {traverse} to define your traversal, and then
78
+ # call <tt>traverse_or_default(data)</tt> within your implementation to safely
79
+ # get a traversal-result to operate on.
80
+ #
81
+ # The value returned by your {traverse} callback will be validated by your
82
+ # calling the {ensure_valid} callback of the _successor accessor_ in the Lens
83
+ # path. {ensure_valid} will replace any value it considers invalid with a
84
+ # reasonable default. This means you don't need to worry about what will
85
+ # happen if your traversal doesn't find a value and so returns +nil+. The
86
+ # successor's {ensure_valid} will replace that +nil+ with a value that works
87
+ # for it.
88
+ def traverse_or_default(data)
89
+ traversal_result = traverse(data) || @default_value
60
90
 
61
- if DEFAULT_NOT_SET_SENTINEL.equal?(@default_value)
62
- @make_default_fn.call
91
+ if @successor
92
+ @successor.ensure_valid(traversal_result)
63
93
  else
64
- @default_value
94
+ traversal_result
65
95
  end
66
96
  end
97
+
98
+ # @!endgroup
99
+
100
+ # Traverses +data+ in some way, and feeds the result of the traversal to the
101
+ # next step in the accessor chain by yielding the traversal-result to the
102
+ # passed-in block +succ+. The result from the yield is the result coming from
103
+ # the end of the accessor chain. Usually, it should be returned as-is.
104
+ #
105
+ # +succ+ can be yielded to multiple times, to run the rest of the accessor
106
+ # chain against multiple parts of +data+. In this case, the yield results
107
+ # should be gathered up into some container object to be returned together.
108
+ #
109
+ # The successor accessor will receive the yielded element as its +data+.
110
+ #
111
+ # After returning, the predecessor accessor will receive the result returned
112
+ # from {get} as the result of its own +yield+.
113
+ #
114
+ # @param data [Enumerable] the data yielded by the predecessor accessor.
115
+ # @param succ [Proc] a thunk to the successor accessor. When {get} is called
116
+ # by a {Lens}, this is passed implicitly.
117
+ # @return [Object] the data to pass back to the predecessor accessor as a
118
+ # yield result.
119
+ def get(data, &succ)
120
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #get"
121
+ end
122
+
123
+ # Traverses +data+ in some way, and feeds the result of the traversal to the
124
+ # next step in the accessor chain by yielding the traversal-result to the
125
+ # passed-in block +succ+.
126
+ #
127
+ # The result of the yield will be a "modification command", one of these two:
128
+ # * an *update command* <tt>[get_result, new_value]</tt>
129
+ # * the symbol <tt>:pop</tt>
130
+ #
131
+ # In the *update command* case:
132
+ # * the +get_result+ should be returned as-is (or gathered together into
133
+ # a container object if this accessor yields multiple times.) The data flow
134
+ # of the +get_result+s should replicate the data flow of {get}.
135
+ # * the +new_value+ should be used to *replace* or *overwrite* the result of
136
+ # this accessor's traversal within +data+. For example, in
137
+ # {Accessors::SubscriptAccessor}, <tt>data[key] = new_value</tt> is
138
+ # executed.
139
+ #
140
+ # In the <tt>:pop</tt> command case:
141
+ # * the result of the traversal (*before* the yield) should be returned. This
142
+ # implies that any {get_and_update} implementation must capture its
143
+ # traversal-results before feeding them into yield, in order to return them
144
+ # here.
145
+ # * the traversal-result should be *removed* from +data+. For example, in
146
+ # {Accessors::SubscriptAccessor}, <tt>data.delete(key)</tt> is executed.
147
+ #
148
+ # The successor in the accessor chain will receive the yielded
149
+ # traversal-results as its own +data+.
150
+ #
151
+ # After returning, the predecessor accessor will receive the result returned
152
+ # from this method as the result of its own +yield+. This implies that
153
+ # this method should almost always be implemented to *return* an update
154
+ # command, looking like one of the following:
155
+ #
156
+ # # given [get_result, new_value]
157
+ # [get_result, data_after_update]
158
+ #
159
+ # # given :pop
160
+ # [traversal_result, data_after_removal]
161
+ #
162
+ # @param data [Enumerable] the data yielded by the predecessor accessor.
163
+ # @param succ [Proc] a thunk to the successor accessor. When {get} is called
164
+ # by a {Lens}, this is passed implicitly.
165
+ # @return [Object] the modification-command to pass back to the predecessor
166
+ # accessor as a yield result.
167
+ def get_and_update(data, &succ)
168
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #get_and_update"
169
+ end
170
+
171
+ # @!group Callbacks
172
+
173
+ # Traverses +data+; called by {traverse_or_default}.
174
+ #
175
+ # This method should traverse +data+ however your accessor does that,
176
+ # producing either one traversal-result or a container-object of gathered
177
+ # traversal-results.
178
+ #
179
+ # This method can assume that +data+ is a valid receiver for the traversal
180
+ # it performs. {traverse_or_default} takes care of feeding in a default +data+
181
+ # in the case where the predecessor passed invalid data.
182
+ #
183
+ # @param data [Object] the object to be traversed
184
+ # @return [Object] the result of traversal
185
+ def traverse(data)
186
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #traverse to use #traverse_or_default"
187
+ end
188
+
189
+ # Ensures that the predecessor accessor's traversal result is one this
190
+ # accessor can operate on; called by {traverse_or_default}.
191
+ #
192
+ # This callback should validate that the +traversal_result+ is one that can
193
+ # be traversed by the traversal-method of this accessor. If it can, the
194
+ # +traversal_result+ should be returned unchanged. If it cannot, an object
195
+ # that _can_ be traversed should be returned instead.
196
+ #
197
+ #
198
+ # For example, if your accessor operates on +Enumerable+ values (like
199
+ # {Accessors::AllAccessor}), then this method should validate that the
200
+ # +traversal_result+ is +Enumerable+; and, if it isn't, return something that
201
+ # is — e.g. an empty +Array+.
202
+ #
203
+ # This logic is used to replace invalid intermediate values (e.g. `nil`s and
204
+ # scalars) with containers during {Lens#put_in} et al.
205
+ #
206
+ # @return [Object] a now-valid traversal result
207
+ def ensure_valid(traversal_result)
208
+ lambda do
209
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #ensure_valid to allow chain-predecessor to use #traverse_or_default"
210
+ end
211
+ end
212
+
213
+ # @!endgroup
67
214
  end
215
+
216
+ module Accessory::Accessors; end
@@ -5,7 +5,7 @@ require 'accessory/accessor'
5
5
  #
6
6
  # *Aliases*
7
7
  # * {Access.all}
8
- # * {Access::FluentHelpers#all} (included in {LensPath} and {Lens})
8
+ # * {Access::FluentHelpers#all} (included in {Lens} and {BoundLens})
9
9
  #
10
10
  # *Equivalents* in Elixir's {https://hexdocs.pm/elixir/Access.html +Access+} module
11
11
  # * {https://hexdocs.pm/elixir/Access.html#all/0 +Access.all/0+}
@@ -15,10 +15,14 @@ require 'accessory/accessor'
15
15
  #
16
16
  # * +Array.new+
17
17
 
18
- class Accessory::AllAccessor < Accessory::Accessor
18
+ class Accessory::Accessors::AllAccessor < Accessory::Accessor
19
19
  # @!visibility private
20
- def default_fn_for_previous_step
21
- lambda{ Array.new }
20
+ def ensure_valid(traversal_result)
21
+ if traversal_result.kind_of?(Enumerable)
22
+ traversal_result
23
+ else
24
+ []
25
+ end
22
26
  end
23
27
 
24
28
  # @!visibility private
@@ -46,17 +50,28 @@ class Accessory::AllAccessor < Accessory::Accessor
46
50
  def get_and_update(data)
47
51
  results = []
48
52
  new_data = []
53
+ dirty = false
49
54
 
50
55
  (data || []).each do |pos|
51
56
  case yield(pos)
52
- in [result, new_value]
57
+ in [:clean, result, _]
58
+ results.push(result)
59
+ new_data.push(pos)
60
+ # ok
61
+ in [:dirty, result, new_value]
53
62
  results.push(result)
54
63
  new_data.push(new_value)
64
+ dirty = true
55
65
  in :pop
56
66
  results.push(pos)
67
+ dirty = true
57
68
  end
58
69
  end
59
70
 
60
- [results, new_data]
71
+ if dirty
72
+ [:dirty, results, new_data]
73
+ else
74
+ [:clean, results, data]
75
+ end
61
76
  end
62
77
  end