gazebo 0.1.2
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 +9 -0
- data/bin/gazebo +6 -0
- data/lib/actioncondor/actioncondor.rb +17 -0
- data/lib/actioncondor/controller_base.rb +111 -0
- data/lib/actioncondor/flash.rb +31 -0
- data/lib/actioncondor/session.rb +29 -0
- data/lib/activeleopard/activeleopard.rb +21 -0
- data/lib/activeleopard/assoc_options.rb +64 -0
- data/lib/activeleopard/db_connection.rb +136 -0
- data/lib/activeleopard/errors.rb +5 -0
- data/lib/activeleopard/modules/associatable.rb +86 -0
- data/lib/activeleopard/modules/searchable.rb +65 -0
- data/lib/activeleopard/modules/validatable.rb +25 -0
- data/lib/activeleopard/query_clauses/all_clauses.rb +7 -0
- data/lib/activeleopard/query_clauses/from_clause.rb +11 -0
- data/lib/activeleopard/query_clauses/group_clause.rb +12 -0
- data/lib/activeleopard/query_clauses/join_clause.rb +46 -0
- data/lib/activeleopard/query_clauses/limit_clause.rb +15 -0
- data/lib/activeleopard/query_clauses/order_clause.rb +12 -0
- data/lib/activeleopard/query_clauses/select_clause.rb +18 -0
- data/lib/activeleopard/query_clauses/where_clause.rb +61 -0
- data/lib/activeleopard/relation.rb +106 -0
- data/lib/activeleopard/sql_object.rb +213 -0
- data/lib/auto_loader.rb +31 -0
- data/lib/cli.rb +16 -0
- data/lib/gazebo.rb +53 -0
- data/lib/router.rb +78 -0
- data/lib/show_exceptions.rb +28 -0
- data/lib/static_asset_server.rb +57 -0
- data/lib/templates/rescue.html.erb +18 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a7e1c03436e96b489059927089d42de8a98f07ff
|
4
|
+
data.tar.gz: f01dc87cbb489db33f283a2467a5f884992ad235
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2ac34da08d89d7f35e323af6eda2800c72187d66d8a0207bafe65d73591b771b57549031dd5f6720c2f831cede9fc990ae8442b824d8031082cb6c6f8c373d8c
|
7
|
+
data.tar.gz: 61aff4beade614102cef92417327088d6f7abe458c30caca8e363f539b72851c344e6d9f7beb5de5d84f06cc7210dee1ab053cb2809696183efa6768a6002cae
|
data/README.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# Gazebo
|
2
|
+
|
3
|
+
A light-weight MVC framework inspired by Rails.
|
4
|
+
|
5
|
+
## ActiveLeopard
|
6
|
+
An ORM with many of the features of Rails ActiveRecord at a fraction of the overhead.
|
7
|
+
|
8
|
+
## ActionCondor
|
9
|
+
A Controller Base Class that is combined with a custom router and asset server to handle requests and build responses.
|
data/bin/gazebo
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'active_support/inflector'
|
4
|
+
require 'erb'
|
5
|
+
require 'byebug'
|
6
|
+
require 'rack'
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
require_relative 'controller_base'
|
10
|
+
require_relative 'flash'
|
11
|
+
require_relative 'session'
|
12
|
+
|
13
|
+
class ActionCondor
|
14
|
+
end
|
15
|
+
|
16
|
+
class ActionCondor::Base < ControllerBase
|
17
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
class ControllerBase
|
2
|
+
attr_reader :req, :res, :params, :token, :flash
|
3
|
+
|
4
|
+
|
5
|
+
def self.protect_from_forgery
|
6
|
+
@@protect_from_forgery = true
|
7
|
+
end
|
8
|
+
|
9
|
+
# Setup the controller
|
10
|
+
def initialize(req, res, route_params = {})
|
11
|
+
@req = req
|
12
|
+
@res = res
|
13
|
+
@params = route_params.merge(req.params)
|
14
|
+
@@protect_from_forgery ||= false
|
15
|
+
end
|
16
|
+
|
17
|
+
def flash
|
18
|
+
@flash ||= Flash.new(req)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Helper method to alias @already_built_response
|
22
|
+
def already_built_response?
|
23
|
+
@already_built_response || false
|
24
|
+
end
|
25
|
+
|
26
|
+
# Set the response status code and header
|
27
|
+
def redirect_to(url)
|
28
|
+
check_for_repeat_action!
|
29
|
+
res.status = 302
|
30
|
+
res['location'] = url
|
31
|
+
|
32
|
+
session.store_session(res)
|
33
|
+
flash.store_flash(res)
|
34
|
+
@already_built_response = true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Populate the response with content.
|
38
|
+
# Set the response's content type to the given type.
|
39
|
+
# Raise an error if the developer tries to double render.
|
40
|
+
def render_content(content, content_type)
|
41
|
+
check_for_repeat_action!
|
42
|
+
res['Content-Type'] = content_type
|
43
|
+
res.write(content)
|
44
|
+
|
45
|
+
session.store_session(res)
|
46
|
+
flash.store_flash(res)
|
47
|
+
@already_built_response = true
|
48
|
+
end
|
49
|
+
|
50
|
+
#raise error if already_built_response
|
51
|
+
def check_for_repeat_action!
|
52
|
+
raise "Cannot call render/redirect twice in one action" if already_built_response?
|
53
|
+
end
|
54
|
+
|
55
|
+
# use ERB and binding to evaluate templates
|
56
|
+
# pass the rendered html to render_content
|
57
|
+
def render(template_name)
|
58
|
+
controller_name = self.class.to_s.underscore[0..-("_controller".length + 1)]
|
59
|
+
file_path = "app/views/#{controller_name}/#{template_name}.html.erb"
|
60
|
+
file_content = File.read(file_path)
|
61
|
+
|
62
|
+
application = File.read("app/views/layout/application.html.erb")
|
63
|
+
application.sub!(/__YIELD__/, file_content)
|
64
|
+
|
65
|
+
content = ERB.new(application).result(binding)
|
66
|
+
render_content(content, 'text/html')
|
67
|
+
end
|
68
|
+
|
69
|
+
# method exposing a `Session` object
|
70
|
+
def session
|
71
|
+
@session ||= Session.new(req)
|
72
|
+
end
|
73
|
+
|
74
|
+
# use this with the router to call action_name (:index, :show, :create...)
|
75
|
+
def invoke_action(name)
|
76
|
+
if protect_from_forgery? && req.request_method != 'GET'
|
77
|
+
check_authenticity_token
|
78
|
+
else
|
79
|
+
form_authenticity_token
|
80
|
+
end
|
81
|
+
|
82
|
+
send(name)
|
83
|
+
render(name) unless already_built_response?
|
84
|
+
end
|
85
|
+
|
86
|
+
def protect_from_forgery?
|
87
|
+
@@protect_from_forgery
|
88
|
+
end
|
89
|
+
|
90
|
+
def check_authenticity_token
|
91
|
+
cookie = req.cookies['authenticity_token']
|
92
|
+
unless cookie && cookie == params['authenticity_token']
|
93
|
+
raise "Invalid authenticity token"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def form_authenticity_token
|
98
|
+
@token ||= generate_authenticity_token
|
99
|
+
res.set_cookie(
|
100
|
+
'authenticity_token',
|
101
|
+
path: '/',
|
102
|
+
value: token
|
103
|
+
)
|
104
|
+
|
105
|
+
@token
|
106
|
+
end
|
107
|
+
|
108
|
+
def generate_authenticity_token
|
109
|
+
SecureRandom.urlsafe_base64(16)
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Flash
|
2
|
+
def initialize(req)
|
3
|
+
@flash = {}
|
4
|
+
|
5
|
+
app_cookie = req.cookies['_gazebo_app_flash']
|
6
|
+
|
7
|
+
@flash_now = app_cookie ? JSON.parse(app_cookie) : {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def now
|
11
|
+
@flash_now
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
@flash[key.to_s] || @flash_now[key.to_s] || @flash_now[key]
|
16
|
+
end
|
17
|
+
|
18
|
+
def []=(key, val)
|
19
|
+
@flash[key.to_s] = val
|
20
|
+
end
|
21
|
+
|
22
|
+
# serialize the hash into json and save in a cookie
|
23
|
+
# add to the responses cookies
|
24
|
+
def store_flash(res)
|
25
|
+
res.set_cookie(
|
26
|
+
'_gazebo_app_flash',
|
27
|
+
path: '/',
|
28
|
+
value: @flash.to_json
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Session
|
2
|
+
# find the cookie for this app
|
3
|
+
# deserialize the cookie into a hash
|
4
|
+
attr_reader :cookie
|
5
|
+
|
6
|
+
def initialize(req)
|
7
|
+
app_cookie = req.cookies['_gazebo_app']
|
8
|
+
|
9
|
+
@cookie = app_cookie ? JSON.parse(app_cookie) : {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
@cookie[key.to_s]
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(key, val)
|
17
|
+
@cookie[key.to_s] = val
|
18
|
+
end
|
19
|
+
|
20
|
+
# serialize the hash into json and save in a cookie
|
21
|
+
# add to the responses cookies
|
22
|
+
def store_session(res)
|
23
|
+
res.set_cookie(
|
24
|
+
'_gazebo_app',
|
25
|
+
path: '/',
|
26
|
+
value: @cookie.to_json
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
require_relative 'query_clauses/all_clauses'
|
5
|
+
require_relative 'modules/associatable'
|
6
|
+
require_relative 'modules/validatable'
|
7
|
+
require_relative 'modules/searchable'
|
8
|
+
require_relative 'assoc_options'
|
9
|
+
require_relative 'db_connection'
|
10
|
+
require_relative 'sql_object'
|
11
|
+
require_relative 'relation'
|
12
|
+
require_relative 'errors'
|
13
|
+
|
14
|
+
class ActiveLeopard
|
15
|
+
end
|
16
|
+
|
17
|
+
class ActiveLeopard::Base < SQLObject
|
18
|
+
extend Associatable
|
19
|
+
extend Searchable
|
20
|
+
extend Validatable
|
21
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class AssocOptions
|
2
|
+
attr_accessor(
|
3
|
+
:foreign_key,
|
4
|
+
:class_name,
|
5
|
+
:primary_key
|
6
|
+
)
|
7
|
+
|
8
|
+
def model_class
|
9
|
+
class_name.constantize
|
10
|
+
end
|
11
|
+
|
12
|
+
def table_name
|
13
|
+
model_class.table_name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class BelongsToOptions < AssocOptions
|
18
|
+
def initialize(name, options = {})
|
19
|
+
name = name.to_s.singularize
|
20
|
+
|
21
|
+
defaults = {
|
22
|
+
foreign_key: ("#{name.underscore}_id").to_sym,
|
23
|
+
class_name: name.camelcase,
|
24
|
+
primary_key: :id
|
25
|
+
}
|
26
|
+
|
27
|
+
defaults.merge(options).each do |option, opt_name|
|
28
|
+
send("#{option}=", opt_name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def own_join_column
|
33
|
+
foreign_key
|
34
|
+
end
|
35
|
+
|
36
|
+
def other_join_column
|
37
|
+
primary_key
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class HasManyOptions < AssocOptions
|
42
|
+
def initialize(name, self_class_name, options = {})
|
43
|
+
name = name.to_s.singularize
|
44
|
+
self_class_name = self_class_name.to_s.singularize
|
45
|
+
|
46
|
+
defaults = {
|
47
|
+
foreign_key: ("#{self_class_name.underscore}_id").to_sym,
|
48
|
+
class_name: name.camelcase,
|
49
|
+
primary_key: :id
|
50
|
+
}
|
51
|
+
|
52
|
+
defaults.merge(options).each do |option, opt_name|
|
53
|
+
send("#{option}=", opt_name)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def own_join_column
|
58
|
+
primary_key
|
59
|
+
end
|
60
|
+
|
61
|
+
def other_join_column
|
62
|
+
foreign_key
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'pg'
|
2
|
+
PRINT_QUERIES = true
|
3
|
+
|
4
|
+
class DBConnection
|
5
|
+
def self.open
|
6
|
+
if ENV['DATABASE_URL']
|
7
|
+
self.open_production
|
8
|
+
else
|
9
|
+
self.open_development
|
10
|
+
end
|
11
|
+
run_migrations
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.open_production
|
15
|
+
uri = URI.parse(ENV['DATABASE_URL'])
|
16
|
+
|
17
|
+
@db = PG::Connection.new(
|
18
|
+
user: uri.user,
|
19
|
+
password: uri.password,
|
20
|
+
host: uri.host,
|
21
|
+
port: uri.port,
|
22
|
+
dbname: uri.path[1..-1]
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.open_development
|
27
|
+
begin
|
28
|
+
@db = PG::Connection.open(dbname: self.database_name)
|
29
|
+
rescue PG::ConnectionBad => e
|
30
|
+
create_database!
|
31
|
+
retry
|
32
|
+
end
|
33
|
+
|
34
|
+
@db
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.ensure_migrations_table!
|
38
|
+
begin
|
39
|
+
execute("SELECT * FROM migrations")
|
40
|
+
rescue PG::UndefinedTable
|
41
|
+
execute(<<-SQL)
|
42
|
+
CREATE TABLE MIGRATIONS(
|
43
|
+
ID SERIAL PRIMARY KEY NOT NULL,
|
44
|
+
NAME CHAR(50) NOT NULL,
|
45
|
+
CREATED_AT CHAR(50) NOT NULL
|
46
|
+
)
|
47
|
+
SQL
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.run_migrations
|
52
|
+
ensure_migrations_table!
|
53
|
+
migrations = Dir.entries("db/migrations").reject { |fname| fname.start_with?('.') }
|
54
|
+
migrations.sort_by! { |fname| Integer(fname[0..1]) }
|
55
|
+
|
56
|
+
migrations.each do |file_name|
|
57
|
+
migration_name = file_name.match(/\w+/).to_s
|
58
|
+
|
59
|
+
next if migration_name.empty? || already_run?(migration_name)
|
60
|
+
|
61
|
+
file = File.join(Gazebo::ROOT, "db/migrations", file_name)
|
62
|
+
migration_sql = File.read(file)
|
63
|
+
|
64
|
+
execute(migration_sql)
|
65
|
+
|
66
|
+
record_migration!(migration_name)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.record_migration!(migration_name)
|
71
|
+
time = Time.new.strftime("%Y%m%dT%H%M")
|
72
|
+
here_doc = <<-SQL
|
73
|
+
INSERT INTO
|
74
|
+
migrations (name, created_at)
|
75
|
+
VALUES
|
76
|
+
($1, $2)
|
77
|
+
SQL
|
78
|
+
|
79
|
+
execute(here_doc, [migration_name, time])
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.already_run?(migration_name)
|
83
|
+
!!execute(<<-SQL, [migration_name]).first
|
84
|
+
SELECT *
|
85
|
+
FROM migrations
|
86
|
+
WHERE name = $1
|
87
|
+
SQL
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.create_database!
|
91
|
+
master_conn = PG::Connection.connect(dbname: 'postgres')
|
92
|
+
master_conn.exec("CREATE DATABASE #{database_name}")
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.database_name
|
96
|
+
Gazebo::ROOT.split('/').last.gsub("-", "_") + '_development'
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.instance
|
100
|
+
open if @db.nil?
|
101
|
+
|
102
|
+
@db
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.execute(*args)
|
106
|
+
print_query(*args)
|
107
|
+
instance.exec(*args)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.async_exec(*args)
|
111
|
+
print_query(*args)
|
112
|
+
instance.send_query(*args)
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.get_first_row(*args)
|
116
|
+
print_query(*args)
|
117
|
+
instance.exec(*args).first
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def self.random_color
|
123
|
+
[:blue, :light_blue, :red, :green, :yellow].sample
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.print_query(query, bind_params = [])
|
127
|
+
return unless PRINT_QUERIES
|
128
|
+
|
129
|
+
output = query.gsub(/\s+/, ' ')
|
130
|
+
unless bind_params.empty?
|
131
|
+
output += " #{bind_params.inspect}"
|
132
|
+
end
|
133
|
+
|
134
|
+
puts output.colorize(random_color)
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Associatable
|
2
|
+
def belongs_to(name, options = {})
|
3
|
+
options = BelongsToOptions.new(name, options)
|
4
|
+
assoc_options[name] = options
|
5
|
+
|
6
|
+
define_method(name) do
|
7
|
+
foreign_key_val = self.send(options.foreign_key)
|
8
|
+
return nil if foreign_key_val.nil?
|
9
|
+
|
10
|
+
options
|
11
|
+
.model_class
|
12
|
+
.where(options.primary_key => foreign_key_val)
|
13
|
+
.first
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def has_many(name, options = {})
|
18
|
+
options = HasManyOptions.new(name, self, options)
|
19
|
+
assoc_options[name] = options
|
20
|
+
|
21
|
+
define_method(name) do
|
22
|
+
primary_key_val = self.send(options.primary_key)
|
23
|
+
|
24
|
+
options.model_class
|
25
|
+
.where(options.foreign_key => primary_key_val)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def assoc_options
|
30
|
+
@assoc_options ||= {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def has_one_through(name, through_name, source_name)
|
34
|
+
through_options = assoc_options[through_name]
|
35
|
+
|
36
|
+
define_method(name) do
|
37
|
+
source_options = through_options.model_class.assoc_options[source_name]
|
38
|
+
|
39
|
+
through_table = through_options.model_class.table_name
|
40
|
+
source_table = source_options.model_class.table_name
|
41
|
+
|
42
|
+
datum = DBConnection.execute(<<-SQL).first
|
43
|
+
SELECT
|
44
|
+
#{source_table}.*
|
45
|
+
FROM
|
46
|
+
#{through_table}
|
47
|
+
JOIN
|
48
|
+
#{source_table}
|
49
|
+
ON
|
50
|
+
#{through_table}.#{source_options.foreign_key} =
|
51
|
+
#{source_table}.#{source_options.primary_key}
|
52
|
+
WHERE
|
53
|
+
#{through_table}.#{through_options.primary_key} =
|
54
|
+
#{self.send(through_options.foreign_key)}
|
55
|
+
SQL
|
56
|
+
|
57
|
+
source_options.model_class.new(datum)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def has_many_through(name, through_name, source_name)
|
62
|
+
through_options = assoc_options[through_name]
|
63
|
+
|
64
|
+
define_method(name) do
|
65
|
+
source_options = through_options.model_class.assoc_options[source_name]
|
66
|
+
through_table = through_options.model_class.table_name
|
67
|
+
source_table = source_options.model_class.table_name
|
68
|
+
|
69
|
+
data = DBConnection.execute(<<-SQL)
|
70
|
+
SELECT
|
71
|
+
#{source_table}.*
|
72
|
+
FROM
|
73
|
+
#{through_table}
|
74
|
+
JOIN
|
75
|
+
#{source_table}
|
76
|
+
ON #{through_table}.#{source_options.primary_key} =
|
77
|
+
#{source_table}.#{source_options.foreign_key}
|
78
|
+
WHERE
|
79
|
+
#{through_table}.#{through_options.foreign_key} =
|
80
|
+
#{self.id}
|
81
|
+
SQL
|
82
|
+
|
83
|
+
data.map { |datum| source_options.model_class.new(datum) }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Searchable
|
2
|
+
def find_by(params)
|
3
|
+
where_clause = WhereClause.new([params])
|
4
|
+
|
5
|
+
search_datum = DBConnection.get_first_row(<<-SQL, where_clause.values)
|
6
|
+
SELECT
|
7
|
+
*
|
8
|
+
FROM
|
9
|
+
#{self.table_name}
|
10
|
+
#{where_clause.as_sql}
|
11
|
+
SQL
|
12
|
+
|
13
|
+
search_datum.nil? ? nil : self.new(search_datum)
|
14
|
+
end
|
15
|
+
|
16
|
+
def where(*params)
|
17
|
+
Relation.new(
|
18
|
+
{where: WhereClause.new(params)},
|
19
|
+
self
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def find(id)
|
24
|
+
search_datum = DBConnection.get_first_row(<<-SQL)
|
25
|
+
SELECT
|
26
|
+
*
|
27
|
+
FROM
|
28
|
+
#{self.table_name}
|
29
|
+
WHERE
|
30
|
+
id = #{id}
|
31
|
+
SQL
|
32
|
+
raise RecordNotFound, "This record does not exist" if search_datum.nil?
|
33
|
+
self.new(search_datum)
|
34
|
+
end
|
35
|
+
|
36
|
+
def joins(association, _ = nil)
|
37
|
+
options = self.assoc_options[association]
|
38
|
+
|
39
|
+
Relation.new(
|
40
|
+
{join: JoinOptions.new(options, self.table_name)},
|
41
|
+
self
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def select(*params)
|
46
|
+
Relation.new(
|
47
|
+
{select: SelectClause.new(params)},
|
48
|
+
self
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def group(group_attr)
|
53
|
+
Relation.new(
|
54
|
+
{group: GroupClause.new(group_attr)},
|
55
|
+
self
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def order(ordering_attr)
|
60
|
+
Relation.new(
|
61
|
+
{order: OrderClause.new(ordering_attr)},
|
62
|
+
self
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Validatable
|
2
|
+
def validates(attribute, options)
|
3
|
+
method_name = "validate_#{attribute}"
|
4
|
+
|
5
|
+
define_method(method_name) do
|
6
|
+
attr_val = self.send(attribute)
|
7
|
+
|
8
|
+
if options[:presence]
|
9
|
+
if attr_val.nil?
|
10
|
+
errors[attribute] << "can't be blank"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
if options[:uniqueness]
|
15
|
+
matching_obj = self.class.find_by(attribute => attr_val)
|
16
|
+
|
17
|
+
unless matching_obj.nil? || matching_obj.id == self.id
|
18
|
+
errors[attribute] << "must be unique"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
self.validations << method_name
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative '../assoc_options'
|
2
|
+
|
3
|
+
class JoinClause
|
4
|
+
def initialize(assoc_options, source_table)
|
5
|
+
unless assoc_options
|
6
|
+
raise InvalidInput, "Argument must be an association(type: symbol)"
|
7
|
+
end
|
8
|
+
|
9
|
+
@assoc_options = assoc_options
|
10
|
+
@source_table = source_table
|
11
|
+
end
|
12
|
+
|
13
|
+
def other_table
|
14
|
+
assoc_options.table_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def on_clause
|
18
|
+
"#{source_table}.#{assoc_options.own_join_column}" +
|
19
|
+
" = " + "#{other_table}.#{assoc_options.other_join_column}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def as_sql
|
23
|
+
"JOIN #{other_table} ON #{on_clause} "
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :assoc_options, :source_table
|
27
|
+
end
|
28
|
+
|
29
|
+
class JoinOptions
|
30
|
+
attr_reader :clauses
|
31
|
+
|
32
|
+
def initialize(assoc_options = nil, source_table = nil)
|
33
|
+
@clauses = []
|
34
|
+
if assoc_options && source_table
|
35
|
+
@clauses << JoinClause.new(assoc_options, source_table)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def as_sql
|
40
|
+
clauses.map(&:as_sql).join(" \n ")
|
41
|
+
end
|
42
|
+
|
43
|
+
def append(assoc_options, source_table)
|
44
|
+
clauses << JoinClause.new(assoc_options, source_table)
|
45
|
+
end
|
46
|
+
end
|