accessory 0.1.5 → 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.
@@ -8,14 +8,14 @@ require 'accessory/accessor'
8
8
  #
9
9
  # *Aliases*
10
10
  # * {Access.ivar}
11
- # * {Access::FluentHelpers#ivar} (included in {LensPath} and {Lens})
11
+ # * {Access::FluentHelpers#ivar} (included in {Lens} and {BoundLens})
12
12
  #
13
13
  # <b>Default constructor</b> used by predecessor accessor
14
14
  #
15
15
  # * +Object.new+
16
16
 
17
17
  class Accessory::InstanceVariableAccessor < Accessory::Accessor
18
- # @param attr_name [Symbol] the instance-variable name
18
+ # @param ivar_name [Symbol] the instance-variable name
19
19
  # @param default [Object] the default to use if the predecessor accessor passes +nil+ data
20
20
  def initialize(ivar_name, default: nil)
21
21
  super(default)
@@ -33,8 +33,8 @@ class Accessory::InstanceVariableAccessor < Accessory::Accessor
33
33
  end
34
34
 
35
35
  # @!visibility private
36
- def default_data_constructor
37
- lambda{ Object.new }
36
+ def ensure_valid(traversal_result)
37
+ traversal_result || Object.new
38
38
  end
39
39
 
40
40
  # @!visibility private
@@ -47,7 +47,7 @@ class Accessory::InstanceVariableAccessor < Accessory::Accessor
47
47
  # @param data [Object] the object to traverse
48
48
  # @return [Object] the value derived from the rest of the accessor chain
49
49
  def get(data)
50
- value = value_or_default(data)
50
+ value = traverse_or_default(data)
51
51
 
52
52
  if block_given?
53
53
  yield(value)
@@ -67,7 +67,7 @@ class Accessory::InstanceVariableAccessor < Accessory::Accessor
67
67
  # @param data [Object] the object to traverse
68
68
  # @return [Array] a two-element array containing 1. the original value found; and 2. the result value from the accessor chain
69
69
  def get_and_update(data)
70
- value = value_or_default(data)
70
+ value = traverse_or_default(data)
71
71
 
72
72
  case yield(value)
73
73
  in [result, new_value]
@@ -9,7 +9,7 @@ require 'accessory/accessor'
9
9
  #
10
10
  # *Aliases*
11
11
  # * {Access.last}
12
- # * {Access::FluentHelpers#last} (included in {LensPath} and {Lens})
12
+ # * {Access::FluentHelpers#last} (included in {Lens} and {BoundLens})
13
13
  #
14
14
  # <b>Default constructor</b> used by predecessor accessor
15
15
  #
@@ -17,8 +17,12 @@ require 'accessory/accessor'
17
17
 
18
18
  class Accessory::LastAccessor < Accessory::Accessor
19
19
  # @!visibility private
20
- def default_data_constructor
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
@@ -33,7 +37,7 @@ class Accessory::LastAccessor < Accessory::Accessor
33
37
  # @param data [Object] the object to traverse
34
38
  # @return [Object] the value derived from the rest of the accessor chain
35
39
  def get(data)
36
- value = value_or_default(data)
40
+ value = traverse_or_default(data)
37
41
 
38
42
  if block_given?
39
43
  yield(value)
@@ -51,7 +55,7 @@ class Accessory::LastAccessor < Accessory::Accessor
51
55
  # @param data [Object] the object to traverse
52
56
  # @return [Array] a two-element array containing 1. the original value found; and 2. the result value from the accessor chain
53
57
  def get_and_update(data)
54
- old_value = value_or_default(data)
58
+ old_value = traverse_or_default(data)
55
59
 
56
60
  case yield(old_value)
57
61
  in [result, new_value]
@@ -7,9 +7,10 @@ require 'accessory/accessor'
7
7
  #
8
8
  # *Aliases*
9
9
  # * {Access.subscript}
10
- # * {Access::FluentHelpers#subscript} (included in {LensPath} and {Lens})
11
- # * {Access::FluentHelpers#[]} (included in {LensPath} and {Lens})
12
- # * just passing a +key+ will also work, when +not(key.kind_of?(Accessor))+ (this is a special case in {LensPath#initialize})
10
+ # * {Access::FluentHelpers#subscript} (included in {Lens} and {BoundLens})
11
+ # * {Access::FluentHelpers#[]} (included in {Lens} and {BoundLens})
12
+ # * just passing a +key+ will also work, when +not(key.kind_of?(Accessor))+
13
+ # (this is a special case in {Lens#initialize})
13
14
  #
14
15
  # *Equivalents* in Elixir's {https://hexdocs.pm/elixir/Access.html +Access+} module
15
16
  # * {https://hexdocs.pm/elixir/Access.html#at/1 +Access.at/1+}
@@ -53,8 +54,12 @@ class Accessory::SubscriptAccessor < Accessory::Accessor
53
54
  end
54
55
 
55
56
  # @!visibility private
56
- def default_data_constructor
57
- lambda{ Hash.new }
57
+ def ensure_valid(traversal_result)
58
+ if traversal_result.respond_to?(:[])
59
+ traversal_result
60
+ else
61
+ {}
62
+ end
58
63
  end
59
64
 
60
65
  # @!visibility private
@@ -67,7 +72,7 @@ class Accessory::SubscriptAccessor < Accessory::Accessor
67
72
  # @param data [Enumerable] the +Enumerable+ to index into
68
73
  # @return [Object] the value derived from the rest of the accessor chain
69
74
  def get(data)
70
- value = value_or_default(data)
75
+ value = traverse_or_default(data)
71
76
 
72
77
  if block_given?
73
78
  yield(value)
@@ -84,7 +89,7 @@ class Accessory::SubscriptAccessor < Accessory::Accessor
84
89
  # @param data [Enumerable] the +Enumerable+ to index into
85
90
  # @return [Array] a two-element array containing 1. the original value found; and 2. the result value from the accessor chain
86
91
  def get_and_update(data)
87
- value = value_or_default(data)
92
+ value = traverse_or_default(data)
88
93
 
89
94
  case yield(value)
90
95
  in [result, new_value]
@@ -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,195 @@ 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
124
+ end
125
+
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
91
152
  end
92
153
 
93
- # (see LensPath#get_and_update_in)
94
- def get_and_update_in(&mutator_fn)
95
- @path.get_and_update_in(@subject, &mutator_fn)
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(data){ |v| [nil, new_value_fn.call(v)] }
168
+ new_data
96
169
  end
97
170
 
98
- # (see LensPath#update_in)
99
- def update_in(&new_value_fn)
100
- @path.update_in(@subject, &new_value_fn)
171
+ # Traverses +subject+ using the chain of accessors held in this Lens,
172
+ # replacing the final value at the end of the traversal chain with
173
+ # +new_value+.
174
+ #
175
+ # *Equivalent* in Elixir: {https://hexdocs.pm/elixir/Kernel.html#put_in/3 +Kernel.put_in/3+}
176
+ #
177
+ # @param subject [Object] the data-structure to traverse
178
+ # @param new_value [Object] a replacement value at the traversal position.
179
+ # @return [Object] the updated +subject+
180
+ def put_in(subject, new_value)
181
+ _, new_data = self.get_and_update_in(subject){ [nil, new_value] }
182
+ new_data
101
183
  end
102
184
 
103
- # (see LensPath#put_in)
104
- def put_in(new_value)
105
- @path.put_in(@subject, new_value)
185
+ # Traverses +subject+ using the chain of accessors held in this Lens,
186
+ # removing the final value at the end of the traversal chain from its position
187
+ # within its parent container.
188
+ #
189
+ # *Equivalent* in Elixir: {https://hexdocs.pm/elixir/Kernel.html#pop_in/2 +Kernel.pop_in/2+}
190
+ #
191
+ # @param subject [Object] the data-structure to traverse
192
+ # @return [Object] the updated +subject+
193
+ def pop_in(subject)
194
+ self.get_and_update_in(subject){ :pop }
106
195
  end
107
196
 
108
- # (see LensPath#pop_in)
109
- def pop_in
110
- @path.pop_in(@subject)
197
+ def append_accessor!(part)
198
+ accessor =
199
+ case part
200
+ when Accessory::Accessor
201
+ part
202
+ when Array
203
+ Accessory::SubscriptAccessor.new(part[0], default: part[1])
204
+ else
205
+ Accessory::SubscriptAccessor.new(part)
206
+ end
207
+
208
+ unless @parts.empty?
209
+ @parts.last.successor = accessor
210
+ end
211
+
212
+ @parts.push(accessor)
111
213
  end
112
- end
214
+ private :append_accessor!
113
215
 
114
- class Accessory::LensPath
115
- # Returns a new {Lens} wrapping this LensPath, bound to the specified
116
- # +subject+.
117
- # @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)
216
+ def get_in_step(data, path)
217
+ step_accessor = path.first
218
+ rest_of_path = path[1..-1]
219
+
220
+ if rest_of_path.empty?
221
+ step_accessor.get(data)
222
+ else
223
+ step_accessor.get(data){ |v| get_in_step(v, rest_of_path) }
224
+ end
225
+ end
226
+ private :get_in_step
227
+
228
+ def get_and_update_in_step(data, path, mutator_fn)
229
+ step_accessor = path.first
230
+ rest_of_path = path[1..-1]
231
+
232
+ if rest_of_path.empty?
233
+ step_accessor.get_and_update(data, &mutator_fn)
234
+ else
235
+ step_accessor.get_and_update(data){ |v| get_and_update_in_step(v, rest_of_path, mutator_fn) }
236
+ end
121
237
  end
238
+ private :get_and_update_in_step
122
239
  end