laris 0.0.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 +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: []
|