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,56 @@
|
|
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
|
+
# Has Many:
|
20
|
+
# Defines the 'one' side a one-to-many relationship.
|
21
|
+
# @param [Hash] options Options defining the relationship - see README
|
22
|
+
def has_many options
|
23
|
+
|
24
|
+
# Internalise Options (so we can mess it up)
|
25
|
+
options = options.clone
|
26
|
+
|
27
|
+
# Pull Relationship Name & Target Model
|
28
|
+
name, target_model_name = options.first
|
29
|
+
options.delete name
|
30
|
+
|
31
|
+
# Determine Foreign Key (which field in the remote model holds our ID)
|
32
|
+
foreign_key = options[:fk] || table_name
|
33
|
+
|
34
|
+
# Construct Proxy Method Module
|
35
|
+
proxy = Module.new do
|
36
|
+
|
37
|
+
# Proxy Method for Relationship:
|
38
|
+
# Allows fetching entities for a given has-many relationship.
|
39
|
+
define_method name do |db, data|
|
40
|
+
|
41
|
+
# Find Target Model
|
42
|
+
target_model = @model_root.const_get target_model_name.to_s.camelcase
|
43
|
+
|
44
|
+
# Fetch Relationship Entities
|
45
|
+
target_model.fetch_by db, foreign_key, data[:id]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Extend Model with Proxy
|
50
|
+
@model.extend proxy
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,138 @@
|
|
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
|
+
|
10
|
+
# Sack Module
|
11
|
+
module Sack
|
12
|
+
|
13
|
+
# Database Class
|
14
|
+
class Database
|
15
|
+
|
16
|
+
# Model Module
|
17
|
+
module Model
|
18
|
+
|
19
|
+
# Validation Module:
|
20
|
+
# Provides a simple entity validation framework.
|
21
|
+
module Validation
|
22
|
+
|
23
|
+
# Validation Exception
|
24
|
+
class ValidationException < Exception
|
25
|
+
attr_reader :errors
|
26
|
+
def initialize msg, errors
|
27
|
+
super msg
|
28
|
+
@errors = errors
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Included:
|
33
|
+
# Inject Validation Methods when included.
|
34
|
+
# @param [Object] base Whatever we've been included into
|
35
|
+
def self.included base
|
36
|
+
base.extend ClassMethods
|
37
|
+
end
|
38
|
+
|
39
|
+
# Class Methods:
|
40
|
+
# Collection of methods to be injected into anything that includes this module.
|
41
|
+
module ClassMethods
|
42
|
+
|
43
|
+
# Is Valid:
|
44
|
+
# Verifies the validity of a given entity (_data_) against this Model.
|
45
|
+
# @param [Database] db Database instance (Sack::Database)
|
46
|
+
# @param [Hash] data Entity data
|
47
|
+
def is_valid? db, data, errors = []
|
48
|
+
|
49
|
+
# Run through Model's Field Schema
|
50
|
+
fields.inject(true) do |a, e|
|
51
|
+
|
52
|
+
# Acquire Field Info
|
53
|
+
name = e[0]
|
54
|
+
info = e[1]
|
55
|
+
ftype = info[:ftype]
|
56
|
+
rules = info[:rules]
|
57
|
+
val = data[name]
|
58
|
+
|
59
|
+
# Handle Required
|
60
|
+
if rules[:required]
|
61
|
+
r = val.try :is_a?, FTYPES_CLASSES[ftype.first]
|
62
|
+
a &&= r
|
63
|
+
errors << "Required field [#{name}] is missing" unless r
|
64
|
+
end
|
65
|
+
|
66
|
+
# Handle Unique
|
67
|
+
if rules[:unique]
|
68
|
+
r = is_uniq? db, data, name, val, rules[:unique]
|
69
|
+
a &&= r
|
70
|
+
errors << "Field [#{name}] has non-unique value [#{val}]#{rules[:unique].is_a?(Array) ? " in scope [#{rules[:unique].join ', '}]" : ''}" unless r
|
71
|
+
end
|
72
|
+
|
73
|
+
# Handle Set Length
|
74
|
+
if rules[:length]
|
75
|
+
r = val.try(:length).try :==, rules[:length]
|
76
|
+
a &&= r
|
77
|
+
errors << "Field [#{name}] has invalid length (#{val.try(:length)} / #{rules[:length]})" unless r
|
78
|
+
end
|
79
|
+
|
80
|
+
# Handle Minimum Length
|
81
|
+
if rules[:min_length]
|
82
|
+
r = val.try(:length).try :>=, rules[:min_length]
|
83
|
+
a &&= r
|
84
|
+
errors << "Field [#{name}] has invalid length (#{val.try(:length)} / Min: #{rules[:min_length]})" unless r
|
85
|
+
end
|
86
|
+
|
87
|
+
# Handle Maximum Length
|
88
|
+
if rules[:max_length]
|
89
|
+
r = val.try(:length).try :<=, rules[:max_length]
|
90
|
+
a &&= r
|
91
|
+
errors << "Field [#{name}] has invalid length (#{val.try(:length)} / Max: #{rules[:max_length]})" unless r
|
92
|
+
end
|
93
|
+
|
94
|
+
# Handle Regex
|
95
|
+
if rules[:regex]
|
96
|
+
r = rules[:regex] =~ val
|
97
|
+
a &&= r
|
98
|
+
errors << "Field [#{name}] doesn't match allowed pattern (#{rules[:regex].inspect})" unless r
|
99
|
+
end
|
100
|
+
|
101
|
+
# Handle Custom
|
102
|
+
if rules[:validate]
|
103
|
+
size = errors.size
|
104
|
+
r = send rules[:validate], db, data, name, val, rules, errors
|
105
|
+
a &&= r
|
106
|
+
errors << "Field [#{name}] is invalid" unless r || (size != errors.size)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Don't leak shit
|
110
|
+
!!a
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Is Unique:
|
115
|
+
# Verifies the unicity of a given field (_name_) value (_val_) on an entity (_data_), possibly within a given _scope_.
|
116
|
+
# @param [Database] db Database instance (Sack::Database)
|
117
|
+
# @param [Hash] data Entity data
|
118
|
+
# @param [Symbol] name Field name
|
119
|
+
# @param [Object] val Field value
|
120
|
+
# @param [Object] scope Either True (if absolute - no scope) or an Array of field names defining the scope
|
121
|
+
def is_uniq? db, data, name, val, scope
|
122
|
+
|
123
|
+
# Fetch all other rows with field [name] equal to [val]
|
124
|
+
others = db.fetch_by table_name, name, val
|
125
|
+
others.reject! { |o| o[:id] == data[:id] } if data[:id]
|
126
|
+
|
127
|
+
# If scopeless (absolutely unique), check empty set and return
|
128
|
+
return others.empty? unless scope.is_a? Array
|
129
|
+
|
130
|
+
# Check Unique throughout Scope
|
131
|
+
scope.each { |f| others.reject! { |o| o[f] != data[f] } }
|
132
|
+
others.empty?
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# Internal Includes
|
5
|
+
require 'sack/database/ftypes'
|
6
|
+
|
7
|
+
# Sack Module
|
8
|
+
module Sack
|
9
|
+
|
10
|
+
# Database Class
|
11
|
+
class Database
|
12
|
+
|
13
|
+
# Santizer Module:
|
14
|
+
# Provides Table and Field name sanitization methods.
|
15
|
+
module Sanitizer
|
16
|
+
|
17
|
+
# Generic Field Name Regex
|
18
|
+
FIELD_NAME_REX = /^[0-9a-z_.-]+$/
|
19
|
+
|
20
|
+
# Sanitize Table Name:
|
21
|
+
# Raises an exception if _name_ is not a valid table in _schema_.
|
22
|
+
# @param [Hash] schema Database schema
|
23
|
+
# @param [Symbol] name Table name to sanitize
|
24
|
+
# @return [Symbol] Table name if sanitization passed
|
25
|
+
def self.table schema, name
|
26
|
+
raise "Illegal table name [#{name}]" unless (name.to_sym.to_s == name.to_s) && schema.has_key?(name.to_sym)
|
27
|
+
name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Sanitize Table Field Name:
|
31
|
+
# Raises an exception if _table_ or _field_ are not valid according to _schema_.
|
32
|
+
# @param [Hash] schema Database schema
|
33
|
+
# @param [Symbol] table Table name
|
34
|
+
# @param [Symbol] field Field name
|
35
|
+
# @return [Symbol] Field name if sanitization passed
|
36
|
+
def self.field schema, table, field
|
37
|
+
table schema, table
|
38
|
+
raise "Illegal field [#{field}] for table [#{table}]" unless (field.to_sym.to_s == field.to_s) && schema[table.to_sym].has_key?(field.to_sym)
|
39
|
+
field
|
40
|
+
end
|
41
|
+
|
42
|
+
# Sanitize Generic Field Name:
|
43
|
+
# Raises an exception if _name_ contains invalid characters (defined in _FIELD_NAME_REX_).
|
44
|
+
# @param [Symbol] name Field name
|
45
|
+
# @return [Symbol] Field name if sanitization passed
|
46
|
+
def self.field_name name
|
47
|
+
raise "Illegal field name [#{name}]" unless FIELD_NAME_REX =~ name
|
48
|
+
name
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sanitize Field Types
|
52
|
+
# Raises an exception if _t_ is not an allowed Field Type (defined in _FTYPES_).
|
53
|
+
# @param [Symbol] t Field type symbol (from _FTYPES_)
|
54
|
+
# @return [Symbol] Field type if sanitization passed
|
55
|
+
def self.ftype t
|
56
|
+
raise "Illegal field type [#{t}]" unless FTYPES.keys.include? t
|
57
|
+
t
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sanitize Field Value:
|
61
|
+
# Escapes single-quotes inside field values.
|
62
|
+
# @param [Object] v Field value
|
63
|
+
# @return [Object] The supplied value, with single quotes escaped if it's a String.
|
64
|
+
def self.value v
|
65
|
+
return v unless v.is_a? String
|
66
|
+
drop_nonprintascii(v).gsub("'") { "''" }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Drop Non-Print-ASCII:
|
70
|
+
# Removes all non-printable-ASCII characters from a String.
|
71
|
+
# @param [String] s Input string
|
72
|
+
# @return [String] The provided string, stripped of any non-printable-ASCII text
|
73
|
+
def self.drop_nonprintascii s
|
74
|
+
s.bytes.select { |b| (b >= 0x20) && (b <= 0x7e) }.inject('') { |a, e| a + e.chr }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Sack
|
2
|
+
# by Eresse <eresse@eresse.net>
|
3
|
+
|
4
|
+
# External Includes
|
5
|
+
require 'aromat'
|
6
|
+
|
7
|
+
# Sack Module
|
8
|
+
module Sack
|
9
|
+
|
10
|
+
# Database Class
|
11
|
+
class Database
|
12
|
+
|
13
|
+
# Schema Module:
|
14
|
+
# Provides utilities for manipulating database schema.
|
15
|
+
module Schema
|
16
|
+
|
17
|
+
# Load from Module:
|
18
|
+
# Constructs a database schema from a given data model module (_mod_).
|
19
|
+
# @param [Module] mod Data model module, containing entity modules
|
20
|
+
# @return [Hash] The complete database schema
|
21
|
+
def self.from_module mod
|
22
|
+
|
23
|
+
# Run through Sub Modules
|
24
|
+
Hash[*(mod.constants
|
25
|
+
.collect { |c| mod.const_get c }
|
26
|
+
.select { |c| c.is_a? Module }
|
27
|
+
.inject([]) { |a, e| (a << e.table_name) << e.field_schema }
|
28
|
+
)]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
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
|
+
# Statement Module:
|
14
|
+
# Provides statement manipulation methods.
|
15
|
+
module Statement
|
16
|
+
|
17
|
+
# Prepare Statement:
|
18
|
+
# Binds _params_ to statement fields in _q_.
|
19
|
+
# @param [String] q Statement
|
20
|
+
# @param [Array] params Statement parameters
|
21
|
+
# @return [String] Final statement with all parameters bound
|
22
|
+
def self.prep q, params = []
|
23
|
+
params.inject(q) { |a, p| a.sub!('?') { "'#{Sanitizer.value(p)}'" } }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/sack/version.rb
ADDED
data/sack.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'sack/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'sack'
|
8
|
+
spec.version = Sack::VERSION
|
9
|
+
spec.authors = ['Eresse']
|
10
|
+
spec.email = ['eresse@eresse.net']
|
11
|
+
|
12
|
+
spec.summary = 'Minimalistic Database Layer based on SQLite3'
|
13
|
+
spec.description = 'Provides a lightweight an easy-to-use database interface.'
|
14
|
+
spec.homepage = 'http://redmine.eresse.net/projects/sack'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler'
|
21
|
+
spec.add_development_dependency 'rake'
|
22
|
+
spec.add_runtime_dependency 'minitest'
|
23
|
+
spec.add_runtime_dependency 'sqlite3'
|
24
|
+
spec.add_runtime_dependency 'aromat'
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sack
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eresse
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: aromat
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Provides a lightweight an easy-to-use database interface.
|
84
|
+
email:
|
85
|
+
- eresse@eresse.net
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- Gemfile
|
92
|
+
- LICENSE.txt
|
93
|
+
- README.md
|
94
|
+
- Rakefile
|
95
|
+
- lib/sack.rb
|
96
|
+
- lib/sack/connectors.rb
|
97
|
+
- lib/sack/connectors/sqlite3.rb
|
98
|
+
- lib/sack/database.rb
|
99
|
+
- lib/sack/database/data.rb
|
100
|
+
- lib/sack/database/ftypes.rb
|
101
|
+
- lib/sack/database/generator.rb
|
102
|
+
- lib/sack/database/model.rb
|
103
|
+
- lib/sack/database/model/data.rb
|
104
|
+
- lib/sack/database/model/relationships.rb
|
105
|
+
- lib/sack/database/model/relationships/belongs_to.rb
|
106
|
+
- lib/sack/database/model/relationships/has_many.rb
|
107
|
+
- lib/sack/database/model/validation.rb
|
108
|
+
- lib/sack/database/sanitizer.rb
|
109
|
+
- lib/sack/database/schema.rb
|
110
|
+
- lib/sack/database/statement.rb
|
111
|
+
- lib/sack/version.rb
|
112
|
+
- sack.gemspec
|
113
|
+
homepage: http://redmine.eresse.net/projects/sack
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubyforge_project:
|
133
|
+
rubygems_version: 2.5.1
|
134
|
+
signing_key:
|
135
|
+
specification_version: 4
|
136
|
+
summary: Minimalistic Database Layer based on SQLite3
|
137
|
+
test_files: []
|