lazy_mapper 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '08c13aa83740c1719069305ba2be381b32c65ed59017fa251d16a2c5590dd945'
4
+ data.tar.gz: 5f1febab53594a456df8b842972f8ea26015270fd3043853a991ab7b467eae9e
5
+ SHA512:
6
+ metadata.gz: d4c44f93957f6b04c7069b3f7991862f75d6539ab8b2ae70ebbbcdfbfe4080d59548b9a7b2364cc18dd0c16a1b4b223df6417fb039d57fd36718fd17aa36da17
7
+ data.tar.gz: 05523ef4f8fc5d26a335c0b8be671d8e38e677ada3a350e6817cc1a13691b616ed6d8c0b7617c16735894bfe6e799117076dfd100abe15ab44c5798afa8aa2e3
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .DS_Store
2
+ coverage
3
+ /.bundle
4
+ vendor/bundle
5
+ bin/
6
+ log/
7
+ tmp/
8
+ .idea/
9
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'i18n', require: false
7
+ platform :mri do
8
+ gem 'simplecov', require: false
9
+ end
10
+ end
11
+
12
+ group :tools do
13
+ gem 'pry-byebug', platform: :mri
14
+ gem 'pry', platform: :jruby
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
+ end
data/LICENCE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Bruun Rasmussen A/S
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # LazyModel
2
+
3
+ Wraps a JSON object and lazily maps its attributes to rich domain objects using either a set of default mappers (for Ruby's built-in types), or custom mappers specified by the client.
4
+
5
+ The mapped values are memoized.
6
+
7
+ Example:
8
+
9
+ class Foo < LazyMapper
10
+ one :id, Integer, from: 'iden'
11
+ one :created_at, Time
12
+ one :amount, Money, map: Money.method(:parse)
13
+ many :users, User, map: ->(u) { User.new(u) }
14
+ end
15
+
16
+ ## License
17
+
18
+ See `LICENSE` file.
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'lazy_mapper'
3
+ spec.version = '0.1.0'
4
+ spec.summary = "A lazy object mapper"
5
+ spec.description = "Wraps primitive data in a semantically rich model"
6
+ spec.authors = ["Adam Lett"]
7
+ spec.email = 'adam@bruun-rasmussen.dk'
8
+ spec.homepage = 'https://github.com/bruun-rasmussen/lazy_mapper'
9
+ spec.license = 'MIT'
10
+ spec.files = `git ls-files -z`.split("\x0") - ['bin/console']
11
+ spec.executables = []
12
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
13
+ spec.require_paths = ['lib']
14
+
15
+ spec.add_runtime_dependency 'activesupport', '>= 3'
16
+
17
+ spec.add_development_dependency 'bundler'
18
+ spec.add_development_dependency 'rspec'
19
+ end
@@ -0,0 +1,341 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+ require 'time'
4
+ require 'active_support/core_ext/class/attribute'
5
+
6
+ ##
7
+ # Wraps a JSON object and lazily maps its attributes to domain objects
8
+ # using either a set of default mappers (for Ruby's built-in types), or
9
+ # custom mappers specified by the client.
10
+ #
11
+ # The mapped values are memoized.
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
+
22
+ class LazyMapper
23
+
24
+ # Default mappings for built-in types
25
+ DEFAULT_MAPPINGS = {
26
+ Object => ->(o) { o },
27
+ String => ->(s) { s.to_s },
28
+ Integer => ->(i) { i.to_i },
29
+ BigDecimal => ->(d) { d.to_d },
30
+ Float => ->(f) { f.to_f },
31
+ Symbol => ->(s) { s.to_sym },
32
+ Hash => ->(h) { h.to_h },
33
+ Time => Time.method(:iso8601),
34
+ Date => Date.method(:parse),
35
+ URI => URI.method(:parse)
36
+ }.freeze
37
+
38
+ # Default values for primitive types
39
+ DEFAULT_VALUES = {
40
+ String => '',
41
+ Integer => 0,
42
+ Numeric => 0,
43
+ Float => 0.0,
44
+ BigDecimal => BigDecimal.new('0'),
45
+ Array => []
46
+ }.freeze
47
+
48
+ def self.mapper_for(type, mapper)
49
+ mappers[type] = mapper
50
+ end
51
+
52
+ class_attribute :mappers
53
+ self.mappers = {}
54
+
55
+ attr_reader :mappers
56
+
57
+ IVAR = -> name {
58
+ name_as_str = name.to_s
59
+ if name_as_str[-1] == '?'
60
+ name_as_str = name_as_str[0...-1]
61
+ end
62
+
63
+ ('@' + name_as_str).freeze
64
+ }
65
+
66
+ WRITER = -> name { (name.to_s.gsub('?', '') + '=').to_sym }
67
+
68
+ # = ::new
69
+ #
70
+ # Create a new instance by giving a Hash of attribues.
71
+ #
72
+ # == Example
73
+ #
74
+ # Foo.new :id => 42,
75
+ # :created_at => Time.parse("2015-07-29 14:07:35 +0200"),
76
+ # :amount => Money.parse("$2.00"),
77
+ # :users => [
78
+ # User.new("id" => 23, "name" => "Adam"),
79
+ # User.new("id" => 45, "name" => "Ole"),
80
+ # User.new("id" => 66, "name" => "Anders"),
81
+ # User.new("id" => 91, "name" => "Kristoffer)
82
+ # ]
83
+
84
+ def initialize(values = {})
85
+ @json = {}
86
+ @mappers = {}
87
+ values.each do |name, value|
88
+ send(WRITER[name], value)
89
+ end
90
+ end
91
+
92
+ # = ::from_json
93
+ #
94
+ # Create a new instance by giving a Hash of unmapped attributes.
95
+ #
96
+ # The keys in the Hash are assumed to be camelCased strings.
97
+ #
98
+ # == Example
99
+ #
100
+ # Foo.from_json "xmlId" => 42,
101
+ # "createdAt" => "2015-07-29 14:07:35 +0200",
102
+ # "amount" => "$2.00",
103
+ # "users" => [
104
+ # { "id" => 23, "name" => "Adam" },
105
+ # { "id" => 45, "name" => "Ole" },
106
+ # { "id" => 66, "name" => "Anders" },
107
+ # { "id" => 91, "name" => "Kristoffer" }
108
+ # ]
109
+ #
110
+ def self.from_json json, mappers: {}
111
+ return nil if json.nil?
112
+ fail TypeError, "#{ json.inspect } is not a Hash" unless json.respond_to? :to_h
113
+ instance = new
114
+ instance.send :json=, json.to_h
115
+ instance.send :mappers=, mappers
116
+ instance
117
+ end
118
+
119
+ def self.attributes
120
+ @attributes ||= {}
121
+ end
122
+
123
+ # = ::one
124
+ #
125
+ # Defines an attribute
126
+ #
127
+ # == Arguments
128
+ #
129
+ # +name+ - The name of the attribue
130
+ #
131
+ # +type+ - The type of the attribute. If the wrapped value is already of
132
+ # that type, the mapper is bypassed.
133
+ #
134
+ # +from:+ - Specifies the name of the wrapped value in the JSON object.
135
+ # Defaults to camelCased version of +name+.
136
+ #
137
+ # +map:+ - Specifies a custom mapper to apply to the wrapped value. Must be
138
+ # a Callable. If unspecified, it defaults to the default mapper for the
139
+ # specified +type+ or simply the identity mapper if no default mapper exists.
140
+ #
141
+ # +default:+ - The default value to use, if the wrapped value is not present
142
+ # in the wrapped JSON object.
143
+ #
144
+ # == Example
145
+ #
146
+ # class Foo < LazyMapper
147
+ # one :boss, Person, from: "supervisor", map: ->(p) { Person.new(p) }
148
+ # # ...
149
+ # end
150
+ #
151
+ def self.one(name, type, from: map_name(name), allow_nil: true, **args)
152
+
153
+ ivar = IVAR[name]
154
+
155
+ # Define writer
156
+ define_method(WRITER[name]) { |val|
157
+ check_type! val, type, allow_nil: allow_nil
158
+ instance_variable_set(ivar, val)
159
+ }
160
+
161
+ # Define reader
162
+ define_method(name) {
163
+ memoize(name, ivar) {
164
+ unmapped_value = json[from]
165
+ mapped_value(name, unmapped_value, type, **args)
166
+ }
167
+ }
168
+
169
+ attributes[name] = type
170
+ end
171
+
172
+ ##
173
+ # Converts a value to true or false according to its truthyness
174
+ TO_BOOL = -> b { !!b }
175
+
176
+ # = ::is
177
+ #
178
+ # Defines an boolean attribute
179
+ #
180
+ # == Arguments
181
+ #
182
+ # +name+ - The name of the attribue
183
+ #
184
+ # +from:+ - Specifies the name of the wrapped value in the JSON object.
185
+ # Defaults to camelCased version of +name+.
186
+ #
187
+ # +map:+ - Specifies a custom mapper to apply to the wrapped value. Must be
188
+ # a Callable.
189
+ # Defaults to TO_BOOL if unspecified.
190
+ #
191
+ # +default:+ - The default value to use if the value is missing. False, if unspecified
192
+ #
193
+ # == Example
194
+ #
195
+ # class Foo < LazyMapper
196
+ # is :green?, from: "isGreen", map: ->(x) { !x.zero? }
197
+ # # ...
198
+ # end
199
+ #
200
+ def self.is name, from: map_name(name), map: TO_BOOL, default: false
201
+ one name, [TrueClass, FalseClass], from: from, allow_nil: false, map: map, default: default
202
+ end
203
+
204
+ singleton_class.send(:alias_method, :has, :is)
205
+
206
+
207
+ # = ::many
208
+ #
209
+ # Wraps a collection
210
+ #
211
+ # == Arguments
212
+ #
213
+ # +name+ - The name of the attribue
214
+ #
215
+ # +type+ - The type of the elemnts in the collection. If an element is
216
+ # already of that type, the mapper is bypassed for that element.
217
+ #
218
+ # +from:+ - Specifies the name of the wrapped array in the JSON object.
219
+ # Defaults to camelCased version of +name+.
220
+ #
221
+ # +map:+ - Specifies a custom mapper to apply to the elements in the wrapped
222
+ # array. Must respond to +#call+. If unspecified, it defaults to the default
223
+ # mapper for the specified +type+ or simply the identity mapper if no default
224
+ # mapper exists.
225
+ #
226
+ # +default:+ - The default value to use, if the wrapped value is not present
227
+ # in the wrapped JSON object.
228
+ #
229
+ # == Example
230
+ #
231
+ # class Bar < LazyMapper
232
+ # many :underlings, Person, from: "serfs", map: ->(p) { Person.new(p) }
233
+ # # ...
234
+ # end
235
+ #
236
+ def self.many(name, type, from: map_name(name), **args)
237
+
238
+ # Define setter
239
+ define_method(WRITER[name]) { |val|
240
+ check_type! val, Enumerable, allow_nil: false
241
+ instance_variable_set(IVAR[name], val)
242
+ }
243
+
244
+ # Define getter
245
+ define_method(name) {
246
+ memoize(name) {
247
+ unmapped_value = json[from]
248
+ if unmapped_value.is_a? Array
249
+ unmapped_value.map { |v| mapped_value(name, v, type, **args) }
250
+ else
251
+ mapped_value name, unmapped_value, Array, **args
252
+ end
253
+ }
254
+ }
255
+ end
256
+
257
+ def add_mapper_for(type, &block)
258
+ mappers[type] = block
259
+ end
260
+
261
+ def inspect
262
+ attributes = self.class.attributes
263
+ if self.class.superclass.respond_to? :attributes
264
+ attributes = self.class.superclass.attributes.merge attributes
265
+ end
266
+ present_attributes = attributes.keys.each_with_object({}) {|name, memo|
267
+ value = self.send name
268
+ memo[name] = value unless value.nil?
269
+ }
270
+ "<#{ self.class.name } #{ present_attributes.map {|k,v| k.to_s + ': ' + v.inspect }.join(', ') } >"
271
+ end
272
+
273
+ protected
274
+
275
+ def json
276
+ @json ||= {}
277
+ end
278
+
279
+ ##
280
+ # Defines how to map an attribute name
281
+ # to the corresponding name in the unmapped
282
+ # JSON object.
283
+ #
284
+ # Defaults to #camelize
285
+ #
286
+ def self.map_name(name)
287
+ camelize(name)
288
+ end
289
+
290
+ private
291
+
292
+ attr_writer :json
293
+ attr_writer :mappers
294
+
295
+ def mapping_for(name, type)
296
+ mappers[name] || mappers[type] || self.class.mappers[type] || DEFAULT_MAPPINGS[type]
297
+ end
298
+
299
+ def default_value(type)
300
+ DEFAULT_VALUES.fetch(type) { nil }
301
+ end
302
+
303
+ def mapped_value(name, unmapped_value, type, map: mapping_for(name, type), default: default_value(type))
304
+ if unmapped_value.nil?
305
+ # Duplicate to prevent accidental sharing between instances
306
+ default.dup
307
+ else
308
+ fail ArgumentError, "missing mapper for #{ name } (#{ type }). Unmapped value: #{ unmapped_value.inspect }" if map.nil?
309
+ result = map.arity > 1 ? map.call(unmapped_value, self) : map.call(unmapped_value)
310
+ result
311
+ end
312
+ end
313
+
314
+ def check_type! value, type, allow_nil:
315
+ permitted_types = allow_nil ? Array(type) + [ NilClass ] : Array(type)
316
+ fail TypeError.new "#{ self.class.name }: #{ value.inspect } is a #{ value.class } but was supposed to be a #{ humanize_list permitted_types, conjunction: ' or ' }" unless permitted_types.any? value.method(:is_a?)
317
+ end
318
+
319
+ # [1,2,3] -> "1, 2 and 3"
320
+ # [1, 2] -> "1 and 2"
321
+ # [1] -> "1"
322
+ def humanize_list list, separator: ', ', conjunction: ' and '
323
+ *all_but_last, last = list
324
+ return last if all_but_last.empty?
325
+ [ all_but_last.join(separator), last ].join conjunction
326
+ end
327
+
328
+
329
+ def memoize name, ivar = IVAR[name]
330
+ send WRITER[name], yield unless instance_variable_defined?(ivar)
331
+ instance_variable_get(ivar)
332
+ end
333
+
334
+ SNAKE_CASE_PATTERN = /(_[a-z])/
335
+
336
+ # "foo_bar_baz" -> "fooBarBaz"
337
+ # "foo_bar?" -> "fooBar"
338
+ def self.camelize(name)
339
+ name.to_s.gsub(SNAKE_CASE_PATTERN) { |x| x[1].upcase }.gsub('?', '')
340
+ end
341
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'lazy_mapper'
5
+
6
+ describe LazyMapper do
7
+
8
+ describe '.from_json' do
9
+
10
+ subject(:instance) { klass.from_json json }
11
+
12
+ let(:json) { nil }
13
+ let(:klass) {
14
+ t = type
15
+ m = map
16
+ Class.new LazyMapper do
17
+ one :created_at, Date
18
+ many :updated_at, Date
19
+ one :foo, t, map: m, default: 666
20
+ is :blue?
21
+ end
22
+ }
23
+ let(:mapper) { spy 'mapper', map: 42 }
24
+ let(:map) { ->(x) { mapper.map(x) } }
25
+ let(:type) { Integer }
26
+
27
+ context 'if the supplied data is nil' do
28
+ it { is_expected.to be_nil }
29
+ end
30
+
31
+ context 'when invalid data is supplied' do
32
+ let(:json) { 'not a hash' }
33
+
34
+ it 'fails with a TypeError' do
35
+ expect { instance }.to raise_error(TypeError)
36
+ end
37
+ end
38
+
39
+ context 'when valid data is supplied' do
40
+
41
+ let(:json) {
42
+ {
43
+ 'createdAt' => '2015-07-27',
44
+ 'updatedAt' => ['2015-01-01', '2015-01-02'],
45
+ 'foo' => '42',
46
+ 'blue' => true
47
+ }
48
+ }
49
+
50
+ it 'maps JSON attributes to domain objects' do
51
+ expect(instance.created_at).to eq(Date.new(2015, 7, 27))
52
+ end
53
+
54
+ it 'maps arrays of JSON values to arrays of domain objects' do
55
+ expect(instance.updated_at).to be_a(Array)
56
+ expect(instance.updated_at.first).to be_a(Date)
57
+ expect(instance).to be_blue
58
+ end
59
+
60
+ it 'memoizes mapped value so that potentially expensive mappings are performed just once' do
61
+ 3.times do
62
+ expect(instance.foo).to eq(42)
63
+ end
64
+ expect(mapper).to have_received(:map).exactly(1).times.with('42')
65
+ end
66
+
67
+ context 'if the mapped value is nil' do
68
+ let(:map) { -> x { mapper.map(x); nil } }
69
+
70
+ it 'even memoizes that' do
71
+ 3.times do
72
+ expect(instance.foo).to be_nil
73
+ end
74
+ expect(mapper).to have_received(:map).exactly(1).times.with('42')
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'the :from option' do
80
+
81
+ let(:klass) {
82
+ Class.new LazyMapper do
83
+ one :baz, Integer, from: 'BAZ'
84
+ is :fuzzy?, from: 'hairy'
85
+ is :sweet?, from: 'sugary'
86
+ end
87
+ }
88
+
89
+ let(:json) { { 'BAZ' => 999, 'hairy' => true } }
90
+
91
+ it 'specifies a different name in the JSON object for the attribute' do
92
+ expect(instance.baz).to eq(999)
93
+ end
94
+
95
+ it { is_expected.to be_fuzzy }
96
+ it { is_expected.to_not be_sweet }
97
+ end
98
+
99
+ context "if the mapper doesn't map to the correct type" do
100
+
101
+ let(:klass) {
102
+ Class.new LazyMapper do
103
+ one :bar, Float, map: ->(x) { x.to_s }
104
+ end
105
+ }
106
+
107
+ it 'fails with a TypeError when an attribute is accessed' do
108
+ instance = klass.from_json 'bar' => 42
109
+ expect { instance.bar }.to raise_error(TypeError)
110
+ end
111
+ end
112
+
113
+ it 'supports adding custom type mappers to instances' do
114
+ type = Struct.new(:val1, :val2)
115
+ klass = Class.new LazyMapper do
116
+ one :composite, type
117
+ end
118
+
119
+ instance = klass.from_json 'composite' => '123 456'
120
+ instance.add_mapper_for(type) { |unmapped_value| type.new(*unmapped_value.split(' ')) }
121
+
122
+ expect(instance.composite).to eq type.new('123', '456')
123
+
124
+ instance = klass.new composite: type.new('abc', 'cde')
125
+ expect(instance.composite).to eq type.new('abc', 'cde')
126
+ end
127
+
128
+ it 'supports adding default mappers to derived classes' do
129
+ type = Struct.new(:val1, :val2)
130
+
131
+ klass = Class.new LazyMapper do
132
+ mapper_for type, ->(unmapped_value) { type.new(*unmapped_value.split(' ')) }
133
+ one :composite, type
134
+ end
135
+
136
+ instance = klass.from_json 'composite' => '123 456'
137
+ expect(instance.composite).to eq type.new('123', '456')
138
+ end
139
+
140
+ it 'supports injection of customer mappers during instantiation' do
141
+ type = Struct.new(:val1, :val2)
142
+ klass = Class.new LazyMapper do
143
+ one :foo, type
144
+ one :bar, type
145
+ end
146
+
147
+ instance = klass.from_json({ 'foo' => '123 456', 'bar' => 'abc def' },
148
+ mappers: {
149
+ foo: ->(f) { type.new(*f.split(' ').reverse) },
150
+ type => ->(t) { type.new(*t.split(' ')) }
151
+ })
152
+
153
+ expect(instance.foo).to eq type.new('456', '123')
154
+ expect(instance.bar).to eq type.new('abc', 'def')
155
+ end
156
+
157
+ it 'expects the supplied mapper to return an Array if the unmapped value of a "many" attribute is not an array' do
158
+ klass = Class.new LazyMapper do
159
+ many :foos, String, map: ->(v) { return v.split '' }
160
+ many :bars, String, map: ->(v) { return v }
161
+ end
162
+
163
+ instance = klass.from_json 'foos' => 'abc', 'bars' => 'abc'
164
+
165
+ expect(instance.foos).to eq %w[a b c]
166
+ expect { instance.bars }.to raise_error(TypeError)
167
+ end
168
+ end
169
+
170
+ context 'construction' do
171
+
172
+ subject(:instance) { klass.new values }
173
+ let(:values) { {} }
174
+
175
+ let(:klass) {
176
+ Class.new LazyMapper do
177
+ one :title, String
178
+ one :count, Integer
179
+ one :rate, Float
180
+ one :tags, Array
181
+ one :widget, Object
182
+ one :things, Array, default: ['something']
183
+ is :green?
184
+ has :flowers?
185
+ end
186
+ }
187
+
188
+ context 'when values are provided' do
189
+
190
+ let(:values) {
191
+ {
192
+ title: 'A title',
193
+ count: 42,
194
+ rate: 3.14,
195
+ tags: %w[red hot],
196
+ widget: Date.new,
197
+ things: %i[one two three],
198
+ green?: true
199
+ }
200
+ }
201
+
202
+ it 'uses those values' do
203
+ values.each do |name, value|
204
+ expect(instance.send(name)).to eq(value)
205
+ end
206
+ end
207
+
208
+ context 'if a value given in the constructor is not of the specified type' do
209
+ let(:values) {
210
+ { title: :'Not a string' }
211
+ }
212
+
213
+ it 'fails with a TypeError' do
214
+ expect { instance }.to raise_error(TypeError)
215
+ end
216
+ end
217
+ end
218
+
219
+ context 'when no values are provided' do
220
+
221
+ it 'have sensible fallback values for primitive types' do
222
+ expect(instance.title).to eq('')
223
+ expect(instance.count).to eq(0)
224
+ expect(instance.rate).to eq(0.0)
225
+ expect(instance.widget).to be_nil
226
+ expect(instance.tags).to eq []
227
+ end
228
+
229
+ it 'use the supplied default values' do
230
+ expect(instance.things).to eq(['something'])
231
+ end
232
+
233
+ it 'fall back to nil in all other cases' do
234
+ expect(instance.widget).to be_nil
235
+ end
236
+
237
+ it 'don\'t share their default values between instances' do
238
+ instance1 = klass.new
239
+ instance2 = klass.new
240
+ instance1.tags << 'dirty'
241
+ instance1.things.pop
242
+ expect(instance2.tags).to be_empty
243
+ expect(instance2.things).to_not be_empty
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift __dir__
5
+
6
+ if ENV['RCOV']
7
+ require 'simplecov'
8
+ SimpleCov.start do
9
+ add_filter '/spec/'
10
+ end
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.pattern = '**{,/*/**}/*{_,.}spec.rb'
15
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lazy_mapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Lett
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-08-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Wraps primitive data in a semantically rich model
56
+ email: adam@bruun-rasmussen.dk
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".gitignore"
62
+ - Gemfile
63
+ - LICENCE
64
+ - README.md
65
+ - lazy_mapper.gemspec
66
+ - lib/lazy_mapper.rb
67
+ - spec/lazy_mapper_spec.rb
68
+ - spec/spec_helper.rb
69
+ homepage: https://github.com/bruun-rasmussen/lazy_mapper
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.7.3
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: A lazy object mapper
93
+ test_files:
94
+ - spec/lazy_mapper_spec.rb
95
+ - spec/spec_helper.rb