sack 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +21 -0
- data/README.md +291 -0
- data/Rakefile +9 -0
- data/lib/sack.rb +14 -0
- data/lib/sack/connectors.rb +14 -0
- data/lib/sack/connectors/sqlite3.rb +46 -0
- data/lib/sack/database.rb +96 -0
- data/lib/sack/database/data.rb +141 -0
- data/lib/sack/database/ftypes.rb +31 -0
- data/lib/sack/database/generator.rb +54 -0
- data/lib/sack/database/model.rb +108 -0
- data/lib/sack/database/model/data.rb +66 -0
- data/lib/sack/database/model/relationships.rb +35 -0
- data/lib/sack/database/model/relationships/belongs_to.rb +62 -0
- data/lib/sack/database/model/relationships/has_many.rb +56 -0
- data/lib/sack/database/model/validation.rb +138 -0
- data/lib/sack/database/sanitizer.rb +78 -0
- data/lib/sack/database/schema.rb +32 -0
- data/lib/sack/database/statement.rb +27 -0
- data/lib/sack/version.rb +9 -0
- data/sack.gemspec +25 -0
- metadata +137 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'aromat'
|
6
|
+
|
7
|
+
# Internal Includes
|
8
|
+
require 'sack/database/ftypes'
|
9
|
+
require 'sack/database/statement'
|
10
|
+
require 'sack/database/generator'
|
11
|
+
require 'sack/database/sanitizer'
|
12
|
+
|
13
|
+
# Sack Module
|
14
|
+
module Sack
|
15
|
+
|
16
|
+
# Database Class
|
17
|
+
class Database
|
18
|
+
|
19
|
+
# Data Class:
|
20
|
+
# Internal Class presented as Data Access Layer when yielding in Database.open.
|
21
|
+
class Data
|
22
|
+
|
23
|
+
# Construct:
|
24
|
+
# Builds a *Data* object around a _db_ instance, set to operate on _schema_.
|
25
|
+
# @param [SQLite3::Database] db Database instance obtained by opening an SQLite3 file
|
26
|
+
# @param [Hash] schema Schema definition - see README
|
27
|
+
def initialize db, schema
|
28
|
+
@db = db
|
29
|
+
@schema = schema
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create Table:
|
33
|
+
# Creates a table _name_ defined by the _fields_ hash.
|
34
|
+
# @param [Symbol] name Table name
|
35
|
+
# @param [Hash] fields A hash mapping of field names to type definition arrays (from _FTYPES_)
|
36
|
+
def create_table name, fields
|
37
|
+
fq = fields.collect { |fname, ftype| "#{Sanitizer.field_name fname} #{ftype.respond_to?(:each) ? ftype.collect { |e| FTYPES[Sanitizer.ftype e] }.join(' ') : FTYPES[Sanitizer.ftype ftype]}" }.join ', '
|
38
|
+
@db.execute "create table #{Sanitizer.table @schema, name} (#{fq});"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Count:
|
42
|
+
# Counts the number of rows for a given table.
|
43
|
+
# @param [Symbol] table Table name
|
44
|
+
# @return [Fixnum] The number of rows present in the given _table_
|
45
|
+
def count table
|
46
|
+
@db.execute("select count(*) from #{Sanitizer.table @schema, table};")[0][0]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Find:
|
50
|
+
# Fetches the first matching row from a given _table_ by _id_.
|
51
|
+
def find table, id
|
52
|
+
fetch(table, id).try :first
|
53
|
+
end
|
54
|
+
|
55
|
+
# Fetch:
|
56
|
+
# Fetches rows from a given _table_ by _id_.
|
57
|
+
# @param [Symbol] table Table name
|
58
|
+
# @param [Object] id ID on which to filter
|
59
|
+
# @return [Array] An array of Hashes, each representing one row
|
60
|
+
def fetch table, id
|
61
|
+
hash_res(table.to_sym, @db.execute(Statement.prep("select * from #{Sanitizer.table @schema, table} where id = ?;", [id])))
|
62
|
+
end
|
63
|
+
|
64
|
+
# Fetch By Field:
|
65
|
+
# Fetches rows from a given _table_ where _field_ matches _val_.
|
66
|
+
# @param [Symbol] table Table name
|
67
|
+
# @param [Symbol] field Field name
|
68
|
+
# @param [Object] val Field value
|
69
|
+
# @return [Array] An array of Hashes, each representing one row
|
70
|
+
def fetch_by table, field, val
|
71
|
+
hash_res(table.to_sym, @db.execute(Statement.prep("select * from #{Sanitizer.table @schema, table} where #{Sanitizer.field @schema, table, field} = ?;", [val])))
|
72
|
+
end
|
73
|
+
|
74
|
+
# Fetch All:
|
75
|
+
# Fetches all rows from a given _table_.
|
76
|
+
# @param [Symbol] table Table name
|
77
|
+
# @return [Array] An array of Hashes, each representing one row
|
78
|
+
def fetch_all table
|
79
|
+
hash_res(table.to_sym, @db.execute("select * from #{Sanitizer.table @schema, table};"))
|
80
|
+
end
|
81
|
+
|
82
|
+
# Create:
|
83
|
+
# Inserts _fields_ as a row into a given _table_.
|
84
|
+
# @param [Symbol] table Table name
|
85
|
+
# @param [Hash] fields Fields to be inserted
|
86
|
+
def create table, fields
|
87
|
+
@db.execute Statement.prep("insert into #{Sanitizer.table @schema, table} (#{Generator.fields @schema, table, fields}) values (#{Generator.marks fields});", Generator.values(fields.values))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Update:
|
91
|
+
# Updates _fields_ in rows identified by _id_ in a given _table_.
|
92
|
+
# @param [Symbol] table Table name
|
93
|
+
# @param [Object] id ID on which to filter
|
94
|
+
# @param [Hash] fields Fields to be updated
|
95
|
+
def update table, id, fields
|
96
|
+
@db.execute Statement.prep("update #{Sanitizer.table @schema, table} set #{Generator.update_marks @schema, table, fields} where id = ?;", [Generator.values(fields.values), id].flatten)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Save:
|
100
|
+
# Creates or updates _fields_ in a given _table_, depending on the presence of an _id_.
|
101
|
+
# @param [Symbol] table Table name
|
102
|
+
# @param [Hash] fields Fields to be inserted / updated
|
103
|
+
def save table, fields
|
104
|
+
if fields[:id]
|
105
|
+
update table, fields.clone.delete(:id), fields
|
106
|
+
else
|
107
|
+
create table, fields
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Destroy:
|
112
|
+
# Removes rows identified by _id_ from a given _table_.
|
113
|
+
# @param [Symbol] table Table name
|
114
|
+
# @param [Object] id ID of rows to be removed
|
115
|
+
def delete table, id
|
116
|
+
@db.execute Statement.prep("delete from #{Sanitizer.table @schema, table} where id = ?;", [id])
|
117
|
+
end
|
118
|
+
|
119
|
+
# Execute statement:
|
120
|
+
# Generic method to execute any SQL statement.
|
121
|
+
# @param [String] q Statement to be executed
|
122
|
+
# @param [Array] params Statement parameters
|
123
|
+
# @return [Object] Whatever the statement returned
|
124
|
+
def exec q, params = []
|
125
|
+
@db.execute Statement.prep(q, params)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Pull Results into Hash:
|
129
|
+
# Converts rows returned by SQLite3 into Hashes matching the provided schema.
|
130
|
+
# @param [Symbol] table Table name
|
131
|
+
# @param [Array] x Results returned by SQLite3
|
132
|
+
# @return [Array] An array of Hashes, each representing a single row
|
133
|
+
def hash_res table, x
|
134
|
+
x
|
135
|
+
.collect { |r| @schema[table].keys.each_with_index.inject([]) { |a, e| a + [e[0], r[e[1]]] } }
|
136
|
+
.collect { |r| Hash[*r] }
|
137
|
+
.sym_keys rescue nil
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# Sack Module
|
5
|
+
module Sack
|
6
|
+
|
7
|
+
# Database Class
|
8
|
+
class Database
|
9
|
+
|
10
|
+
# Field Types:
|
11
|
+
# Available field types to be used when defining schemas.
|
12
|
+
FTYPES = {
|
13
|
+
str: 'VARCHAR(255)',
|
14
|
+
txt: 'TEXT',
|
15
|
+
int: 'INTEGER',
|
16
|
+
flo: 'REAL',
|
17
|
+
uniq: 'UNIQUE',
|
18
|
+
pk: 'PRIMARY KEY',
|
19
|
+
ai: 'AUTOINCREMENT'
|
20
|
+
}
|
21
|
+
|
22
|
+
# Ruby Type Conversions:
|
23
|
+
# Maps Field Types to Ruby Classes.
|
24
|
+
FTYPES_CLASSES = {
|
25
|
+
str: String,
|
26
|
+
txt: String,
|
27
|
+
int: Fixnum,
|
28
|
+
flo: Float
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# Internal Includes
|
5
|
+
require 'sack/database/sanitizer'
|
6
|
+
|
7
|
+
# Sack Module
|
8
|
+
module Sack
|
9
|
+
|
10
|
+
# Database Class
|
11
|
+
class Database
|
12
|
+
|
13
|
+
# Generator Module:
|
14
|
+
# Provides SQL generator methods for building queries.
|
15
|
+
module Generator
|
16
|
+
|
17
|
+
# Generate Field Name List:
|
18
|
+
# Builds a list of field names present in _field_hash_, separated by commas.
|
19
|
+
# @param [Hash] schema Database schema
|
20
|
+
# @param [Symbol] table Table name
|
21
|
+
# @param [Hash] field_hash Hash containing field names as keys
|
22
|
+
# @return [String] A comma-separated list of field names
|
23
|
+
def self.fields schema, table, field_hash
|
24
|
+
field_hash.keys.collect { |k| Sanitizer.field schema, table, k }.join ', '
|
25
|
+
end
|
26
|
+
|
27
|
+
# Value Marks:
|
28
|
+
# Builds a list of field markers ('?') for fields present in _field_hash_, separated by commas.
|
29
|
+
# @param [Hash] field_hash Hash containing fields
|
30
|
+
# @return [String] A comma-separated list of field markers ('?')
|
31
|
+
def self.marks field_hash
|
32
|
+
(['?'] * field_hash.size).join ', '
|
33
|
+
end
|
34
|
+
|
35
|
+
# Update Marks:
|
36
|
+
# Builds a list of field value markers ('field = ?') for fields present in _field_hash_, separated by commas.
|
37
|
+
# @param [Hash] schema Database schema
|
38
|
+
# @param [Symbol] table Table name
|
39
|
+
# @param [Hash] field_hash Hash containing field names as keys
|
40
|
+
# @return [String] A comma-separated list of field value markers ('field = ?')
|
41
|
+
def self.update_marks schema, table, field_hash
|
42
|
+
field_hash.keys.collect { |k| "#{Sanitizer.field schema, table, k} = ?" }.join ', '
|
43
|
+
end
|
44
|
+
|
45
|
+
# Values:
|
46
|
+
# Replaces all symbols in _vals_ by their stringified values.
|
47
|
+
# @param [Array] vals Array of value objects
|
48
|
+
# @return [Array] A copy of _vals_ where every symbol has been replaced by its string representation
|
49
|
+
def self.values vals
|
50
|
+
vals.collect { |v| v.is_a?(Symbol) ? v.to_s : v }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'aromat'
|
6
|
+
|
7
|
+
# Internal Includes
|
8
|
+
require 'sack/database/ftypes'
|
9
|
+
require 'sack/database/sanitizer'
|
10
|
+
require 'sack/database/model/data'
|
11
|
+
require 'sack/database/model/validation'
|
12
|
+
require 'sack/database/model/relationships'
|
13
|
+
|
14
|
+
# Sack Module
|
15
|
+
module Sack
|
16
|
+
|
17
|
+
# Database Class
|
18
|
+
class Database
|
19
|
+
|
20
|
+
# Model Module:
|
21
|
+
# Provides abstractions for defining database models.
|
22
|
+
module Model
|
23
|
+
|
24
|
+
# Included:
|
25
|
+
# Inject stuff when included.
|
26
|
+
# @param [Object] base Whatever we've been included into
|
27
|
+
def self.included base
|
28
|
+
|
29
|
+
# Set Model
|
30
|
+
base.instance_variable_set '@model', base
|
31
|
+
|
32
|
+
# Link to parent Data Model Root
|
33
|
+
base.instance_variable_set '@model_root', base.mod_parent
|
34
|
+
|
35
|
+
# Set Model Name
|
36
|
+
base.instance_variable_set '@model_name', base.mod_name
|
37
|
+
|
38
|
+
# Extend Class Methods
|
39
|
+
base.extend ClassMethods
|
40
|
+
|
41
|
+
# Extend with Data Access Methods
|
42
|
+
base.extend Data
|
43
|
+
|
44
|
+
# Include Validation
|
45
|
+
base.include Validation
|
46
|
+
|
47
|
+
# Include Relationships
|
48
|
+
base.include Relationships
|
49
|
+
end
|
50
|
+
|
51
|
+
# Class Methods:
|
52
|
+
# Collection of methods to be injected into anything that includes this module.
|
53
|
+
module ClassMethods
|
54
|
+
|
55
|
+
# Get / Set Table Name:
|
56
|
+
# Determines the default table name through introspection, or overrides the default table name (if _name_ is provided).
|
57
|
+
# @param [Symbol] name Override table name with this
|
58
|
+
# @return [Symbol] The table name
|
59
|
+
def table_name name = nil
|
60
|
+
|
61
|
+
# Introspect Default Name
|
62
|
+
@table_name ||= self.mod_name.snakecase
|
63
|
+
|
64
|
+
# Override with custom name
|
65
|
+
@table_name = name if name
|
66
|
+
|
67
|
+
@table_name.to_sym
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set Field:
|
71
|
+
# Configures a field on the current Model.
|
72
|
+
# @param [Hash] options Options defining the field - see README
|
73
|
+
def field options
|
74
|
+
|
75
|
+
# Internalise Options (so we can mess it up)
|
76
|
+
options = options.clone
|
77
|
+
|
78
|
+
# Pull Name
|
79
|
+
name, ftype = options.first
|
80
|
+
options.delete name
|
81
|
+
|
82
|
+
# Check Primary Type
|
83
|
+
raise "Invalid Field Primary Type [#{ftype.first}] for [#{name}] in #{mod_name}" unless FTYPES_CLASSES.include? ftype.first
|
84
|
+
|
85
|
+
# Collect Validation Rules
|
86
|
+
@fields ||= {}
|
87
|
+
@fields[name] ||= {}
|
88
|
+
@fields[name][:ftype] = ftype
|
89
|
+
@fields[name][:rules] = options
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get Fields:
|
93
|
+
# Simply returns the model's field map.
|
94
|
+
# @return [Hash] The field map for the current model
|
95
|
+
def fields
|
96
|
+
@fields
|
97
|
+
end
|
98
|
+
|
99
|
+
# Get Field Schema:
|
100
|
+
# Builds a schema for the current model.
|
101
|
+
# @return [Hash] The current model's schema
|
102
|
+
def field_schema
|
103
|
+
Hash[*(@fields.inject([]) { |a, e| a << e[0] << e[1][:ftype] })]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'aromat'
|
6
|
+
|
7
|
+
# Internal Includes
|
8
|
+
require 'sack/database'
|
9
|
+
require 'sack/database/model/validation'
|
10
|
+
|
11
|
+
# Sack Module
|
12
|
+
module Sack
|
13
|
+
|
14
|
+
# Database Class
|
15
|
+
class Database
|
16
|
+
|
17
|
+
# Model Module
|
18
|
+
module Model
|
19
|
+
|
20
|
+
# Data Module:
|
21
|
+
# Provides a simple data access layer through models.
|
22
|
+
module Data
|
23
|
+
|
24
|
+
# Actions requiring Validation
|
25
|
+
VALIDATED_ACTIONS = [
|
26
|
+
:create,
|
27
|
+
:update,
|
28
|
+
:save
|
29
|
+
]
|
30
|
+
|
31
|
+
# Method Missing:
|
32
|
+
# Catches and routes model actions through database.
|
33
|
+
def method_missing name, *args
|
34
|
+
|
35
|
+
# Check action allowed
|
36
|
+
super name, *args unless ACTIONS.include? name
|
37
|
+
|
38
|
+
# Acquire Database
|
39
|
+
db = args.slice! 0
|
40
|
+
|
41
|
+
# Check Validation Required
|
42
|
+
if VALIDATED_ACTIONS.include? name
|
43
|
+
|
44
|
+
# Acquire Entity
|
45
|
+
data = args.last
|
46
|
+
|
47
|
+
# Pre-load for updates
|
48
|
+
data = data.clone.merge find(db, args.slice(0)) if name == :update
|
49
|
+
|
50
|
+
# Validate
|
51
|
+
errors = []
|
52
|
+
raise Validation::ValidationException.new "Invalid Entity [#{data}] for Model #{@model}", errors unless is_valid? db, data, errors
|
53
|
+
end
|
54
|
+
|
55
|
+
# Forward to Database
|
56
|
+
result = db.send name, table_name, *args
|
57
|
+
|
58
|
+
# Inject Model Proxy into Entity/ies
|
59
|
+
(result.is_a?(Array) ? result : [result]).each { |e| e.instance_variable_set('@model_mod', self); e.define_singleton_method(:method_missing) { |n, *a| @model_mod.send n, *a, self } }
|
60
|
+
|
61
|
+
result
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# Internal Includes
|
5
|
+
require 'sack/database/model/relationships/has_many'
|
6
|
+
require 'sack/database/model/relationships/belongs_to'
|
7
|
+
|
8
|
+
# Sack Module
|
9
|
+
module Sack
|
10
|
+
|
11
|
+
# Database Class
|
12
|
+
class Database
|
13
|
+
|
14
|
+
# Model Module
|
15
|
+
module Model
|
16
|
+
|
17
|
+
# Relationships Module:
|
18
|
+
# Provides Model Relationships.
|
19
|
+
module Relationships
|
20
|
+
|
21
|
+
# Included:
|
22
|
+
# Inject Relationship Methods when included.
|
23
|
+
# @param [Object] base Whatever we've been included into
|
24
|
+
def self.included base
|
25
|
+
base.extend ClassMethods
|
26
|
+
end
|
27
|
+
|
28
|
+
# Class Methods:
|
29
|
+
# Collection of methods to be injected into anything that includes this module.
|
30
|
+
module ClassMethods
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# Sack Module
|
5
|
+
module Sack
|
6
|
+
|
7
|
+
# Database Class
|
8
|
+
class Database
|
9
|
+
|
10
|
+
# Model Module
|
11
|
+
module Model
|
12
|
+
|
13
|
+
# Relationships Module
|
14
|
+
module Relationships
|
15
|
+
|
16
|
+
# Class Methods
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
# Belongs To:
|
20
|
+
# Defines the 'slave' end of any relationship (the one that holds the other entity's ID).
|
21
|
+
# @param [Object] options Either a Symbol indicating both the name of the relationship and the target model, or an option Hash defining the relationship - see README
|
22
|
+
def belongs_to options
|
23
|
+
|
24
|
+
# Check Options
|
25
|
+
if options.is_a? Hash
|
26
|
+
|
27
|
+
# Internalise Options (so we can mess it up)
|
28
|
+
options = options.clone
|
29
|
+
|
30
|
+
# Pull Relationship Name & Target Model
|
31
|
+
name, target_model_name = options.first
|
32
|
+
options.delete name
|
33
|
+
else
|
34
|
+
|
35
|
+
# Acquire everything from just a Symbol
|
36
|
+
name = options
|
37
|
+
target_model_name = name
|
38
|
+
end
|
39
|
+
|
40
|
+
# Construct Proxy Method Module
|
41
|
+
proxy = Module.new do
|
42
|
+
|
43
|
+
# Proxy Method for Relationship:
|
44
|
+
# Allows fetching entities for a given belongs-to relationship.
|
45
|
+
define_method name do |db, data|
|
46
|
+
|
47
|
+
# Find Target Model
|
48
|
+
target_model = @model_root.const_get target_model_name.to_s.camelcase
|
49
|
+
|
50
|
+
# Fetch Relationship Entity
|
51
|
+
target_model.find db, data[name]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Extend Model with Proxy
|
56
|
+
@model.extend proxy
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|