panda_frwk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circle.yml +12 -0
- data/.gitignore +10 -0
- data/.gitmodules +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +238 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +245 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/panda.rb +47 -0
- data/lib/panda/base_controller.rb +68 -0
- data/lib/panda/dependencies.rb +6 -0
- data/lib/panda/record/base.rb +107 -0
- data/lib/panda/record/base_helper.rb +84 -0
- data/lib/panda/record/database.rb +15 -0
- data/lib/panda/routing/mapper.rb +34 -0
- data/lib/panda/routing/router.rb +48 -0
- data/lib/panda/utils.rb +14 -0
- data/lib/panda/version.rb +3 -0
- data/panda.gemspec +43 -0
- data/pull_request_template.md +9 -0
- metadata +238 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "panda"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/panda.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require "panda/version"
|
2
|
+
require "panda/utils"
|
3
|
+
require "panda/record/base"
|
4
|
+
require "panda/routing/router"
|
5
|
+
require "panda/routing/mapper"
|
6
|
+
require "panda/base_controller"
|
7
|
+
require "panda/dependencies"
|
8
|
+
|
9
|
+
module Panda
|
10
|
+
class Application
|
11
|
+
attr_reader :routes
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@routes = Routing::Router.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
return [500, {}, []] if env["PATH_INFO"] == "/favicon.ico"
|
19
|
+
request = Rack::Request.new(env)
|
20
|
+
handler = mapper.perform(request)
|
21
|
+
if handler
|
22
|
+
call_controller_action(request, handler[:target])
|
23
|
+
else
|
24
|
+
process_invalid_request(request)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def call_controller_action(request, target)
|
31
|
+
controller = Object.const_get("#{target[0]}Controller")
|
32
|
+
controller.new(request).dispatch(target[1])
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_invalid_request(request)
|
36
|
+
[
|
37
|
+
404,
|
38
|
+
{},
|
39
|
+
["Oops! No route for #{request.request_method} #{request.path_info}"]
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
def mapper
|
44
|
+
@mapper ||= Routing::Mapper.new(routes.endpoints)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "erb"
|
2
|
+
require "tilt"
|
3
|
+
|
4
|
+
module Panda
|
5
|
+
class BaseController
|
6
|
+
attr_reader :request
|
7
|
+
|
8
|
+
def initialize(request)
|
9
|
+
@request ||= request
|
10
|
+
end
|
11
|
+
|
12
|
+
def params
|
13
|
+
request.params
|
14
|
+
end
|
15
|
+
|
16
|
+
def redirect_to(location, status: 301)
|
17
|
+
response([], status, "Location" => location)
|
18
|
+
end
|
19
|
+
|
20
|
+
def response(body, status = 200, header = {})
|
21
|
+
@response = Rack::Response.new(body, status, header)
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_response
|
25
|
+
@response
|
26
|
+
end
|
27
|
+
|
28
|
+
def render(*args)
|
29
|
+
response(render_template(*args))
|
30
|
+
end
|
31
|
+
|
32
|
+
def render_template(view_name, locals = {})
|
33
|
+
layout_template, view_template = layout_view_template(view_name)
|
34
|
+
title = view_name.to_s.tr("_", " ")
|
35
|
+
layout_template.render(self, title: title) do
|
36
|
+
view_template.render(self, locals)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def controller_name
|
41
|
+
self.class.to_s.gsub(/Controller$/, "").to_snake_case
|
42
|
+
end
|
43
|
+
|
44
|
+
def dispatch(action)
|
45
|
+
send(action)
|
46
|
+
render(action) unless get_response
|
47
|
+
get_response
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def layout_view_template(view_name)
|
53
|
+
layout_template = Tilt::ERBTemplate.new(
|
54
|
+
File.join(APP_ROOT, "app", "views", "layouts", "application.html.erb")
|
55
|
+
)
|
56
|
+
view_template = Tilt::ERBTemplate.new(
|
57
|
+
File.join(
|
58
|
+
APP_ROOT,
|
59
|
+
"app",
|
60
|
+
"views",
|
61
|
+
controller_name,
|
62
|
+
"#{view_name}.html.erb"
|
63
|
+
)
|
64
|
+
)
|
65
|
+
[layout_template, view_template]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require_relative "database"
|
2
|
+
require_relative "base_helper"
|
3
|
+
|
4
|
+
module Panda
|
5
|
+
module Record
|
6
|
+
class Base
|
7
|
+
include Record::BaseHelper
|
8
|
+
|
9
|
+
def initialize(attributes = {})
|
10
|
+
attributes.each { |column, value| send("#{column}=", value) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def save
|
14
|
+
query = if id
|
15
|
+
"UPDATE #{model_table} SET " \
|
16
|
+
"#{update_placeholders} WHERE id = ?"
|
17
|
+
else
|
18
|
+
"INSERT INTO #{model_table} (#{current_table_columns})" \
|
19
|
+
" VALUES (#{current_table_placeholders})"
|
20
|
+
end
|
21
|
+
values = id ? record_values << id : record_values
|
22
|
+
Database.execute_query(query, values)
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def update(attributes)
|
27
|
+
attributes.each do |key, value|
|
28
|
+
send("#{key}=", value)
|
29
|
+
end
|
30
|
+
save
|
31
|
+
end
|
32
|
+
|
33
|
+
alias save! save
|
34
|
+
|
35
|
+
def destroy
|
36
|
+
self.class.destroy(id)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.to_table(name)
|
40
|
+
@table = name.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.property(column_name, constraints)
|
44
|
+
@properties ||= {}
|
45
|
+
@properties[column_name] = constraints
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.create_table
|
49
|
+
Database.execute_query(
|
50
|
+
"CREATE TABLE IF NOT EXISTS #{table} " \
|
51
|
+
"(#{column_names_with_constraints.join(', ')})"
|
52
|
+
)
|
53
|
+
build_column_methods
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.create(attributes)
|
57
|
+
model = new(attributes)
|
58
|
+
model.save
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.all
|
63
|
+
Database.execute_query(
|
64
|
+
"SELECT * FROM #{table} ORDER BY id ASC"
|
65
|
+
).map(&method(:get_model_object))
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.find(id)
|
69
|
+
row = Database.execute_query(
|
70
|
+
"SELECT * FROM #{table} WHERE id = ?",
|
71
|
+
id.to_i
|
72
|
+
).first
|
73
|
+
get_model_object(row)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.find_by(option)
|
77
|
+
row = Database.execute_query(
|
78
|
+
"SELECT * FROM #{table} WHERE #{option.keys.first} = ?",
|
79
|
+
option.values.first
|
80
|
+
).first
|
81
|
+
get_model_object(row)
|
82
|
+
end
|
83
|
+
|
84
|
+
[%w(last DESC), %w(first ASC)].each do |method_name_and_order|
|
85
|
+
define_singleton_method((method_name_and_order[0]).to_sym) do
|
86
|
+
row = Database.execute_query(
|
87
|
+
"SELECT * FROM #{table} ORDER BY id " \
|
88
|
+
"#{method_name_and_order[1]} LIMIT 1"
|
89
|
+
).first
|
90
|
+
get_model_object(row) unless row.nil?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.count
|
95
|
+
Database.execute_query("SELECT COUNT (*) FROM #{table}")[0][0]
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.destroy(id)
|
99
|
+
Database.execute_query("DELETE FROM #{table} WHERE id = ?", id)
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.destroy_all
|
103
|
+
Database.execute_query "DELETE FROM #{table}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Panda
|
2
|
+
module Record
|
3
|
+
module BaseHelper
|
4
|
+
def self.included(base)
|
5
|
+
class << base
|
6
|
+
attr_reader :properties, :table
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def column_names_with_constraints
|
11
|
+
name_with_constraints = []
|
12
|
+
properties.each do |column_name, constraints|
|
13
|
+
query_string = []
|
14
|
+
query_string << column_name.to_s
|
15
|
+
parse_constraints(constraints, query_string)
|
16
|
+
name_with_constraints << query_string.join(" ")
|
17
|
+
end
|
18
|
+
name_with_constraints
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_constraints(constraints, query_string)
|
22
|
+
constraints.each do |attribute, value|
|
23
|
+
query_string << send(attribute.to_s, value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_column_methods
|
28
|
+
properties.keys.each(&method(:attr_accessor))
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_model_object(row)
|
32
|
+
return nil unless row
|
33
|
+
model ||= new
|
34
|
+
properties.keys.each_with_index do |key, index|
|
35
|
+
model.send("#{key}=", row[index])
|
36
|
+
end
|
37
|
+
model
|
38
|
+
end
|
39
|
+
|
40
|
+
def type(value)
|
41
|
+
value.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def primary_key(is_primary)
|
45
|
+
"PRIMARY KEY AUTOINCREMENT" if is_primary
|
46
|
+
end
|
47
|
+
|
48
|
+
def nullable(is_null)
|
49
|
+
"NOT NULL" unless is_null
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def model_table
|
57
|
+
self.class.table
|
58
|
+
end
|
59
|
+
|
60
|
+
def current_table_columns
|
61
|
+
columns_without_id.map(&:to_s).join(", ")
|
62
|
+
end
|
63
|
+
|
64
|
+
def current_table_placeholders
|
65
|
+
(["?"] * (self.class.properties.keys.size - 1)).join(", ")
|
66
|
+
end
|
67
|
+
|
68
|
+
def record_values
|
69
|
+
columns_without_id.map(&method(:send))
|
70
|
+
end
|
71
|
+
|
72
|
+
def update_placeholders(columns = self.class.properties.keys)
|
73
|
+
columns.delete(:id)
|
74
|
+
columns.map { |column| "#{column} = ?" }.join(", ")
|
75
|
+
end
|
76
|
+
|
77
|
+
def columns_without_id
|
78
|
+
columns ||= self.class.properties.keys
|
79
|
+
columns.delete(:id)
|
80
|
+
columns
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "sqlite3"
|
2
|
+
|
3
|
+
module Panda
|
4
|
+
module Record
|
5
|
+
class Database
|
6
|
+
def self.connection
|
7
|
+
@db ||= SQLite3::Database.new(File.join(APP_ROOT, "db", "app.db"))
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.execute_query(*query)
|
11
|
+
connection.execute(*query)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Panda
|
2
|
+
module Routing
|
3
|
+
class Mapper
|
4
|
+
attr_reader :request, :endpoints
|
5
|
+
|
6
|
+
def initialize(endpoints)
|
7
|
+
@endpoints = endpoints
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform(request)
|
11
|
+
@request = request
|
12
|
+
path = request.path_info
|
13
|
+
verb = request.request_method
|
14
|
+
|
15
|
+
endpoints[verb].detect do |endpoint|
|
16
|
+
match_path_with_endpoint(path, endpoint)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def match_path_with_endpoint(path, endpoint)
|
23
|
+
regex, placeholders = endpoint[:pattern]
|
24
|
+
if regex =~ path
|
25
|
+
match_data = $~
|
26
|
+
placeholders.each do |placeholder|
|
27
|
+
request.update_param(placeholder, match_data[placeholder])
|
28
|
+
end
|
29
|
+
return true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Panda
|
2
|
+
module Routing
|
3
|
+
class Router
|
4
|
+
attr_reader :endpoints
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@endpoints ||= Hash.new { |h, k| h[k] = [] }
|
8
|
+
end
|
9
|
+
|
10
|
+
def draw(&block)
|
11
|
+
instance_eval(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
%w(get post delete put patch).each do |method_name|
|
15
|
+
define_method(method_name) do |path, options|
|
16
|
+
route method_name.upcase, path, options
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def root(target) get "/", to: target end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def route(verb, url, options = {})
|
25
|
+
url = "/#{url}" unless url[0] == "/"
|
26
|
+
@endpoints[verb] << {
|
27
|
+
pattern: match_placeholders(url),
|
28
|
+
path: Regexp.new("^#{url}$"),
|
29
|
+
target: set_controller_action(options[:to])
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def match_placeholders(path)
|
34
|
+
placeholders = []
|
35
|
+
path_ = path.gsub(/(:\w+)/) do |match|
|
36
|
+
placeholders << match[1..-1].freeze
|
37
|
+
"(?<#{placeholders.last}>\\w+)"
|
38
|
+
end
|
39
|
+
[/^#{path_}$/, placeholders]
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_controller_action(string)
|
43
|
+
string =~ /^([^#]+)#([^#]+)$/
|
44
|
+
[$1.to_camel_case, $2]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|