accessory 0.1.2 → 0.1.7

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: f8065ca0c8cb368dc5a7e0a6f11b4d582a2448a0255590a6aa4ae557c62c321f
4
- data.tar.gz: 2854d2fc74ffa690a95f0ad9db698497f037b2a7b74e4ceae9a380ba89796b7b
3
+ metadata.gz: 9f4517af7f1ae501a9dea6721f8dd0966a85c2fd9e8ada3d1d444f8b58cb297e
4
+ data.tar.gz: 4e8bcc0130b9cb5078779642cc0e81f4d4121ac6275ead2fe7a5decd873a5047
5
5
  SHA512:
6
- metadata.gz: 274e4559b3d8e4a788b0f23c7831bfe5dbc1ada32dddb82a21ba0bcff9097ec7c04aa0d82af746e22f959dc7c336f2473c5ade8445f0035d704a64e218681e7a
7
- data.tar.gz: 6320c7cc7a436ef043d6993d33b68ad56f4e1e30e0bd37b03b110d067f55ebc3c03aec7a4e14602d0c64d9bea15625069b5da34e9d8d13f5c4e92db8557c243c
6
+ metadata.gz: 295b43b1f100522c1dcc9e659acbcf174a9725c38448d68b8eb3be10603b9ad9df643f82dced994849616d194ccef080697febc3f934440b45420eae8229fa31
7
+ data.tar.gz: 5d9d93d0a17668ba39313dd8151cf7324a562df362fbbeb0a34bdc6c24b2dc7a8996f36f8e0b1be1c0d489dafc435f645e1b8a4348e3a080bdd66e9efe5dbc61
@@ -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
@@ -1,7 +1,7 @@
1
1
  module Accessory; end
2
2
 
3
3
  require 'accessory/accessors/subscript_accessor'
4
- require 'accessory/accessors/field_accessor'
4
+ require 'accessory/accessors/attribute_accessor'
5
5
  require 'accessory/accessors/filter_accessor'
6
6
  require 'accessory/accessors/instance_variable_accessor'
7
7
  require 'accessory/accessors/betwixt_accessor'
@@ -10,91 +10,137 @@ 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
- def self.field(...)
15
- Accessory::FieldAccessor.new(...)
21
+ # (see Accessory::Accessors::SubscriptAccessor)
22
+ def self.subscript(...)
23
+ Accessory::Accessors::SubscriptAccessor.new(...)
24
+ end
25
+
26
+ # (see Accessory::Accessors::AttributeAccessor)
27
+ def self.attr(...)
28
+ Accessory::Accessors::AttributeAccessor.new(...)
16
29
  end
17
30
 
31
+ # (see Accessory::Accessors::InstanceVariableAccessor)
18
32
  def self.ivar(...)
19
- Accessory::InstanceVariableAccessor.new(...)
33
+ Accessory::Accessors::InstanceVariableAccessor.new(...)
20
34
  end
21
35
 
36
+ # (see Accessory::Accessors::BetwixtAccessor)
22
37
  def self.betwixt(...)
23
- Accessory::BetwixtAccessor.new(...)
38
+ Accessory::Accessors::BetwixtAccessor.new(...)
24
39
  end
25
40
 
41
+ # Alias for +Accessory::Accessors::Access.betwixt(0)+. See {Access.betwixt}
26
42
  def self.before_first
27
43
  self.betwixt(0)
28
44
  end
29
45
 
46
+ # Alias for +Accessory::Accessors::Access.betwixt(-1)+. See {Access.betwixt}
30
47
  def self.after_last
31
48
  self.betwixt(-1)
32
49
  end
33
50
 
51
+ # (see Accessory::Accessors::BetweenEachAccessor)
34
52
  def self.between_each
35
- Accessory::BetweenEachAccessor.new
53
+ Accessory::Accessors::BetweenEachAccessor.new
36
54
  end
37
55
 
56
+ # (see Accessory::Accessors::AllAccessor)
38
57
  def self.all
39
- Accessory::AllAccessor.new
58
+ Accessory::Accessors::AllAccessor.new
40
59
  end
41
60
 
61
+ # (see Accessory::Accessors::FirstAccessor)
42
62
  def self.first
43
- Accessory::FirstAccessor.new
63
+ Accessory::Accessors::FirstAccessor.new
44
64
  end
45
65
 
66
+ # (see Accessory::Accessors::LastAccessor)
46
67
  def self.last
47
- Accessory::LastAccessor.new
68
+ Accessory::Accessors::LastAccessor.new
48
69
  end
49
70
 
71
+ # (see Accessory::Accessors::FilterAccessor)
50
72
  def self.filter(&pred)
51
- Accessory::FilterAccessor.new(pred)
73
+ Accessory::Accessors::FilterAccessor.new(pred)
52
74
  end
53
75
  end
54
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
+
55
85
  module Accessory::Access::FluentHelpers
86
+ # (see Accessory::Accessors::SubscriptAccessor)
87
+ def subscript(...)
88
+ self.then(Accessory::Accessors::SubscriptAccessor.new(...))
89
+ end
90
+
91
+ # Alias for {#subscript}
56
92
  def [](...)
57
- self.then(Accessory::SubscriptAccessor.new(...))
93
+ self.then(Accessory::Accessors::SubscriptAccessor.new(...))
58
94
  end
59
95
 
60
- def field(...)
61
- self.then(Accessory::FieldAccessor.new(...))
96
+ # (see Accessory::Accessors::AttributeAccessor)
97
+ def attr(...)
98
+ self.then(Accessory::Accessors::AttributeAccessor.new(...))
62
99
  end
63
100
 
101
+ # (see Accessory::Accessors::InstanceVariableAccessor)
64
102
  def ivar(...)
65
- self.then(Accessory::InstanceVariableAccessor.new(...))
103
+ self.then(Accessory::Accessors::InstanceVariableAccessor.new(...))
66
104
  end
67
105
 
106
+ # (see Accessory::Accessors::BetwixtAccessor)
68
107
  def betwixt(...)
69
- self.then(Accessory::BetwixtAccessor.new(...))
108
+ self.then(Accessory::Accessors::BetwixtAccessor.new(...))
70
109
  end
71
110
 
111
+ # Alias for +#betwixt(0)+. See {#betwixt}
72
112
  def before_first
73
113
  self.betwixt(0)
74
114
  end
75
115
 
116
+ # Alias for +#betwixt(-1)+. See {#betwixt}
76
117
  def after_last
77
118
  self.betwixt(-1)
78
119
  end
79
120
 
121
+ # (see Accessory::Accessors::BetweenEachAccessor)
80
122
  def between_each
81
- self.then(Accessory::BetweenEachAccessor.new)
123
+ self.then(Accessory::Accessors::BetweenEachAccessor.new)
82
124
  end
83
125
 
126
+ # (see Accessory::Accessors::AllAccessor)
84
127
  def all
85
- self.then(Accessory::AllAccessor.new)
128
+ self.then(Accessory::Accessors::AllAccessor.new)
86
129
  end
87
130
 
131
+ # (see Accessory::Accessors::FirstAccessor)
88
132
  def first
89
- self.then(Accessory::FirstAccessor.new)
133
+ self.then(Accessory::Accessors::FirstAccessor.new)
90
134
  end
91
135
 
136
+ # (see Accessory::Accessors::LastAccessor)
92
137
  def last
93
- self.then(Accessory::LastAccessor.new)
138
+ self.then(Accessory::Accessors::LastAccessor.new)
94
139
  end
95
140
 
141
+ # (see Accessory::Accessors::FilterAccessor)
96
142
  def filter(&pred)
97
- self.then(Accessory::FilterAccessor.new(pred))
143
+ self.then(Accessory::Accessors::FilterAccessor.new(pred))
98
144
  end
99
145
  end
100
146
 
@@ -102,6 +148,6 @@ class Accessory::Lens
102
148
  include Accessory::Access::FluentHelpers
103
149
  end
104
150
 
105
- class Accessory::LensPath
151
+ class Accessory::BoundLens
106
152
  include Accessory::Access::FluentHelpers
107
153
  end
@@ -1,14 +1,36 @@
1
1
  module Accessory; end
2
2
 
3
+ ##
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}
23
+
3
24
  class Accessory::Accessor
4
- DEFAULT_NOT_SET_SENTINEL = :"98e47971-e708-42ca-bee7-0c62fe5e11c9"
5
25
  TERMINAL_DEFAULT_FN = lambda{ nil }
26
+ private_constant :TERMINAL_DEFAULT_FN
6
27
 
7
- def initialize(default: DEFAULT_NOT_SET_SENTINEL)
8
- @default_value = default
9
- @make_default_fn = TERMINAL_DEFAULT_FN
28
+ # @!visibility private
29
+ def initialize(default_value = nil)
30
+ @default_value = default_value
10
31
  end
11
32
 
33
+ # @!visibility private
12
34
  def name
13
35
  n = self.class.name.split('::').last.gsub(/Accessor$/, '')
14
36
  n.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
@@ -18,6 +40,7 @@ class Accessory::Accessor
18
40
  n
19
41
  end
20
42
 
43
+ # @!visibility private
21
44
  def inspect(format: :long)
22
45
  case format
23
46
  when :long
@@ -30,7 +53,11 @@ class Accessory::Accessor
30
53
  end
31
54
  end
32
55
 
33
- HIDDEN_IVARS = [:@default_value, :@make_default_fn]
56
+ # @!visibility private
57
+ HIDDEN_IVARS = [:@default_value, :@successor]
58
+ private_constant :HIDDEN_IVARS
59
+
60
+ # @!visibility private
34
61
  def inspect_args
35
62
  (instance_variables - HIDDEN_IVARS).map do |ivar_k|
36
63
  ivar_v = instance_variable_get(ivar_k)
@@ -38,18 +65,151 @@ class Accessory::Accessor
38
65
  end.join(' ')
39
66
  end
40
67
 
41
- attr_accessor :make_default_fn
68
+ # @!visibility private
69
+ attr_accessor :successor
42
70
 
43
- def value_or_default(data)
44
- return nil if data.nil?
71
+ # @!group Helpers
45
72
 
46
- maybe_value = value_from(data)
47
- 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
48
90
 
49
- if DEFAULT_NOT_SET_SENTINEL.equal?(@default_value)
50
- @make_default_fn.call
91
+ if @successor
92
+ @successor.ensure_valid(traversal_result)
51
93
  else
52
- @default_value
94
+ traversal_result
53
95
  end
54
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
+ # {SubscriptAccessor}, <tt>data[key] = new_value</tt> is executed.
138
+ #
139
+ # In the <tt>:pop</tt> command case:
140
+ # * the result of the traversal (*before* the yield) should be returned. This
141
+ # implies that any {get_and_update} implementation must capture its
142
+ # traversal-results before feeding them into yield, in order to return them
143
+ # here.
144
+ # * the traversal-result should be *removed* from +data+. For example, in
145
+ # {SubscriptAccessor}, <tt>data.delete(key)</tt> is executed.
146
+ #
147
+ # The successor in the accessor chain will receive the yielded
148
+ # traversal-results as its own +data+.
149
+ #
150
+ # After returning, the predecessor accessor will receive the result returned
151
+ # from this method as the result of its own +yield+. This implies that
152
+ # this method should almost always be implemented to *return* an update
153
+ # command, looking like one of the following:
154
+ #
155
+ # # given [get_result, new_value]
156
+ # [get_result, data_after_update]
157
+ #
158
+ # # given :pop
159
+ # [traversal_result, data_after_removal]
160
+ #
161
+ # @param data [Enumerable] the data yielded by the predecessor accessor.
162
+ # @param succ [Proc] a thunk to the successor accessor. When {get} is called
163
+ # by a {Lens}, this is passed implicitly.
164
+ # @return [Object] the modification-command to pass back to the predecessor
165
+ # accessor as a yield result.
166
+ def get_and_update(data, &succ)
167
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #get_and_update"
168
+ end
169
+
170
+ # @!group Callbacks
171
+
172
+ # Traverses +data+; called by {traverse_or_default}.
173
+ #
174
+ # This method should traverse +data+ however your accessor does that,
175
+ # producing either one traversal-result or a container-object of gathered
176
+ # traversal-results.
177
+ #
178
+ # This method can assume that +data+ is a valid receiver for the traversal
179
+ # it performs. {traverse_or_default} takes care of feeding in a default +data+
180
+ # in the case where the predecessor passed invalid data.
181
+ #
182
+ # @param data [Object] the object to be traversed
183
+ # @return [Object] the result of traversal
184
+ def traverse(data)
185
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #traverse to use #traverse_or_default"
186
+ end
187
+
188
+ # Ensures that the predecessor accessor's traversal result is one this
189
+ # accessor can operate on; called by {traverse_or_default}.
190
+ #
191
+ # This callback should validate that the +traversal_result+ is one that can
192
+ # be traversed by the traversal-method of this accessor. If it can, the
193
+ # +traversal_result+ should be returned unchanged. If it cannot, an object
194
+ # that _can_ be traversed should be returned instead.
195
+ #
196
+ #
197
+ # For example, if your accessor operates on +Enumerable+ values (like
198
+ # {AllAccessor}), then this method should validate that the +traversal_result+
199
+ # is +Enumerable+; and, if it isn't, return something that is — e.g. an empty
200
+ # +Array+.
201
+ #
202
+ # This logic is used to replace invalid intermediate values (e.g. `nil`s and
203
+ # scalars) with containers during {Lens#put_in} et al.
204
+ #
205
+ # @return [Object] a now-valid traversal result
206
+ def ensure_valid(traversal_result)
207
+ lambda do
208
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #ensure_valid to allow chain-predecessor to use #traverse_or_default"
209
+ end
210
+ end
211
+
212
+ # @!endgroup
55
213
  end
214
+
215
+ module Accessory::Accessors; end
@@ -1,12 +1,37 @@
1
1
  require 'accessory/accessor'
2
2
 
3
- class Accessory::AllAccessor < Accessory::Accessor
4
- def default_fn_for_previous_step
5
- lambda{ Array.new }
3
+ ##
4
+ # Traverses all elements of an +Enumerable+.
5
+ #
6
+ # *Aliases*
7
+ # * {Access.all}
8
+ # * {Access::FluentHelpers#all} (included in {Lens} and {BoundLens})
9
+ #
10
+ # *Equivalents* in Elixir's {https://hexdocs.pm/elixir/Access.html +Access+} module
11
+ # * {https://hexdocs.pm/elixir/Access.html#all/0 +Access.all/0+}
12
+ # * {https://hexdocs.pm/elixir/Access.html#key/2 +Access.key/2+}
13
+ #
14
+ # <b>Default constructor</b> used by predecessor accessor
15
+ #
16
+ # * +Array.new+
17
+
18
+ class Accessory::Accessors::AllAccessor < Accessory::Accessor
19
+ # @!visibility private
20
+ def ensure_valid(traversal_result)
21
+ if traversal_result.kind_of?(Enumerable)
22
+ traversal_result
23
+ else
24
+ []
25
+ end
6
26
  end
7
27
 
28
+ # @!visibility private
8
29
  def inspect_args; nil; end
9
30
 
31
+ # Feeds each element of +data+ down the accessor chain, and returns
32
+ # the results.
33
+ # @param data [Enumerable] the +Enumerable+ to iterate through
34
+ # @return [Array] the values derived from the rest of the accessor chain
10
35
  def get(data, &succ)
11
36
  if succ
12
37
  (data || []).map(&succ)
@@ -15,6 +40,13 @@ class Accessory::AllAccessor < Accessory::Accessor
15
40
  end
16
41
  end
17
42
 
43
+ # Feeds each element of +data+ down the accessor chain, overwriting
44
+ # +data+ with the results.
45
+ #
46
+ # If +:pop+ is returned from the accessor chain, the element is dropped
47
+ # from the new +data+.
48
+ # @param data [Enumerable] the +Enumerable+ to iterate through
49
+ # @return [Array] a two-element array containing 1. the original values found during iteration; and 2. the new +data+
18
50
  def get_and_update(data)
19
51
  results = []
20
52
  new_data = []