attr_pouch 0.0.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.
data/lib/attr_pouch.rb ADDED
@@ -0,0 +1,407 @@
1
+ require 'pg'
2
+ require 'sequel'
3
+ require 'attr_pouch/errors'
4
+
5
+ module AttrPouch
6
+ def self.configure
7
+ @@config ||= Config.new
8
+ yield @@config
9
+ end
10
+
11
+ def self.config
12
+ @@config ||= Config.new
13
+ end
14
+
15
+ def self.included(base)
16
+ base.extend(ClassMethods)
17
+ end
18
+
19
+ class Field
20
+ attr_reader :name, :type, :raw_type, :opts
21
+
22
+ def self.encode(type, &block)
23
+ @@encoders ||= {}
24
+ @@encoders[type] = block
25
+ end
26
+
27
+ def self.decode(type, &block)
28
+ @@decoders ||= {}
29
+ @@decoders[type] = block
30
+ end
31
+
32
+ def self.encoders; @@encoders; end
33
+ def self.decoders; @@decoders; end
34
+
35
+ def self.infer_type(field=nil, &block)
36
+ if block_given?
37
+ @@type_inferrer = block
38
+ else
39
+ if @@type_inferrer.nil?
40
+ raise InvalidFieldError, "No type inference configured"
41
+ else
42
+ type = @@type_inferrer.call(field)
43
+ if type.nil?
44
+ raise InvalidFieldError, "Could not infer type of field #{field}"
45
+ end
46
+ type
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize(name, opts)
52
+ @name = name
53
+ if opts.has_key?(:type)
54
+ @type = to_class(opts.fetch(:type))
55
+ else
56
+ @type = self.class.infer_type(self)
57
+ end
58
+ @raw_type = type
59
+ @opts = opts
60
+ end
61
+
62
+ def alias_as(new_name)
63
+ if new_name == name
64
+ self
65
+ else
66
+ self.class.new(new_name, opts)
67
+ end
68
+ end
69
+
70
+ def required?
71
+ !(has_default? || deletable?)
72
+ end
73
+
74
+ def has_default?
75
+ opts.has_key?(:default)
76
+ end
77
+
78
+ def default
79
+ opts.fetch(:default, nil)
80
+ end
81
+
82
+ def mutable?
83
+ opts.fetch(:mutable, true)
84
+ end
85
+
86
+ def deletable?
87
+ opts.fetch(:deletable, false)
88
+ end
89
+
90
+ def previous_aliases
91
+ was = opts.fetch(:was, [])
92
+ if was.respond_to?(:to_a)
93
+ was.to_a
94
+ else
95
+ [ was ]
96
+ end
97
+ end
98
+
99
+ def all_names
100
+ [ name ] + previous_aliases
101
+ end
102
+
103
+ def write(store, value, encode: true)
104
+ if store.has_key?(name)
105
+ raise ImmutableFieldUpdateError unless mutable?
106
+ end
107
+ if encode
108
+ value = self.encode(value)
109
+ end
110
+ if !store.has_key?(name) || value != store[name]
111
+ store[name] = value
112
+ previous_aliases.each { |a| store.delete(a) }
113
+ true
114
+ else
115
+ false
116
+ end
117
+ end
118
+
119
+ def read(store, decode: true)
120
+ present_as = all_names.find { |n| !store.nil? && store.has_key?(n) }
121
+ if store.nil? || present_as.nil?
122
+ if required?
123
+ raise MissingRequiredFieldError,
124
+ "Expected field #{inspect} to exist"
125
+ else
126
+ default if decode
127
+ end
128
+ elsif present_as == name
129
+ raw = store.fetch(name)
130
+ if decode
131
+ decode(raw)
132
+ else
133
+ raw
134
+ end
135
+ else
136
+ alias_as(present_as).read(store)
137
+ end
138
+ end
139
+
140
+ def decode(value)
141
+ decoder.call(self, value) unless value.nil?
142
+ end
143
+
144
+ def encode(value)
145
+ encoder.call(self, value) unless value.nil?
146
+ end
147
+
148
+ def decoder
149
+ @decoder ||= self.class.decoders
150
+ .find(method(:ensure_decoder)) do |decoder_type, _|
151
+ compatible_codec?(decoder_type)
152
+ end.last
153
+ end
154
+
155
+ def encoder
156
+ @encoder ||= self.class.encoders
157
+ .find(method(:ensure_encoder)) do |encoder_type, _|
158
+ compatible_codec?(encoder_type)
159
+ end.last
160
+ end
161
+
162
+ private
163
+
164
+ def compatible_codec?(codec_type)
165
+ if self.type.is_a?(Class) && codec_type.is_a?(Class)
166
+ self.type <= codec_type
167
+ else
168
+ self.type == codec_type
169
+ end
170
+ rescue
171
+ false
172
+ end
173
+
174
+ def ensure_encoder
175
+ raise MissingCodecError,
176
+ "No encoder found for #{inspect}"
177
+ end
178
+
179
+ def ensure_decoder
180
+ raise MissingCodecError,
181
+ "No decoder found for #{inspect}"
182
+ end
183
+
184
+ def to_class(type)
185
+ return type if type.is_a?(Class) || type.is_a?(Symbol)
186
+ type.to_s.split('::').inject(Object) do |moodule, klass|
187
+ moodule.const_get(klass)
188
+ end
189
+ end
190
+ end
191
+
192
+ class Config
193
+ def initialize
194
+ @encoders = {}
195
+ @decoders = {}
196
+ end
197
+
198
+ def infer_type(&block)
199
+ if block_given?
200
+ Field.infer_type(&block)
201
+ else
202
+ raise ArgumentError, "Expected block to infer types with"
203
+ end
204
+ end
205
+
206
+ def encode(type, &block)
207
+ Field.encode(type, &block)
208
+ end
209
+
210
+ def decode(type, &block)
211
+ Field.decode(type, &block)
212
+ end
213
+ end
214
+
215
+ class Pouch
216
+ VALID_FIELD_NAME_REGEXP = %r{\A[a-zA-Z0-9_]+\??\z}
217
+
218
+ def initialize(host, storage_field, default_pouch: Sequel.hstore({}))
219
+ @host = host
220
+ @storage_field = storage_field
221
+ @default_pouch = default_pouch
222
+ @fields = {}
223
+ end
224
+
225
+ def field_definition(name)
226
+ @fields[name]
227
+ end
228
+
229
+ def field(name, opts={})
230
+ unless VALID_FIELD_NAME_REGEXP.match(name)
231
+ raise InvalidFieldError, "Field name must match #{VALID_FIELD_NAME_REGEXP}"
232
+ end
233
+
234
+ field = Field.new(name, opts)
235
+ @fields[name] = field
236
+
237
+ storage_field = @storage_field
238
+ default = @default_pouch
239
+
240
+ @host.class_eval do
241
+ def_dataset_method(:where_pouch) do |pouch_field, expr_hash|
242
+ # TODO: encode the values so we can query properly
243
+ ds = self
244
+ expr_hash.each do |key, value|
245
+ pouch = model.pouch(pouch_field)
246
+ if pouch.nil?
247
+ raise ArgumentError,
248
+ "No pouch defined for #{pouch_field}"
249
+ end
250
+ field = pouch.field_definition(key)
251
+ if field.nil?
252
+ raise ArgumentError,
253
+ "No field #{key} defined for pouch #{pouch_field}"
254
+ end
255
+
256
+ if value.respond_to?(:each)
257
+ value.each_with_index do |v,i|
258
+ encoded_val = field.encode(v)
259
+ if i == 0
260
+ ds = ds.where(Sequel.hstore(pouch_field)
261
+ .contains(Sequel.hstore(key => encoded_val)))
262
+ else
263
+ ds = ds.or(Sequel.hstore(pouch_field)
264
+ .contains(Sequel.hstore(key => encoded_val)))
265
+ end
266
+ end
267
+ elsif value.nil?
268
+ ds = ds.where(Sequel.hstore(pouch_field).has_key?(key.to_s) => false)
269
+ .or(Sequel.hstore(pouch_field)
270
+ .contains(Sequel.hstore(key => nil)))
271
+ else
272
+ ds = ds.where(Sequel.hstore(pouch_field)
273
+ .contains(Sequel.hstore(key => field.encode(value))))
274
+ end
275
+ end
276
+ ds
277
+ end
278
+
279
+ define_method(name) do
280
+ store = self[storage_field]
281
+ field.read(store)
282
+ end
283
+
284
+ define_method("#{name.to_s.sub(/\?\z/, '')}=") do |value|
285
+ store = self[storage_field]
286
+ was_nil = store.nil?
287
+ store = default if was_nil
288
+ changed = field.write(store, value)
289
+ if was_nil
290
+ self[storage_field] = store
291
+ else
292
+ modified! storage_field if changed
293
+ end
294
+ end
295
+
296
+ if field.deletable?
297
+ delete_method = "delete_#{name.to_s.sub(/\?\z/, '')}"
298
+ define_method(delete_method) do
299
+ store = self[storage_field]
300
+ unless store.nil?
301
+ field.all_names.each { |a| store.delete(a) }
302
+ modified! storage_field
303
+ end
304
+ end
305
+
306
+ define_method("#{delete_method}!") do
307
+ self.public_send(delete_method)
308
+ save_changes
309
+ end
310
+ end
311
+
312
+ if opts.has_key?(:raw_field)
313
+ raw_name = opts[:raw_field]
314
+
315
+ define_method(raw_name) do
316
+ store = self[storage_field]
317
+ field.read(store, decode: false)
318
+ end
319
+
320
+ define_method("#{raw_name.to_s.sub(/\?\z/, '')}=") do |value|
321
+ store = self[storage_field]
322
+ was_nil = store.nil?
323
+ store = default if was_nil
324
+ changed = field.write(store, value, encode: false)
325
+ if was_nil
326
+ self[storage_field] = store
327
+ else
328
+ modified! storage_field if changed
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ module ClassMethods
337
+ def pouch(field, &block)
338
+ if block_given?
339
+ pouch = Pouch.new(self, field)
340
+ @pouches ||= {}
341
+ @pouches[field] = pouch
342
+ pouch.instance_eval(&block)
343
+ else
344
+ @pouches[field]
345
+ end
346
+ end
347
+ end
348
+ end
349
+
350
+ AttrPouch.configure do |config|
351
+ config.encode(String) do |field, value|
352
+ value.to_s
353
+ end
354
+ config.decode(String) do |field, value|
355
+ value
356
+ end
357
+
358
+ config.encode(Integer) do |field, value|
359
+ value.to_s
360
+ end
361
+ config.decode(Integer) do |field, value|
362
+ Integer(value)
363
+ end
364
+
365
+ config.encode(Float) do |field, value|
366
+ value.to_s
367
+ end
368
+ config.decode(Float) do |field, value|
369
+ Float(value)
370
+ end
371
+
372
+ config.encode(Time) do |field, value|
373
+ value.strftime('%Y-%m-%d %H:%M:%S.%N')
374
+ end
375
+ config.decode(Time) do |field, value|
376
+ Time.parse(value)
377
+ end
378
+
379
+ config.encode(:bool) do |field, value|
380
+ value.to_s
381
+ end
382
+ config.decode(:bool) do |field, value|
383
+ value == 'true'
384
+ end
385
+
386
+ config.encode(Sequel::Model) do |field, value|
387
+ klass = field.type
388
+ value[klass.primary_key]
389
+ end
390
+ config.decode(Sequel::Model) do |field, value|
391
+ klass = field.type
392
+ klass[value]
393
+ end
394
+
395
+ config.infer_type do |field|
396
+ case field.name
397
+ when /\Anum_|_(?:count|size)\z/
398
+ Integer
399
+ when /_(?:at|by)\z/
400
+ Time
401
+ when /\?\z/
402
+ :bool
403
+ else
404
+ String
405
+ end
406
+ end
407
+ end