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.
@@ -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: []