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 +4 -4
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +38 -0
- data/Gemfile +0 -6
- data/README.md +6 -2
- data/lazy_mapper.gemspec +6 -1
- data/lib/lazy_mapper/defaults.rb +36 -0
- data/lib/lazy_mapper/lazy_mapper.rb +375 -0
- data/lib/lazy_mapper/version.rb +3 -0
- data/lib/lazy_mapper.rb +4 -399
- data/spec/lazy_mapper_spec.rb +23 -14
- data/spec/spec_helper.rb +0 -3
- metadata +10 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac0899bdea6ac8c42e98bea1c5772ac775fbab63d91142a73f8d30e956b39588
|
4
|
+
data.tar.gz: d8068e7cbe26f50d2672f10e378358e96c167048da488c22f45826cf4a476e8c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f46dca3d8d74b632fd33944f9d606c4182bbcb5047e2478d65b2f5091a6ad05bcb05d3984ceea9ddea2f8070a43074e3b06ab09c1f3bff8370c7fd798c49729
|
7
|
+
data.tar.gz: 915d348aaee1c3a8c3829e1bc0372f28c83132c7a65ac22fac33a01bcbea8d8d8ab256fa0a855c247af402eb2c0f0ec18c9a232022555668922908a18d1a6a8d
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
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.
|
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 =
|
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
|
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'
|
data/spec/lazy_mapper_spec.rb
CHANGED
@@ -20,7 +20,7 @@ describe LazyMapper do
|
|
20
20
|
let(:klass) {
|
21
21
|
t = type
|
22
22
|
m = map
|
23
|
-
Class.new
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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(
|
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
|
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 '
|
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 '
|
290
|
+
it 'uses the supplied default values' do
|
282
291
|
expect(instance.things).to eq(['something'])
|
283
292
|
end
|
284
293
|
|
285
|
-
it '
|
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 '
|
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
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.
|
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:
|
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.
|
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:
|