sack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: efb08ab6965016ac346ea8e63141972a58ef0228
4
+ data.tar.gz: 37f648bcc771fc4ec30a09f2fb04b370a88b0a6d
5
+ SHA512:
6
+ metadata.gz: c8e4f163b354e31e8fd9d2cf537a0e9211ed545caf64887f4fd68dc24b79e3859d9226bb80dec0862ffdf0aed07d718a6c52b8fd18ecdc6f2551294abed7de00
7
+ data.tar.gz: a2279b88ad6eb8083f53ce9ced7abe3c17ea4d113716daae0ff6966459f9975b1bee78eba99f03be0c5800c58cb2fa7e8336d1bd688825a196e58b51ed51d0ff
@@ -0,0 +1,3 @@
1
+ .idea
2
+ Gemfile.lock
3
+ pkg
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Paul Duncan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,291 @@
1
+ # Sack
2
+
3
+ Minimalistic Database Layer based on SQLite3
4
+
5
+ ## Presentation
6
+
7
+ This library provides a lightweight an easy-to-use database interface.
8
+
9
+ ## Installation
10
+
11
+ ### Gemfile
12
+ ```ruby
13
+ gem 'sack'
14
+ ```
15
+
16
+ ### Terminal
17
+ ```bash
18
+ gem install -V sack
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Sack may be used at different levels to achieve various levels of complexity.
24
+ Let's explore the major uses.
25
+
26
+ ### Most basic
27
+
28
+ In its simplest form, Sack provides a direct CRUD to any database, as long as you provide it with *schema* information, a connector and a connection string.
29
+
30
+ ```ruby
31
+ # Define the Schema
32
+ SCHEMA = {
33
+
34
+ # 'user' Table
35
+ user: {
36
+
37
+ # ID - Auto-Increment (:ai) Primary-Key (:pk) Integer (:int)
38
+ id: [:int, :pk, :ai],
39
+
40
+ # Name - String
41
+ name: [:str]
42
+ }
43
+ }
44
+
45
+ # Create a Database object using the SQLite3 Connector
46
+ db = Sack::Database.new Sack::Connectors::SQLite3Connector, 'example.db', SCHEMA
47
+
48
+ # Create
49
+ db.create :user, name: 'foobar'
50
+
51
+ # Fetch by Field
52
+ u = db.fetch_by(:user, :name, 'foobar').first
53
+
54
+ # Fetch by ID
55
+ u = db.fetch(:user, u[:id]).first
56
+
57
+ # Fetch All
58
+ users = db.fetch_all :user
59
+
60
+ # Find (fetch by ID - first result)
61
+ u = db.find :user, u[:id]
62
+
63
+ # Count
64
+ user_count = db.count :user
65
+
66
+ # Update
67
+ db.update :user, u[:id], name: 'John Doe'
68
+
69
+ # Save (Combined Create / Update)
70
+ u[:name] = 'Jane Doe'
71
+ db.save :user, u
72
+
73
+ # Delete
74
+ db.delete :user, u[:id]
75
+ ```
76
+
77
+ ### Model-based Schema
78
+
79
+ Defining a Sack Schema by hand can be painful.
80
+ Also, the concept of *relationships* between entities is not available.
81
+ Finally, all validation has to be performed by the application.
82
+
83
+ Another option is to use Sack's *Model* abstraction, providing schema generation, validation and relationships.
84
+
85
+ Each model is defined using a module which includes *Sack::Database::Model*, placed inside a _root module_.
86
+
87
+ The schema for the whole entity model can then be derived from this root module using *Sack::Schema.from_module*.
88
+
89
+ The module presented above can be re-written as such:
90
+
91
+ ```ruby
92
+ # Model Root
93
+ module DataModel
94
+
95
+ # Users
96
+ module User
97
+ include Sack::Database::Model
98
+
99
+ # Fields
100
+ field id: [:int, :pk, :ai]
101
+ field name: [:str]
102
+ end
103
+ end
104
+
105
+ # Create a Database object using the SQLite3 Connector and a Schema derived from our DataModel
106
+ db = Sack::Database.new Sack::Connectors::SQLite3Connector, 'example.db', Sack::Database::Schema.from_module(DataModel)
107
+ ```
108
+
109
+ ### Validation
110
+
111
+ The Model abstraction provided by Sack offers validation, which can be specified directly on the fields defined in each model.
112
+ Let's add some validation on our user model:
113
+
114
+ ```ruby
115
+ # Users
116
+ module User
117
+ include Sack::Database::Model
118
+
119
+ # Fields
120
+ field id: [:int, :pk, :ai]
121
+ field name: [:str],
122
+ required: true,
123
+ unique: true,
124
+ min_length: 3,
125
+ max_length: 24,
126
+ regex: /^[a-zA-Z][a-zA-Z0-9_-]+$/,
127
+ validate: :no_prefix
128
+
129
+ # Custom Validation Method - No Prefix
130
+ # @param [Database] db Database instance
131
+ # @param [Hash] data The entity being validated
132
+ # @param [Symbol] name The field being validated
133
+ # @param [Object] val The field's value
134
+ # @param [Hash] rules The field's validation rules hash (possibly containing any custom validation params)
135
+ # @param [Array] errors The list of errors for the entity being validated (if no error is added but the method returns false, a generic error message will be added by Sack)
136
+ # @return [Object] true / false (true = valid)
137
+ def self.no_prefix db, data, name, val, rules, errors
138
+ valid = !(/^mr|ms|dr|mrs /i =~ val)
139
+ errors << "Field [#{name}] should not include prefixes such as mr. ms. dr. etc..."
140
+ valid
141
+ end
142
+ end
143
+ ```
144
+
145
+ *Note*: The *unique* validator can be either set to _true_ to validate global unicity, or it can be set to an array of other fields to scope unicity.
146
+
147
+ Validity can then be checked by the application at any time using the *is_valid?* method injected into every model.
148
+
149
+ ```ruby
150
+ User.is_valid? db, name: 'foobar'
151
+ # => true
152
+
153
+ User.is_valid? db, name: nil
154
+ # => false
155
+
156
+ errors = []
157
+ User.is_valid?(db, { name: '000' }, errors)
158
+ # => false
159
+
160
+ errors.each { |e| puts e }
161
+ # Field [name] doesn't match allowed pattern (/^[a-zA-Z][a-zA-Z0-9_-]+$/)
162
+ ```
163
+
164
+ ### Relationships
165
+
166
+ Using the Model abstraction, we can define relationships between our entities.
167
+ Let's add an _article_ model and define a one-to-many relationship from users to articles.
168
+
169
+ ```ruby
170
+ # User Model
171
+ module User
172
+ include Sack::Database::Model
173
+
174
+ # Fields
175
+ field id: [:int, :pk, :ai]
176
+ field name: [:str]
177
+
178
+ # Relationships
179
+ has_many articles: :article, fk: :author
180
+ end
181
+
182
+ # Article Model
183
+ module Article
184
+ include Sack::Database::Model
185
+
186
+ # Fields
187
+ field id: [:int, :pk, :ai]
188
+ field author: [:int]
189
+ field title: [:str]
190
+ field body: [:txt]
191
+
192
+ # Relationships
193
+ belongs_to author: :user
194
+ end
195
+ ```
196
+
197
+ This then allows us to use the relationships to simplify our lives a little.
198
+
199
+ ```ruby
200
+ # Fetching association with direct parameters
201
+ articles = User.articles db, id: 0
202
+
203
+ # Fetching association with entity
204
+ u = User.find db, id
205
+ articles = User.articles db, u
206
+
207
+ a = articles.first
208
+ u = Article.author db, a
209
+ ```
210
+
211
+ This is nice, but we can do better.
212
+ Actually, although every entity handled by Sack is really just a *Hash* (to keep things simple), the models also inject a small router into any entity it loads.
213
+ This allows to access an entity's associations directly, and through multiple levels.
214
+
215
+ ```ruby
216
+ u = User.find db, id
217
+
218
+ articles = u.articles(db)
219
+ u = articles.first.author(db)
220
+
221
+ # we can recurse as many levels as we want
222
+ articles = u.articles(db).first.author(db).articles(db).first.author(db).articles(db)
223
+ ```
224
+
225
+ #### Belongs To
226
+
227
+ The belongs-to relationship injects a method in the model, with the name of the association. This method allows fetching the associated entity.
228
+ The name of the association _MUST MATCH_ the name of the field storing the key to the associated entity.
229
+
230
+ If given ONLY a name, belongs_to will auto-guess the name of the associated model (CamelCased version of the association's name).
231
+ To alter this behavior (as in the example above - the association is called 'author' but the target model is actually 'user'), simply provide a model name:
232
+
233
+ ```ruby
234
+ module Foo
235
+ include Sack::Database::Model
236
+ field id: [:int, :pk, :ai]
237
+ end
238
+
239
+ module Bar
240
+ include Sack::Database::Model
241
+ field id: [:int, :pk, :ai]
242
+
243
+ # A 'Bar' belongs to two 'Foo's
244
+ field foo: [:int]
245
+ field other_foo: [:int]
246
+
247
+ # Target model is Foo - no need to specify it
248
+ belongs_to :foo
249
+
250
+ # Explicitly use Foo as the target model
251
+ belongs_to other_foo: :foo
252
+ end
253
+ ```
254
+
255
+ #### Has Many
256
+
257
+ The has-many relationship injects a method in the model, with the name of the association. This method allows fetching the associated entities.
258
+ The name of the association can be anything. The target model NEEDS to be specified.
259
+
260
+ Unless explicitly specified, Sack will use the name of the current model to determine the foreign key (the field within the target model which holds the ID of the current model).
261
+
262
+ ```ruby
263
+ module Foo
264
+ include Sack::Database::Model
265
+ field id: [:int, :pk, :ai]
266
+
267
+ # Foreign Key in Bar is 'foo'
268
+ has_many bars: :bar
269
+
270
+ # Foreign Key in Bork is 'parent'
271
+ has_many borks: :bork, fk: :parent
272
+ end
273
+
274
+ module Bar
275
+ include Sack::Database::Model
276
+ field id: [:int, :pk, :ai]
277
+ field foo: [:int]
278
+ belongs_to :foo
279
+ end
280
+
281
+ module Bork
282
+ include Sack::Database::Model
283
+ field id: [:int, :pk, :ai]
284
+ field parent: [:int]
285
+ belongs_to parent: :foo
286
+ end
287
+ ```
288
+
289
+ ## License
290
+
291
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+ require 'bundler/gem_tasks'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/test*.rb'
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ # Sack
2
+ # by Eresse <eresse@eresse.net>
3
+
4
+ # Internal Includes
5
+ require 'sack/version'
6
+ require 'sack/database'
7
+ require 'sack/database/model'
8
+ require 'sack/database/schema'
9
+ require 'sack/connectors'
10
+
11
+ # Sack Module:
12
+ # Root Module for Sack.
13
+ module Sack
14
+ end
@@ -0,0 +1,14 @@
1
+ # Sack
2
+ # by Eresse <eresse@eresse.net>
3
+
4
+ # Connectors
5
+ require 'sack/connectors/sqlite3'
6
+
7
+ # Sack Module
8
+ module Sack
9
+
10
+ # Connectors Module:
11
+ # Provides the available backend connectors for Sack database.
12
+ module Connectors
13
+ end
14
+ end
@@ -0,0 +1,46 @@
1
+ # Sack
2
+ # by Eresse <eresse@eresse.net>
3
+
4
+ # External Includes
5
+ require 'sqlite3'
6
+
7
+ # Internal Includes
8
+ require 'sack/database/sanitizer'
9
+
10
+ # Sack Module
11
+ module Sack
12
+
13
+ # Connectors Module
14
+ module Connectors
15
+
16
+ # SQLite3 Connector Module:
17
+ # Provides SQLite3 connectivity for Sack Database.
18
+ module SQLite3Connector
19
+
20
+ # Open:
21
+ # Opens a connection to an SQLite3 database.
22
+ # @param [String] conn_string The connection string (path to a db file)
23
+ # @return [Object] Database connection
24
+ def self.open conn_string
25
+ SQLite3::Database.open conn_string
26
+ end
27
+
28
+ # Close:
29
+ # Closes a previously-opened database connection.
30
+ # @param [Object] dbc Database connection
31
+ def self.close dbc
32
+ dbc.close
33
+ end
34
+
35
+ # Execute
36
+ # Executes an SQL statement with parameters
37
+ # @param [Object] dbc Database connection
38
+ # @param [String] q Statement
39
+ # @param [Array] params Statement parameters
40
+ # @return [Array] Statement results
41
+ def self.exec dbc, q
42
+ dbc.exec q
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,96 @@
1
+ # Sack
2
+ # by Eresse <eresse@eresse.net>
3
+
4
+ # External Includes
5
+ require 'thread'
6
+ require 'sqlite3'
7
+
8
+ # Internal Includes
9
+ require 'sack/database/data'
10
+
11
+ # Sack Module
12
+ module Sack
13
+
14
+ # Database Class:
15
+ # Main Database Access Class - provides CRUD methods (and a little more) against a given Schema on a provided Database Connector and Connection String.
16
+ class Database
17
+
18
+ # Actions:
19
+ # Allowed data methods
20
+ ACTIONS = [
21
+ :create_table,
22
+ :count,
23
+ :find,
24
+ :fetch,
25
+ :fetch_by,
26
+ :fetch_all,
27
+ :create,
28
+ :update,
29
+ :save,
30
+ :delete
31
+ ]
32
+
33
+ # Construct:
34
+ # Builds a *Database* on top of a backend _connector_ and _connstring_, set to operate on _schema_.
35
+ # @param [Object] connector Generic database backend connector
36
+ # @param [String] connstring Connector-specific connection string
37
+ # @param [Hash] schema Schema definition - see README
38
+ def initialize connector, connstring, schema
39
+
40
+ # Set Connector & Connstring
41
+ @connector = connector
42
+ @connstring = connstring
43
+
44
+ # Set Schema
45
+ @schema = schema
46
+
47
+ # Create Lock
48
+ @lock = Mutex.new
49
+
50
+ # Verify Schema
51
+ verify_schema
52
+ end
53
+
54
+ # Open:
55
+ # Opens a session on the database for yielding.
56
+ def open
57
+
58
+ # Lock DB
59
+ @lock.synchronize do
60
+
61
+ # Open Database
62
+ db = @connector.open @connstring
63
+
64
+ # Yield Block
65
+ yield Data.new(db, @schema) if block_given?
66
+
67
+ # Close Database
68
+ db.close
69
+ end
70
+ end
71
+
72
+ # Verify Schema:
73
+ # Verifies the supplied schema against the actual database & re-creates it if necessary.
74
+ def verify_schema
75
+ open { |data| rebuild_schema data unless @schema.keys.inject(true) { |a, table| a && (data.exec "select count(*) from #{table};" rescue nil) } }
76
+ end
77
+
78
+ # Rebuild Schema:
79
+ # Re-creates the database according to the supplied schema.
80
+ # @param [Data] data Data access interface
81
+ def rebuild_schema data
82
+ @schema.each { |table, fields| data.create_table table, fields }
83
+ end
84
+
85
+ # Method Missing:
86
+ # Catches and routes database actions through data access interface.
87
+ def method_missing name, *args
88
+
89
+ # Check Action
90
+ raise "Unknown action [#{name}]" unless ACTIONS.include? name
91
+
92
+ # Open Database { Perform Action }
93
+ open { |db| return db.send name, *args }
94
+ end
95
+ end
96
+ end