sack 1.0.0

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