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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 58a432e44f2c9f4bdcc8bcf5b505134b1c0c56ab
|
4
|
+
data.tar.gz: 91d784b4dfc7b673f3cb46d07fb94152dea35b09
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 93891ecdbe1a8eb5a31ee34a956472352abbf53ce76a006dbe5e58cffe8087af64e816e425366d5b51609d472d0dd28f9333b8ac1862b2b6f8c1f89b62f26a86
|
7
|
+
data.tar.gz: 973525f442cfd29f521046b3fe1b6a4a5efbf4273134c752b18dab565ba1a55f0eb0ec736cc6ad708941cabdbfa5449894471246f1500ac989a29f5e177183d1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
attr_pouch (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.2.5)
|
10
|
+
pg (0.18.3)
|
11
|
+
rspec (3.1.0)
|
12
|
+
rspec-core (~> 3.1.0)
|
13
|
+
rspec-expectations (~> 3.1.0)
|
14
|
+
rspec-mocks (~> 3.1.0)
|
15
|
+
rspec-core (3.1.7)
|
16
|
+
rspec-support (~> 3.1.0)
|
17
|
+
rspec-expectations (3.1.2)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.1.0)
|
20
|
+
rspec-mocks (3.1.3)
|
21
|
+
rspec-support (~> 3.1.0)
|
22
|
+
rspec-support (3.1.2)
|
23
|
+
sequel (4.13.0)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
attr_pouch!
|
30
|
+
pg (~> 0.18.3)
|
31
|
+
rspec (~> 3.0)
|
32
|
+
sequel (~> 4.13)
|
33
|
+
|
34
|
+
BUNDLED WITH
|
35
|
+
1.10.6
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 AttrVault Contributors
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,304 @@
|
|
1
|
+
[](https://travis-ci.org/uhoh-itsmaciek/attr_pouch)
|
2
|
+
|
3
|
+
# AttrPouch
|
4
|
+
|
5
|
+
Schema-less attribute storage plugin for
|
6
|
+
[Sequel](https://github.com/jeremyevans/sequel.git).
|
7
|
+
|
8
|
+
|
9
|
+
### Philosophy
|
10
|
+
|
11
|
+
Database schemas are great: they enforce data integrity and help
|
12
|
+
ensure that your data always looks like you expect it to. Furthermore,
|
13
|
+
indexing can dramatically speed up many types of complex queries with
|
14
|
+
a schema.
|
15
|
+
|
16
|
+
However, schemas can also get in the way: dozens of columns get to be
|
17
|
+
unwieldy with most database tools, and migrations to add and drop
|
18
|
+
columns can be difficult on large data sets.
|
19
|
+
|
20
|
+
Consequently, schema-less storage can be an excellent complement to a
|
21
|
+
rigid database schema: you can define a schema for the well-understood
|
22
|
+
parts of your data model, but augment it with schema-less storage for
|
23
|
+
the parts that are in flux or just awkward to fit into the relational
|
24
|
+
model.
|
25
|
+
|
26
|
+
|
27
|
+
### Usage
|
28
|
+
|
29
|
+
AttrPouch allows you to designate a "pouch" database `hstore` field
|
30
|
+
that provides schema-less storage for any `Sequel::Model`
|
31
|
+
object. Within this pouch, you define additional fields that behave
|
32
|
+
largely like standard Sequel fields (i.e., as if they were backed by
|
33
|
+
their own database columns), but are all in fact stored in the pouch:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class User < Sequel::Model
|
37
|
+
pouch(:preferences) do
|
38
|
+
field :theme
|
39
|
+
field :autoplay_videos?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
bob = User.create(name: 'bob')
|
44
|
+
bob.theme = 'gothic'
|
45
|
+
bob.autoplay_videos = false
|
46
|
+
bob.save_changes
|
47
|
+
bob.update(theme: 'ponies!', autoplay_videos: false)
|
48
|
+
```
|
49
|
+
|
50
|
+
Note that if a field name ends in a question mark, its setter is
|
51
|
+
defined with the trailing question mark stripped.
|
52
|
+
|
53
|
+
Because there is no schema defined for these fields, changes to the
|
54
|
+
pouch definition do not require a database migration. You can add
|
55
|
+
and remove fields with just code changes.
|
56
|
+
|
57
|
+
#### Defaults
|
58
|
+
|
59
|
+
Fields are required by default; attempting to read a field that has
|
60
|
+
not been set will raise `AttrPouch::MissingRequiredFieldError`. They
|
61
|
+
can be marked as optional by providing a default:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class User < Sequel::Model
|
65
|
+
pouch(:preferences) do
|
66
|
+
field :favorite_color, default: 'puce'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
ursula = User.create(name: 'ursula')
|
71
|
+
ursula.favorite_color # 'puce'
|
72
|
+
```
|
73
|
+
|
74
|
+
#### Types
|
75
|
+
|
76
|
+
AttrPouch supports per-type serializers and deserializers to allow any
|
77
|
+
Ruby objects to be handled transparently as field values (as long as
|
78
|
+
they can be serialized and deserialized in some fashion).
|
79
|
+
|
80
|
+
AttrPouch comes with a number of built-in encoders and decoders for
|
81
|
+
some common data types, and these are specified either directly using
|
82
|
+
Ruby classes or via symbols representing an encoding mechanism. The
|
83
|
+
simple built-in types are `String`, `Integer`, `Float`, `Time`, and
|
84
|
+
`:bool` (since Ruby has no single class representing a boolean type,
|
85
|
+
`attr_pouch` uses a symbol to stand in as a "logical" type here).
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class User < Sequel::Model
|
89
|
+
pouch(:preferences) do
|
90
|
+
field :favorite_color, type: String
|
91
|
+
field :lucky_number, type: Integer
|
92
|
+
field :gluten_free?, type: :bool
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
You can override these built-ins or register entirely new types:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
AttrPouch.configure do |config|
|
101
|
+
config.write(:obfuscated_string) do |field, value|
|
102
|
+
value.reverse
|
103
|
+
end
|
104
|
+
config.read(:obfuscated_string) do |field, value|
|
105
|
+
value.reverse
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
Note that your encoder and decoder do have access to the field object,
|
111
|
+
which includes name, type, and any options you've configured in the
|
112
|
+
field definition. Option names are not checked by `attr_vault`, so
|
113
|
+
custom decoder or encoder options are possible.
|
114
|
+
|
115
|
+
When an encoder or decoder is specified via symbol, it will only work
|
116
|
+
for fields whose type is declared to be exactly that symbol. When
|
117
|
+
specified via class, it will also be used to encode and decode any
|
118
|
+
fields whose declared type is a subclass of the encoder/decoder class.
|
119
|
+
|
120
|
+
This can be illustrated via the last built-in codec, for
|
121
|
+
`Sequel::Model` objects:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class User < Sequel::Model
|
125
|
+
pouch(:preferences) do
|
126
|
+
field :bff, User
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
alonzo = User.create(name: 'alonzo')
|
131
|
+
alonzo.update(bff: User[name: 'ursula'])
|
132
|
+
```
|
133
|
+
|
134
|
+
Even though the built-in encoder is specified for just `Sequel::Model`
|
135
|
+
(no custom encoder was specified for `User` here), it can handle the
|
136
|
+
`bff` field above with no additional configuration because `User`
|
137
|
+
descends from `Sequel::Model`.
|
138
|
+
|
139
|
+
If the field type is not specified, it is inferred from the field
|
140
|
+
definition. The default mechanism only considers the field name and
|
141
|
+
infers types as follows:
|
142
|
+
|
143
|
+
* `Integer`: name starts with `num_` or ends in `_size` or `_count`
|
144
|
+
* `Time`: name ends with `_at` or `_by`
|
145
|
+
* `:bool`: name ends with `?`
|
146
|
+
* `String`: anything else
|
147
|
+
|
148
|
+
If this is not suitable, you can register your own type inference
|
149
|
+
mechanism instead:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
AttrPouch.configure do |config|
|
153
|
+
config.infer_type { |field| String }
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
The above just considers every field without a declared type to be a
|
158
|
+
`String`.
|
159
|
+
|
160
|
+
#### Deletable fields
|
161
|
+
|
162
|
+
Fields can be marked `deletable`, which will generate two deleter
|
163
|
+
methods for them:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class User < Sequel::Model
|
167
|
+
pouch(:preferences) do
|
168
|
+
field :proxy_address, deletable: true
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
karen = User.create(name: 'karen')
|
173
|
+
karen.update(proxy_address: '10.11.12.13:8001')
|
174
|
+
karen.delete_proxy_address
|
175
|
+
```
|
176
|
+
|
177
|
+
Deletable fields are automatically given a default of `nil` if no
|
178
|
+
other default is present; reading a deletable field does not raise an
|
179
|
+
error.
|
180
|
+
|
181
|
+
N.B.: You can still delete fields without this flag by deleting their
|
182
|
+
corresponding keys directly from the underlying storage column,
|
183
|
+
flagging the field as modified (to ensure Sequel knows the field has
|
184
|
+
changed, since the object reference it holds does *not* change when
|
185
|
+
the object itself is modified). You can also set the value to `nil`
|
186
|
+
explicitly which, while not semantically identical, can be sufficient.
|
187
|
+
|
188
|
+
#### Immutable fields
|
189
|
+
|
190
|
+
Fields are mutable by default but can be flagged immutable to reject
|
191
|
+
updates once an initial value has been set:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
class User < Sequel::Model
|
195
|
+
pouch(:preferences) do
|
196
|
+
field :lucky?, mutable: false
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
abbas = User.create(name: 'abbas', lucky: true)
|
201
|
+
abbas.lucky? # true
|
202
|
+
abbas.lucky = false # raises ImmutableFieldUpdateError
|
203
|
+
```
|
204
|
+
|
205
|
+
#### Renaming fields
|
206
|
+
|
207
|
+
Fields can be renamed by providing an a previous name or array of
|
208
|
+
previous names under the `was` option.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class User < Sequel::Model
|
212
|
+
pouch(:preferences) do
|
213
|
+
field :tls?, was: :ssl?
|
214
|
+
field :instabul?, was: %i[constantinople? byzantion?]
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
nils = User[name: 'nils'] # in db we have `{ ssl?: true, byzantion?: true }`
|
219
|
+
nils.tls? # true
|
220
|
+
nils.consantinople? # true
|
221
|
+
```
|
222
|
+
|
223
|
+
Note that no direct accessors are defined for the old names, and if
|
224
|
+
the value is updated, it is written under the new name and any old
|
225
|
+
values in the pouch are deleted:
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
nils.tls? # true
|
229
|
+
nils.instanbul? # true
|
230
|
+
nils.save_changes # now in db as `{ tls?: true, instanbul?: true }`
|
231
|
+
```
|
232
|
+
|
233
|
+
#### Raw value access
|
234
|
+
|
235
|
+
Any field can be accessed directly, bypassing the encoder and decoder,
|
236
|
+
by specifying the `raw_field` option to provide the name of the setter
|
237
|
+
and getter that will directly manipulate the underlying value.
|
238
|
+
Required fields are still required when read via `raw_field`, and
|
239
|
+
immutable fields are still immutable, but if a `default` is set, the
|
240
|
+
raw value will be `nil`, rather than the default itself, to allow the
|
241
|
+
user to distinguish between a field value equal to the default and an
|
242
|
+
absent field value deferring to the default:
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
class User < Sequel::Model
|
246
|
+
pouch(:preferences) do
|
247
|
+
field :bff, User, raw_field: :bff_id
|
248
|
+
field :arch_nemesis, raw_field: :nemesis_id, default: User[name: 'donald']
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
alonzo = User.create(name: 'alonzo')
|
253
|
+
alonzo.update(bff: User[name: 'ursula'])
|
254
|
+
alonzo.bff_id # Ursula's user id
|
255
|
+
alonzo.arch_nemesis # the User object representing the 'donald' record
|
256
|
+
alonzo.nemesis_id # nil
|
257
|
+
```
|
258
|
+
|
259
|
+
Raw fields also obey the `was` option for renames, as above. If the
|
260
|
+
raw field value is updated, values present under any of the `was` keys
|
261
|
+
will be deleted.
|
262
|
+
|
263
|
+
|
264
|
+
### Schema
|
265
|
+
|
266
|
+
AttrPouch requires a new storage field for each pouch added to a
|
267
|
+
model. It is currently designed for and tested with `hstore`. Consider
|
268
|
+
using a single pouch per model class unless you clearly need several
|
269
|
+
distinct pouches.
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
Sequel.migration do
|
273
|
+
change do
|
274
|
+
alter_table(:users) do
|
275
|
+
add_column :preferences, :hstore
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
```
|
280
|
+
|
281
|
+
### Contributing
|
282
|
+
|
283
|
+
Patches are warmly welcome.
|
284
|
+
|
285
|
+
To run tests locally, you'll need a `DATABASE_URL` environment
|
286
|
+
variable pointing to a database AttrPouch can use for testing. E.g.,
|
287
|
+
|
288
|
+
```console
|
289
|
+
$ createdb attr_pouch_test
|
290
|
+
$ DATABASE_URL=postgres:///attr_pouch_test bundle exec rspec
|
291
|
+
```
|
292
|
+
|
293
|
+
Please follow the project's general coding style and open issues for
|
294
|
+
any significant behavior or API changes.
|
295
|
+
|
296
|
+
A pull request is understood to mean you are offering your code to the
|
297
|
+
project under the MIT License.
|
298
|
+
|
299
|
+
|
300
|
+
### License
|
301
|
+
|
302
|
+
Copyright (c) 2015 AttrPouch Contributors
|
303
|
+
|
304
|
+
MIT License. See LICENSE for full text.
|
data/TODO
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# TODO thoughts:
|
2
|
+
- docs
|
3
|
+
- support for storage in JSON / plain text / arbitrary backing format
|
4
|
+
- opt: eager_default on create for fields
|
5
|
+
- opt: run tracking
|
6
|
+
- more efficient updates via merging with structure in-database field-by-field
|
7
|
+
- whole-store encryption and/or interop with attr_vault
|
8
|
+
- per-field encryption
|
data/attr_pouch.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path('../lib/attr_pouch/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Maciek Sakrejda"]
|
5
|
+
gem.email = ["m.sakrejda@gmail.com"]
|
6
|
+
gem.description = %q{Schema-less attribute storage}
|
7
|
+
gem.summary = %q{Sequel plugin for schema-less attribute storage}
|
8
|
+
gem.homepage = "https://github.com/uhoh-itsmaciek/attr_pouch"
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split($\)
|
11
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "attr_pouch"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = AttrPouch::VERSION
|
16
|
+
gem.license = "MIT"
|
17
|
+
|
18
|
+
gem.add_development_dependency "rspec", '~> 3.0'
|
19
|
+
gem.add_development_dependency "pg", '~> 0.18.3'
|
20
|
+
gem.add_development_dependency "sequel", '~> 4.13'
|
21
|
+
end
|