lazy_mapper 0.3.2 → 0.4.1

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: 702528c73dfd2a707526f0e792472df55077dd97bf4cf034f6187b97f885ff41
4
- data.tar.gz: c2b4708374ca625b5e81a6645cf279854241721705bfafdfee3f258f27f2ab8a
3
+ metadata.gz: ac0899bdea6ac8c42e98bea1c5772ac775fbab63d91142a73f8d30e956b39588
4
+ data.tar.gz: d8068e7cbe26f50d2672f10e378358e96c167048da488c22f45826cf4a476e8c
5
5
  SHA512:
6
- metadata.gz: 0540ab08d1cdcaac33e99876987df7d64ed6f4a31c4bac7f03581cf2d7ec64ee45ac35c9692614c6b78b8ca85952803fc5eab7f61818f790f784490b7c804271
7
- data.tar.gz: e22803584b494a25427b059fa32ae059091ab614b72ef2293581dd81486a05b6193372f32b4b576262f7b8f876c74d9b41e1660cce43355472fcf5f1fbcbce31
6
+ metadata.gz: 0f46dca3d8d74b632fd33944f9d606c4182bbcb5047e2478d65b2f5091a6ad05bcb05d3984ceea9ddea2f8070a43074e3b06ab09c1f3bff8370c7fd798c49729
7
+ data.tar.gz: 915d348aaee1c3a8c3829e1bc0372f28c83132c7a65ac22fac33a01bcbea8d8d8ab256fa0a855c247af402eb2c0f0ec18c9a232022555668922908a18d1a6a8d
data/.gitignore CHANGED
@@ -9,3 +9,4 @@ doc/
9
9
  .idea/
10
10
  Gemfile.lock
11
11
  *.gem
12
+ .ruby-version
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.6.1
1
+ 2.7.4
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ #0.4.1
2
+
3
+ ## Fixed
4
+
5
+ * Fix uninitialized constant
6
+
7
+ #0.4.0
8
+
9
+ ## Changed
10
+
11
+ * Removed deprecated factory method `.from_json`
12
+ * Enable inheritance of mappers added after the derived class is defined
13
+
14
+ #0.3.2
15
+
16
+ ## Fixed
17
+
18
+ * No more BigDecimal deprecation warning
19
+
20
+ #0.3.1
21
+
22
+ ## Fixed
23
+
24
+ * Collections no longer missing from `#to_h` and `#inspect`
25
+
26
+ #0.3.0
27
+
28
+ ## Changed
29
+ * `.from_json` renamed to `.from`. `.from_json` is kept as an alias to the new method.
30
+
31
+ ## Added
32
+
33
+ * `#to_h` method
34
+
35
+ #0.2.1
36
+
37
+ (No user visible changes)
38
+
data/Gemfile CHANGED
@@ -3,7 +3,6 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :test do
6
- gem 'i18n', require: false
7
6
  platform :mri do
8
7
  gem 'simplecov', require: false
9
8
  end
@@ -12,9 +11,4 @@ end
12
11
  group :tools do
13
12
  gem 'pry', platform: :jruby
14
13
  gem 'pry-byebug', platform: :mri
15
-
16
- unless ENV['TRAVIS']
17
- gem 'mutant', git: 'https://github.com/mbj/mutant'
18
- gem 'mutant-rspec', git: 'https://github.com/mbj/mutant'
19
- end
20
14
  end
data/README.md CHANGED
@@ -6,7 +6,7 @@ The mapped values are memoized.
6
6
 
7
7
  Example:
8
8
 
9
- class Foo < LazyMapper
9
+ class Foo < LazyMapper::Model
10
10
  one :id, Integer, from: 'iden'
11
11
  one :created_at, Time
12
12
  one :amount, Money, map: Money.method(:parse)
@@ -15,8 +15,12 @@ Example:
15
15
 
16
16
  ## Documentation
17
17
 
18
- See [RubyDoc](https://www.rubydoc.info/gems/lazy_mapper/0.3.1)
18
+ See [RubyDoc](https://www.rubydoc.info/gems/lazy_mapper/0.4.0)
19
19
 
20
20
  ## License
21
21
 
22
22
  See LICENSE file.
23
+
24
+ ## Changes
25
+
26
+ See CHANGES.md
data/lazy_mapper.gemspec CHANGED
@@ -1,6 +1,11 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'lazy_mapper/version'
5
+
1
6
  Gem::Specification.new do |spec|
2
7
  spec.name = 'lazy_mapper'
3
- spec.version = '0.3.2'
8
+ spec.version = LazyMapper::VERSION
4
9
  spec.summary = 'A lazy object mapper'
5
10
  spec.description = 'Wraps primitive data in a semantically rich model'
6
11
  spec.authors = ['Adam Lett']
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+ require 'bigdecimal/util'
5
+ require 'time'
6
+ require 'uri'
7
+
8
+ class LazyMapper
9
+ #
10
+ # Default mappings for built-in types
11
+ #
12
+ DEFAULT_MAPPINGS = {
13
+ Object => :itself.to_proc,
14
+ String => :to_s.to_proc,
15
+ Integer => :to_i.to_proc,
16
+ BigDecimal => :to_d.to_proc,
17
+ Float => :to_f.to_proc,
18
+ Symbol => :to_sym.to_proc,
19
+ Hash => :to_h.to_proc,
20
+ Time => Time.method(:iso8601),
21
+ Date => Date.method(:parse),
22
+ URI => URI.method(:parse)
23
+ }.freeze
24
+
25
+ #
26
+ # Default values for built-in value types
27
+ #
28
+ DEFAULT_VALUES = {
29
+ String => '',
30
+ Integer => 0,
31
+ Numeric => 0,
32
+ Float => 0.0,
33
+ BigDecimal => BigDecimal(0),
34
+ Array => []
35
+ }.freeze
36
+ end
@@ -0,0 +1,375 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Wraps a Hash or Hash-like data structure of primitive values and lazily maps
5
+ # its attributes to semantically rich domain objects using either a set of
6
+ # default mappers (for Ruby's built-in value types), or custom mappers which
7
+ # can be added either at the class level or at the instance level.
8
+ #
9
+ # Example:
10
+ # class Foo < LazyMapper
11
+ # one :id, Integer, from: 'xmlId'
12
+ # one :created_at, Time
13
+ # one :amount, Money, map: Money.method(:parse)
14
+ # many :users, User, map: ->(u) { User.new(u) }
15
+ # end
16
+ #
17
+ class LazyMapper
18
+
19
+ #
20
+ # Adds (or overrides) a default type for a given type
21
+ #
22
+ def self.default_value_for type, value
23
+ default_values[type] = value
24
+ end
25
+
26
+ def self.default_values
27
+ @default_values ||= DEFAULT_VALUES
28
+ end
29
+
30
+ #
31
+ # Adds a mapper for a give type
32
+ #
33
+ def self.mapper_for(type, mapper)
34
+ mappers[type] = mapper
35
+ end
36
+
37
+ def self.mappers
38
+ @mappers ||= DEFAULT_MAPPINGS
39
+ end
40
+
41
+ def self.attributes
42
+ @attributes ||= {}
43
+ end
44
+
45
+ def self.inherited klass # :nodoc:
46
+ # Make the subclass "inherit" the values of these class instance variables
47
+ %i[
48
+ default_values
49
+ attributes
50
+ ].each do |s|
51
+ klass.instance_variable_set IVAR[s], self.send(s).dup
52
+ end
53
+
54
+ # If a mapper is does not exist in the derived class, look it up in this class
55
+ klass.instance_variable_set('@mappers', Hash.new { |_mappers, type| mappers[type] })
56
+ end
57
+
58
+ def mappers
59
+ @mappers ||= self.class.mappers
60
+ end
61
+
62
+ IVAR = lambda { |name| # :nodoc:
63
+ name_as_str = name.to_s
64
+ name_as_str = name_as_str[0...-1] if name_as_str[-1] == '?'
65
+
66
+ ('@' + name_as_str).freeze
67
+ }
68
+
69
+ WRITER = -> name { (name.to_s.delete('?') + '=').to_sym }
70
+
71
+ #
72
+ # Creates a new instance by giving a Hash of attribues.
73
+ #
74
+ # Attribute values are type checked according to how they were defined.
75
+ #
76
+ # Fails with +TypeError+, if a value doesn't have the expected type.
77
+ #
78
+ # == Example
79
+ #
80
+ # Foo.new :id => 42,
81
+ # :created_at => Time.parse("2015-07-29 14:07:35 +0200"),
82
+ # :amount => Money.parse("$2.00"),
83
+ # :users => [
84
+ # User.new("id" => 23, "name" => "Adam"),
85
+ # User.new("id" => 45, "name" => "Ole"),
86
+ # User.new("id" => 66, "name" => "Anders"),
87
+ # User.new("id" => 91, "name" => "Kristoffer)
88
+ # ]
89
+
90
+ def initialize(values = {})
91
+ @mappers = {}
92
+ values.each do |name, value|
93
+ send(WRITER[name], value)
94
+ end
95
+ end
96
+
97
+ #
98
+ # Create a new instance by giving a Hash of unmapped attributes.
99
+ #
100
+ # The keys in the Hash are assumed to be camelCased strings.
101
+ #
102
+ # == Arguments
103
+ #
104
+ # +unmapped_data+ - The unmapped data as a Hash(-like object). Must respond to #to_h.
105
+ # Keys are assumed to be camelCased string
106
+ #
107
+ # +mappers:+ - Optional instance-level mappers.
108
+ # Keys can either be classes or symbols corresponding to named attributes.
109
+ #
110
+ #
111
+ # == Example
112
+ #
113
+ # Foo.from({
114
+ # "xmlId" => 42,
115
+ # "createdAt" => "2015-07-29 14:07:35 +0200",
116
+ # "amount" => "$2.00",
117
+ # "users" => [
118
+ # { "id" => 23, "name" => "Adam" },
119
+ # { "id" => 45, "name" => "Ole" },
120
+ # { "id" => 66, "name" => "Anders" },
121
+ # { "id" => 91, "name" => "Kristoffer" } ]},
122
+ # mappers: {
123
+ # :amount => -> x { Money.new(x) },
124
+ # User => User.method(:new) })
125
+ #
126
+ def self.from unmapped_data, mappers: {}
127
+ return nil if unmapped_data.nil?
128
+ fail TypeError, "#{ unmapped_data.inspect } is not a Hash" unless unmapped_data.respond_to? :to_h
129
+
130
+ instance = new
131
+ instance.send :unmapped_data=, unmapped_data.to_h
132
+ instance.send :mappers=, mappers
133
+ instance
134
+ end
135
+
136
+ #
137
+ # Defines an attribute and creates a reader and a writer for it.
138
+ # The writer verifies the type of it's supplied value.
139
+ #
140
+ # == Arguments
141
+ #
142
+ # +name+ - The name of the attribue
143
+ #
144
+ # +type+ - The type of the attribute. If the wrapped value is already of that type, the mapper is bypassed.
145
+ # If the type is allowed be one of several, use an Array to to specify which ones
146
+ #
147
+ # +from:+ - Specifies the name of the wrapped value in the JSON object. Defaults to camelCased version of +name+.
148
+ #
149
+ # +map:+ - Specifies a custom mapper to apply to the wrapped value.
150
+ # If unspecified, it defaults to the default mapper for the specified +type+ or simply the identity mapper
151
+ # if no default mapper exists.
152
+ #
153
+ # +default:+ - The default value to use, if the wrapped value is not present in the wrapped JSON object.
154
+ #
155
+ # +allow_nil:+ - If true, allows the mapped value to be nil. Defaults to true.
156
+ #
157
+ # == Example
158
+ #
159
+ # class Foo < LazyMapper
160
+ # one :boss, Person, from: "supervisor", map: ->(p) { Person.new(p) }
161
+ # one :weapon, [BladedWeapon, Firearm], default: Sixshooter.new
162
+ # # ...
163
+ # end
164
+ #
165
+ def self.one(name, type, from: map_name(name), allow_nil: true, **args)
166
+
167
+ ivar = IVAR[name]
168
+
169
+ # Define writer
170
+ define_method(WRITER[name]) { |val|
171
+ check_type! val, type, allow_nil: allow_nil
172
+ instance_variable_set(ivar, val)
173
+ }
174
+
175
+ # Define reader
176
+ define_method(name) {
177
+ memoize(name, ivar) {
178
+ unmapped_value = unmapped_data[from]
179
+ mapped_value(name, unmapped_value, type, **args)
180
+ }
181
+ }
182
+
183
+ attributes[name] = type
184
+ end
185
+
186
+ #
187
+ # Converts a value to +true+ or +false+ according to its truthyness
188
+ #
189
+ TO_BOOL = -> b { !!b }
190
+
191
+ #
192
+ # Defines an boolean attribute
193
+ #
194
+ # == Arguments
195
+ #
196
+ # +name+ - The name of the attribue
197
+ #
198
+ # +from:+ - Specifies the name of the wrapped value in the JSON object.
199
+ # Defaults to camelCased version of +name+.
200
+ #
201
+ # +map:+ - Specifies a custom mapper to apply to the wrapped value. Must be a Callable.
202
+ # Defaults to TO_BOOL if unspecified.
203
+ #
204
+ # +default:+ The default value to use if the value is missing. False, if unspecified
205
+ #
206
+ # == Example
207
+ #
208
+ # class Foo < LazyMapper
209
+ # is :green?, from: "isGreen", map: ->(x) { !x.zero? }
210
+ # # ...
211
+ # end
212
+ #
213
+ def self.is name, from: map_name(name), map: TO_BOOL, default: false
214
+ one name, [TrueClass, FalseClass], from: from, allow_nil: false, map: map, default: default
215
+ end
216
+
217
+ singleton_class.send(:alias_method, :has, :is)
218
+
219
+ #
220
+ # Defines a collection attribute
221
+ #
222
+ # == Arguments
223
+ #
224
+ # +name+ - The name of the attribute
225
+ #
226
+ # +type+ - The type of the elements in the collection.
227
+ #
228
+ # +from:+ - Specifies the name of the wrapped array in the unmapped data.
229
+ # Defaults to camelCased version of +name+.
230
+ #
231
+ # +map:+ - Specifies a custom mapper to apply to each elements in the wrapped collection.
232
+ # If unspecified, it defaults to the default mapper for the specified +type+ or simply the identity mapper
233
+ # if no default mapper exists.
234
+ #
235
+ # +default:+ - The default value to use, if the unmapped value is missing.
236
+ #
237
+ # == Example
238
+ #
239
+ # class Bar < LazyMapper
240
+ # many :underlings, Person, from: "serfs", map: ->(p) { Person.new(p) }
241
+ # # ...
242
+ # end
243
+ #
244
+ def self.many(name, type, from: map_name(name), **args)
245
+
246
+ # Define setter
247
+ define_method(WRITER[name]) { |val|
248
+ check_type! val, Enumerable, allow_nil: false
249
+ instance_variable_set(IVAR[name], val)
250
+ }
251
+
252
+ # Define getter
253
+ define_method(name) {
254
+ memoize(name) {
255
+ unmapped_value = unmapped_data[from]
256
+ if unmapped_value.is_a? Array
257
+ unmapped_value.map { |v| mapped_value(name, v, type, **args) }
258
+ else
259
+ mapped_value name, unmapped_value, Array, **args
260
+ end
261
+ }
262
+ }
263
+
264
+ attributes[name] = Array
265
+ end
266
+
267
+ #
268
+ # Adds an instance-level type mapper
269
+ #
270
+ def add_mapper_for(type, &block)
271
+ mappers[type] = block
272
+ end
273
+
274
+ #
275
+ # Returns a +Hash+ with keys corresponding to attribute names, and values
276
+ # corresponding to *mapped* attribute values.
277
+ #
278
+ # Note: This will eagerly map all attributes that haven't yet been mapped
279
+ #
280
+ def to_h
281
+ attributes.each_with_object({}) { |(key, _value), h|
282
+ h[key] = self.send key
283
+ }
284
+ end
285
+
286
+ def inspect
287
+ @__under_inspection__ ||= 0
288
+ return "<#{ self.class.name } ... >" if @__under_inspection__.positive?
289
+
290
+ @__under_inspection__ += 1
291
+ present_attributes = attributes.keys.each_with_object({}) { |name, memo|
292
+ ivar = IVAR[name]
293
+ next unless self.instance_variable_defined? ivar
294
+
295
+ memo[name] = self.instance_variable_get ivar
296
+ }
297
+
298
+ res = "<#{ self.class.name } #{ present_attributes.map { |k, v| k.to_s + ': ' + v.inspect }.join(', ') } >"
299
+ @__under_inspection__ -= 1
300
+ res
301
+ end
302
+
303
+ #
304
+ # Defines how to map an attribute name
305
+ # to the corresponding name in the unmapped
306
+ # JSON object.
307
+ #
308
+ # Defaults to CAMELIZE
309
+ #
310
+ def self.map_name(name)
311
+ CAMELIZE[name]
312
+ end
313
+
314
+ private
315
+
316
+ attr_writer :unmapped_data
317
+ attr_writer :mappers
318
+
319
+ def unmapped_data
320
+ @unmapped_data ||= {}
321
+ end
322
+
323
+ def mapping_for(name, type)
324
+ mappers[name] || mappers[type] || self.class.mappers[type]
325
+ end
326
+
327
+ def default_value(type)
328
+ self.class.default_values[type]
329
+ end
330
+
331
+ def attributes
332
+ self.class.attributes
333
+ end
334
+
335
+ def mapped_value(name, unmapped_value, type, map: mapping_for(name, type), default: default_value(type))
336
+ return default.dup if unmapped_value.nil? # Duplicate to prevent accidental sharing between instances
337
+
338
+ if map.nil?
339
+ fail ArgumentError, "missing mapper for #{ name } (#{ type }). "\
340
+ "Unmapped value: #{ unmapped_value.inspect }"
341
+ end
342
+
343
+ return map.call(unmapped_value, self) if map.arity > 1
344
+
345
+ map.call(unmapped_value)
346
+ end
347
+
348
+ def check_type! value, type, allow_nil:
349
+ permitted_types = allow_nil ? Array(type) + [ NilClass ] : Array(type)
350
+ return if permitted_types.any? value.method(:is_a?)
351
+
352
+ fail TypeError.new "#{ self.class.name }: "\
353
+ "#{ value.inspect } is a #{ value.class } "\
354
+ "but was supposed to be a #{ humanize_list permitted_types, conjunction: ' or ' }"
355
+ end
356
+
357
+ # [1,2,3] -> "1, 2 and 3"
358
+ # [1, 2] -> "1 and 2"
359
+ # [1] -> "1"
360
+ def humanize_list terms, separator: ', ', conjunction: ' and '
361
+ *all_but_last, last = terms
362
+ return last if all_but_last.empty?
363
+
364
+ [ all_but_last.join(separator), last ].join conjunction
365
+ end
366
+
367
+ def memoize name, ivar = IVAR[name]
368
+ send WRITER[name], yield unless instance_variable_defined?(ivar)
369
+ instance_variable_get(ivar)
370
+ end
371
+
372
+ SNAKE_CASE_PATTERN = /(_[a-z])/ # :nodoc:
373
+ CAMELIZE = -> name { name.to_s.gsub(SNAKE_CASE_PATTERN) { |x| x[1].upcase }.delete('?') }
374
+
375
+ end
@@ -0,0 +1,3 @@
1
+ class LazyMapper
2
+ VERSION = '0.4.1'
3
+ end
data/lib/lazy_mapper.rb CHANGED
@@ -1,403 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bigdecimal'
4
- require 'bigdecimal/util'
5
- require 'time'
6
-
7
- #
8
- # Wraps a Hash or Hash-like data structure of primitive values and lazily maps
9
- # its attributes to semantically rich domain objects using either a set of
10
- # default mappers (for Ruby's built-in value types), or custom mappers which
11
- # can be added either at the class level or at the instance level.
12
- #
13
- # Example:
14
- # class Foo < LazyMapper
15
- # one :id, Integer, from: 'xmlId'
16
- # one :created_at, Time
17
- # one :amount, Money, map: Money.method(:parse)
18
- # many :users, User, map: ->(u) { User.new(u) }
19
- # end
20
- #
21
3
  class LazyMapper
22
-
23
- #
24
- # Default mappings for built-in types
25
- #
26
- DEFAULT_MAPPINGS = {
27
- Object => :itself.to_proc,
28
- String => :to_s.to_proc,
29
- Integer => :to_i.to_proc,
30
- BigDecimal => :to_d.to_proc,
31
- Float => :to_f.to_proc,
32
- Symbol => :to_sym.to_proc,
33
- Hash => :to_h.to_proc,
34
- Time => Time.method(:iso8601),
35
- Date => Date.method(:parse),
36
- URI => URI.method(:parse)
37
- }.freeze
38
-
39
- #
40
- # Adds (or overrides) a default type for a given type
41
- #
42
- def self.default_value_for type, value
43
- default_values[type] = value
44
- end
45
-
46
- def self.default_values
47
- @default_values ||= DEFAULT_VALUES
48
- end
49
-
50
- #
51
- # Default values for built-in value types
52
- #
53
- DEFAULT_VALUES = {
54
- String => '',
55
- Integer => 0,
56
- Numeric => 0,
57
- Float => 0.0,
58
- BigDecimal => BigDecimal(0),
59
- Array => []
60
- }.freeze
61
-
62
- #
63
- # Adds a mapper for a give type
64
- #
65
- def self.mapper_for(type, mapper)
66
- mappers[type] = mapper
67
- end
68
-
69
- def self.mappers
70
- @mappers ||= DEFAULT_MAPPINGS
71
- end
72
-
73
- def self.attributes
74
- @attributes ||= {}
75
- end
76
-
77
- def self.inherited klass # :nodoc:
78
- # Make the subclass "inherit" the values of these class instance variables
79
- %i[
80
- mappers
81
- default_values
82
- attributes
83
- ].each do |s|
84
- klass.instance_variable_set IVAR[s], self.send(s).dup
85
- end
86
- end
87
-
88
- def mappers
89
- @mappers ||= self.class.mappers
90
- end
91
-
92
- IVAR = lambda { |name| # :nodoc:
93
- name_as_str = name.to_s
94
- name_as_str = name_as_str[0...-1] if name_as_str[-1] == '?'
95
-
96
- ('@' + name_as_str).freeze
97
- }
98
-
99
- WRITER = -> name { (name.to_s.delete('?') + '=').to_sym }
100
-
101
- #
102
- # Creates a new instance by giving a Hash of attribues.
103
- #
104
- # Attribute values are type checked according to how they were defined.
105
- #
106
- # Fails with +TypeError+, if a value doesn't have the expected type.
107
- #
108
- # == Example
109
- #
110
- # Foo.new :id => 42,
111
- # :created_at => Time.parse("2015-07-29 14:07:35 +0200"),
112
- # :amount => Money.parse("$2.00"),
113
- # :users => [
114
- # User.new("id" => 23, "name" => "Adam"),
115
- # User.new("id" => 45, "name" => "Ole"),
116
- # User.new("id" => 66, "name" => "Anders"),
117
- # User.new("id" => 91, "name" => "Kristoffer)
118
- # ]
119
-
120
- def initialize(values = {})
121
- @mappers = {}
122
- values.each do |name, value|
123
- send(WRITER[name], value)
124
- end
125
- end
126
-
127
- #
128
- # Create a new instance by giving a Hash of unmapped attributes.
129
- #
130
- # The keys in the Hash are assumed to be camelCased strings.
131
- #
132
- # == Arguments
133
- #
134
- # +unmapped_data+ - The unmapped data as a Hash(-like object). Must respond to #to_h.
135
- # Keys are assumed to be camelCased string
136
- #
137
- # +mappers:+ - Optional instance-level mappers.
138
- # Keys can either be classes or symbols corresponding to named attributes.
139
- #
140
- #
141
- # == Example
142
- #
143
- # Foo.from({
144
- # "xmlId" => 42,
145
- # "createdAt" => "2015-07-29 14:07:35 +0200",
146
- # "amount" => "$2.00",
147
- # "users" => [
148
- # { "id" => 23, "name" => "Adam" },
149
- # { "id" => 45, "name" => "Ole" },
150
- # { "id" => 66, "name" => "Anders" },
151
- # { "id" => 91, "name" => "Kristoffer" } ]},
152
- # mappers: {
153
- # :amount => -> x { Money.new(x) },
154
- # User => User.method(:new) })
155
- #
156
- def self.from unmapped_data, mappers: {}
157
- return nil if unmapped_data.nil?
158
- fail TypeError, "#{ unmapped_data.inspect } is not a Hash" unless unmapped_data.respond_to? :to_h
159
-
160
- instance = new
161
- instance.send :unmapped_data=, unmapped_data.to_h
162
- instance.send :mappers=, mappers
163
- instance
164
- end
165
-
166
- def self.from_json *args, &block # :nodoc:
167
- warn "#{ self }.from_json is deprecated. Use #{ self }.from instead."
168
- from(*args, &block)
169
- end
170
-
171
- #
172
- # Defines an attribute and creates a reader and a writer for it.
173
- # The writer verifies the type of it's supplied value.
174
- #
175
- # == Arguments
176
- #
177
- # +name+ - The name of the attribue
178
- #
179
- # +type+ - The type of the attribute. If the wrapped value is already of that type, the mapper is bypassed.
180
- # If the type is allowed be one of several, use an Array to to specify which ones
181
- #
182
- # +from:+ - Specifies the name of the wrapped value in the JSON object. Defaults to camelCased version of +name+.
183
- #
184
- # +map:+ - Specifies a custom mapper to apply to the wrapped value.
185
- # If unspecified, it defaults to the default mapper for the specified +type+ or simply the identity mapper
186
- # if no default mapper exists.
187
- #
188
- # +default:+ - The default value to use, if the wrapped value is not present in the wrapped JSON object.
189
- #
190
- # +allow_nil:+ - If true, allows the mapped value to be nil. Defaults to true.
191
- #
192
- # == Example
193
- #
194
- # class Foo < LazyMapper
195
- # one :boss, Person, from: "supervisor", map: ->(p) { Person.new(p) }
196
- # one :weapon, [BladedWeapon, Firearm], default: Sixshooter.new
197
- # # ...
198
- # end
199
- #
200
- def self.one(name, type, from: map_name(name), allow_nil: true, **args)
201
-
202
- ivar = IVAR[name]
203
-
204
- # Define writer
205
- define_method(WRITER[name]) { |val|
206
- check_type! val, type, allow_nil: allow_nil
207
- instance_variable_set(ivar, val)
208
- }
209
-
210
- # Define reader
211
- define_method(name) {
212
- memoize(name, ivar) {
213
- unmapped_value = unmapped_data[from]
214
- mapped_value(name, unmapped_value, type, **args)
215
- }
216
- }
217
-
218
- attributes[name] = type
219
- end
220
-
221
- #
222
- # Converts a value to +true+ or +false+ according to its truthyness
223
- #
224
- TO_BOOL = -> b { !!b }
225
-
226
- #
227
- # Defines an boolean attribute
228
- #
229
- # == Arguments
230
- #
231
- # +name+ - The name of the attribue
232
- #
233
- # +from:+ - Specifies the name of the wrapped value in the JSON object.
234
- # Defaults to camelCased version of +name+.
235
- #
236
- # +map:+ - Specifies a custom mapper to apply to the wrapped value. Must be a Callable.
237
- # Defaults to TO_BOOL if unspecified.
238
- #
239
- # +default:+ The default value to use if the value is missing. False, if unspecified
240
- #
241
- # == Example
242
- #
243
- # class Foo < LazyMapper
244
- # is :green?, from: "isGreen", map: ->(x) { !x.zero? }
245
- # # ...
246
- # end
247
- #
248
- def self.is name, from: map_name(name), map: TO_BOOL, default: false
249
- one name, [TrueClass, FalseClass], from: from, allow_nil: false, map: map, default: default
250
- end
251
-
252
- singleton_class.send(:alias_method, :has, :is)
253
-
254
- #
255
- # Defines a collection attribute
256
- #
257
- # == Arguments
258
- #
259
- # +name+ - The name of the attribute
260
- #
261
- # +type+ - The type of the elements in the collection.
262
- #
263
- # +from:+ - Specifies the name of the wrapped array in the unmapped data.
264
- # Defaults to camelCased version of +name+.
265
- #
266
- # +map:+ - Specifies a custom mapper to apply to each elements in the wrapped collection.
267
- # If unspecified, it defaults to the default mapper for the specified +type+ or simply the identity mapper
268
- # if no default mapper exists.
269
- #
270
- # +default:+ - The default value to use, if the unmapped value is missing.
271
- #
272
- # == Example
273
- #
274
- # class Bar < LazyMapper
275
- # many :underlings, Person, from: "serfs", map: ->(p) { Person.new(p) }
276
- # # ...
277
- # end
278
- #
279
- def self.many(name, type, from: map_name(name), **args)
280
-
281
- # Define setter
282
- define_method(WRITER[name]) { |val|
283
- check_type! val, Enumerable, allow_nil: false
284
- instance_variable_set(IVAR[name], val)
285
- }
286
-
287
- # Define getter
288
- define_method(name) {
289
- memoize(name) {
290
- unmapped_value = unmapped_data[from]
291
- if unmapped_value.is_a? Array
292
- unmapped_value.map { |v| mapped_value(name, v, type, **args) }
293
- else
294
- mapped_value name, unmapped_value, Array, **args
295
- end
296
- }
297
- }
298
-
299
- attributes[name] = Array
300
- end
301
-
302
- #
303
- # Adds an instance-level type mapper
304
- #
305
- def add_mapper_for(type, &block)
306
- mappers[type] = block
307
- end
308
-
309
- def to_h
310
- attributes.each_with_object({}) { |(key, _value), h|
311
- h[key] = self.send key
312
- }
313
- end
314
-
315
- def inspect
316
- @__under_inspection__ ||= 0
317
- return "<#{ self.class.name } ... >" if @__under_inspection__.positive?
318
-
319
- @__under_inspection__ += 1
320
- present_attributes = attributes.keys.each_with_object({}) { |name, memo|
321
- ivar = IVAR[name]
322
- next unless self.instance_variable_defined? ivar
323
-
324
- memo[name] = self.instance_variable_get ivar
325
- }
326
-
327
- res = "<#{ self.class.name } #{ present_attributes.map { |k, v| k.to_s + ': ' + v.inspect }.join(', ') } >"
328
- @__under_inspection__ -= 1
329
- res
330
- end
331
-
332
- #
333
- # Defines how to map an attribute name
334
- # to the corresponding name in the unmapped
335
- # JSON object.
336
- #
337
- # Defaults to CAMELIZE
338
- #
339
- def self.map_name(name)
340
- CAMELIZE[name]
341
- end
342
-
343
- private
344
-
345
- attr_writer :unmapped_data
346
- attr_writer :mappers
347
-
348
- def unmapped_data
349
- @unmapped_data ||= {}
350
- end
351
-
352
- def mapping_for(name, type)
353
- mappers[name] || mappers[type] || self.class.mappers[type]
354
- end
355
-
356
- def default_value(type)
357
- self.class.default_values[type]
358
- end
359
-
360
- def attributes
361
- self.class.attributes
362
- end
363
-
364
- def mapped_value(name, unmapped_value, type, map: mapping_for(name, type), default: default_value(type))
365
- return default.dup if unmapped_value.nil? # Duplicate to prevent accidental sharing between instances
366
-
367
- if map.nil?
368
- fail ArgumentError, "missing mapper for #{ name } (#{ type }). "\
369
- "Unmapped value: #{ unmapped_value.inspect }"
370
- end
371
-
372
- return map.call(unmapped_value, self) if map.arity > 1
373
-
374
- map.call(unmapped_value)
375
- end
376
-
377
- def check_type! value, type, allow_nil:
378
- permitted_types = allow_nil ? Array(type) + [ NilClass ] : Array(type)
379
- return if permitted_types.any? value.method(:is_a?)
380
-
381
- fail TypeError.new "#{ self.class.name }: "\
382
- "#{ value.inspect } is a #{ value.class } "\
383
- "but was supposed to be a #{ humanize_list permitted_types, conjunction: ' or ' }"
384
- end
385
-
386
- # [1,2,3] -> "1, 2 and 3"
387
- # [1, 2] -> "1 and 2"
388
- # [1] -> "1"
389
- def humanize_list list, separator: ', ', conjunction: ' and '
390
- *all_but_last, last = list
391
- return last if all_but_last.empty?
392
-
393
- [ all_but_last.join(separator), last ].join conjunction
394
- end
395
-
396
- def memoize name, ivar = IVAR[name]
397
- send WRITER[name], yield unless instance_variable_defined?(ivar)
398
- instance_variable_get(ivar)
399
- end
400
-
401
- SNAKE_CASE_PATTERN = /(_[a-z])/ # :nodoc:
402
- CAMELIZE = -> name { name.to_s.gsub(SNAKE_CASE_PATTERN) { |x| x[1].upcase }.delete('?') }
403
4
  end
5
+
6
+ require 'lazy_mapper/version'
7
+ require 'lazy_mapper/defaults'
8
+ require 'lazy_mapper/lazy_mapper'
@@ -20,7 +20,7 @@ describe LazyMapper do
20
20
  let(:klass) {
21
21
  t = type
22
22
  m = map
23
- Class.new LazyMapper do
23
+ Class.new described_class do
24
24
  one :created_at, Date
25
25
  many :updated_at, Date
26
26
  one :foo, t, map: m, default: 666
@@ -92,14 +92,14 @@ describe LazyMapper do
92
92
 
93
93
  let(:klass_foo) {
94
94
  b = bar_builder
95
- Class.new LazyMapper do
95
+ Class.new described_class do
96
96
  one :bar, Object, map: b
97
97
  end
98
98
  }
99
99
 
100
100
  let(:klass_bar) {
101
101
  b = foo_builder
102
- Class.new LazyMapper do
102
+ Class.new described_class do
103
103
  one :foo, Object, map: b
104
104
  end
105
105
  }
@@ -116,7 +116,7 @@ describe LazyMapper do
116
116
  describe 'the :from option' do
117
117
 
118
118
  let(:klass) {
119
- Class.new LazyMapper do
119
+ Class.new described_class do
120
120
  one :baz, Integer, from: 'BAZ'
121
121
  is :fuzzy?, from: 'hairy'
122
122
  is :sweet?, from: 'sugary'
@@ -136,7 +136,7 @@ describe LazyMapper do
136
136
  context "if the mapper doesn't map to the correct type" do
137
137
 
138
138
  let(:klass) {
139
- Class.new LazyMapper do
139
+ Class.new described_class do
140
140
  one :bar, Float, map: ->(x) { x.to_s }
141
141
  end
142
142
  }
@@ -149,7 +149,7 @@ describe LazyMapper do
149
149
 
150
150
  it 'supports adding custom type mappers to instances' do
151
151
  type = Struct.new(:val1, :val2)
152
- klass = Class.new LazyMapper do
152
+ klass = Class.new described_class do
153
153
  one :composite, type
154
154
  end
155
155
 
@@ -164,7 +164,7 @@ describe LazyMapper do
164
164
 
165
165
  it 'supports injection of customer mappers during instantiation' do
166
166
  type = Struct.new(:val1, :val2)
167
- klass = Class.new LazyMapper do
167
+ klass = Class.new described_class do
168
168
  one :foo, type
169
169
  one :bar, type
170
170
  end
@@ -180,7 +180,7 @@ describe LazyMapper do
180
180
  end
181
181
 
182
182
  it 'expects the supplied mapper to return an Array if the unmapped value of a "many" attribute is not an array' do
183
- klass = Class.new LazyMapper do
183
+ klass = Class.new described_class do
184
184
  many :foos, String, map: ->(v) { return v.split '' }
185
185
  many :bars, String, map: ->(v) { return v }
186
186
  end
@@ -194,9 +194,11 @@ describe LazyMapper do
194
194
  context 'when it is derived from another LazyMapper' do
195
195
  let(:klass) { Class.new(base) }
196
196
  let(:composite_type) { Struct.new(:val1, :val2) }
197
+ let(:new_type) { Class.new }
198
+ let(:new_type_mapper) { new_type.method(:new) }
197
199
  let(:base) {
198
200
  type = composite_type
199
- Class.new(LazyMapper) do
201
+ Class.new(described_class) do
200
202
  default_value_for type, type.new('321', '123')
201
203
  mapper_for type, ->(unmapped_value) { type.new(*unmapped_value.split(' ')) }
202
204
  one :composite, type
@@ -215,6 +217,13 @@ describe LazyMapper do
215
217
  it 'inherits default mappers' do
216
218
  expect(klass.from('composite' => 'abc def').composite).to eq composite_type.new('abc', 'def')
217
219
  end
220
+
221
+ it 'inherits default mappers that are added to its parent after it has been defined', wip: true do
222
+ expect(klass.mappers[new_type]).to be_nil
223
+ base.mapper_for new_type, new_type_mapper
224
+ expect(base.mappers[new_type]).to eq new_type_mapper
225
+ expect(klass.mappers[new_type]).to eq new_type_mapper
226
+ end
218
227
  end
219
228
  end
220
229
 
@@ -224,7 +233,7 @@ describe LazyMapper do
224
233
  let(:values) { {} }
225
234
 
226
235
  let(:klass) {
227
- Class.new LazyMapper do
236
+ Class.new described_class do
228
237
  one :title, String
229
238
  one :count, Integer
230
239
  one :rate, Float
@@ -270,7 +279,7 @@ describe LazyMapper do
270
279
 
271
280
  context 'when no values are provided' do
272
281
 
273
- it 'have sensible fallback values for primitive types' do
282
+ it 'has sensible default values for primitive types' do
274
283
  expect(instance.title).to eq('')
275
284
  expect(instance.count).to eq(0)
276
285
  expect(instance.rate).to eq(0.0)
@@ -278,15 +287,15 @@ describe LazyMapper do
278
287
  expect(instance.tags).to eq []
279
288
  end
280
289
 
281
- it 'use the supplied default values' do
290
+ it 'uses the supplied default values' do
282
291
  expect(instance.things).to eq(['something'])
283
292
  end
284
293
 
285
- it 'fall back to nil in all other cases' do
294
+ it 'falls back to nil in all other cases' do
286
295
  expect(instance.widget).to be_nil
287
296
  end
288
297
 
289
- it 'don\'t share their default values between instances' do
298
+ it 'dosn\'t share its default values with other instances' do
290
299
  instance1 = klass.new
291
300
  instance2 = klass.new
292
301
  instance1.tags << 'dirty'
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
- $LOAD_PATH.unshift __dir__
5
-
6
3
  if ENV['RCOV']
7
4
  require 'simplecov'
8
5
  SimpleCov.start do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lazy_mapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Lett
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-05 00:00:00.000000000 Z
11
+ date: 2022-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -47,18 +47,22 @@ files:
47
47
  - ".gitignore"
48
48
  - ".rubocop.yml"
49
49
  - ".ruby-version"
50
+ - CHANGELOG.md
50
51
  - Gemfile
51
52
  - LICENCE
52
53
  - README.md
53
54
  - lazy_mapper.gemspec
54
55
  - lib/lazy_mapper.rb
56
+ - lib/lazy_mapper/defaults.rb
57
+ - lib/lazy_mapper/lazy_mapper.rb
58
+ - lib/lazy_mapper/version.rb
55
59
  - spec/lazy_mapper_spec.rb
56
60
  - spec/spec_helper.rb
57
61
  homepage: https://github.com/bruun-rasmussen/lazy_mapper
58
62
  licenses:
59
63
  - MIT
60
64
  metadata: {}
61
- post_install_message:
65
+ post_install_message:
62
66
  rdoc_options: []
63
67
  require_paths:
64
68
  - lib
@@ -73,8 +77,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
77
  - !ruby/object:Gem::Version
74
78
  version: '0'
75
79
  requirements: []
76
- rubygems_version: 3.0.1
77
- signing_key:
80
+ rubygems_version: 3.1.6
81
+ signing_key:
78
82
  specification_version: 4
79
83
  summary: A lazy object mapper
80
84
  test_files: