trails-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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.gitmodules +0 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +74 -0
  8. data/Rakefile +2 -0
  9. data/bin/console +14 -0
  10. data/bin/server.rb +31 -0
  11. data/bin/setup +8 -0
  12. data/bin/trails +4 -0
  13. data/dynamic-archive/.rspec +2 -0
  14. data/dynamic-archive/.ruby-version +1 -0
  15. data/dynamic-archive/Gemfile +6 -0
  16. data/dynamic-archive/Gemfile.lock +38 -0
  17. data/dynamic-archive/README.md +40 -0
  18. data/dynamic-archive/lib/associatable.rb +145 -0
  19. data/dynamic-archive/lib/base.rb +143 -0
  20. data/dynamic-archive/lib/db_connection.rb +151 -0
  21. data/dynamic-archive/lib/searchable.rb +39 -0
  22. data/dynamic-archive/lib/validatable.rb +71 -0
  23. data/dynamic-archive/spec/associatable_spec.rb +294 -0
  24. data/dynamic-archive/spec/base_spec.rb +252 -0
  25. data/dynamic-archive/spec/searchable_spec.rb +44 -0
  26. data/dynamic-archive/spec/validatable_spec.rb +81 -0
  27. data/dynamic-archive/testing_database/database.db +0 -0
  28. data/dynamic-archive/testing_database/database.rb +6 -0
  29. data/dynamic-archive/testing_database/migrations/bats_migration.sql +7 -0
  30. data/dynamic-archive/testing_database/migrations/cats_migration.sql +7 -0
  31. data/dynamic-archive/testing_database/migrations/houses_migration.sql +4 -0
  32. data/dynamic-archive/testing_database/migrations/humans_migration.sql +8 -0
  33. data/dynamic-archive/testing_database/migrations/play_times_migration.sql +8 -0
  34. data/dynamic-archive/testing_database/migrations/toys_migration.sql +4 -0
  35. data/dynamic-archive/testing_database/schema.sql +44 -0
  36. data/dynamic-archive/testing_database/seed.sql +44 -0
  37. data/lib/asset_server.rb +28 -0
  38. data/lib/cli.rb +109 -0
  39. data/lib/controller_base.rb +78 -0
  40. data/lib/exception_handler.rb +14 -0
  41. data/lib/flash.rb +24 -0
  42. data/lib/router.rb +61 -0
  43. data/lib/session.rb +28 -0
  44. data/lib/trails.rb +30 -0
  45. data/lib/version.rb +3 -0
  46. data/public/assets/application.css +231 -0
  47. data/public/assets/application.js +9 -0
  48. data/template/Gemfile +1 -0
  49. data/template/README.md +0 -0
  50. data/template/app/controllers/application_controller.rb +2 -0
  51. data/template/app/controllers/static_controller.rb +4 -0
  52. data/template/app/views/layouts/application.html.erb +15 -0
  53. data/template/app/views/static/root.html.erb +73 -0
  54. data/template/config/database.rb +6 -0
  55. data/template/config/routes.rb +17 -0
  56. data/template/db/database.db +0 -0
  57. data/template/db/database.sql +0 -0
  58. data/template/db/schema.sql +0 -0
  59. data/template/db/seed.sql +0 -0
  60. data/template/public/assets/application.css +1 -0
  61. data/template/public/assets/application.js +1 -0
  62. data/trails-mvc.gemspec +42 -0
  63. metadata +257 -0
@@ -0,0 +1,151 @@
1
+ require 'sqlite3'
2
+
3
+ PRINT_QUERIES = ENV['PRINT_QUERIES'] == 'true'
4
+ MIGRATION_FILES = Dir.glob("#{ENV['MIGRATIONS_FOLDER']}/*.sql").to_a
5
+
6
+ class DBConnection
7
+ def self.open(db_file_name)
8
+ options = {
9
+ results_as_hash: true,
10
+ type_translation: true
11
+ }
12
+ @db = SQLite3::Database.new(db_file_name, options)
13
+
14
+ @db
15
+ end
16
+
17
+ def self.db
18
+ @db
19
+ end
20
+
21
+ def self.reset
22
+ `rm '#{ENV['DB_FILE']}'`
23
+ `rm '#{ENV['SCHEMA_FILE']}'`
24
+ self.open(ENV['DB_FILE'])
25
+
26
+ self.migrate
27
+ self.seed
28
+ end
29
+
30
+ def self.instance
31
+ self.open(ENV['DB_FILE']) if @db.nil?
32
+
33
+ @db
34
+ end
35
+
36
+ def self.execute(*args)
37
+ print_query(*args)
38
+ instance.execute(*args)
39
+ end
40
+
41
+ def self.execute2(*args)
42
+ print_query(*args)
43
+ instance.execute2(*args)
44
+ end
45
+
46
+ def self.execute_batch(statement)
47
+ print_query(statement)
48
+ instance.execute_batch(statement)
49
+ end
50
+
51
+ def self.last_insert_row_id
52
+ instance.last_insert_row_id
53
+ end
54
+
55
+ def self.migrate(resetting = false)
56
+ self.ensure_migrations_table
57
+ already_migrated = self.previous_migrations
58
+
59
+ MIGRATION_FILES.each do |migration_file|
60
+ migration_file_name = (/\/(\w+)\.sql$/).match(migration_file)[1]
61
+ next if already_migrated.include?(migration_file_name)
62
+
63
+ migration = IO.read(migration_file)
64
+ self.execute(migration)
65
+ `echo '#{migration}' >> '#{ENV['SCHEMA_FILE']}'`
66
+ self.record_migration(migration_file_name)
67
+ end
68
+
69
+ @db
70
+ end
71
+
72
+ def self.reset_schema
73
+ `rm '#{ENV['SCHEMA_FILE']}'`
74
+ self.execute(<<-SQL)
75
+ DROP TABLE IF EXISTS migrations
76
+ SQL
77
+ `touch '#{ENV['SCHEMA_FILE']}'`
78
+ end
79
+
80
+ def self.seed
81
+ seed_data = IO.read(ENV['SEED_FILE'])
82
+ self.execute_batch(seed_data)
83
+ end
84
+
85
+ private
86
+
87
+ def self.record_migration(file_name)
88
+ self.execute(<<-SQL, file_name)
89
+ INSERT INTO
90
+ migrations (file_name)
91
+ VALUES
92
+ (?)
93
+ SQL
94
+ end
95
+
96
+ def self.ensure_migrations_table
97
+ self.execute(<<-SQL)
98
+ CREATE TABLE IF NOT EXISTS migrations (
99
+ id INTEGER PRIMARY KEY,
100
+ file_name VARCHAR(255) NOT NULL
101
+ );
102
+ SQL
103
+ end
104
+
105
+ def self.previous_migrations
106
+ ensure_migrations_table
107
+ migrations = self.execute(<<-SQL)
108
+ SELECT
109
+ file_name
110
+ FROM
111
+ migrations
112
+ SQL
113
+
114
+ migrations.map do |migration|
115
+ migration["file_name"]
116
+ end
117
+ end
118
+
119
+ def self.create_table(table_name, columns = {})
120
+ column_text = columns.map { |name, options| "#{name} #{options}" }.join(", ")
121
+ self.execute(<<-SQL)
122
+ CREATE TABLE #{table_name} (
123
+ id INTEGER PRIMARY KEY,
124
+ #{column_text}
125
+ );
126
+ SQL
127
+ end
128
+
129
+ def self.add_column(table_name, column_name, column_type)
130
+ self.execute(<<-SQL)
131
+ ALTER TABLE #{table_name} ADD #{column_name} #{column_type};
132
+ SQL
133
+ end
134
+
135
+ def self.remove_column(table_name, column_name)
136
+ self.execute(<<-SQL)
137
+ ALTER TABLE #{table_name} DROP COLUMN #{column_name};
138
+ SQL
139
+ end
140
+
141
+ def self.print_query(query, *interpolation_args)
142
+ return unless PRINT_QUERIES
143
+
144
+ puts '--------------------'
145
+ puts query
146
+ unless interpolation_args.empty?
147
+ puts "interpolate: #{interpolation_args.inspect}"
148
+ end
149
+ puts '--------------------'
150
+ end
151
+ end
@@ -0,0 +1,39 @@
1
+ module DynamicArchive
2
+ module Searchable
3
+ def where(*args)
4
+ if args[0].is_a?(Hash)
5
+ # without 'LIKE', query cannot find ids correctly
6
+ where_line = args[0].map { |key, v| "#{key} LIKE ?"}.join(" AND ")
7
+ results = DBConnection.execute(<<-SQL, args[0].values)
8
+ SELECT
9
+ *
10
+ FROM
11
+ #{self.table_name}
12
+ WHERE
13
+ #{where_line}
14
+ SQL
15
+ else
16
+ results = sql_query_where(args)
17
+ end
18
+
19
+ self.parse_all(results)
20
+ end
21
+
22
+ def sql_query_where(args)
23
+ where_line = args[0]
24
+ DBConnection.execute(<<-SQL, args[1..-1])
25
+ SELECT
26
+ *
27
+ FROM
28
+ #{self.table_name}
29
+ WHERE
30
+ #{where_line}
31
+ SQL
32
+ end
33
+
34
+ def find_by(params)
35
+ results = where(params)
36
+ results.first
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,71 @@
1
+ module DynamicArchive
2
+ module Validatable
3
+ def validates(*args)
4
+ validated_attrs = []
5
+ options = {}
6
+ args.each do |arg|
7
+ if arg.is_a?(Hash)
8
+ options = arg
9
+ else
10
+ validated_attrs << arg
11
+ end
12
+ end
13
+
14
+ validated_attrs.each do |attr|
15
+ validations[attr] = validations[attr].merge(options)
16
+ end
17
+ end
18
+
19
+ def is_valid?(instance)
20
+ instance.errors.clear
21
+ valid = true
22
+ columns.each do |attr|
23
+ validations[attr].each do |validation, value|
24
+ validated = send(validation, instance, attr, value)
25
+ unless validated
26
+ instance.errors << "#{attr} did not pass #{validation} validation"
27
+ valid = false
28
+ end
29
+ end
30
+ end
31
+
32
+ valid
33
+ end
34
+
35
+ def length(instance, attr, value)
36
+ min = value[:minimum] || 0
37
+ max = value[:maximum]
38
+ if max
39
+ instance.send(attr).length.between?(min, max)
40
+ else
41
+ instance.send(attr).length >= min
42
+ end
43
+ end
44
+
45
+ def presence(instance, attr, value)
46
+ if value
47
+ !!instance.send(attr)
48
+ else
49
+ !instance.send(attr)
50
+ end
51
+ end
52
+
53
+ def uniqueness(instance, attr, value)
54
+ if value == true
55
+ if instance.id
56
+ return !instance.class
57
+ .where("#{attr} = (?) AND id != (?)", instance.send(attr), instance.id).first
58
+ else
59
+ return !instance.class
60
+ .where({ attr => instance.send(attr) }).first
61
+ end
62
+ end
63
+
64
+ true
65
+ end
66
+
67
+ def validations
68
+ @validations ||= Hash.new { |h, k| h[k] = {} }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,294 @@
1
+ require_relative '../testing_database/database'
2
+ require 'associatable'
3
+
4
+ describe 'AssocOptions' do
5
+ describe 'DynamicArchive::BelongsToOptions' do
6
+ it 'provides defaults' do
7
+ options = DynamicArchive::BelongsToOptions.new('house')
8
+
9
+ expect(options.foreign_key).to eq(:house_id)
10
+ expect(options.class_name).to eq('House')
11
+ expect(options.primary_key).to eq(:id)
12
+ end
13
+
14
+ it 'allows overrides' do
15
+ options = DynamicArchive::BelongsToOptions.new('owner',
16
+ foreign_key: :human_id,
17
+ class_name: 'Human',
18
+ primary_key: :human_id
19
+ )
20
+
21
+ expect(options.foreign_key).to eq(:human_id)
22
+ expect(options.class_name).to eq('Human')
23
+ expect(options.primary_key).to eq(:human_id)
24
+ end
25
+ end
26
+
27
+ describe 'DynamicArchive::HasManyOptions' do
28
+ it 'provides defaults' do
29
+ options = DynamicArchive::HasManyOptions.new('cats', 'Human')
30
+
31
+ expect(options.foreign_key).to eq(:human_id)
32
+ expect(options.class_name).to eq('Cat')
33
+ expect(options.primary_key).to eq(:id)
34
+ end
35
+
36
+ it 'allows overrides' do
37
+ options = DynamicArchive::HasManyOptions.new('cats', 'Human',
38
+ foreign_key: :owner_id,
39
+ class_name: 'Kitten',
40
+ primary_key: :human_id
41
+ )
42
+
43
+ expect(options.foreign_key).to eq(:owner_id)
44
+ expect(options.class_name).to eq('Kitten')
45
+ expect(options.primary_key).to eq(:human_id)
46
+ end
47
+ end
48
+
49
+ describe 'AssocOptions' do
50
+ before(:all) do
51
+ class Cat < DynamicArchive::Base
52
+ self.finalize!
53
+ end
54
+
55
+ class Human < DynamicArchive::Base
56
+ self.table_name = 'humans'
57
+
58
+ self.finalize!
59
+ end
60
+ end
61
+
62
+ it '#model_class returns class of associated object' do
63
+ options = DynamicArchive::BelongsToOptions.new('human')
64
+ expect(options.model_class).to eq(Human)
65
+
66
+ options = DynamicArchive::HasManyOptions.new('cats', 'Human')
67
+ expect(options.model_class).to eq(Cat)
68
+ end
69
+
70
+ it '#table_name returns table name of associated object' do
71
+ options = DynamicArchive::BelongsToOptions.new('human')
72
+ expect(options.table_name).to eq('humans')
73
+
74
+ options = DynamicArchive::HasManyOptions.new('cats', 'Human')
75
+ expect(options.table_name).to eq('cats')
76
+ end
77
+ end
78
+ end
79
+
80
+ describe 'Associatable' do
81
+ before(:each) { DBConnection.reset }
82
+ after(:each) { DBConnection.reset }
83
+
84
+ before(:all) do
85
+ class Cat < DynamicArchive::Base
86
+ belongs_to :human, foreign_key: :owner_id
87
+ has_many :play_times
88
+
89
+ finalize!
90
+ end
91
+
92
+ class Human < DynamicArchive::Base
93
+ self.table_name = 'humans'
94
+
95
+ has_many :cats, foreign_key: :owner_id
96
+ belongs_to :house
97
+
98
+ finalize!
99
+ end
100
+
101
+ class House < DynamicArchive::Base
102
+ has_many :humans
103
+
104
+ finalize!
105
+ end
106
+
107
+ class Toy < DynamicArchive::Base
108
+ has_many :play_times
109
+ end
110
+
111
+ class PlayTime < DynamicArchive::Base
112
+ belongs_to :toy
113
+ belongs_to :cat
114
+ end
115
+ end
116
+
117
+ describe '#belongs_to' do
118
+ let(:breakfast) { Cat.find(1) }
119
+ let(:devon) { Human.find(1) }
120
+
121
+ it 'fetches `human` from `Cat` correctly' do
122
+ expect(breakfast).to respond_to(:human)
123
+ human = breakfast.human
124
+
125
+ expect(human).to be_instance_of(Human)
126
+ expect(human.fname).to eq('Devon')
127
+ end
128
+
129
+ it 'fetches `house` from `Human` correctly' do
130
+ expect(devon).to respond_to(:house)
131
+ house = devon.house
132
+
133
+ expect(house).to be_instance_of(House)
134
+ expect(house.address).to eq('26th and Guerrero')
135
+ end
136
+
137
+ it 'returns nil if no associated object' do
138
+ stray_cat = Cat.find(5)
139
+ expect(stray_cat.human).to eq(nil)
140
+ end
141
+ end
142
+
143
+ describe '#has_many' do
144
+ let(:ned) { Human.find(3) }
145
+ let(:ned_house) { House.find(2) }
146
+
147
+ it 'fetches `cats` from `Human`' do
148
+ expect(ned).to respond_to(:cats)
149
+ cats = ned.cats
150
+
151
+ expect(cats.length).to eq(2)
152
+
153
+ expected_cat_names = %w(Haskell Markov)
154
+ 2.times do |i|
155
+ cat = cats[i]
156
+
157
+ expect(cat).to be_instance_of(Cat)
158
+ expect(cat.name).to eq(expected_cat_names[i])
159
+ end
160
+ end
161
+
162
+ it 'fetches `humans` from `House`' do
163
+ expect(ned_house).to respond_to(:humans)
164
+ humans = ned_house.humans
165
+
166
+ expect(humans.length).to eq(1)
167
+ expect(humans[0]).to be_instance_of(Human)
168
+ expect(humans[0].fname).to eq('Ned')
169
+ end
170
+
171
+ it 'returns an empty array if no associated items' do
172
+ catless_human = Human.find(4)
173
+ expect(catless_human.cats).to eq([])
174
+ end
175
+ end
176
+
177
+ describe '::assoc_options' do
178
+ it 'defaults to empty hash' do
179
+ class TempClass < DynamicArchive::Base
180
+ end
181
+
182
+ expect(TempClass.assoc_options).to eq({})
183
+ end
184
+
185
+ it 'stores `belongs_to` options' do
186
+ cat_assoc_options = Cat.assoc_options
187
+ human_options = cat_assoc_options[:human]
188
+
189
+ expect(human_options).to be_instance_of(DynamicArchive::BelongsToOptions)
190
+ expect(human_options.foreign_key).to eq(:owner_id)
191
+ expect(human_options.class_name).to eq('Human')
192
+ expect(human_options.primary_key).to eq(:id)
193
+ end
194
+
195
+ it 'stores options separately for each class' do
196
+ expect(Cat.assoc_options).to have_key(:human)
197
+ expect(Human.assoc_options).to_not have_key(:human)
198
+
199
+ expect(Human.assoc_options).to have_key(:house)
200
+ expect(Cat.assoc_options).to_not have_key(:house)
201
+ end
202
+ end
203
+
204
+ describe '#has_many_through' do
205
+ before(:all) do
206
+ class House
207
+ has_many_through :cats, :humans, :cats
208
+
209
+ self.finalize!
210
+ end
211
+
212
+ class Toy
213
+ has_many_through :cats, :play_times, :cat
214
+
215
+ self.finalize!
216
+ end
217
+
218
+ class Cat
219
+ has_many_through :toys, :play_times, :toy
220
+
221
+ self.finalize!
222
+ end
223
+ end
224
+
225
+ let(:house) { House.find(1) }
226
+ let(:breakfast) { Cat.find(1) }
227
+ let(:stringy) { Toy.find(2) }
228
+
229
+ it 'adds getter method' do
230
+ expect(house).to respond_to(:cats)
231
+ end
232
+
233
+ it 'fetches associated `cats` for a `house`' do
234
+ cats = house.cats
235
+
236
+ expect(cats).to be_instance_of(Array)
237
+ expect(cats.length).to eq(2)
238
+ expect(cats[0]).to be_instance_of(Cat)
239
+
240
+ expect(cats[0].name).to eq('Breakfast')
241
+ expect(cats[1].name).to eq('Earl')
242
+ end
243
+
244
+ it 'fetches associated `toys` for a `cat`' do
245
+ toys = breakfast.toys
246
+
247
+ expect(toys).to be_instance_of(Array)
248
+ expect(toys.length).to eq(3)
249
+ expect(toys[0]).to be_instance_of(Toy)
250
+
251
+ expect(toys[0].name).to eq('fuzzy')
252
+ expect(toys[1].name).to eq('stringy')
253
+ end
254
+
255
+ it 'fetches associated `cats` for a `toy`' do
256
+ cats = stringy.cats
257
+
258
+ expect(cats).to be_instance_of(Array)
259
+ expect(cats.length).to eq(2)
260
+ expect(cats[0]).to be_instance_of(Cat)
261
+
262
+ expect(cats[0].name).to eq('Breakfast')
263
+ expect(cats[1].name).to eq('Earl')
264
+ end
265
+ end
266
+
267
+ describe '#has_one_through' do
268
+ before(:all) do
269
+ class Cat
270
+ has_one_through :home, :human, :house
271
+
272
+ self.finalize!
273
+ end
274
+ end
275
+
276
+ let(:cat) { Cat.find(1) }
277
+ let(:cat2) { Cat.find(4) }
278
+
279
+ it 'adds getter method' do
280
+ expect(cat).to respond_to(:home)
281
+ end
282
+
283
+ it 'fetches associated `home` for a `Cat`' do
284
+ house = cat.home
285
+
286
+ expect(house).to be_instance_of(House)
287
+ expect(house.address).to eq('26th and Guerrero')
288
+
289
+ house2 = cat2.home
290
+
291
+ expect(house2.address).to eq('Dolores and Market')
292
+ end
293
+ end
294
+ end