grails-mvc 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.
@@ -0,0 +1,19 @@
1
+ class AttrAccessorObject
2
+ def self.my_attr_accessor(*names)
3
+ # ...
4
+
5
+
6
+ names.each do |name|
7
+ define_method(name) do
8
+ instance_variable_get("@#{name}")
9
+ end
10
+
11
+ define_method("#{name}=") do |*args|
12
+ args.each do |arg|
13
+ instance_variable_set("@#{name}", arg)
14
+ end
15
+ end
16
+ # AttrAccessorObject.instance_variable_set(name)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,121 @@
1
+ require_relative 'db_connection'
2
+ require 'active_support/inflector'
3
+ require_relative '02_searchable'
4
+
5
+
6
+ module GrailedORM
7
+ class Base
8
+ extend GrailedORM::Searchable
9
+ extend GrailedORM::Associatable
10
+
11
+
12
+ def self.columns
13
+ @columns ||= DBConnection.execute2(<<-SQL).first.map(&:to_sym)
14
+ SELECT
15
+ *
16
+ FROM
17
+ #{self.table_name}
18
+ SQL
19
+ end
20
+
21
+ def self.finalize!
22
+ columns.each do |column|
23
+ define_method("#{column}=") { |val| attributes[column] = val }
24
+
25
+ define_method(column) { attributes[column]}
26
+ end
27
+ end
28
+
29
+ def self.table_name=(table_name)
30
+ @table_name = table_name.tableize
31
+ end
32
+
33
+ def self.table_name
34
+ @table_name ||= self.to_s.downcase.pluralize
35
+ end
36
+
37
+ def self.all
38
+ results = DBConnection.execute(<<-SQL)
39
+ SELECT
40
+ *
41
+ FROM
42
+ #{self.table_name}
43
+ SQL
44
+
45
+ parse_all(results)
46
+ end
47
+
48
+ def self.parse_all(results)
49
+ results.map do |optionshash|
50
+ self.new(optionshash)
51
+ end
52
+ end
53
+
54
+ def self.find(id)
55
+ result = DBConnection.execute(<<-SQL, id)
56
+ SELECT
57
+ *
58
+ FROM
59
+ #{self.table_name}
60
+ WHERE
61
+ id = ?
62
+ LIMIT 1
63
+ SQL
64
+ parse_all(result).first
65
+ end
66
+
67
+ def initialize(params = {})
68
+ params.each do |attri, val|
69
+ if !self.class.columns.include?(attri.to_sym)
70
+ raise "unknown attribute '#{attri}'"
71
+ end
72
+ self.send("#{attri}=", val)
73
+ end
74
+ end
75
+
76
+ def attributes
77
+ @attributes ||= {}
78
+ end
79
+
80
+ def attribute_values
81
+ self.class.columns.map { |col| self.send(col) }
82
+ end
83
+
84
+ def insert
85
+ columns = self.class.columns.drop(1)
86
+ col_names = columns.map(&:to_s).join(", ")
87
+ questionmarks = (["?"] * columns.length).join(", ")
88
+
89
+ DBConnection.execute(<<-SQL, *attribute_values.drop(1))
90
+ INSERT INTO
91
+ #{self.class.table_name} (#{col_names})
92
+ VALUES
93
+ (#{questionmarks})
94
+ SQL
95
+ self.id = DBConnection.last_insert_row_id
96
+ end
97
+
98
+ def update
99
+ columns = self.class.columns.drop(1)
100
+ col_names = columns.map { |name| "#{name.to_s} = ?" }.join(", ")
101
+
102
+
103
+ DBConnection.execute(<<-SQL, *attribute_values.drop(1), self.id)
104
+ UPDATE
105
+ #{self.class.table_name}
106
+ SET
107
+ #{col_names}
108
+ WHERE
109
+ id = ?
110
+ SQL
111
+ end
112
+
113
+ def save
114
+ if self.id.nil?
115
+ insert
116
+ else
117
+ update
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,60 @@
1
+ require 'sqlite3'
2
+
3
+ PRINT_QUERIES = ENV['PRINT_QUERIES'] == 'true'
4
+ # https://tomafro.net/2010/01/tip-relative-paths-with-file-expand-path
5
+ ROOT_FOLDER = File.join(File.dirname(__FILE__), '..')
6
+ CATS_SQL_FILE = File.join(ROOT_FOLDER, 'cats.sql')
7
+ CATS_DB_FILE = File.join(ROOT_FOLDER, 'cats.db')
8
+
9
+ class DBConnection
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 '#{CATS_DB_FILE}'",
21
+ "cat '#{CATS_SQL_FILE}' | sqlite3 '#{CATS_DB_FILE}'"
22
+ ]
23
+
24
+ commands.each { |command| `#{command}` }
25
+ DBConnection.open(CATS_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
+ private
49
+
50
+ def self.print_query(query, *interpolation_args)
51
+ return unless PRINT_QUERIES
52
+
53
+ puts '--------------------'
54
+ puts query
55
+ unless interpolation_args.empty?
56
+ puts "interpolate: #{interpolation_args.inspect}"
57
+ end
58
+ puts '--------------------'
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'db_connection'
2
+ require_relative '01_sql_object'
3
+
4
+
5
+ module GrailedORM
6
+ module Searchable
7
+ def where(params)
8
+ cols = params.keys
9
+ vals = params.values
10
+ where = cols.map { |name| "#{name.to_s} = ?"}.join(" AND ")
11
+ result = DBConnection.execute(<<-SQL, vals)
12
+ SELECT
13
+ *
14
+ FROM
15
+ #{self.table_name}
16
+ WHERE
17
+ #{where}
18
+ SQL
19
+ self.parse_all(result)
20
+ end
21
+
22
+ def find_by(params)
23
+ results = where(params)
24
+ results.first
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,79 @@
1
+ require '04_associatable2'
2
+
3
+ describe 'Associatable' do
4
+ before(:each) { DBConnection.reset }
5
+ after(:each) { DBConnection.reset }
6
+
7
+ before(:all) do
8
+ class Cat < SQLObject
9
+ belongs_to :human, foreign_key: :owner_id
10
+
11
+ finalize!
12
+ end
13
+
14
+ class Human < SQLObject
15
+ self.table_name = 'humans'
16
+
17
+ has_many :cats, foreign_key: :owner_id
18
+ belongs_to :house
19
+
20
+ finalize!
21
+ end
22
+
23
+ class House < SQLObject
24
+ has_many :humans
25
+
26
+ finalize!
27
+ end
28
+ end
29
+
30
+ describe '::assoc_options' do
31
+ it 'defaults to empty hash' do
32
+ class TempClass < SQLObject
33
+ end
34
+
35
+ expect(TempClass.assoc_options).to eq({})
36
+ end
37
+
38
+ it 'stores `belongs_to` options' do
39
+ cat_assoc_options = Cat.assoc_options
40
+ human_options = cat_assoc_options[:human]
41
+
42
+ expect(human_options).to be_instance_of(BelongsToOptions)
43
+ expect(human_options.foreign_key).to eq(:owner_id)
44
+ expect(human_options.class_name).to eq('Human')
45
+ expect(human_options.primary_key).to eq(:id)
46
+ end
47
+
48
+ it 'stores options separately for each class' do
49
+ expect(Cat.assoc_options).to have_key(:human)
50
+ expect(Human.assoc_options).to_not have_key(:human)
51
+
52
+ expect(Human.assoc_options).to have_key(:house)
53
+ expect(Cat.assoc_options).to_not have_key(:house)
54
+ end
55
+ end
56
+
57
+ describe '#has_one_through' do
58
+ before(:all) do
59
+ class Cat
60
+ has_one_through :home, :human, :house
61
+
62
+ self.finalize!
63
+ end
64
+ end
65
+
66
+ let(:cat) { Cat.find(1) }
67
+
68
+ it 'adds getter method' do
69
+ expect(cat).to respond_to(:home)
70
+ end
71
+
72
+ it 'fetches associated `home` for a `Cat`' do
73
+ house = cat.home
74
+
75
+ expect(house).to be_instance_of(House)
76
+ expect(house.address).to eq('26th and Guerrero')
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,165 @@
1
+ require '03_associatable'
2
+
3
+ describe 'AssocOptions' do
4
+ describe 'BelongsToOptions' do
5
+ it 'provides defaults' do
6
+ options = BelongsToOptions.new('house')
7
+
8
+ expect(options.foreign_key).to eq(:house_id)
9
+ expect(options.class_name).to eq('House')
10
+ expect(options.primary_key).to eq(:id)
11
+ end
12
+
13
+ it 'allows overrides' do
14
+ options = BelongsToOptions.new('owner',
15
+ foreign_key: :human_id,
16
+ class_name: 'Human',
17
+ primary_key: :human_id
18
+ )
19
+
20
+ expect(options.foreign_key).to eq(:human_id)
21
+ expect(options.class_name).to eq('Human')
22
+ expect(options.primary_key).to eq(:human_id)
23
+ end
24
+ end
25
+
26
+ describe 'HasManyOptions' do
27
+ it 'provides defaults' do
28
+ options = HasManyOptions.new('cats', 'Human')
29
+
30
+ expect(options.foreign_key).to eq(:human_id)
31
+ expect(options.class_name).to eq('Cat')
32
+ expect(options.primary_key).to eq(:id)
33
+ end
34
+
35
+ it 'allows overrides' do
36
+ options = HasManyOptions.new('cats', 'Human',
37
+ foreign_key: :owner_id,
38
+ class_name: 'Kitten',
39
+ primary_key: :human_id
40
+ )
41
+
42
+ expect(options.foreign_key).to eq(:owner_id)
43
+ expect(options.class_name).to eq('Kitten')
44
+ expect(options.primary_key).to eq(:human_id)
45
+ end
46
+ end
47
+
48
+ describe 'AssocOptions' do
49
+ before(:all) do
50
+ class Cat < SQLObject
51
+ self.finalize!
52
+ end
53
+
54
+ class Human < SQLObject
55
+ self.table_name = 'humans'
56
+
57
+ self.finalize!
58
+ end
59
+ end
60
+
61
+ it '#model_class returns class of associated object' do
62
+ options = BelongsToOptions.new('human')
63
+ expect(options.model_class).to eq(Human)
64
+
65
+ options = HasManyOptions.new('cats', 'Human')
66
+ expect(options.model_class).to eq(Cat)
67
+ end
68
+
69
+ it '#table_name returns table name of associated object' do
70
+ options = BelongsToOptions.new('human')
71
+ expect(options.table_name).to eq('humans')
72
+
73
+ options = HasManyOptions.new('cats', 'Human')
74
+ expect(options.table_name).to eq('cats')
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'Associatable' do
80
+ before(:each) { DBConnection.reset }
81
+ after(:each) { DBConnection.reset }
82
+
83
+ before(:all) do
84
+ class Cat < SQLObject
85
+ belongs_to :human, foreign_key: :owner_id
86
+
87
+ finalize!
88
+ end
89
+
90
+ class Human < SQLObject
91
+ self.table_name = 'humans'
92
+
93
+ has_many :cats, foreign_key: :owner_id
94
+ belongs_to :house
95
+
96
+ finalize!
97
+ end
98
+
99
+ class House < SQLObject
100
+ has_many :humans
101
+
102
+ finalize!
103
+ end
104
+ end
105
+
106
+ describe '#belongs_to' do
107
+ let(:breakfast) { Cat.find(1) }
108
+ let(:devon) { Human.find(1) }
109
+
110
+ it 'fetches `human` from `Cat` correctly' do
111
+ expect(breakfast).to respond_to(:human)
112
+ human = breakfast.human
113
+
114
+ expect(human).to be_instance_of(Human)
115
+ expect(human.fname).to eq('Devon')
116
+ end
117
+
118
+ it 'fetches `house` from `Human` correctly' do
119
+ expect(devon).to respond_to(:house)
120
+ house = devon.house
121
+
122
+ expect(house).to be_instance_of(House)
123
+ expect(house.address).to eq('26th and Guerrero')
124
+ end
125
+
126
+ it 'returns nil if no associated object' do
127
+ stray_cat = Cat.find(5)
128
+ expect(stray_cat.human).to eq(nil)
129
+ end
130
+ end
131
+
132
+ describe '#has_many' do
133
+ let(:ned) { Human.find(3) }
134
+ let(:ned_house) { House.find(2) }
135
+
136
+ it 'fetches `cats` from `Human`' do
137
+ expect(ned).to respond_to(:cats)
138
+ cats = ned.cats
139
+
140
+ expect(cats.length).to eq(2)
141
+
142
+ expected_cat_names = %w(Haskell Markov)
143
+ 2.times do |i|
144
+ cat = cats[i]
145
+
146
+ expect(cat).to be_instance_of(Cat)
147
+ expect(cat.name).to eq(expected_cat_names[i])
148
+ end
149
+ end
150
+
151
+ it 'fetches `humans` from `House`' do
152
+ expect(ned_house).to respond_to(:humans)
153
+ humans = ned_house.humans
154
+
155
+ expect(humans.length).to eq(1)
156
+ expect(humans[0]).to be_instance_of(Human)
157
+ expect(humans[0].fname).to eq('Ned')
158
+ end
159
+
160
+ it 'returns an empty array if no associated items' do
161
+ catless_human = Human.find(4)
162
+ expect(catless_human.cats).to eq([])
163
+ end
164
+ end
165
+ end