lazy_mapper 0.1.0

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 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