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