sack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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