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