accessory 0.1.1 → 0.1.6

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: 53b2b8b8fbe8103678c758221525a6a52d693dacd337cc3ee0c68c88e7b2c563
4
- data.tar.gz: f38ef7a0bfdfb1470bbefb9c4b6692a100873d002dbdd1e38dd85e22884a228c
3
+ metadata.gz: 9880e7e5ea88c0b373a1cc3d5d4861788f368905c761b2ec3a7236d7282ada39
4
+ data.tar.gz: 7386d98dc728a663904564e0a6825de0d7690e806950e5e95d6e8b39d4d20cd6
5
5
  SHA512:
6
- metadata.gz: 595c30b506f8c0547366660a33bdc9bea94bc3367bc4210ca54c3033061b12d2da8fc008e2fc1e642a4ad86b3445aa2fad84a2fb8a229d197f0d6d9e48238ed0
7
- data.tar.gz: 96fafa2029446d184645b823e8398d5002e8d8487fbeb869442d37cd393b20ec8d7fae13ce9d71b7baec29cc56a551d02c86b7f9463bb3590d1024e456d1dba7
6
+ metadata.gz: dbee544b8dc699adb962559b98278d48edeed2a4d5b9c4d3c6c92ad3c4a38ffb4df5ced18274b465dfe68fd34dd15a50c06c2febae4e54049a4049d377cb10cd
7
+ data.tar.gz: 0d1be0f43650e87a1616b5c2ac968b7407c5d8e380abb7e30ad84823816d486341a2c1b791814c55695dc77f2bf0337df834816c2a703bacbb2a3f582f362907
@@ -1,14 +1,17 @@
1
+ ##
2
+ #
3
+
1
4
  module Accessory; end
2
5
 
3
6
  require 'accessory/version'
4
- require 'accessory/lens_path'
5
7
  require 'accessory/lens'
8
+ require 'accessory/bound_lens'
6
9
  require 'accessory/access'
7
10
 
8
11
  module Accessory
9
12
  refine ::Object do
10
- def lens
11
- ::Accessory::Lens.on(self)
13
+ def lens(...)
14
+ ::Accessory::BoundLens.on(self, ...)
12
15
  end
13
16
  end
14
17
  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,89 +10,135 @@ 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::SubscriptAccessor)
22
+ def self.subscript(...)
23
+ Accessory::SubscriptAccessor.new(...)
24
+ end
25
+
26
+ # (see Accessory::AttributeAccessor)
27
+ def self.attr(...)
28
+ Accessory::AttributeAccessor.new(...)
16
29
  end
17
30
 
31
+ # (see Accessory::InstanceVariableAccessor)
18
32
  def self.ivar(...)
19
33
  Accessory::InstanceVariableAccessor.new(...)
20
34
  end
21
35
 
36
+ # (see Accessory::BetwixtAccessor)
22
37
  def self.betwixt(...)
23
38
  Accessory::BetwixtAccessor.new(...)
24
39
  end
25
40
 
41
+ # Alias for +Accessory::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::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::BetweenEachAccessor)
34
52
  def self.between_each
35
53
  Accessory::BetweenEachAccessor.new
36
54
  end
37
55
 
56
+ # (see Accessory::AllAccessor)
38
57
  def self.all
39
58
  Accessory::AllAccessor.new
40
59
  end
41
60
 
61
+ # (see Accessory::FirstAccessor)
42
62
  def self.first
43
63
  Accessory::FirstAccessor.new
44
64
  end
45
65
 
66
+ # (see Accessory::LastAccessor)
46
67
  def self.last
47
68
  Accessory::LastAccessor.new
48
69
  end
49
70
 
71
+ # (see Accessory::FilterAccessor)
50
72
  def self.filter(&pred)
51
73
  Accessory::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::SubscriptAccessor)
87
+ def subscript(...)
88
+ self.then(Accessory::SubscriptAccessor.new(...))
89
+ end
90
+
91
+ # Alias for {#subscript}
56
92
  def [](...)
57
93
  self.then(Accessory::SubscriptAccessor.new(...))
58
94
  end
59
95
 
60
- def field(...)
61
- self.then(Accessory::FieldAccessor.new(...))
96
+ # (see Accessory::AttributeAccessor)
97
+ def attr(...)
98
+ self.then(Accessory::AttributeAccessor.new(...))
62
99
  end
63
100
 
101
+ # (see Accessory::InstanceVariableAccessor)
64
102
  def ivar(...)
65
103
  self.then(Accessory::InstanceVariableAccessor.new(...))
66
104
  end
67
105
 
106
+ # (see Accessory::BetwixtAccessor)
68
107
  def betwixt(...)
69
108
  self.then(Accessory::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::BetweenEachAccessor)
80
122
  def between_each
81
123
  self.then(Accessory::BetweenEachAccessor.new)
82
124
  end
83
125
 
126
+ # (see Accessory::AllAccessor)
84
127
  def all
85
128
  self.then(Accessory::AllAccessor.new)
86
129
  end
87
130
 
131
+ # (see Accessory::FirstAccessor)
88
132
  def first
89
133
  self.then(Accessory::FirstAccessor.new)
90
134
  end
91
135
 
136
+ # (see Accessory::LastAccessor)
92
137
  def last
93
138
  self.then(Accessory::LastAccessor.new)
94
139
  end
95
140
 
141
+ # (see Accessory::FilterAccessor)
96
142
  def filter(&pred)
97
143
  self.then(Accessory::FilterAccessor.new(pred))
98
144
  end
@@ -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#default_data_constructor}
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,152 @@ 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
+ # This method will return +nil+ if the input-data is +nil+, _without_ calling
82
+ # your {traverse} callback. This means that accessors that use
83
+ # {traverse_or_default} will _forward_ +nil+ traversal-results along the chain
84
+ # without being confused by them.
85
+ #
86
+ # If your {traverse} callback returns <tt>:error</tt>, a default value will
87
+ # be used. This is either the +default+ passed to {initialize} by your
88
+ # implementation calling <tt>super(default)</tt>; or it's the result of
89
+ # calling {default_data_constructor} on the successor-accessor in the accessor
90
+ # chain.
91
+ def traverse_or_default(data)
92
+ traversal_result = traverse(data) || @default_value
48
93
 
49
- if DEFAULT_NOT_SET_SENTINEL.equal?(@default_value)
50
- @make_default_fn.call
94
+ if @successor
95
+ @successor.ensure_valid(traversal_result)
51
96
  else
52
- @default_value
97
+ traversal_result
98
+ end
99
+ end
100
+
101
+ # @!endgroup
102
+
103
+ # Traverses +data+ in some way, and feeds the result of the traversal to the
104
+ # next step in the accessor chain by yielding the traversal-result to the
105
+ # passed-in block +succ+. The result from the yield is the result coming from
106
+ # the end of the accessor chain. Usually, it should be returned as-is.
107
+ #
108
+ # +succ+ can be yielded to multiple times, to run the rest of the accessor
109
+ # chain against multiple parts of +data+. In this case, the yield results
110
+ # should be gathered up into some container object to be returned together.
111
+ #
112
+ # The successor accessor will receive the yielded element as its +data+.
113
+ #
114
+ # After returning, the predecessor accessor will receive the result returned
115
+ # from {get} as the result of its own +yield+.
116
+ #
117
+ # @param data [Enumerable] the data yielded by the predecessor accessor.
118
+ # @param succ [Proc] a thunk to the successor accessor. When {get} is called
119
+ # by a {Lens}, this is passed implicitly.
120
+ # @return [Object] the data to pass back to the predecessor accessor as a
121
+ # yield result.
122
+ def get(data, &succ)
123
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #get"
124
+ end
125
+
126
+ # Traverses +data+ in some way, and feeds the result of the traversal to the
127
+ # next step in the accessor chain by yielding the traversal-result to the
128
+ # passed-in block +succ+.
129
+ #
130
+ # The result of the yield will be a "modification command", one of these two:
131
+ # * an *update command* <tt>[get_result, new_value]</tt>
132
+ # * the symbol <tt>:pop</tt>
133
+ #
134
+ # In the *update command* case:
135
+ # * the +get_result+ should be returned as-is (or gathered together into
136
+ # a container object if this accessor yields multiple times.) The data flow
137
+ # of the +get_result+s should replicate the data flow of {get}.
138
+ # * the +new_value+ should be used to *replace* or *overwrite* the result of
139
+ # this accessor's traversal within +data+. For example, in
140
+ # {SubscriptAccessor}, <tt>data[key] = new_value</tt> is executed.
141
+ #
142
+ # In the <tt>:pop</tt> command case:
143
+ # * the result of the traversal (*before* the yield) should be returned. This
144
+ # implies that any {get_and_update} implementation must capture its
145
+ # traversal-results before feeding them into yield, in order to return them
146
+ # here.
147
+ # * the traversal-result should be *removed* from +data+. For example, in
148
+ # {SubscriptAccessor}, <tt>data.delete(key)</tt> is executed.
149
+ #
150
+ # The successor in the accessor chain will receive the yielded
151
+ # traversal-results as its own +data+.
152
+ #
153
+ # After returning, the predecessor accessor will receive the result returned
154
+ # from this method as the result of its own +yield+. This implies that
155
+ # this method should almost always be implemented to *return* an update
156
+ # command, looking like one of the following:
157
+ #
158
+ # # given [get_result, new_value]
159
+ # [get_result, data_after_update]
160
+ #
161
+ # # given :pop
162
+ # [traversal_result, data_after_removal]
163
+ #
164
+ # @param data [Enumerable] the data yielded by the predecessor accessor.
165
+ # @param succ [Proc] a thunk to the successor accessor. When {get} is called
166
+ # by a {Lens}, this is passed implicitly.
167
+ # @return [Object] the modification-command to pass back to the predecessor
168
+ # accessor as a yield result.
169
+ def get_and_update(data, &succ)
170
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #get_and_update"
171
+ end
172
+
173
+ # @!group Callbacks
174
+
175
+ # Traverses +data+; called by {traverse_or_default}.
176
+ #
177
+ # This method should traverse +data+ however your accessor does that,
178
+ # producing either one traversal-result or a container-object of gathered
179
+ # traversal-results.
180
+ #
181
+ # This method can assume that +data+ is a valid receiver for the traversal
182
+ # it performs. {traverse_or_default} takes care of feeding in a default +data+
183
+ # in the case where the predecessor passed invalid data.
184
+ #
185
+ # @param data [Object] the object to be traversed
186
+ # @return [Object] the result of traversal
187
+ def traverse(data)
188
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #traverse to use #traverse_or_default"
189
+ end
190
+
191
+ # Ensures that the predecessor accessor's traversal result is one this
192
+ # accessor can operate on; called by {traverse_or_default}.
193
+ #
194
+ # This callback should validate that the +traversal_result+ is one that can
195
+ # be traversed by the traversal-method of this accessor. If it can, the
196
+ # +traversal_result+ should be returned unchanged. If it cannot, an object
197
+ # that _can_ be traversed should be returned instead.
198
+ #
199
+ #
200
+ # For example, if your accessor operates on +Enumerable+ values (like
201
+ # {AllAccessor}), then this method should validate that the +traversal_result+
202
+ # is +Enumerable+; and, if it isn't, return something that is — e.g. an empty
203
+ # +Array+.
204
+ #
205
+ # This logic is used to replace invalid intermediate values (e.g. `nil`s and
206
+ # scalars) with containers during {Lens#put_in} et al.
207
+ #
208
+ # @return [Object] a now-valid traversal result
209
+ def ensure_valid(traversal_result)
210
+ lambda do
211
+ raise NotImplementedError, "Accessor subclass #{self.class} must implement #ensure_valid to allow chain-predecessor to use #traverse_or_default"
53
212
  end
54
213
  end
214
+
215
+ # @!endgroup
55
216
  end
@@ -1,12 +1,37 @@
1
1
  require 'accessory/accessor'
2
2
 
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
+
3
18
  class Accessory::AllAccessor < Accessory::Accessor
4
- def default_fn_for_previous_step
5
- lambda{ Array.new }
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 = []