laris 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +101 -0
- data/lib/laris.rb +41 -0
- data/lib/laris/asset_server.rb +30 -0
- data/lib/laris/autoloader.rb +34 -0
- data/lib/laris/controller.rb +8 -0
- data/lib/laris/controller/controller_base.rb +90 -0
- data/lib/laris/controller/csrf.rb +24 -0
- data/lib/laris/controller/flash.rb +39 -0
- data/lib/laris/controller/session.rb +21 -0
- data/lib/laris/exception_view.html.erb +15 -0
- data/lib/laris/exception_viewer.rb +60 -0
- data/lib/laris/larisrecord.rb +15 -0
- data/lib/laris/larisrecord/associatable.rb +101 -0
- data/lib/laris/larisrecord/db_connection.rb +105 -0
- data/lib/laris/larisrecord/larisrecord_base.rb +113 -0
- data/lib/laris/larisrecord/relation.rb +85 -0
- data/lib/laris/larisrecord/searchable.rb +43 -0
- data/lib/laris/router.rb +61 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 38b0b2681b75e2aad93d4c3c678131d46b08f61c
|
4
|
+
data.tar.gz: d64820d9c71788474c8dadd23172a555c6b1aff6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: db0c1085ad333187cc821e260151e6a49bd3e3802253042609fc866d76fc18cd759859e1c780428ef97a770d2acd2c9c976db99bb97230cf9d98f84ff2efe1c1
|
7
|
+
data.tar.gz: 94df81f213657b6233ec2ecd7af02f0b039bada81bdc430e6d272e161cb4b3cdf2361da4ee7fc6974bfc19786e64bc8bd1b646d9a4cda32833ae726f59ba24b7
|
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
#Laris
|
2
|
+
|
3
|
+
A lightweight MVC framework inspired by Ruby on Rails.
|
4
|
+
|
5
|
+
To see it in action, check out my minesweeper game: [live][minesweeper] • [github][minesweeper-github]
|
6
|
+
|
7
|
+
[minesweeper]: http://minesweepers.herokuapp.com
|
8
|
+
[minesweeper-github]: http://github.com/composerinteralia/minesweeper/
|
9
|
+
|
10
|
+
##Getting Started
|
11
|
+
* `gem install laris`
|
12
|
+
* Put sql files in `db/migrations/` numbered in the order you want them to be
|
13
|
+
executed (NB I need to add a rake task for migrating. At the moment you will
|
14
|
+
need to run `DBConnection.migrate` yourself)
|
15
|
+
* Create models in app/models/
|
16
|
+
|
17
|
+
```rb
|
18
|
+
# app/models/post.rb
|
19
|
+
|
20
|
+
class Post < LarisrecordBase
|
21
|
+
belongs_to :author, class_name: "User", foreign_key: :user_id
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
* Construct routes using a regex, controller name, and method name
|
26
|
+
|
27
|
+
```rb
|
28
|
+
# config/routes.rb
|
29
|
+
|
30
|
+
ROUTER.draw do
|
31
|
+
get Regexp.new("^/users$"), UsersController, :index
|
32
|
+
get Regexp.new("^/users/new$"), UsersController, :new
|
33
|
+
post Regexp.new("^/users$"), UsersController, :create
|
34
|
+
get Regexp.new("^/users/(?<id>\\d+)$"), UsersController, :show
|
35
|
+
get Regexp.new("^/users/(?<id>\\d+)/edit$"), UsersController, :edit
|
36
|
+
patch Regexp.new("^/users/(?<id>\\d+)$"), UsersController, :update
|
37
|
+
delete Regexp.new("^/users/(?<id>\\d+)$"), UsersController, :destroy
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
* Add controllers in app/controllers/
|
42
|
+
|
43
|
+
```rb
|
44
|
+
# app/controllers/users_controller.rb
|
45
|
+
|
46
|
+
class UsersController < ControllerBase
|
47
|
+
def index
|
48
|
+
@users = User.all
|
49
|
+
|
50
|
+
render :index
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
* Create erb views in app/views/<controller>
|
56
|
+
|
57
|
+
```
|
58
|
+
# app/views/users/index.html.erb
|
59
|
+
|
60
|
+
<ul>
|
61
|
+
<% @users.each do |user| %>
|
62
|
+
<li><%= user.name %></li>
|
63
|
+
<% end %>
|
64
|
+
</ul>
|
65
|
+
```
|
66
|
+
|
67
|
+
* Place any assets in app/assets
|
68
|
+
* You will need a database URL to run locally (yeah, I have work to do). If you
|
69
|
+
are feeling adventurous, you can use your Heroku database URL, but probably you shouldn't...
|
70
|
+
|
71
|
+
```sh
|
72
|
+
export DATABASE_URL=$(heroku config -s | grep DATABASE_URL | sed -e "s/^DATABASE_URL='//" -e "s/'//")
|
73
|
+
```
|
74
|
+
|
75
|
+
* Add these to the root of your project:
|
76
|
+
|
77
|
+
```rb
|
78
|
+
# laris.ru
|
79
|
+
|
80
|
+
require 'laris'
|
81
|
+
Laris.root = File.expand_path(File.dirname(__FILE__))
|
82
|
+
|
83
|
+
require_relative 'config/routes'
|
84
|
+
|
85
|
+
run Laris.app
|
86
|
+
```
|
87
|
+
|
88
|
+
```
|
89
|
+
# Procfile
|
90
|
+
|
91
|
+
web: bundle exec rackup laris.ru -p $PORT
|
92
|
+
```
|
93
|
+
|
94
|
+
* Start up your app with `bundle exec rackup laris.ru` or push to Heroku
|
95
|
+
* You did it!
|
96
|
+
|
97
|
+
## TODO
|
98
|
+
* Database config file
|
99
|
+
* Rake task for migrations
|
100
|
+
* Migrations in Ruby, not raw SQL
|
101
|
+
* `laris new`
|
data/lib/laris.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
require 'active_support/inflector'
|
4
|
+
require 'cgi'
|
5
|
+
require 'erb'
|
6
|
+
require 'json'
|
7
|
+
require 'pg'
|
8
|
+
require 'rack'
|
9
|
+
require 'uri'
|
10
|
+
|
11
|
+
require 'laris/asset_server'
|
12
|
+
require 'laris/autoloader'
|
13
|
+
require 'laris/controller'
|
14
|
+
require 'laris/exception_viewer'
|
15
|
+
require 'laris/larisrecord'
|
16
|
+
require 'laris/router'
|
17
|
+
|
18
|
+
module Laris
|
19
|
+
VERSION = '0.0.0'
|
20
|
+
|
21
|
+
def self.app
|
22
|
+
DBConnection.open
|
23
|
+
|
24
|
+
app = Proc.new do |env|
|
25
|
+
req = Rack::Request.new(env)
|
26
|
+
res = Rack::Response.new
|
27
|
+
Laris::Router.run(req, res)
|
28
|
+
res.finish
|
29
|
+
end
|
30
|
+
|
31
|
+
Rack::Builder.new do
|
32
|
+
use ExceptionViewer
|
33
|
+
use AssetServer
|
34
|
+
run app
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.root=(root)
|
39
|
+
const_set(:ROOT, root)
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class AssetServer
|
2
|
+
attr_reader :app, :res
|
3
|
+
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
@res = Rack::Response.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
req = Rack::Request.new(env)
|
11
|
+
if req.path =~ (/^\/assets/)
|
12
|
+
respond_with_asset(req)
|
13
|
+
else
|
14
|
+
app.call(env)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def respond_with_asset(req)
|
20
|
+
dir_path = File.dirname(__FILE__)
|
21
|
+
path = File.join(Laris::ROOT, "app", req.path)
|
22
|
+
|
23
|
+
ext = File.extname(path)
|
24
|
+
ext = ".json" if ext == ".map"
|
25
|
+
res["Content-Type"] = Rack::Mime::MIME_TYPES[ext]
|
26
|
+
|
27
|
+
res.write(File.read(path))
|
28
|
+
res.finish
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Laris
|
2
|
+
AUTOLOAD_PATHS = [
|
3
|
+
"app/models",
|
4
|
+
"app/controllers",
|
5
|
+
]
|
6
|
+
end
|
7
|
+
|
8
|
+
class Object
|
9
|
+
def self.const_missing(const)
|
10
|
+
auto_load(const)
|
11
|
+
Kernel.const_get(const)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def auto_load(const)
|
16
|
+
Laris::AUTOLOAD_PATHS.each do |folder|
|
17
|
+
file = File.join(Laris::ROOT, folder, const.to_s.underscore)
|
18
|
+
return if try_auto_load(file)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def try_auto_load(file)
|
23
|
+
require_relative(file)
|
24
|
+
return true
|
25
|
+
rescue LoadError
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Module
|
31
|
+
def const_missing(const)
|
32
|
+
Object.const_missing(const)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
class ControllerBase
|
2
|
+
class DoubleRenderError < StandardError
|
3
|
+
def message
|
4
|
+
"Render and/or redirect_to were called multiple times in a single action."
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :req, :res, :params
|
9
|
+
|
10
|
+
def initialize(req, res, params = {})
|
11
|
+
@req = req
|
12
|
+
@res = res
|
13
|
+
@params = params.merge(req.params)
|
14
|
+
|
15
|
+
body = req.body.read
|
16
|
+
if body =~ /^{.*}$/
|
17
|
+
@params.merge!(JSON.parse(body))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def already_built_response?
|
22
|
+
@already_built_response
|
23
|
+
end
|
24
|
+
|
25
|
+
def h(text)
|
26
|
+
CGI::escapeHTML(text)
|
27
|
+
end
|
28
|
+
|
29
|
+
def redirect_to(url)
|
30
|
+
raise DoubleRenderError if already_built_response?
|
31
|
+
|
32
|
+
res['Location'] = url
|
33
|
+
res.status = 302
|
34
|
+
store_cookies(res)
|
35
|
+
|
36
|
+
@already_built_response = true
|
37
|
+
end
|
38
|
+
|
39
|
+
def render_content(content, content_type)
|
40
|
+
raise DoubleRenderError if already_built_response?
|
41
|
+
|
42
|
+
res['Content-Type'] = content_type
|
43
|
+
res.write(content)
|
44
|
+
store_cookies(res)
|
45
|
+
|
46
|
+
@already_built_response = true
|
47
|
+
end
|
48
|
+
|
49
|
+
def render(template_name)
|
50
|
+
path = File.join(
|
51
|
+
Laris::ROOT,
|
52
|
+
'app/views',
|
53
|
+
controller_name.remove("_controller"),
|
54
|
+
"#{template_name}.html.erb",
|
55
|
+
)
|
56
|
+
|
57
|
+
template = File.read(path)
|
58
|
+
content = ERB.new(template).result(binding)
|
59
|
+
|
60
|
+
render_content(content, "text/html")
|
61
|
+
end
|
62
|
+
|
63
|
+
def session
|
64
|
+
@session ||= Session.new(req)
|
65
|
+
end
|
66
|
+
|
67
|
+
def flash
|
68
|
+
@flash ||= Flash.new(req)
|
69
|
+
end
|
70
|
+
|
71
|
+
def invoke_action(name, method)
|
72
|
+
unless method == :get || req.xhr?
|
73
|
+
verify_authenticity
|
74
|
+
end
|
75
|
+
|
76
|
+
self.send(name)
|
77
|
+
render(name) unless already_built_response?
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
def store_cookies(req)
|
82
|
+
set_session_auth_token
|
83
|
+
session.store_session(res)
|
84
|
+
flash.store_flash(res)
|
85
|
+
end
|
86
|
+
|
87
|
+
def controller_name
|
88
|
+
self.class.name.underscore
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module CSRF
|
2
|
+
def form_authenticity_token
|
3
|
+
@token ||= SecureRandom.urlsafe_base64
|
4
|
+
end
|
5
|
+
|
6
|
+
def verify_authenticity
|
7
|
+
unless session_auth_token == form_auth_token
|
8
|
+
raise "Invalid Authenticity Token"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def form_auth_token
|
14
|
+
params['authenticity_token']
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_session_auth_token
|
18
|
+
session['authenticity_token'] = form_authenticity_token
|
19
|
+
end
|
20
|
+
|
21
|
+
def session_auth_token
|
22
|
+
session['authenticity_token']
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Flash
|
2
|
+
attr_reader :stale_cookie, :fresh_cookie
|
3
|
+
|
4
|
+
def initialize(req)
|
5
|
+
raw_cookie = req.cookies['_laris_flash']
|
6
|
+
@stale_cookie = raw_cookie ? JSON.parse(raw_cookie) : {}
|
7
|
+
@fresh_cookie = {}
|
8
|
+
@persistent = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def persistent?
|
12
|
+
@persistent
|
13
|
+
end
|
14
|
+
|
15
|
+
def now
|
16
|
+
@persistent = false
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](key)
|
21
|
+
fresh_cookie.merge(stale_cookie)[key]
|
22
|
+
end
|
23
|
+
|
24
|
+
def []=(key, val)
|
25
|
+
if persistent?
|
26
|
+
fresh_cookie[key] = val
|
27
|
+
else
|
28
|
+
stale_cookie[key] = val
|
29
|
+
end
|
30
|
+
|
31
|
+
@persistent = true
|
32
|
+
val
|
33
|
+
end
|
34
|
+
|
35
|
+
def store_flash(res)
|
36
|
+
new_cookie = { path: '/', value: fresh_cookie.to_json }
|
37
|
+
res.set_cookie('_laris_flash', new_cookie)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Session
|
2
|
+
attr_reader :cookie
|
3
|
+
|
4
|
+
def initialize(req)
|
5
|
+
raw_cookie = req.cookies['_laris_session']
|
6
|
+
@cookie = raw_cookie ? JSON.parse(raw_cookie) : {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
cookie[key]
|
11
|
+
end
|
12
|
+
|
13
|
+
def []=(key, val)
|
14
|
+
cookie[key] = val
|
15
|
+
end
|
16
|
+
|
17
|
+
def store_session(res)
|
18
|
+
new_cookie = { path: '/', value: cookie.to_json }
|
19
|
+
res.set_cookie('_laris_session', new_cookie)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class ExceptionViewer
|
2
|
+
attr_reader :app, :exception, :res
|
3
|
+
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
@res = Rack::Response.new
|
7
|
+
@exception = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
begin
|
12
|
+
app.call(env)
|
13
|
+
rescue => e
|
14
|
+
@exception = e
|
15
|
+
exception_page
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def exception_page
|
21
|
+
res.status = 500
|
22
|
+
res["Content-Type"] = "text/html"
|
23
|
+
res.write(content)
|
24
|
+
res.finish
|
25
|
+
end
|
26
|
+
|
27
|
+
def content
|
28
|
+
template = File.read("#{File.dirname(__FILE__)}/exception_view.html.erb")
|
29
|
+
ERB.new(template).result(binding)
|
30
|
+
end
|
31
|
+
|
32
|
+
def backtrace
|
33
|
+
exception.backtrace
|
34
|
+
end
|
35
|
+
|
36
|
+
def source
|
37
|
+
backtrace.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def message
|
41
|
+
exception.message
|
42
|
+
end
|
43
|
+
|
44
|
+
def code_preview
|
45
|
+
match_data = source.match(/^(.+):(\d+)(:in.+)?$/)
|
46
|
+
file_name, line = match_data.captures
|
47
|
+
|
48
|
+
lines = File.readlines(file_name).map(&:chomp)
|
49
|
+
i = line.to_i - 1
|
50
|
+
|
51
|
+
lines[i] << "<b> <---------- What were you thinking?</b>"
|
52
|
+
|
53
|
+
3.times do
|
54
|
+
i -= 1 if i > 0
|
55
|
+
end
|
56
|
+
|
57
|
+
lines[i, 6].join('<br>')
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'larisrecord/associatable'
|
2
|
+
require_relative 'larisrecord/db_connection'
|
3
|
+
require_relative 'larisrecord/larisrecord_base'
|
4
|
+
require_relative 'larisrecord/relation'
|
5
|
+
require_relative 'larisrecord/searchable'
|
6
|
+
|
7
|
+
class LarisrecordBase
|
8
|
+
extend Associatable
|
9
|
+
extend Searchable
|
10
|
+
end
|
11
|
+
|
12
|
+
TracePoint.new(:end) do |tp|
|
13
|
+
klass = tp.binding.receiver
|
14
|
+
klass.laris_finalize! if klass.respond_to?(:laris_finalize!)
|
15
|
+
end.enable
|
@@ -0,0 +1,101 @@
|
|
1
|
+
class AssocOptions
|
2
|
+
attr_accessor :class_name, :foreign_key, :primary_key
|
3
|
+
|
4
|
+
def model_class
|
5
|
+
class_name.constantize
|
6
|
+
end
|
7
|
+
|
8
|
+
def table_name
|
9
|
+
model_class.table_name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class BelongsToOptions < AssocOptions
|
14
|
+
def initialize(assoc_name, options = {})
|
15
|
+
defaults = {
|
16
|
+
class_name: assoc_name.to_s.camelcase,
|
17
|
+
foreign_key: "#{assoc_name}_id".to_sym,
|
18
|
+
primary_key: :id
|
19
|
+
}
|
20
|
+
|
21
|
+
defaults.each do |attr, default|
|
22
|
+
send("#{attr}=", (options[attr] || default))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class HasManyOptions < AssocOptions
|
28
|
+
def initialize(assoc_name, self_name, options = {})
|
29
|
+
defaults = {
|
30
|
+
class_name: assoc_name.to_s.singularize.camelcase,
|
31
|
+
foreign_key: "#{self_name.underscore}_id".to_sym,
|
32
|
+
primary_key: :id
|
33
|
+
}
|
34
|
+
|
35
|
+
defaults.each do |attr, default|
|
36
|
+
send("#{attr}=", (options[attr] || default))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Associatable
|
42
|
+
def belongs_to(assoc_name, options = {})
|
43
|
+
options = BelongsToOptions.new(assoc_name, options)
|
44
|
+
assoc_options[assoc_name] = options
|
45
|
+
|
46
|
+
define_method(assoc_name) do
|
47
|
+
klass = options.model_class
|
48
|
+
foreign_key_value = send(options.foreign_key)
|
49
|
+
primary_key = options.primary_key
|
50
|
+
|
51
|
+
klass.where(primary_key => foreign_key_value).first
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def has_many(assoc_name, options = {})
|
56
|
+
options = HasManyOptions.new(assoc_name, self.name, options)
|
57
|
+
|
58
|
+
define_method(assoc_name) do
|
59
|
+
klass = options.model_class
|
60
|
+
foreign_key = options.foreign_key
|
61
|
+
primary_key_value = send(options.primary_key)
|
62
|
+
|
63
|
+
klass.where(foreign_key => primary_key_value)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def assoc_options
|
68
|
+
@assoc_options ||= {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def has_one_through(assoc_name, through_name, source_name)
|
72
|
+
define_method(assoc_name) do
|
73
|
+
through_options = assoc_options[through_name]
|
74
|
+
through_klass = through_options.model_class
|
75
|
+
through_table = through_klass.table_name
|
76
|
+
through_fk_value = send(through_options.foreign_key)
|
77
|
+
through_pk = through_options.primary_key
|
78
|
+
|
79
|
+
source_options = through_klass.assoc_options[source_name]
|
80
|
+
source_klass = source_options.model_class
|
81
|
+
source_table = source_klass.table_name
|
82
|
+
source_fk = source_options.foreign_key
|
83
|
+
source_pk = source_options.primary_key
|
84
|
+
|
85
|
+
result = DBConnection.execute(<<-SQL, through_fk_value)
|
86
|
+
SELECT
|
87
|
+
#{source_table}.*
|
88
|
+
FROM
|
89
|
+
#{through_table}
|
90
|
+
JOIN
|
91
|
+
#{source_table}
|
92
|
+
ON
|
93
|
+
#{through_table}.#{source_fk} = #{source_table}.#{source_pk}
|
94
|
+
WHERE
|
95
|
+
#{through_table}.#{through_pk} = ?
|
96
|
+
SQL
|
97
|
+
|
98
|
+
source_klass.new(result.first)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
class DBConnection
|
2
|
+
def self.open
|
3
|
+
uri = URI.parse(ENV['DATABASE_URL'])
|
4
|
+
|
5
|
+
@conn = PG::Connection.new(
|
6
|
+
user: uri.user,
|
7
|
+
password: uri.password,
|
8
|
+
host: uri.host,
|
9
|
+
port: uri.port,
|
10
|
+
dbname: uri.path[1..-1],
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.migrate
|
15
|
+
ensure_migrations_table
|
16
|
+
|
17
|
+
migrations = Dir[File.join(Laris::ROOT, "/db/migrations/*.sql")]
|
18
|
+
migrations.each do |file|
|
19
|
+
filename = file.match(/([\w|-]*)\.sql$/)[1]
|
20
|
+
|
21
|
+
unless migrated_files.include?(filename)
|
22
|
+
instance.exec(File.read(file))
|
23
|
+
instance.exec(<<-SQL)
|
24
|
+
INSERT INTO
|
25
|
+
migrations (filename)
|
26
|
+
VALUES
|
27
|
+
('#{filename}')
|
28
|
+
SQL
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.execute(query, params=[])
|
34
|
+
query = number_placeholders(query)
|
35
|
+
print_query(query, params)
|
36
|
+
res = instance.exec(query, params)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.columns(table_name)
|
40
|
+
cols = instance.exec(<<-SQL)
|
41
|
+
SELECT
|
42
|
+
attname
|
43
|
+
FROM
|
44
|
+
pg_attribute
|
45
|
+
WHERE
|
46
|
+
attrelid = '#{table_name}'::regclass AND
|
47
|
+
attnum > 0 AND
|
48
|
+
NOT attisdropped
|
49
|
+
SQL
|
50
|
+
|
51
|
+
cols.map { |col| col['attname'].to_sym }
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def self.ensure_migrations_table
|
57
|
+
res = instance.exec(<<-SQL)
|
58
|
+
SELECT to_regclass('migrations') AS exists
|
59
|
+
SQL
|
60
|
+
|
61
|
+
unless res[0]['exists']
|
62
|
+
instance.exec(<<-SQL)
|
63
|
+
CREATE TABLE migrations (
|
64
|
+
id SERIAL PRIMARY KEY,
|
65
|
+
filename VARCHAR(255) NOT NULL
|
66
|
+
)
|
67
|
+
SQL
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.instance
|
72
|
+
open if @conn.nil?
|
73
|
+
@conn
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.migrated_files
|
77
|
+
Set.new instance.exec(<<-SQL).values.flatten
|
78
|
+
SELECT
|
79
|
+
filename
|
80
|
+
FROM
|
81
|
+
migrations
|
82
|
+
SQL
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.number_placeholders(query_string)
|
86
|
+
count = 0
|
87
|
+
query_string.chars.map do |char|
|
88
|
+
if char == "?"
|
89
|
+
count += 1
|
90
|
+
"$#{count}"
|
91
|
+
else
|
92
|
+
char
|
93
|
+
end
|
94
|
+
end.join("")
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.print_query(query, interpolation_args)
|
98
|
+
puts '--------------------'
|
99
|
+
puts query
|
100
|
+
unless interpolation_args.empty?
|
101
|
+
puts "interpolate: #{interpolation_args.inspect}"
|
102
|
+
end
|
103
|
+
puts '--------------------'
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class LarisrecordBase
|
2
|
+
def self.columns
|
3
|
+
return @columns if @columns
|
4
|
+
|
5
|
+
@columns = DBConnection.columns(table_name)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.destroy_all
|
9
|
+
all.each { |row| row.destroy }
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.destroy(row)
|
13
|
+
row = find(row) if row.is_a?(Integer)
|
14
|
+
row.destroy
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.laris_finalize!
|
18
|
+
columns.each do |attr_name|
|
19
|
+
define_method(attr_name) do
|
20
|
+
attributes[attr_name]
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method("#{attr_name}=") do |value|
|
24
|
+
attributes[attr_name] = value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.table_name=(table_name)
|
30
|
+
@table_name = table_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.table_name
|
34
|
+
@table_name ||= self.to_s.tableize
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(params = {})
|
38
|
+
params.each do |attr_name, value|
|
39
|
+
if self.class.columns.include?(attr_name.to_sym)
|
40
|
+
send("#{attr_name}=", value)
|
41
|
+
else
|
42
|
+
raise "unknown attribute '#{attr_name}'"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def attributes
|
48
|
+
@attributes ||= {}
|
49
|
+
end
|
50
|
+
|
51
|
+
def attribute_values
|
52
|
+
columns.map { |attr_name| send(attr_name) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def destroy
|
56
|
+
DBConnection.execute(<<-SQL, [id])
|
57
|
+
DELETE FROM
|
58
|
+
#{table_name}
|
59
|
+
WHERE
|
60
|
+
#{table_name}.id = ?
|
61
|
+
SQL
|
62
|
+
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def insert
|
67
|
+
cols = columns.reject { |col| col == :id }
|
68
|
+
col_values = cols.map { |attr_name| send(attr_name) }
|
69
|
+
col_names = cols.join(", ")
|
70
|
+
question_marks = (["?"] * cols.size).join(", ")
|
71
|
+
|
72
|
+
result = DBConnection.execute(<<-SQL, col_values)
|
73
|
+
INSERT INTO
|
74
|
+
#{table_name} (#{col_names})
|
75
|
+
VALUES
|
76
|
+
(#{question_marks})
|
77
|
+
RETURNING id
|
78
|
+
SQL
|
79
|
+
|
80
|
+
self.id = result.first['id']
|
81
|
+
# DBConnection.last_insert_row_id
|
82
|
+
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
def save
|
87
|
+
id ? update : insert
|
88
|
+
end
|
89
|
+
|
90
|
+
def update
|
91
|
+
set_sql = columns.map { |attr_name| "#{attr_name} = ?" }.join(", ")
|
92
|
+
|
93
|
+
result = DBConnection.execute(<<-SQL, attribute_values << id)
|
94
|
+
UPDATE
|
95
|
+
#{table_name}
|
96
|
+
SET
|
97
|
+
#{set_sql}
|
98
|
+
WHERE
|
99
|
+
#{table_name}.id = ?
|
100
|
+
SQL
|
101
|
+
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
def columns
|
107
|
+
self.class.columns
|
108
|
+
end
|
109
|
+
|
110
|
+
def table_name
|
111
|
+
self.class.table_name
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Relation
|
2
|
+
SQL_COMMANDS = {
|
3
|
+
select: 'SELECT',
|
4
|
+
from: 'FROM',
|
5
|
+
joins: 'JOIN',
|
6
|
+
where: 'WHERE',
|
7
|
+
order: 'ORDER BY',
|
8
|
+
limit: 'LIMIT'
|
9
|
+
}
|
10
|
+
|
11
|
+
attr_reader :klass, :sql_clauses
|
12
|
+
attr_accessor :cache, :values
|
13
|
+
|
14
|
+
def initialize(klass, values = [], sql_clauses = {})
|
15
|
+
@klass = klass
|
16
|
+
@values = values
|
17
|
+
@sql_clauses = sql_defaults.merge(sql_clauses)
|
18
|
+
@cache = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(method, *args, &blk)
|
22
|
+
query if cache.nil?
|
23
|
+
cache.send(method, *args, &blk)
|
24
|
+
end
|
25
|
+
|
26
|
+
def reload!
|
27
|
+
query
|
28
|
+
end
|
29
|
+
|
30
|
+
def order(column, direction = 'ASC')
|
31
|
+
sql_clauses[:order] = "#{table_name}.#{column} #{direction}"
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# allows only a single join
|
36
|
+
def joins(table, on = nil)
|
37
|
+
condition = "#{table} ON #{on}"
|
38
|
+
|
39
|
+
sql_clauses[:joins] =
|
40
|
+
[sql_clauses[:joins], condition].compact.join(' JOIN ')
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def limit(n)
|
46
|
+
sql_clauses[:limit] = n
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def where(conditions)
|
51
|
+
new_fragments = conditions.map do |attr_name, value|
|
52
|
+
values << value
|
53
|
+
"#{table_name}.#{attr_name} = ?"
|
54
|
+
end
|
55
|
+
|
56
|
+
where_fragments = new_fragments.unshift(sql_clauses[:where])
|
57
|
+
sql_clauses[:where] = where_fragments.compact.join(" AND ")
|
58
|
+
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def query
|
64
|
+
results = DBConnection.execute(statement, values)
|
65
|
+
self.cache = klass.parse_all(results)
|
66
|
+
end
|
67
|
+
|
68
|
+
def sql_defaults
|
69
|
+
{
|
70
|
+
select: "#{table_name}.*",
|
71
|
+
from: "#{table_name}"
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
def statement
|
76
|
+
clauses = SQL_COMMANDS.map do |keyword, command|
|
77
|
+
"#{command} #{sql_clauses[keyword]}" if sql_clauses[keyword]
|
78
|
+
end
|
79
|
+
clauses.compact.join(" ")
|
80
|
+
end
|
81
|
+
|
82
|
+
def table_name
|
83
|
+
klass.table_name
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Searchable
|
2
|
+
def all
|
3
|
+
Relation.new(self)
|
4
|
+
end
|
5
|
+
|
6
|
+
def find(id)
|
7
|
+
all.where(id: id).limit(1).first
|
8
|
+
end
|
9
|
+
|
10
|
+
def find_by(conditions)
|
11
|
+
all.where(conditions).limit(1).first
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_by_sql(sql, values = [])
|
15
|
+
results = DBConnection.execute(sql, values)
|
16
|
+
parse_all(results)
|
17
|
+
end
|
18
|
+
|
19
|
+
def first
|
20
|
+
all.order(:id).limit(1).first
|
21
|
+
end
|
22
|
+
|
23
|
+
def last
|
24
|
+
all.order(:id, :DESC).limit(1).first
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(method_name, *args)
|
28
|
+
if method_name.to_s.start_with?("find_by_")
|
29
|
+
columns = method_name[8..-1].split('_and_')
|
30
|
+
|
31
|
+
conditions = {}
|
32
|
+
columns.size.times { |i| conditions[columns[i]] = args[i] }
|
33
|
+
|
34
|
+
all.where(conditions).limit(1).first
|
35
|
+
else
|
36
|
+
all.send(method_name, *args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_all(results)
|
41
|
+
results.map { |params| new(params) }
|
42
|
+
end
|
43
|
+
end
|
data/lib/laris/router.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
class Route
|
2
|
+
attr_reader :pattern, :http_method, :controller_class, :action_name
|
3
|
+
|
4
|
+
def initialize(pattern, http_method, controller_class, action_name)
|
5
|
+
@pattern, @http_method, @controller_class, @action_name =
|
6
|
+
pattern, http_method, controller_class, action_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(req)
|
10
|
+
pattern =~ req.path &&
|
11
|
+
http_method == req.request_method.downcase.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(req, res)
|
15
|
+
match_data = pattern.match(req.path)
|
16
|
+
route_params = Hash[match_data.names.zip(match_data.captures)]
|
17
|
+
|
18
|
+
controller = controller_class.new(req, res, route_params)
|
19
|
+
controller.invoke_action(action_name, http_method)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Router
|
24
|
+
attr_reader :routes
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@routes = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_route(pattern, http_method, controller_class, action_name)
|
31
|
+
@routes << Route.new(pattern, http_method, controller_class, action_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def draw(&proc)
|
35
|
+
instance_eval(&proc)
|
36
|
+
end
|
37
|
+
|
38
|
+
[:get, :post, :patch, :delete].each do |http_method|
|
39
|
+
define_method(http_method) do |pattern, controller_class, action_name|
|
40
|
+
add_route(pattern, http_method, controller_class, action_name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def match(req)
|
45
|
+
@routes.find { |route| route.matches?(req) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def run(req, res)
|
49
|
+
route = match(req)
|
50
|
+
|
51
|
+
if route.nil?
|
52
|
+
res.status = 404
|
53
|
+
|
54
|
+
res.write("Oops! The requested URL #{req.path} was not not found!")
|
55
|
+
else
|
56
|
+
route.run(req, res)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Laris::Router = Router.new
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: laris
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Colson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-01-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.13'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.13'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pg
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Laris is a rails-inspired web application framework.
|
84
|
+
email:
|
85
|
+
- danieljamescolson@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- README.md
|
91
|
+
- lib/laris.rb
|
92
|
+
- lib/laris/asset_server.rb
|
93
|
+
- lib/laris/autoloader.rb
|
94
|
+
- lib/laris/controller.rb
|
95
|
+
- lib/laris/controller/controller_base.rb
|
96
|
+
- lib/laris/controller/csrf.rb
|
97
|
+
- lib/laris/controller/flash.rb
|
98
|
+
- lib/laris/controller/session.rb
|
99
|
+
- lib/laris/exception_view.html.erb
|
100
|
+
- lib/laris/exception_viewer.rb
|
101
|
+
- lib/laris/larisrecord.rb
|
102
|
+
- lib/laris/larisrecord/associatable.rb
|
103
|
+
- lib/laris/larisrecord/db_connection.rb
|
104
|
+
- lib/laris/larisrecord/larisrecord_base.rb
|
105
|
+
- lib/laris/larisrecord/relation.rb
|
106
|
+
- lib/laris/larisrecord/searchable.rb
|
107
|
+
- lib/laris/router.rb
|
108
|
+
homepage: https://github.com/composerinteralia/laris
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata: {}
|
112
|
+
post_install_message:
|
113
|
+
rdoc_options: []
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
requirements: []
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 2.5.1
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: Lightweight MVC framework
|
132
|
+
test_files: []
|