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