attr_pouch 0.0.1

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
+ 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
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ cache: bundler
3
+ before_script: createdb attr_pouch
4
+ sudo: false
5
+ script: DATABASE_URL="postgres:///attr_pouch" bundle exec rspec
6
+ addons:
7
+ postgresql: "9.4"
8
+ env:
9
+ rvm:
10
+ - 2.2.3
11
+ notifications:
12
+ email: false
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ ruby '2.2.3'
4
+
5
+ # Specify your gem's dependencies in attr_pouch.gemspec
6
+ gemspec
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
+ [![Build Status](https://travis-ci.org/uhoh-itsmaciek/attr_pouch.svg)](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
@@ -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
@@ -0,0 +1,8 @@
1
+ module AttrPouch
2
+ # Base class for AttrPouch errors
3
+ class Error < StandardError; end
4
+ class MissingCodecError < Error; end
5
+ class InvalidFieldError < Error; end
6
+ class MissingRequiredFieldError < Error; end
7
+ class ImmutableFieldUpdateError < Error; end
8
+ end
@@ -0,0 +1,3 @@
1
+ module AttrPouch
2
+ VERSION = "0.0.1"
3
+ end