sack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,9 @@
1
+ # Sack
2
+ # by Eresse <eresse@eresse.net>
3
+
4
+ # Sack Module
5
+ module Sack
6
+
7
+ # Version
8
+ VERSION = '1.0.0'
9
+ end
@@ -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: []