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.
- 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: []
|