taupe 0.5.3

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,73 @@
1
+ # File: sqlite.rb
2
+ # Time-stamp: <2014-09-11 16:29:19 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library sqlite driver class
5
+
6
+ module Taupe
7
+ class Database
8
+ # Sqlite database driver
9
+ class SqliteDriver
10
+ # Accessors
11
+ attr_accessor :connection
12
+
13
+ # Constructor
14
+ # @param dsn [Hash] The data source name
15
+ def initialize(dsn)
16
+ db = File.expand_path(dsn)
17
+ fail "Database #{db} not found" unless File.exist? db
18
+ @connection = SQLite3::Database.new db
19
+ @connection.results_as_hash = true
20
+ end
21
+
22
+ # Execute a single query
23
+ # @param query [String] The query to execute
24
+ # @return [Object]
25
+ def exec(query)
26
+ @connection.execute query
27
+ end
28
+
29
+ # Fetch objects from database
30
+ # @param query [String] The query to fetch
31
+ # @return [Array, Object]
32
+ def fetch(query)
33
+ exec(query).map(&:symbolize_keys)
34
+ end
35
+
36
+ # Get last inserted id
37
+ # @return [Integer]
38
+ def last_id
39
+ @connection.last_insert_row_id.to_i
40
+ end
41
+
42
+ # Guess schema of a table
43
+ # @param table [String] The table name
44
+ # @return [Hash]
45
+ def guess_schema(table)
46
+ results = {}
47
+
48
+ query = format('pragma table_info(%s)', table)
49
+
50
+ fetch(query).each do |values|
51
+ type = Taupe::Validate.standardize_sql_type values[:type]
52
+
53
+ results[values[:name].to_sym] = {
54
+ type: type,
55
+ null: values[:notnull] == 0,
56
+ primary_key: values[:pk] == 1
57
+ }
58
+ end
59
+
60
+ results
61
+ end
62
+
63
+ # Escape a string
64
+ # @param str [String]
65
+ # @return [String]
66
+ def escape(str)
67
+ # Sqlite3 does not implement this kind of thing
68
+ # Use prepare statements instead
69
+ str
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,138 @@
1
+ # File: database.rb
2
+ # Time-stamp: <2014-09-11 14:52:41 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library database class
5
+
6
+ require 'taupe/database/postgresql'
7
+ require 'taupe/database/mysql'
8
+ require 'taupe/database/sqlite'
9
+
10
+ module Taupe
11
+ # Database class
12
+ # Manage database connection and serve as query proxy
13
+ class Database
14
+ # Includes
15
+ include Accessorized
16
+
17
+ # Custom accessors
18
+ # Accessible via _name and _name=
19
+ single_accessor :type, :host, :port, :username, :password, :database
20
+
21
+ # Accessors
22
+ attr_accessor :instance, :driver
23
+
24
+ # Constructor
25
+ # @param block [Proc] A given block
26
+ def initialize(&block)
27
+ instance_eval(&block)
28
+ end
29
+
30
+ # Setup the Database instance
31
+ # @param block [Proc] A given block
32
+ def self.setup(&block)
33
+ @instance = new(&block)
34
+ setup_defaults
35
+ driver_factory
36
+ end
37
+
38
+ # Get the database type
39
+ # @return [Symbol]
40
+ def self.type
41
+ @instance._type
42
+ end
43
+
44
+ # Get the database name
45
+ # @return [Symbol]
46
+ def self.database
47
+ @instance._database
48
+ end
49
+
50
+ # Setup default values
51
+ def self.setup_defaults
52
+ case @instance._type
53
+ when :pg, :pgsql, :postgres, :postgresql
54
+ Taupe.require_gem 'pg', 'PostgreSQL database engine'
55
+ @instance._type = :postgresql
56
+ @instance._host ||= :localhost
57
+ @instance._port ||= 5432
58
+ when :mysql, :mysql2
59
+ Taupe.require_gem 'mysql2', 'MySQL database engine'
60
+ @instance._type = :mysql
61
+ @instance._host ||= :localhost
62
+ @instance._port ||= 3306
63
+ when :sqlite, :sqlite3
64
+ Taupe.require_gem 'sqlite3', 'SQLite database engine'
65
+ @instance._type = :sqlite
66
+ @instance._database ||= File.expand_path('~/.taupe.db')
67
+ else
68
+ fail 'Unknown database type'
69
+ end
70
+ end
71
+
72
+ # Get the data source name
73
+ # @return [Hash]
74
+ def self.dsn
75
+ case @instance._type
76
+ when :postgresql
77
+ {
78
+ host: @instance._host.to_s,
79
+ user: @instance._username.to_s,
80
+ password: @instance._password.to_s,
81
+ dbname: @instance._database.to_s
82
+ }
83
+ when :mysql
84
+ {
85
+ host: @instance._host.to_s,
86
+ username: @instance._username.to_s,
87
+ password: @instance._password.to_s,
88
+ database: @instance._database.to_s
89
+ }
90
+ when :sqlite
91
+ @instance._database.to_s
92
+ end
93
+ end
94
+
95
+ # Setup the connection driver
96
+ def self.driver_factory
97
+ cname = "Taupe::Database::#{@instance._type.capitalize}Driver"
98
+ klass = cname.split('::').reduce(Object) { |a, e| a.const_get e }
99
+ @instance.driver = klass.new dsn
100
+ end
101
+
102
+ # Guess schema of a table
103
+ # @param table [String] The table name
104
+ # @return [Hash]
105
+ def self.guess_schema(table)
106
+ @instance.driver.guess_schema table
107
+ end
108
+
109
+ # Execute a single query
110
+ # @param query [String] The query to execute
111
+ # @return [Object]
112
+ def self.exec(query)
113
+ @instance.driver.exec query
114
+ end
115
+
116
+ # Fetch objects from database
117
+ # @param query [String] The query to fetch
118
+ # @param single [Boolean] Must return one or more results?
119
+ # @return [Array, Object]
120
+ def self.fetch(query, single = false)
121
+ results = @instance.driver.fetch query
122
+ single ? results[0] : results
123
+ end
124
+
125
+ # Fetch last inserted id
126
+ # @return [Integer]
127
+ def self.last_id
128
+ @instance.driver.last_id
129
+ end
130
+
131
+ # Escape a string
132
+ # @param str [String]
133
+ # @return [String]
134
+ def self.escape(str)
135
+ @instance.driver.escape str
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # File: table.rb
4
+ # Time-stamp: <2014-09-11 16:28:23 pierre>
5
+ # Copyright (C) 2014 Pierre Lecocq
6
+ # Description: Taupe library model table class
7
+
8
+ module Taupe
9
+ class Model
10
+ # Model table base class
11
+ class Table
12
+ # Set instance variables
13
+ class << self
14
+ attr_accessor :_cname, :_table, :_columns
15
+ attr_accessor :_pkey, :_values, :_pkey_id, :_cache_key
16
+ end
17
+
18
+ # Load a new object
19
+ # @param pkey_id [Numeric]
20
+ # @param cache_key [String]
21
+ def self.load(pkey_id = nil, cache_key = nil)
22
+ full_cname = "Taupe::Model::#{@_cname}"
23
+ klass = full_cname.split('::').reduce(Object) { |a, e| a.const_get e }
24
+
25
+ options = {
26
+ pkey_id: pkey_id,
27
+ cache_key: cache_key,
28
+ values: nil
29
+ }
30
+
31
+ klass.new @_table, @_columns, options
32
+ end
33
+
34
+ # Load a new object from existing values
35
+ # @param values [Hash]
36
+ # @param pkey_id [Numeric]
37
+ # @param cache_key [String]
38
+ def self.load_from_hash(values, pkey_id = nil, cache_key = nil)
39
+ full_cname = "Taupe::Model::#{@_cname}"
40
+ klass = full_cname.split('::').reduce(Object) { |a, e| a.const_get e }
41
+
42
+ options = {
43
+ pkey_id: pkey_id,
44
+ cache_key: cache_key,
45
+ values: values
46
+ }
47
+
48
+ klass.new @_table, @_columns, options
49
+ end
50
+
51
+ # Constructor
52
+ # @param table [String]
53
+ # @param columns [Hash]
54
+ # @param options [Hash]
55
+ def initialize(table, columns = nil, options = {})
56
+ columns = Taupe::Database.guess_schema(table) if columns.nil?
57
+
58
+ @_table = table
59
+ @_columns = columns
60
+ @_pkey = nil
61
+ @_pkey_id = options[:pkey_id] || nil
62
+ @_cache_key = options[:cache_key] || nil
63
+ @_values = options[:values] || {}
64
+
65
+ @_pkey = columns.select { |_k, v| v[:primary_key] == true }.first[0]
66
+ fail "Primary key undefined for model #{table}" if @_pkey.nil?
67
+
68
+ if @_values.empty?
69
+ return if @_pkey_id.nil?
70
+ retrieve_from_database unless retrieve_from_cache
71
+ else
72
+ @_pkey_id = @_values[@_pkey] if @_pkey_id.nil?
73
+ end
74
+ end
75
+
76
+ # Retrieve data from cache
77
+ # @return [Boolean]
78
+ def retrieve_from_cache
79
+ retrieved = false
80
+ return retrieved if @_cache_key.nil?
81
+
82
+ data = Taupe::Cache.get(@_cache_key) || nil
83
+ unless data.nil? || data.empty?
84
+ @_values = data
85
+ retrieved = true
86
+ end
87
+
88
+ retrieved
89
+ end
90
+
91
+ # Retrieve data from database
92
+ def retrieve_from_database
93
+ query = "SELECT * FROM #{@_table} WHERE #{@_pkey} = #{@_pkey_id}"
94
+ result = Taupe::Database.fetch(query, true)
95
+
96
+ return nil if result.nil? || result.empty?
97
+
98
+ result.map do |k, v|
99
+ @_values[k.to_sym] = v unless k.is_a? Numeric
100
+ end
101
+
102
+ Taupe::Cache.set @_cache_key, @_values unless @_cache_key.nil?
103
+ end
104
+
105
+ # Save the model object
106
+ # @param with_validations [Boolean]
107
+ def save(with_validations = true)
108
+ Taupe::Validate.check(@_values, @_columns) if with_validations
109
+
110
+ if @_pkey_id.nil?
111
+ query = "
112
+ INSERT INTO #{@_table}
113
+ (#{@_values.keys.map(&:to_s).join(', ')})
114
+ VALUES
115
+ (#{@_values.values.map { |e| "'" + e.to_s + "'" }.join(', ')})
116
+ "
117
+
118
+ Taupe::Database.exec query
119
+ @_pkey_id = Taupe::Database.last_id
120
+ else
121
+ joined_values = @_values.map { |k, v| "#{k} = '#{v}'" }.join(', ')
122
+ query = "
123
+ UPDATE #{@_table} SET #{joined_values}
124
+ WHERE #{@_pkey} = #{@_pkey_id}
125
+ "
126
+
127
+ Taupe::Database.exec query
128
+ end
129
+
130
+ Taupe::Cache.delete @_cache_key unless @_cache_key.nil?
131
+ end
132
+
133
+ # Delete the model object
134
+ def delete
135
+ fail 'Can not delete an unsaved model object' if @_pkey_id.nil?
136
+
137
+ query = "DELETE FROM #{@_table} WHERE #{@_pkey} = #{@_pkey_id}"
138
+
139
+ Taupe::Database.exec query
140
+
141
+ Taupe::Cache.delete @_cache_key unless @_cache_key.nil?
142
+ end
143
+
144
+ # Is the object empty?
145
+ # @return [Boolean]
146
+ def empty?
147
+ @_values.empty?
148
+ end
149
+
150
+ # Method missing
151
+ def method_missing(m, *args, &block)
152
+ return @_pkey_id if m.to_sym == @_pkey.to_sym
153
+ return @_values[m] if @_values.key? m
154
+
155
+ ms = m.to_s
156
+ if ms.include? '='
157
+ ms = ms[0..-2]
158
+ if @_columns.include? ms.to_sym
159
+ @_values[ms.to_sym] = args[0]
160
+ return true
161
+ end
162
+ end
163
+
164
+ super
165
+ end
166
+
167
+ # Execute a single query
168
+ # @param query [String] The query to execute
169
+ # @return [Object]
170
+ def self.exec(query)
171
+ Taupe::Database.exec query
172
+ end
173
+
174
+ # Fetch objects from database
175
+ # @param query [String] The query to fetch
176
+ # @param single [Boolean] Must return one or more results?
177
+ # @return [Array, Object]
178
+ def self.fetch(query, single = false)
179
+ results = []
180
+ data = Taupe::Database.fetch(query)
181
+
182
+ if data
183
+ data.each do |h|
184
+ results << load_from_hash(h)
185
+ end
186
+ end
187
+
188
+ single ? results[0] : results
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,50 @@
1
+ # File: validate.rb
2
+ # Time-stamp: <2014-09-11 16:13:29 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library validate class
5
+
6
+ module Taupe
7
+ # Validator class
8
+ class Validate
9
+ # Check data integrity
10
+ # @param values [Hash]
11
+ # @param definitions [Hash]
12
+ def self.check(values, definitions)
13
+ errors = []
14
+ definitions.each do |name, props|
15
+ can_be_null = props[:null] || true
16
+ if values.include?(name)
17
+ value = values[name]
18
+ expected_type = props[:type] || String
19
+ unless value.is_a? expected_type
20
+ errors << "#{name} (#{value.class.name}) must be a #{expected_type}"
21
+ end
22
+ else
23
+ errors << "#{name} can not be null" unless can_be_null
24
+ end
25
+ end
26
+
27
+ fail errors.join(' - ') unless errors.empty?
28
+
29
+ values
30
+ end
31
+
32
+ # Transform a SQL type into a standard type
33
+ def self.standardize_sql_type(sql_type)
34
+ standard_type = nil
35
+ case sql_type.to_s.downcase
36
+ when 'integer', 'int', 'int(11)', 'bigint', 'smallint', 'tinyint'
37
+ standard_type = Integer
38
+ when 'float'
39
+ standard_type = Float
40
+ when 'date', 'time', 'datetime', 'timestamp',
41
+ 'timestamp wit time zone', 'timestamp without time zone'
42
+ standard_type = Time
43
+ else
44
+ standard_type = String
45
+ end
46
+
47
+ standard_type
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,47 @@
1
+ # File: model.rb
2
+ # Time-stamp: <2014-09-11 13:44:58 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library model class
5
+
6
+ require 'taupe/model/table'
7
+ require 'taupe/model/validate'
8
+
9
+ module Taupe
10
+ # Model class
11
+ class Model
12
+ # Includes
13
+ include Accessorized
14
+
15
+ # Custom accessors
16
+ # Accessible via _name and _name=
17
+ single_accessor :table
18
+ stacked_accessor :column
19
+
20
+ # Accessors
21
+ attr_accessor :instance
22
+
23
+ # Setup the Cache instance
24
+ # @param block [Proc] A given block
25
+ def self.setup(&block)
26
+ @instance = new(&block)
27
+ end
28
+
29
+ # Constructor
30
+ # @param block [Proc] A given block
31
+ def initialize(&block)
32
+ instance_eval(&block)
33
+ _write_class_code
34
+ end
35
+
36
+ # Build the table related class
37
+ def _write_class_code
38
+ cname = @table.to_s.split('_').map(&:capitalize).join
39
+ klass = Taupe::Model.const_set cname, Class.new(Taupe::Model::Table)
40
+ klass._cname = cname
41
+ klass._table = @table
42
+ klass._columns = @_column_stack
43
+ end
44
+
45
+ private :_write_class_code
46
+ end
47
+ end
data/lib/taupe.rb ADDED
@@ -0,0 +1,26 @@
1
+ # File: taupe.rb
2
+ # Time-stamp: <2014-09-11 16:27:47 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library main file
5
+
6
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__))
7
+
8
+ require 'taupe/core'
9
+ require 'taupe/database'
10
+ require 'taupe/cache'
11
+ require 'taupe/model'
12
+
13
+ # Main Taupe module
14
+ module Taupe
15
+ # Current version constant in the form major.minor.patch
16
+ VERSION = [0, 5, 3].join('.')
17
+
18
+ # Require a gem
19
+ # @param gem_name [String] the gem name
20
+ # @param description [String] a description of the gem
21
+ def self.require_gem(gem_name, description)
22
+ require gem_name
23
+ rescue LoadError
24
+ raise format('To use %s, install the "%s" gem', description, gem_name)
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ # File: database_spec.rb
2
+ # Time-stamp: <2014-08-01 12:00:00 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library database tests file
5
+
6
+ require_relative '../lib/taupe'
7
+
8
+ describe Taupe::Database do
9
+ # Before hook
10
+ before :all do
11
+ require 'sqlite3'
12
+ path = File.expand_path('/tmp/taupe-test.db')
13
+ File.delete path if File.exist? path
14
+ File.new(path, 'w')
15
+ end
16
+
17
+ # Setup method
18
+ describe '#setup' do
19
+ it 'should setup the database driver' do
20
+ database = Taupe::Database.setup do
21
+ type :sqlite
22
+ database File.expand_path('/tmp/taupe-test.db')
23
+ end
24
+
25
+ expect(database).to be_a Taupe::Database::SqliteDriver
26
+ end
27
+ end
28
+
29
+ # Query method
30
+ describe '#query' do
31
+ it 'should execute some direct queries and return true' do
32
+ queries = []
33
+ queries << %Q(CREATE TABLE article(
34
+ article_id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ title text NOT NULL,
36
+ content text,
37
+ state INTEGER NOT NULL default 1,
38
+ creation DATETIME DEFAULT CURRENT_TIMESTAMP);)
39
+ queries << %Q(INSERT INTO article (title, content, state) VALUES (
40
+ 'Article one', 'This is the first article', 1);)
41
+ queries << %Q(INSERT INTO article (title, content, state) VALUES (
42
+ 'Article two', 'This is the second article', 0);)
43
+ queries.each do |q|
44
+ expect(Taupe::Database.exec(q)).to eql []
45
+ end
46
+ end
47
+ end
48
+
49
+ # Fetch method
50
+ describe '#fetch' do
51
+ it 'should fetch two database entries' do
52
+ q = 'SELECT * FROM article'
53
+ results = Taupe::Database.fetch(q)
54
+ expect(results).to be_a Array
55
+ expect(results[0]).to be_a Hash
56
+ expect(results.length).to eql 2
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,56 @@
1
+ # File: model_spec.rb
2
+ # Time-stamp: <2014-09-11 16:03:43 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library model tests file
5
+
6
+ require_relative '../lib/taupe'
7
+
8
+ describe Taupe::Model do
9
+
10
+ # Setup method
11
+ describe '#setup' do
12
+ it 'should setup the Article model' do
13
+ model = Taupe::Model.setup do
14
+ table :article
15
+ column :article_id, { type: Integer, :primary_key => true }
16
+ column :title, { type: String, :null => false }
17
+ column :content, { type: String }
18
+ column :state, { type: Integer }
19
+ column :creation, { type: Date, :locked => true }
20
+ end
21
+ expect(model).to be_a Taupe::Model
22
+
23
+ model_object = Taupe::Model::Article.load
24
+ expect(model_object).to be_a Taupe::Model::Article
25
+ end
26
+ end
27
+
28
+ # Fetch method
29
+ describe '#fetch' do
30
+ it 'should fetch data from Article' do
31
+ query = "SELECT * FROM article"
32
+ results = Taupe::Model::Article.fetch(query)
33
+
34
+ expect(results).to be_a Array
35
+ expect(results.length).to eql 2
36
+ end
37
+ end
38
+
39
+ # Query method
40
+ describe '#query and #save' do
41
+ it 'should execute an insert into Article' do
42
+ model_object = Taupe::Model::Article.load
43
+ model_object.title = 'This is a new article'
44
+ model_object.content = 'This is a new article content'
45
+ model_object.state = 1
46
+ model_object.save
47
+
48
+ query = "SELECT * FROM article"
49
+ results = Taupe::Model::Article.fetch(query)
50
+
51
+ expect(results).to be_a Array
52
+ expect(results.length).to eql 3
53
+ expect(results.last.title).to eql 'This is a new article'
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ # File: taupe_spec.rb
2
+ # Time-stamp: <2014-08-01 12:00:00 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library main tests file
5
+
6
+ require_relative '../lib/taupe'
7
+
8
+ describe Taupe do
9
+ describe 'VERSION' do
10
+ it 'should return 0.5.3' do
11
+ expect(Taupe::VERSION).to eql '0.5.3'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ # File: validator_spec.rb
2
+ # Time-stamp: <2014-09-11 15:56:04 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library validator tests file
5
+
6
+ describe Taupe::Validate do
7
+ describe '#check' do
8
+ it 'should succeed' do
9
+ values = {
10
+ :article_id => 1,
11
+ :title => 'This is an article',
12
+ :amount => 3.0
13
+ }
14
+
15
+ definitions = {
16
+ :article_id => { :type => Integer, :primary_key => true },
17
+ :amount => { :type => Float },
18
+ :title => { :type => String, :null => false },
19
+ :content => { :type => String }
20
+ }
21
+
22
+ expect(Taupe::Validate.check(values, definitions)).to eql values
23
+ end
24
+
25
+ it 'should send a failure' do
26
+ values = {
27
+ :article_id => '1',
28
+ :title => 'This is an article',
29
+ :content => 'and the content',
30
+ :amount => 3.0
31
+ }
32
+
33
+ definitions = {
34
+ :article_id => { :type => Integer, :primary_key => true },
35
+ :amount => { :type => Float },
36
+ :title => { :type => String, :null => false },
37
+ :content => { :type => String }
38
+ }
39
+
40
+ expect{ Taupe::Validate.check(values, definitions) }.to raise_error
41
+ end
42
+ end
43
+ end