accessory 0.1.5 → 0.1.10

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.
@@ -0,0 +1,139 @@
1
+ module Accessory; end
2
+
3
+ require 'accessory/lens'
4
+
5
+ ##
6
+ # A BoundLens represents a {Lens} bound to a specified subject document.
7
+ # See {Lens} for the general theory.
8
+ #
9
+ # A BoundLens can be used to traverse its subject document, using {get_in},
10
+ # {put_in}, {pop_in}, etc.
11
+ #
12
+ # Ordinarily, you don't create and hold onto a BoundLens, but rather you will
13
+ # temporarily create Lenses in method-call chains when doing traversals.
14
+ #
15
+ # It may sometimes be useful to create a collection of Lenses and then "build
16
+ # them up" by extending their {Lens}es over various collection-passes, rather
17
+ # than building up {Lens}es and only binding them to subjects at the end.
18
+ #
19
+ # Lenses are created frozen. Methods that "extend" a BoundLens actually
20
+ # create and return new derived Lenses.
21
+
22
+ class Accessory::BoundLens
23
+
24
+ # Creates a BoundLens that will traverse +subject+.
25
+ #
26
+ # @overload on(subject, lens)
27
+ # Creates a BoundLens that will traverse +subject+ along +lens+.
28
+ #
29
+ # @param subject [Object] the data-structure this BoundLens will traverse
30
+ # @param lens [Lens] the {Lens} that will be used to traverse +subject+
31
+ #
32
+ # @overload on(subject, *accessors)
33
+ # Creates a BoundLens that will traverse +subject+ using a {Lens} built
34
+ # from +accessors+.
35
+ #
36
+ # @param subject [Object] the data-structure this BoundLens will traverse
37
+ # @param accessors [Array] the accessors for the new {Lens}
38
+ def self.on(subject, *accessors)
39
+ lens =
40
+ if accessors.length == 1 && accessors[0].kind_of?(Lens)
41
+ accessors[0]
42
+ else
43
+ Accessory::Lens[*accessors]
44
+ end
45
+
46
+ self.new(subject, lens).freeze
47
+ end
48
+
49
+ class << self
50
+ private :new
51
+ end
52
+
53
+ # @!visibility private
54
+ def initialize(subject, lens)
55
+ @subject = subject
56
+ @lens = lens
57
+ end
58
+
59
+ # @return [Lens] the +subject+ this BoundLens will traverse
60
+ attr_reader :subject
61
+
62
+ # @return [Lens] the {Lens} for this BoundLens
63
+ attr_reader :lens
64
+
65
+ # @!visibility private
66
+ def inspect
67
+ "#<BoundLens on=#{@subject.inspect} #{@lens.inspect(format: :short)}>"
68
+ end
69
+
70
+ # Returns a new BoundLens resulting from appending +accessor+ to the
71
+ # receiver's {Lens}.
72
+ #
73
+ # === See also:
74
+ # * {Lens#then}
75
+ #
76
+ # @param accessor [Object] the accessor to append
77
+ # @return [Lens] the new BoundLens, containing a new joined Lens
78
+ def then(accessor)
79
+ d = self.dup
80
+ d.instance_eval do
81
+ @lens = @lens.then(accessor)
82
+ end
83
+ d.freeze
84
+ end
85
+
86
+ # Returns a new BoundLens resulting from concatenating +other+ to the
87
+ # receiver's {Lens}.
88
+ #
89
+ # === See also:
90
+ # * {Lens#+}
91
+ #
92
+ # @param other [Object] an accessor, an +Array+ of accessors, or a {Lens}
93
+ # @return [Lens] the new BoundLens, containing a new joined Lens
94
+ def +(other)
95
+ d = self.dup
96
+ d.instance_eval do
97
+ @lens = @lens + other
98
+ end
99
+ d.freeze
100
+ end
101
+
102
+ alias_method :/, :+
103
+
104
+ # (see Lens#get_in)
105
+ def get_in
106
+ @lens.get_in(@subject)
107
+ end
108
+
109
+ # (see Lens#get_and_update_in)
110
+ def get_and_update_in(&mutator_fn)
111
+ @lens.get_and_update_in(@subject, &mutator_fn)
112
+ end
113
+
114
+ # (see Lens#update_in)
115
+ def update_in(&new_value_fn)
116
+ @lens.update_in(@subject, &new_value_fn)
117
+ end
118
+
119
+ # (see Lens#put_in)
120
+ def put_in(new_value)
121
+ @lens.put_in(@subject, new_value)
122
+ end
123
+
124
+ # (see Lens#pop_in)
125
+ def pop_in
126
+ @lens.pop_in(@subject)
127
+ end
128
+ end
129
+
130
+ class Accessory::Lens
131
+ # Returns a new {BoundLens} wrapping this Lens, bound to the specified
132
+ # +subject+.
133
+ # @param subject [Object] the data-structure to traverse
134
+ # @return [BoundLens] a new BoundLens that will traverse +subject+ using
135
+ # this Lens
136
+ def on(subject)
137
+ Accessory::BoundLens.on(subject, self)
138
+ end
139
+ end
@@ -1,33 +1,43 @@
1
1
  module Accessory; end
2
2
 
3
- require 'accessory/lens_path'
3
+ require 'accessory/accessor'
4
+ require 'accessory/accessors/subscript_accessor'
5
+
6
+ ##
7
+ # A Lens is a "free-floating" lens (i.e. not bound to a subject document.)
8
+ # It serves as a container for {Accessor} instances, and represents the
9
+ # traversal path one would take to get from a hypothetical subject document,
10
+ # to a data value nested somewhere within it.
11
+ #
12
+ # A Lens can be used directly to traverse documents, using {get_in},
13
+ # {put_in}, {pop_in}, etc. These methods take an explicit subject document to
14
+ # traverse, rather than requiring that the Lens be bound to a document
15
+ # first.
16
+ #
17
+ # As such, a Lens is reusable. A common use of a Lens is to access the
18
+ # same deeply-nested traversal position within a large collection of subject
19
+ # documents, e.g.:
20
+ #
21
+ # foo_bar_baz = Lens[:foo, :bar, :baz]
22
+ # docs.map{ |doc| foo_bar_baz.get_in(doc) }
23
+ #
24
+ # A Lens can also be bound to a specific subject document to create a
25
+ # {BoundLens}. See {BoundLens.on}.
26
+ #
27
+ # Lenses are created frozen. Methods that "extend" a Lens actually create and
28
+ # return new derived Lenses.
4
29
 
5
30
  class Accessory::Lens
31
+ # Returns the empty (identity) Lens.
32
+ # @return [Lens] the empty (identity) Lens.
33
+ def self.empty
34
+ @empty_lens ||= (new([]).freeze)
35
+ end
6
36
 
7
- # Creates a Lens that will traverse +subject+.
8
- #
9
- # @overload on(subject, lens_path)
10
- # Creates a Lens that will traverse +subject+ along +lens_path+.
11
- #
12
- # @param subject [Object] the data-structure this Lens will traverse
13
- # @param lens_path [LensPath] the {LensPath} that will be used to
14
- # traverse +subject+
15
- #
16
- # @overload on(subject, *accessors)
17
- # Creates a Lens that will traverse +subject+ using an {LensPath} built
18
- # from +accessors+.
19
- #
20
- # @param subject [Object] the data-structure this Lens will traverse
21
- # @param accessors [Array] the accessors for the new {LensPath}
22
- def self.on(subject, *accessors)
23
- lens_path =
24
- if accessors.length == 1 && accessors[0].kind_of?(LensPath)
25
- accessors[0]
26
- else
27
- LensPath[*accessors]
28
- end
29
-
30
- self.new(subject, lens_path).freeze
37
+ # Returns a {Lens} containing the specified +accessors+.
38
+ # @return [Lens] a Lens containing the specified +accessors+.
39
+ def self.[](*accessors)
40
+ new(accessors).freeze
31
41
  end
32
42
 
33
43
  class << self
@@ -35,88 +45,206 @@ class Accessory::Lens
35
45
  end
36
46
 
37
47
  # @!visibility private
38
- def initialize(subject, lens_path)
39
- @subject = subject
40
- @path = lens_path
41
- end
48
+ def initialize(initial_parts)
49
+ @parts = []
42
50
 
43
- # @return [LensPath] the +subject+ this Lens will traverse
44
- attr_reader :subject
51
+ for part in initial_parts
52
+ append_accessor!(part)
53
+ end
54
+ end
45
55
 
46
- # @return [LensPath] the {LensPath} for this Lens
47
- attr_reader :path
56
+ # @!visibility private
57
+ def to_a
58
+ @parts
59
+ end
48
60
 
49
61
  # @!visibility private
50
- def inspect
51
- "#<Lens on=#{@subject.inspect} #{@path.inspect(format: :short)}>"
62
+ def inspect(format: :long)
63
+ parts_desc = @parts.map{ |part| part.inspect(format: :short) }.join(', ')
64
+ parts_desc = "[#{parts_desc}]"
65
+
66
+ case format
67
+ when :long
68
+ "#Lens#{parts_desc}"
69
+ when :short
70
+ parts_desc
71
+ end
52
72
  end
53
73
 
54
- # Returns a new Lens resulting from appending +accessor+ to the receiver's
55
- # {LensPath}.
56
- #
57
- # === See also:
58
- # * {LensPath#then}
59
- #
74
+ # Returns a new {Lens} resulting from appending +accessor+ to the receiver.
60
75
  # @param accessor [Object] the accessor to append
61
- # @return [LensPath] the new Lens, containing a new joined LensPath
76
+ # @return [Lens] the new joined Lens
62
77
  def then(accessor)
63
78
  d = self.dup
64
79
  d.instance_eval do
65
- @path = @path.then(accessor)
80
+ @parts = @parts.dup
81
+ append_accessor!(accessor)
66
82
  end
67
83
  d.freeze
68
84
  end
69
85
 
70
- # Returns a new Lens resulting from concatenating +other+ to the receiver's
71
- # {LensPath}.
72
- #
73
- # === See also:
74
- # * {LensPath#+}
75
- #
76
- # @param other [Object] an accessor, an +Array+ of accessors, or a {LensPath}
77
- # @return [LensPath] the new Lens, containing a new joined LensPath
86
+ # Returns a new {Lens} resulting from concatenating +other+ to the end
87
+ # of the receiver.
88
+ # @param other [Object] an accessor, an +Array+ of accessors, or another Lens
89
+ # @return [Lens] the new joined Lens
78
90
  def +(other)
91
+ parts =
92
+ case other
93
+ when Accessory::Lens
94
+ other.to_a
95
+ when Array
96
+ other
97
+ else
98
+ [other]
99
+ end
100
+
79
101
  d = self.dup
80
102
  d.instance_eval do
81
- @path = @path + other
103
+ for part in parts
104
+ append_accessor!(part)
105
+ end
82
106
  end
83
107
  d.freeze
84
108
  end
85
109
 
86
110
  alias_method :/, :+
87
111
 
88
- # (see LensPath#get_in)
89
- def get_in
90
- @path.get_in(@subject)
112
+ # Traverses +subject+ using the chain of accessors held in this Lens,
113
+ # returning the discovered value.
114
+ #
115
+ # *Equivalent* in Elixir: {https://hexdocs.pm/elixir/Kernel.html#get_in/2 +Kernel.get_in/2+}
116
+ #
117
+ # @return [Object] the value found after all traversals.
118
+ def get_in(subject)
119
+ if @parts.empty?
120
+ subject
121
+ else
122
+ get_in_step(subject, @parts)
123
+ end
91
124
  end
92
125
 
93
- # (see LensPath#get_and_update_in)
94
- def get_and_update_in(&mutator_fn)
95
- @path.get_and_update_in(@subject, &mutator_fn)
126
+ # Traverses +subject+ using the chain of accessors held in this Lens,
127
+ # modifying the final value at the end of the traversal chain using
128
+ # the passed +mutator_fn+, and returning the original targeted value(s)
129
+ # pre-modification.
130
+ #
131
+ # +mutator_fn+ must return one of two data "shapes":
132
+ # * a two-element +Array+, representing:
133
+ # 1. the value to surface as the "get" value of the traversal
134
+ # 2. the new value to replace at the traversal-position
135
+ # * the Symbol +:pop+ — which will remove the value from its parent, and
136
+ # return it as-is.
137
+ #
138
+ # *Equivalent* in Elixir: {https://hexdocs.pm/elixir/Kernel.html#get_and_update_in/3 +Kernel.get_and_update_in/3+}
139
+ #
140
+ # @param subject [Object] the data-structure to traverse
141
+ # @param mutator_fn [Proc] a block taking the original value derived from
142
+ # traversing +subject+, and returning a modification operation.
143
+ # @return [Array] a two-element +Array+, consisting of
144
+ # 1. the _old_ value(s) found after all traversals, and
145
+ # 2. the updated +subject+
146
+ def get_and_update_in(subject, &mutator_fn)
147
+ if @parts.empty?
148
+ subject
149
+ else
150
+ get_and_update_in_step(subject, @parts, mutator_fn)
151
+ end
96
152
  end
97
153
 
98
- # (see LensPath#update_in)
99
- def update_in(&new_value_fn)
100
- @path.update_in(@subject, &new_value_fn)
101
- end
154
+ # Traverses +subject+ using the chain of accessors held in this Lens,
155
+ # replacing the final value at the end of the traversal chain with the
156
+ # result from the passed +new_value_fn+.
157
+ #
158
+ # *Equivalent* in Elixir: {https://hexdocs.pm/elixir/Kernel.html#update_in/3 +Kernel.update_in/3+}
159
+ #
160
+ # @param subject [Object] the data-structure to traverse
161
+ # @param new_value_fn [Proc] a block taking the original value derived from
162
+ # traversing +subject+, and returning a replacement value.
163
+ # @return [Array] a two-element +Array+, consisting of
164
+ # 1. the _old_ value(s) found after all traversals, and
165
+ # 2. the updated +subject+
166
+ def update_in(subject, &new_value_fn)
167
+ _, _, new_data = self.get_and_update_in(subject) do |v|
168
+ case new_value_fn.call(v)
169
+ in [:set, new_value]
170
+ [:dirty, nil, new_value]
171
+ in :keep
172
+ [:clean, nil, v]
173
+ in :pop
174
+ :pop
175
+ end
176
+ end
102
177
 
103
- # (see LensPath#put_in)
104
- def put_in(new_value)
105
- @path.put_in(@subject, new_value)
178
+ new_data
106
179
  end
107
180
 
108
- # (see LensPath#pop_in)
109
- def pop_in
110
- @path.pop_in(@subject)
181
+ # Traverses +subject+ using the chain of accessors held in this Lens,
182
+ # replacing the final value at the end of the traversal chain with
183
+ # +new_value+.
184
+ #
185
+ # *Equivalent* in Elixir: {https://hexdocs.pm/elixir/Kernel.html#put_in/3 +Kernel.put_in/3+}
186
+ #
187
+ # @param subject [Object] the data-structure to traverse
188
+ # @param new_value [Object] a replacement value at the traversal position.
189
+ # @return [Object] the updated +subject+
190
+ def put_in(subject, new_value)
191
+ _, _, new_data = self.get_and_update_in(subject){ [:dirty, nil, new_value] }
192
+ new_data
111
193
  end
112
- end
113
194
 
114
- class Accessory::LensPath
115
- # Returns a new {Lens} wrapping this LensPath, bound to the specified
116
- # +subject+.
195
+ # Traverses +subject+ using the chain of accessors held in this Lens,
196
+ # removing the final value at the end of the traversal chain from its position
197
+ # within its parent container.
198
+ #
199
+ # *Equivalent* in Elixir: {https://hexdocs.pm/elixir/Kernel.html#pop_in/2 +Kernel.pop_in/2+}
200
+ #
117
201
  # @param subject [Object] the data-structure to traverse
118
- # @return [Lens] a new Lens that will traverse +subject+ using this LensPath
119
- def on(subject)
120
- Accessory::Lens.on(subject, self)
202
+ # @return [Object] the updated +subject+
203
+ def pop_in(subject)
204
+ _, popped_item, new_data = self.get_and_update_in(subject){ :pop }
205
+ [popped_item, new_data]
206
+ end
207
+
208
+ def append_accessor!(part)
209
+ accessor =
210
+ case part
211
+ when Accessory::Accessor
212
+ part
213
+ when Array
214
+ Accessory::Accessors::SubscriptAccessor.new(part[0], default: part[1])
215
+ else
216
+ Accessory::Accessors::SubscriptAccessor.new(part)
217
+ end
218
+
219
+ unless @parts.empty?
220
+ @parts.last.successor = accessor
221
+ end
222
+
223
+ @parts.push(accessor)
224
+ end
225
+ private :append_accessor!
226
+
227
+ def get_in_step(data, path)
228
+ step_accessor = path.first
229
+ rest_of_path = path[1..-1]
230
+
231
+ if rest_of_path.empty?
232
+ step_accessor.get(data)
233
+ else
234
+ step_accessor.get(data){ |v| get_in_step(v, rest_of_path) }
235
+ end
236
+ end
237
+ private :get_in_step
238
+
239
+ def get_and_update_in_step(data, path, mutator_fn)
240
+ step_accessor = path.first
241
+ rest_of_path = path[1..-1]
242
+
243
+ if rest_of_path.empty?
244
+ step_accessor.get_and_update(data, &mutator_fn)
245
+ else
246
+ step_accessor.get_and_update(data){ |v| get_and_update_in_step(v, rest_of_path, mutator_fn) }
247
+ end
121
248
  end
249
+ private :get_and_update_in_step
122
250
  end