laris 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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`
@@ -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,8 @@
1
+ require_relative 'controller/controller_base'
2
+ require_relative 'controller/csrf'
3
+ require_relative 'controller/flash'
4
+ require_relative 'controller/session'
5
+
6
+ class ControllerBase
7
+ include CSRF
8
+ 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,15 @@
1
+ <h2>WTF!</h2>
2
+
3
+ <h4><%= CGI::escapeHTML(message) %></h4>
4
+
5
+ <pre>
6
+ <%= code_preview %>
7
+ </pre>
8
+
9
+ <ul>
10
+ <% backtrace.each do |line| %>
11
+ <li>
12
+ <%= CGI::escapeHTML(line) %><br>
13
+ </li>
14
+ <% end %>
15
+ </ul>
@@ -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
@@ -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: []