wormy 0.1.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/README.md +63 -0
- data/lib/wormy.rb +164 -0
- data/lib/wormy/associatable.rb +113 -0
- data/lib/wormy/db_connection.rb +67 -0
- data/lib/wormy/searchable.rb +24 -0
- metadata +50 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 44ab7bc1fb2372bd0e89215a9f65bcd8dbd82288
|
|
4
|
+
data.tar.gz: a2adef7a24cf3bc38d12162d6cbf2680fc663a3e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 53e439ae8c3f49775e1a2ce39b2d1642ee3a8343352bc2792f7dd5256582d6e1bbc5885e159b01047a8973fe812a7d6f71d55cd1c399a61ad45020b879361775
|
|
7
|
+
data.tar.gz: '0797403ef2c5263d1d052ec4dbf504e50ebcae1b7fbe9fa72ccb10959ae9e18ec428498a6296f468416a49db9d3333bb3f53bbfc01c444ac68997a5ec542f8b2'
|
data/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# WORMY 🐛
|
|
2
|
+
A lightweight Object-Relational Mapping (ORM) library for Ruby. Allows you to keep code DRY and easily perform database operations in an object-oriented manner.
|
|
3
|
+
|
|
4
|
+
## Demo
|
|
5
|
+
1. From the demo directory of this repo, open `pry` or `irb` in the console
|
|
6
|
+
2. `load 'hp_demo.rb'`
|
|
7
|
+
3. Use `hp_demo.rb` and the API section below as a reference to play around with the data
|
|
8
|
+
|
|
9
|
+
## How to Use WORMY
|
|
10
|
+
* Navigate to the folder in your directory where you would like your .db database file to be saved.
|
|
11
|
+
* If you have an existing database.rb file you need to rewrite, run `rm database.db`
|
|
12
|
+
* Run `cat '{YOUR_SQL_FILE_NAME}' | sqlite3 'database.db'` (replacing {YOUR_SQL_FILE_NAME} with your own .sql file)
|
|
13
|
+
* Then, in your project, open a connection with `DBConnection.open('database.db')`
|
|
14
|
+
|
|
15
|
+
## Libraries
|
|
16
|
+
* SQLite3
|
|
17
|
+
* ActiveSupport::Inflector
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
Associations between models are defined by simple class methods, like so:
|
|
21
|
+
```
|
|
22
|
+
class Pet < WORMY::Base
|
|
23
|
+
belongs_to :owner,
|
|
24
|
+
class_name: "Wizard"
|
|
25
|
+
|
|
26
|
+
has_one_through :house, :owner, :house
|
|
27
|
+
|
|
28
|
+
finalize!
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Querying and updating the database is made easy with WORMY::Base's methods like:
|
|
33
|
+
* `::all`
|
|
34
|
+
* `::count`
|
|
35
|
+
* `::destroy_all`
|
|
36
|
+
* `::find`
|
|
37
|
+
* `::first`
|
|
38
|
+
* `::last`
|
|
39
|
+
* `::where`
|
|
40
|
+
* `#create`
|
|
41
|
+
* `#save`
|
|
42
|
+
* `#destroy`
|
|
43
|
+
|
|
44
|
+
Perform custom model validations by adding a call to `validates` in your subclass definition:
|
|
45
|
+
```
|
|
46
|
+
class House < WORMY::Base
|
|
47
|
+
has_many :wizards
|
|
48
|
+
has_many_through :pets, :wizards, :pets
|
|
49
|
+
validates :house_name
|
|
50
|
+
|
|
51
|
+
def house_name
|
|
52
|
+
["Gryffindor", "Slytherin", "Ravenclaw", "Hufflepuff"].include?(self.name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
finalize!
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
## About WORMY
|
|
61
|
+
WORMY opens a connection to a provided database file by instantiating a singleton of SQLite3::Database via DBConnection. DBConnection uses native SQLite3::Database methods (`execute`, `execute2`, `last_insert_row_id`) to allow WORMY to perform complex SQL queries using heredocs. The `Searchable` and `Associatable` modules extend WORMY::Base to provide an intuitive API.
|
|
62
|
+
|
|
63
|
+
WORMY emphasizes convention over configuration by setting sensible defaults for associations, but also allows for easy overrides if desired.
|
data/lib/wormy.rb
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
require 'active_support/inflector'
|
|
2
|
+
require 'wormy/db_connection'
|
|
3
|
+
require 'wormy/searchable'
|
|
4
|
+
require 'wormy/associatable'
|
|
5
|
+
|
|
6
|
+
module WORMY
|
|
7
|
+
class Base
|
|
8
|
+
extend Searchable
|
|
9
|
+
extend Associatable
|
|
10
|
+
|
|
11
|
+
def self.all
|
|
12
|
+
results = DBConnection.execute(<<-SQL)
|
|
13
|
+
SELECT
|
|
14
|
+
*
|
|
15
|
+
FROM
|
|
16
|
+
#{table_name}
|
|
17
|
+
SQL
|
|
18
|
+
|
|
19
|
+
parse_all(results)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.columns
|
|
23
|
+
@columns ||= DBConnection.execute2(<<-SQL)
|
|
24
|
+
SELECT
|
|
25
|
+
*
|
|
26
|
+
FROM
|
|
27
|
+
#{self.table_name}
|
|
28
|
+
SQL
|
|
29
|
+
.first.map(&:to_sym)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.count(params)
|
|
33
|
+
where(params).count
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.destroy_all(params)
|
|
37
|
+
self.where(params).each(&:destroy)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.finalize!
|
|
41
|
+
# make sure to call finalize! at the end of any class that inherits from WORMY::Base
|
|
42
|
+
columns.each do |col|
|
|
43
|
+
define_method(col) { attributes[col] }
|
|
44
|
+
define_method("#{col}=") { |new_attr| attributes[col] = new_attr }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.find(id)
|
|
49
|
+
result = DBConnection.execute(<<-SQL, id)
|
|
50
|
+
SELECT
|
|
51
|
+
*
|
|
52
|
+
FROM
|
|
53
|
+
#{table_name}
|
|
54
|
+
WHERE
|
|
55
|
+
#{table_name}.id = ?
|
|
56
|
+
SQL
|
|
57
|
+
|
|
58
|
+
parse_all(result).first
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.first
|
|
62
|
+
all.first
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.last
|
|
66
|
+
all.last
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.parse_all(results)
|
|
70
|
+
# create new instances of self from the query results
|
|
71
|
+
results.map { |options| self.new(options) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.table_name
|
|
75
|
+
@table_name ||= self.to_s.tableize
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.table_name=(table_name)
|
|
79
|
+
@table_name = table_name
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.validates(*methods)
|
|
83
|
+
@validations = methods
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.validations
|
|
87
|
+
@validations ||= []
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def attributes
|
|
91
|
+
@attributes ||= {}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def attribute_values
|
|
95
|
+
# look at all the columns and get the values for this instance
|
|
96
|
+
self.class.columns.map { |col| self.send(col) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def initialize(params = {})
|
|
100
|
+
params.each do |key, value|
|
|
101
|
+
raise "unknown attribute '#{key}'" unless self.class.columns.include?(key.to_sym)
|
|
102
|
+
send("#{key}=", value)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def destroy
|
|
107
|
+
if self.class.find(id)
|
|
108
|
+
DBConnection.execute(<<-SQL)
|
|
109
|
+
DELETE
|
|
110
|
+
FROM
|
|
111
|
+
#{self.class.table_name}
|
|
112
|
+
WHERE
|
|
113
|
+
id = #{id}
|
|
114
|
+
SQL
|
|
115
|
+
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def insert
|
|
121
|
+
col_names = self.class.columns.join(", ")
|
|
122
|
+
question_marks = ["?"] * self.class.columns.count
|
|
123
|
+
|
|
124
|
+
DBConnection.execute(<<-SQL, *attribute_values)
|
|
125
|
+
INSERT INTO
|
|
126
|
+
#{self.class.table_name} (#{col_names})
|
|
127
|
+
VALUES
|
|
128
|
+
(#{question_marks.join(',')})
|
|
129
|
+
SQL
|
|
130
|
+
|
|
131
|
+
self.id = DBConnection.last_insert_row_id
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def save
|
|
135
|
+
self.class.validations.each do |method|
|
|
136
|
+
raise "Validation error" unless self.send(method)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# if it alredy exists then update it, otherwise insert it
|
|
140
|
+
id ? update : insert
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def update
|
|
144
|
+
set = self.class.columns.map { |col_name| "#{col_name} = ?" }.drop(1).join(", ")
|
|
145
|
+
|
|
146
|
+
DBConnection.execute(<<-SQL, *attribute_values.rotate)
|
|
147
|
+
UPDATE
|
|
148
|
+
#{self.class.table_name}
|
|
149
|
+
SET
|
|
150
|
+
#{set}
|
|
151
|
+
WHERE
|
|
152
|
+
id = ?
|
|
153
|
+
SQL
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def valid?
|
|
157
|
+
self.class.validations.each do |method|
|
|
158
|
+
return false unless self.send(method)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require_relative 'searchable'
|
|
2
|
+
require 'active_support/inflector'
|
|
3
|
+
|
|
4
|
+
class AssociationOptions
|
|
5
|
+
attr_accessor(
|
|
6
|
+
:foreign_key,
|
|
7
|
+
:class_name,
|
|
8
|
+
:primary_key
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def model_class
|
|
12
|
+
class_name.constantize
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def table_name
|
|
16
|
+
model_class.table_name
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class BelongsToOptions < AssociationOptions
|
|
21
|
+
def initialize(name, options = {})
|
|
22
|
+
@foreign_key = options[:foreign_key] || "#{name.to_s.underscore}_id".to_sym
|
|
23
|
+
@primary_key = options[:primary_key] || :id
|
|
24
|
+
@class_name = options[:class_name] || name.to_s.camelcase
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class HasManyOptions < AssociationOptions
|
|
29
|
+
def initialize(name, self_class_name, options = {})
|
|
30
|
+
@foreign_key = options[:foreign_key] || "#{self_class_name.to_s.underscore}_id".to_sym
|
|
31
|
+
@primary_key = options[:primary_key] || :id
|
|
32
|
+
@class_name = options[:class_name] || name.to_s.singularize.camelcase
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module Associatable
|
|
37
|
+
def association_options
|
|
38
|
+
@association_options ||= {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def belongs_to(name, options = {})
|
|
42
|
+
options = BelongsToOptions.new(name, options)
|
|
43
|
+
|
|
44
|
+
define_method(name) do
|
|
45
|
+
key_value = self.send(options.foreign_key)
|
|
46
|
+
options.model_class.where(options.primary_key => key_value).first
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
self.association_options[name] = options
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def has_many(name, options = {})
|
|
53
|
+
options = HasManyOptions.new(name, self, options)
|
|
54
|
+
|
|
55
|
+
define_method(name) do
|
|
56
|
+
options.model_class.where(options.foreign_key => self.id)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
self.association_options[name] = options
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def has_one_through(association_name, through:, source:)
|
|
63
|
+
define_method(association_name) do
|
|
64
|
+
through_options = self.class.association_options[through]
|
|
65
|
+
source_options = through_options.model_class.association_options[source]
|
|
66
|
+
|
|
67
|
+
source_table = source_options.table_name
|
|
68
|
+
through_table = through_options.table_name
|
|
69
|
+
key_value = self.send(through_options.foreign_key)
|
|
70
|
+
|
|
71
|
+
results = WORMY::DBConnection.execute(<<-SQL, key_value)
|
|
72
|
+
SELECT
|
|
73
|
+
#{source_table}.*
|
|
74
|
+
FROM
|
|
75
|
+
#{through_table}
|
|
76
|
+
JOIN
|
|
77
|
+
#{source_table}
|
|
78
|
+
ON
|
|
79
|
+
#{through_table}.#{source_options.foreign_key} = #{source_table}.#{source_options.primary_key}
|
|
80
|
+
WHERE
|
|
81
|
+
#{through_table}.#{through_options.primary_key} = ?
|
|
82
|
+
SQL
|
|
83
|
+
|
|
84
|
+
source_options.model_class.parse_all(results).first
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def has_many_through(association_name, through:, source:)
|
|
89
|
+
define_method(association_name) do
|
|
90
|
+
through_options = self.class.association_options[through]
|
|
91
|
+
source_options = through_options.model_class.association_options[source]
|
|
92
|
+
|
|
93
|
+
source_table = source_options.table_name
|
|
94
|
+
through_table = through_options.table_name
|
|
95
|
+
|
|
96
|
+
results = WORMY::DBConnection.execute(<<-SQL, self.id)
|
|
97
|
+
SELECT
|
|
98
|
+
#{source_table}.*
|
|
99
|
+
FROM
|
|
100
|
+
#{through_table}
|
|
101
|
+
JOIN
|
|
102
|
+
#{source_table}
|
|
103
|
+
ON
|
|
104
|
+
#{source_table}.#{source_options.foreign_key} = #{through_table}.#{source_options.primary_key}
|
|
105
|
+
WHERE
|
|
106
|
+
#{through_table}.#{through_options.foreign_key} = ?
|
|
107
|
+
SQL
|
|
108
|
+
|
|
109
|
+
source_options.model_class.parse_all(results)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'sqlite3'
|
|
2
|
+
|
|
3
|
+
PRINT_QUERIES = ENV['PRINT_QUERIES'] == 'true'
|
|
4
|
+
ROOT_FOLDER = File.join(File.dirname(__FILE__), '..')
|
|
5
|
+
|
|
6
|
+
module WORMY
|
|
7
|
+
class DBConnection
|
|
8
|
+
attr_accessor :seed_file, :db_file
|
|
9
|
+
|
|
10
|
+
def self.open(db_file_name)
|
|
11
|
+
@db = SQLite3::Database.new(db_file_name)
|
|
12
|
+
@db.results_as_hash = true
|
|
13
|
+
@db.type_translation = true
|
|
14
|
+
|
|
15
|
+
@db
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.reset
|
|
19
|
+
commands = [
|
|
20
|
+
"rm '#{DB_FILE}'",
|
|
21
|
+
"cat '#{SQL_FILE}' | sqlite3 '#{DB_FILE}'"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
commands.each { |command| `#{command}` }
|
|
25
|
+
DBConnection.open(DB_FILE)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.instance
|
|
29
|
+
reset if @db.nil?
|
|
30
|
+
|
|
31
|
+
@db
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.execute(*args)
|
|
35
|
+
print_query(*args)
|
|
36
|
+
instance.execute(*args)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.execute2(*args)
|
|
40
|
+
print_query(*args)
|
|
41
|
+
instance.execute2(*args)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.last_insert_row_id
|
|
45
|
+
instance.last_insert_row_id
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def configure(seed_file, db_file)
|
|
50
|
+
@seed_file = seed_file
|
|
51
|
+
@db_file = db_file
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def self.print_query(query, *interpolation_args)
|
|
57
|
+
return unless PRINT_QUERIES
|
|
58
|
+
|
|
59
|
+
puts '--------------------'
|
|
60
|
+
puts query
|
|
61
|
+
unless interpolation_args.empty?
|
|
62
|
+
puts "interpolate: #{interpolation_args.inspect}"
|
|
63
|
+
end
|
|
64
|
+
puts '--------------------'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative 'db_connection'
|
|
2
|
+
require 'wormy'
|
|
3
|
+
|
|
4
|
+
module Searchable
|
|
5
|
+
def where(params = {})
|
|
6
|
+
return all if params == {}
|
|
7
|
+
|
|
8
|
+
search_results = []
|
|
9
|
+
where_line = params.keys.map { |key| "#{key} = ?" }.join(" AND ")
|
|
10
|
+
|
|
11
|
+
results = WORMY::DBConnection.execute(<<-SQL, *params.values)
|
|
12
|
+
SELECT
|
|
13
|
+
*
|
|
14
|
+
FROM
|
|
15
|
+
#{table_name}
|
|
16
|
+
WHERE
|
|
17
|
+
#{where_line}
|
|
18
|
+
SQL
|
|
19
|
+
|
|
20
|
+
results.each { |result| search_results << self.new(result) }
|
|
21
|
+
|
|
22
|
+
search_results
|
|
23
|
+
end
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wormy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mallory Bulkley
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-11-04 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A lightweight Object-Relational Mapping (ORM) library for Ruby. Allows
|
|
14
|
+
you to keep code DRY and easily perform database operations in an object-oriented
|
|
15
|
+
manner.
|
|
16
|
+
email: mallorybulkley@gmail.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/wormy.rb
|
|
23
|
+
- lib/wormy/associatable.rb
|
|
24
|
+
- lib/wormy/db_connection.rb
|
|
25
|
+
- lib/wormy/searchable.rb
|
|
26
|
+
homepage: https://github.com/mallorybulkley/WORM
|
|
27
|
+
licenses:
|
|
28
|
+
- MIT
|
|
29
|
+
metadata: {}
|
|
30
|
+
post_install_message:
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0'
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubyforge_project:
|
|
46
|
+
rubygems_version: 2.6.12
|
|
47
|
+
signing_key:
|
|
48
|
+
specification_version: 4
|
|
49
|
+
summary: Lightweight Ruby ORM
|
|
50
|
+
test_files: []
|