algernon 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/.coveralls.yml +1 -0
- data/.gitignore +12 -0
- data/.hound.yml +4 -0
- data/.rspec +3 -0
- data/.rubocop.yml +233 -0
- data/.swp +0 -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 +273 -0
- data/Rakefile +6 -0
- data/algernon.gemspec +44 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/database/data.sqlite +0 -0
- data/lib/algernon/application.rb +53 -0
- data/lib/algernon/controller/base_controller.rb +54 -0
- data/lib/algernon/dependencies.rb +7 -0
- data/lib/algernon/model/model.rb +101 -0
- data/lib/algernon/model/orm.rb +51 -0
- data/lib/algernon/model/table_maker.rb +39 -0
- data/lib/algernon/routing/finder.rb +44 -0
- data/lib/algernon/routing/route.rb +31 -0
- data/lib/algernon/routing/router.rb +81 -0
- data/lib/algernon/version.rb +3 -0
- data/lib/algernon.rb +11 -0
- data/lib/utility/array_utils.rb +9 -0
- data/lib/utility/method_override.rb +11 -0
- data/lib/utility/request_handler.rb +38 -0
- data/lib/utility/string_utils.rb +39 -0
- metadata +244 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "algernon"
|
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
Binary file
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "utility/request_handler"
|
3
|
+
require "utility/method_override"
|
4
|
+
|
5
|
+
module Algernon
|
6
|
+
class Application
|
7
|
+
attr_reader :request, :verb, :path, :router
|
8
|
+
|
9
|
+
alias_method :routes, :router
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@router = Routing::Router.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
env = MethodOverride.apply_to(env)
|
17
|
+
@request = Rack::Request.new(env)
|
18
|
+
if router.has_routes?
|
19
|
+
respond_to_request
|
20
|
+
else
|
21
|
+
default_response
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def respond_to_request
|
26
|
+
route = router.get_match(request.request_method, request.path_info)
|
27
|
+
if route
|
28
|
+
handler = RequestHandler.new(request, route)
|
29
|
+
handler.response
|
30
|
+
else
|
31
|
+
page_not_found
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_response
|
36
|
+
Rack::Response.new(
|
37
|
+
"<center><b>Algernon::Penny wise, Pound foolish</center>",
|
38
|
+
200,
|
39
|
+
"Content-Type" => "text/html"
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def page_not_found
|
44
|
+
Rack::Response.new(
|
45
|
+
"<center><h1>404 Error</h1>Page not found</center>",
|
46
|
+
404,
|
47
|
+
"Content-Type" => "text/html"
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
private :respond_to_request, :default_response, :page_not_found
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "erubis"
|
2
|
+
require "active_support/core_ext/hash/indifferent_access"
|
3
|
+
|
4
|
+
module Algernon
|
5
|
+
class BaseController
|
6
|
+
attr_reader :request
|
7
|
+
def initialize(request)
|
8
|
+
@request = request
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_response
|
12
|
+
@response
|
13
|
+
end
|
14
|
+
|
15
|
+
def render(view_name, locals = {})
|
16
|
+
filename = File.join(APP_ROOT,
|
17
|
+
"app",
|
18
|
+
"views",
|
19
|
+
controller_for_views,
|
20
|
+
"#{view_name}.html.erb"
|
21
|
+
)
|
22
|
+
template = File.read(filename)
|
23
|
+
parameters = process_view_variables(locals)
|
24
|
+
res_body = Erubis::Eruby.new(template).result(parameters)
|
25
|
+
response_setter(res_body)
|
26
|
+
end
|
27
|
+
|
28
|
+
def process_view_variables(locals)
|
29
|
+
hash = {}
|
30
|
+
vars = instance_variables
|
31
|
+
vars.each { |name| hash[name] = instance_variable_get(name) }
|
32
|
+
|
33
|
+
hash.merge(locals)
|
34
|
+
end
|
35
|
+
|
36
|
+
def response_setter(body, status = 200, headers = {})
|
37
|
+
@response = Rack::Response.new(body, status, headers)
|
38
|
+
end
|
39
|
+
|
40
|
+
def redirect_to(destination)
|
41
|
+
response_setter([], 302, "Location" => destination)
|
42
|
+
end
|
43
|
+
|
44
|
+
def params
|
45
|
+
request.params.with_indifferent_access
|
46
|
+
end
|
47
|
+
|
48
|
+
def controller_for_views
|
49
|
+
controller_class = self.class
|
50
|
+
controller_class = controller_class.to_s.gsub(/Controller$/, "")
|
51
|
+
controller_class.snakify
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "sqlite3"
|
2
|
+
require "algernon/model/table_maker"
|
3
|
+
require "algernon/model/orm"
|
4
|
+
|
5
|
+
module Algernon
|
6
|
+
class Model
|
7
|
+
extend Algernon::TableMaker
|
8
|
+
extend Algernon::Orm
|
9
|
+
|
10
|
+
DB ||= SQLite3::Database.new(File.join("database", "data.sqlite"))
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
if id
|
17
|
+
query = "UPDATE #{self.class.table_name} "\
|
18
|
+
"SET #{update_field_set} "\
|
19
|
+
"WHERE id = ?"
|
20
|
+
DB.execute(query, table_values, id)
|
21
|
+
else
|
22
|
+
query = "INSERT INTO #{self.class.table_name} "\
|
23
|
+
"(#{table_fields}) "\
|
24
|
+
"VALUES(#{values_placeholders})"
|
25
|
+
DB.execute(query, table_values)
|
26
|
+
|
27
|
+
self.id = DB.execute("SELECT last_insert_rowid()")
|
28
|
+
end
|
29
|
+
self.class.find(id)
|
30
|
+
end
|
31
|
+
|
32
|
+
def update(parameters)
|
33
|
+
parameters.each do |key, value|
|
34
|
+
send("#{key}=", value)
|
35
|
+
end
|
36
|
+
|
37
|
+
save
|
38
|
+
end
|
39
|
+
|
40
|
+
def destroy
|
41
|
+
DB.execute("DELETE FROM #{self.class.table_name} WHERE id= ?", id)
|
42
|
+
end
|
43
|
+
|
44
|
+
def table_fields
|
45
|
+
columns_except_id.join(", ")
|
46
|
+
end
|
47
|
+
|
48
|
+
def columns_except_id
|
49
|
+
self.class.columns_array.without_id.with_value(self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def values_placeholders
|
53
|
+
count = columns_except_id.count
|
54
|
+
(["?"] * count).join(", ")
|
55
|
+
end
|
56
|
+
|
57
|
+
def table_values
|
58
|
+
values = []
|
59
|
+
columns_except_id.each do |field|
|
60
|
+
values << send(field)
|
61
|
+
end
|
62
|
+
values
|
63
|
+
end
|
64
|
+
|
65
|
+
def update_field_set
|
66
|
+
values = []
|
67
|
+
columns_except_id.each do |field|
|
68
|
+
values << "#{field} = ? "
|
69
|
+
end
|
70
|
+
values.join(", ")
|
71
|
+
end
|
72
|
+
|
73
|
+
class << self
|
74
|
+
attr_reader :table_name, :fields
|
75
|
+
|
76
|
+
def to_table(table_name)
|
77
|
+
@table_name = table_name.to_s
|
78
|
+
@fields = {}
|
79
|
+
property :id, type: :integer, primary_key: true
|
80
|
+
end
|
81
|
+
|
82
|
+
def property(field_name, options = {})
|
83
|
+
@fields[field_name] = options
|
84
|
+
attr_accessor field_name
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_table
|
88
|
+
query = "CREATE TABLE IF NOT EXISTS #{@table_name}"\
|
89
|
+
"(#{fields_builder(@fields)})"
|
90
|
+
DB.execute(query)
|
91
|
+
end
|
92
|
+
|
93
|
+
def columns_array
|
94
|
+
@columns_array ||= fields.keys
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private :table_fields, :columns_except_id, :values_placeholders,
|
99
|
+
:table_values, :update_field_set
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Algernon
|
2
|
+
module Orm
|
3
|
+
DB ||= SQLite3::Database.new(File.join("database", "data.sqlite"))
|
4
|
+
|
5
|
+
def create(params)
|
6
|
+
model = new
|
7
|
+
params.each do |key, value|
|
8
|
+
model.send("#{key}=", value)
|
9
|
+
end
|
10
|
+
|
11
|
+
model.save
|
12
|
+
end
|
13
|
+
|
14
|
+
def all
|
15
|
+
fields = columns_array.join(", ")
|
16
|
+
data = DB.execute("SELECT id, #{fields} FROM #{table_name}")
|
17
|
+
data.map! do |row|
|
18
|
+
row_to_model(row)
|
19
|
+
end
|
20
|
+
|
21
|
+
data
|
22
|
+
end
|
23
|
+
|
24
|
+
def row_to_model(row)
|
25
|
+
model = new
|
26
|
+
|
27
|
+
columns_array.each_with_index do |field, index|
|
28
|
+
model.send("#{field}=", row[index + 1]) if row
|
29
|
+
end
|
30
|
+
|
31
|
+
model
|
32
|
+
end
|
33
|
+
|
34
|
+
def find(id)
|
35
|
+
fields = columns_array.join(", ")
|
36
|
+
row = DB.execute(
|
37
|
+
"SELECT id, #{fields} FROM #{table_name} WHERE id = ?",
|
38
|
+
id
|
39
|
+
).first
|
40
|
+
row_to_model(row) if row
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy(id)
|
44
|
+
DB.execute("DELETE FROM #{table_name} WHERE id= ?", id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def destroy_all
|
48
|
+
DB.execute("DELETE FROM #{table_name}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Algernon
|
2
|
+
module TableMaker
|
3
|
+
def fields_builder(fields)
|
4
|
+
columns_definition = ""
|
5
|
+
fields.each do |field, constraints|
|
6
|
+
columns_definition += field.to_s
|
7
|
+
|
8
|
+
constraints.each do |constraint_type, value|
|
9
|
+
columns_definition += " "
|
10
|
+
columns_definition += send(constraint_type, value)
|
11
|
+
end
|
12
|
+
columns_definition += ","
|
13
|
+
end
|
14
|
+
columns_definition[0..-2]
|
15
|
+
end
|
16
|
+
|
17
|
+
def primary_key(value)
|
18
|
+
return "PRIMARY KEY" if value
|
19
|
+
""
|
20
|
+
end
|
21
|
+
|
22
|
+
def auto_increment(value)
|
23
|
+
"AUTOINCREMENT" if value
|
24
|
+
end
|
25
|
+
|
26
|
+
def type(value)
|
27
|
+
value.to_s.upcase
|
28
|
+
end
|
29
|
+
|
30
|
+
def nullable(value)
|
31
|
+
return "NOT NULL" unless value
|
32
|
+
"NULL"
|
33
|
+
end
|
34
|
+
|
35
|
+
def default(value)
|
36
|
+
"DEFAULT `#{value}`"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Algernon
|
2
|
+
module Routing
|
3
|
+
class Finder
|
4
|
+
attr_reader :controller_name, :action, :path_regex, :url_placeholders
|
5
|
+
def initialize(path_regex, to, url_placeholders = {})
|
6
|
+
@controller_name, @action = get_controller_and_action(to)
|
7
|
+
@path_regex = path_regex
|
8
|
+
@url_placeholders = url_placeholders
|
9
|
+
end
|
10
|
+
|
11
|
+
def controller
|
12
|
+
Object.const_get(controller_name.camelify)
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_url_parameters(actual_path)
|
16
|
+
parameters = {}
|
17
|
+
path = actual_path.split("/")
|
18
|
+
url_placeholders.each do |index, key|
|
19
|
+
parameters[key] = path[index.to_i]
|
20
|
+
end
|
21
|
+
parameters
|
22
|
+
end
|
23
|
+
|
24
|
+
def check_path(path)
|
25
|
+
(path_regex =~ path) == 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def ==(other)
|
29
|
+
controller_name == other.controller_name &&
|
30
|
+
action == other.action &&
|
31
|
+
path_regex == other.path_regex &&
|
32
|
+
url_placeholders == other.url_placeholders
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def get_controller_and_action(to)
|
38
|
+
controller, action = to.split("#")
|
39
|
+
controller_name = controller + "_controller"
|
40
|
+
[controller_name, action]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Algernon
|
2
|
+
module Routing
|
3
|
+
module Route
|
4
|
+
def root(target)
|
5
|
+
get("/", to: target)
|
6
|
+
end
|
7
|
+
|
8
|
+
def resources(controller, options = {})
|
9
|
+
actions = detect_action(options)
|
10
|
+
|
11
|
+
# rubocop:disable Metrics/LineLength
|
12
|
+
get("/#{controller}", to: "#{controller}#index") if actions.include?(:index)
|
13
|
+
get("/#{controller}/new", to: "#{controller}#new") if actions.include?(:new)
|
14
|
+
post("/#{controller}", to: "#{controller}#create") if actions.include?(:create)
|
15
|
+
get("/#{controller}/:id", to: "#{controller}#show") if actions.include?(:show)
|
16
|
+
get("/#{controller}/:id/edit", to: "#{controller}#edit") if actions.include?(:edit)
|
17
|
+
put("/#{controller}/:id", to: "#{controller}#update") if actions.include?(:update)
|
18
|
+
patch("/#{controller}/:id", to: "#{controller}#update") if actions.include?(:update)
|
19
|
+
delete("/#{controller}/:id", to: "#{controller}#destroy") if actions.include?(:destroy)
|
20
|
+
# rubocop:enable Metrics/LineLength
|
21
|
+
end
|
22
|
+
|
23
|
+
def detect_action(options)
|
24
|
+
actions = [:index, :new, :create, :show, :edit, :update, :destroy]
|
25
|
+
actions -= options[:except] if options.key?(:except)
|
26
|
+
actions &= options[:only] if options.key?(:only)
|
27
|
+
actions
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "algernon/routing/finder"
|
2
|
+
require "algernon/routing/route"
|
3
|
+
|
4
|
+
module Algernon
|
5
|
+
module Routing
|
6
|
+
class Router
|
7
|
+
include Algernon::Routing::Route
|
8
|
+
attr_accessor :routes, :url_placeholders, :part_regex
|
9
|
+
|
10
|
+
HTTP_VERBS = %w(get post put patch delete).freeze
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@routes = {}
|
14
|
+
@url_placeholders = {}
|
15
|
+
@part_regex = []
|
16
|
+
http_verb_creator
|
17
|
+
end
|
18
|
+
|
19
|
+
def draw(&block)
|
20
|
+
instance_eval(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_routes?
|
24
|
+
true unless routes.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_match(http_verb, path)
|
28
|
+
http_verb = http_verb.downcase
|
29
|
+
routes[http_verb].detect do |route|
|
30
|
+
route.check_path(path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def http_verb_creator
|
35
|
+
self.class.class_eval do
|
36
|
+
HTTP_VERBS.each do |http_verb|
|
37
|
+
define_method(http_verb) do |path, to:|
|
38
|
+
process_and_store_route(http_verb, path, to)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def process_and_store_route(http_verb, path, to)
|
45
|
+
regex_parts, url_placeholders = extract_regex_and_placeholders(path)
|
46
|
+
path_regex = convert_regex_parts_to_path(regex_parts)
|
47
|
+
route_object = Algernon::Routing::Finder.new(
|
48
|
+
path_regex, to, url_placeholders
|
49
|
+
)
|
50
|
+
routes[http_verb.downcase.freeze] ||= []
|
51
|
+
routes[http_verb.downcase] << route_object
|
52
|
+
end
|
53
|
+
|
54
|
+
def extract_regex_and_placeholders(path)
|
55
|
+
path.path_format!
|
56
|
+
|
57
|
+
@part_regex = []
|
58
|
+
@url_placeholders = {}
|
59
|
+
path.split("/").each_with_index do |path_part, index|
|
60
|
+
store_part_and_placeholder(path_part, index)
|
61
|
+
end
|
62
|
+
|
63
|
+
[part_regex, url_placeholders]
|
64
|
+
end
|
65
|
+
|
66
|
+
def store_part_and_placeholder(path_part, index)
|
67
|
+
if path_part.start_with?(":")
|
68
|
+
url_placeholders[index] = path_part.delete(":").freeze
|
69
|
+
part_regex << "[a-zA-Z0-9_]+"
|
70
|
+
else
|
71
|
+
part_regex << path_part
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def convert_regex_parts_to_path(regex_match)
|
76
|
+
regex_string = "^" + regex_match.join("/") + "/*$"
|
77
|
+
Regexp.new(regex_string)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/algernon.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "algernon/version"
|
2
|
+
require "algernon/dependencies"
|
3
|
+
require "utility/string_utils"
|
4
|
+
require "algernon/routing/router"
|
5
|
+
require "algernon/routing/route"
|
6
|
+
require "algernon/routing/finder"
|
7
|
+
require "algernon/application"
|
8
|
+
require "algernon/controller/base_controller"
|
9
|
+
require "utility/array_utils"
|
10
|
+
require "algernon/model/model"
|
11
|
+
require "pry"
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Algernon
|
2
|
+
class RequestHandler
|
3
|
+
attr_reader :request, :route, :response
|
4
|
+
|
5
|
+
def initialize(request, route)
|
6
|
+
@request = request
|
7
|
+
@route = route
|
8
|
+
process_request
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def process_request
|
14
|
+
params = collage_parameters
|
15
|
+
|
16
|
+
request.instance_variable_set "@params", params
|
17
|
+
|
18
|
+
controller_constant = route.controller
|
19
|
+
controller_class = controller_constant.new(request)
|
20
|
+
|
21
|
+
@response = controller_response(controller_class, route.action.to_sym)
|
22
|
+
end
|
23
|
+
|
24
|
+
def collage_parameters
|
25
|
+
route_url_params = route.get_url_parameters(request.path_info)
|
26
|
+
request.params.merge(route_url_params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def controller_response(controller, action)
|
30
|
+
controller.send(action)
|
31
|
+
|
32
|
+
unless controller.get_response
|
33
|
+
controller.render(action)
|
34
|
+
end
|
35
|
+
controller.get_response
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class String
|
2
|
+
def snakify
|
3
|
+
gsub!("::", "/")
|
4
|
+
gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
5
|
+
gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
6
|
+
tr!("-", "_")
|
7
|
+
downcase!
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def camelify
|
12
|
+
return self if self !~ /_/ && self =~ /[A-Z]+.*/
|
13
|
+
split("_").map(&:capitalize).join
|
14
|
+
end
|
15
|
+
|
16
|
+
def constantify
|
17
|
+
Object.const_get(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def pluralize
|
21
|
+
gsub!(/([^aeiouy]|qu)y$/i, '\1ies')
|
22
|
+
gsub!(/(ss|z|ch|sh|x)$/i, '\1es')
|
23
|
+
gsub!(/(is)$/i, "es")
|
24
|
+
gsub!(/(f|fe)$/i, "ves")
|
25
|
+
gsub!(/(ex|ix)$/i, "ices")
|
26
|
+
gsub!(/(a)$/i, "ae")
|
27
|
+
gsub!(/(um|on)$/i, "a")
|
28
|
+
gsub!(/(us)$/i, "i")
|
29
|
+
gsub!(/(eau)$/i, "eaux")
|
30
|
+
gsub!(/([^saeix])$/i, '\1s')
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def path_format!
|
36
|
+
replace(self[0..-2]) if self[-1] == "/"
|
37
|
+
replace("/" + self) if self[0] != "/"
|
38
|
+
end
|
39
|
+
end
|