algernon 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/.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
|