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.
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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,7 @@
1
+ class Object
2
+ def self.const_missing(c)
3
+ const = c.to_s
4
+ require c.to_s.snakify
5
+ const.constantify
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ module Algernon
2
+ VERSION = "0.1.0".freeze
3
+ 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,9 @@
1
+ class Array
2
+ def without_id
3
+ reject { |key| key == :id }
4
+ end
5
+
6
+ def with_value(base)
7
+ reject { |field| base.send(field).nil? }
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require "rack"
2
+
3
+ class MethodOverride
4
+ def self.apply_to(env)
5
+ request = Rack::Request.new(env)
6
+ env["REQUEST_METHOD"] = request.params["_method"].
7
+ upcase if request.params["_method"]
8
+
9
+ env
10
+ end
11
+ end
@@ -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