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.
- checksums.yaml +7 -0
- data/.travis.yml +12 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +35 -0
- data/LICENSE +22 -0
- data/README.md +304 -0
- data/TODO +8 -0
- data/attr_pouch.gemspec +21 -0
- data/lib/attr_pouch/errors.rb +8 -0
- data/lib/attr_pouch/version.rb +3 -0
- data/lib/attr_pouch.rb +407 -0
- data/spec/attr_pouch_spec.rb +482 -0
- data/spec/spec_helper.rb +44 -0
- metadata +101 -0
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
|