panda_frwk 0.1.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/.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
|