kwipper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +2 -0
- data/app/controllers/comments_controller.rb +32 -0
- data/app/controllers/post_favorites_controller.rb +17 -0
- data/app/controllers/posts_controller.rb +41 -0
- data/app/controllers/sessions_controller.rb +32 -0
- data/app/controllers/users_controller.rb +70 -0
- data/app/models/comment.rb +20 -0
- data/app/models/post.rb +31 -0
- data/app/models/post_favorite.rb +6 -0
- data/app/models/session.rb +12 -0
- data/app/models/user.rb +21 -0
- data/app/views/edit_user.erb +26 -0
- data/app/views/fave_button.erb +8 -0
- data/app/views/home.erb +126 -0
- data/app/views/layout.erb +110 -0
- data/app/views/login_user.erb +24 -0
- data/app/views/new_comment.erb +18 -0
- data/app/views/new_post.erb +11 -0
- data/app/views/new_user.erb +26 -0
- data/app/views/not_found.erb +11 -0
- data/app/views/pagination.erb +29 -0
- data/app/views/posts.erb +7 -0
- data/app/views/posts_list.erb +44 -0
- data/app/views/reply_button.erb +4 -0
- data/app/views/server_error.erb +17 -0
- data/app/views/show_post.erb +37 -0
- data/app/views/show_user.erb +11 -0
- data/app/views/users.erb +38 -0
- data/db/.DS_Store +0 -0
- data/kwipper.gemspec +27 -0
- data/lib/kwipper.rb +46 -0
- data/lib/kwipper/application.rb +87 -0
- data/lib/kwipper/controller.rb +36 -0
- data/lib/kwipper/controller_helpers.rb +22 -0
- data/lib/kwipper/errors.rb +5 -0
- data/lib/kwipper/http_parser.rb +51 -0
- data/lib/kwipper/http_server.rb +61 -0
- data/lib/kwipper/inflect.rb +19 -0
- data/lib/kwipper/model.rb +174 -0
- data/lib/kwipper/paginator.rb +71 -0
- data/lib/kwipper/renders_views.rb +18 -0
- data/lib/kwipper/request.rb +35 -0
- data/lib/kwipper/request_headers.rb +36 -0
- data/lib/kwipper/response.rb +85 -0
- data/lib/kwipper/version.rb +3 -0
- data/public/css/bootstrap-lumen.min.css +7 -0
- data/public/css/styles.css +48 -0
- data/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/public/js/bootstrap.min.js +7 -0
- metadata +173 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
module Kwipper
|
2
|
+
class Application
|
3
|
+
include RendersViews
|
4
|
+
include ControllerHelpers
|
5
|
+
|
6
|
+
attr_reader :request, :response, :action
|
7
|
+
|
8
|
+
def respond_to(request)
|
9
|
+
@request = request
|
10
|
+
@response = Response.new request
|
11
|
+
|
12
|
+
begin
|
13
|
+
start_time = Time.now.to_f
|
14
|
+
process!
|
15
|
+
|
16
|
+
log.debug "#{"Processed #{request.info}".blue} in #{sprintf '%.8f', Time.now.to_f - start_time}s"
|
17
|
+
rescue Kwipper::AuthenticationRequired
|
18
|
+
redirect '/'
|
19
|
+
log.debug "401 Not Authorized".yellow
|
20
|
+
rescue Kwipper::NotFoundError
|
21
|
+
render_not_found
|
22
|
+
log.warn @response.info.yellow
|
23
|
+
rescue => e
|
24
|
+
render_error e
|
25
|
+
log.fatal "#{@response.info.red}\n#{verbose_error(e)}"
|
26
|
+
end
|
27
|
+
@response
|
28
|
+
end
|
29
|
+
|
30
|
+
def process!
|
31
|
+
if Controller::ROUTES.key? request.route_key
|
32
|
+
response.set_status :ok
|
33
|
+
response.content_type = 'text/html'
|
34
|
+
|
35
|
+
controller_class, @action = Controller::ROUTES[request.route_key]
|
36
|
+
controller = controller_class.new request, response
|
37
|
+
|
38
|
+
if controller.respond_to? @action
|
39
|
+
@view = controller.process @action
|
40
|
+
response.body = render :layout
|
41
|
+
else
|
42
|
+
raise Kwipper::NotFoundError, "#{self} does not know #{@action}"
|
43
|
+
end
|
44
|
+
elsif (file_name = public_file_request?)
|
45
|
+
response.set_status :ok
|
46
|
+
response.content_type = get_content_type file_name
|
47
|
+
response.body = File.read file_name
|
48
|
+
else
|
49
|
+
raise Kwipper::NotFoundError
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def public_file_request?
|
56
|
+
file = File.join(Kwipper::ROOT, 'public', request.path)
|
57
|
+
File.exists?(file) && file
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_content_type(file_name)
|
61
|
+
if (mime = MIME::Types.of(file_name).first)
|
62
|
+
mime.content_type
|
63
|
+
else
|
64
|
+
log.warn "Unknown content type for file: #{file_name}"
|
65
|
+
'text/plain'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def render_not_found
|
70
|
+
response.set_status :not_found
|
71
|
+
response.status_message = "#{response.status_message}: #{request.info}"
|
72
|
+
@view = render :not_found
|
73
|
+
response.body = render :layout
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_error(e)
|
77
|
+
@error = e
|
78
|
+
response.set_status :server_error
|
79
|
+
@view = render :server_error
|
80
|
+
response.body = render :layout
|
81
|
+
end
|
82
|
+
|
83
|
+
def verbose_error(e)
|
84
|
+
"#{e.class} #{e.message}\n#{e.backtrace.join "\n"}".red
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Kwipper
|
2
|
+
class Controller
|
3
|
+
include RendersViews
|
4
|
+
include ControllerHelpers
|
5
|
+
|
6
|
+
ROUTES = {
|
7
|
+
[:GET, '/'] => [self, :home]
|
8
|
+
}
|
9
|
+
|
10
|
+
def self.add_routes(routes)
|
11
|
+
routes.each do |route_key, action|
|
12
|
+
ROUTES.merge! route_key => [self, action]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :app, :request, :response, :action
|
17
|
+
|
18
|
+
def initialize(request, response)
|
19
|
+
@request, @response = request, response
|
20
|
+
end
|
21
|
+
|
22
|
+
def process(action)
|
23
|
+
send @action = action
|
24
|
+
end
|
25
|
+
|
26
|
+
def home
|
27
|
+
render :home
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def require_login!
|
33
|
+
raise Kwipper::AuthenticationRequired unless current_session
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Kwipper
|
2
|
+
module ControllerHelpers
|
3
|
+
protected
|
4
|
+
|
5
|
+
def params
|
6
|
+
request.params
|
7
|
+
end
|
8
|
+
|
9
|
+
def redirect(path, status = :found)
|
10
|
+
response.redirect = path
|
11
|
+
response.set_status status
|
12
|
+
end
|
13
|
+
|
14
|
+
def current_user
|
15
|
+
response.current_user
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_session
|
19
|
+
response.current_session
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Kwipper
|
2
|
+
class HttpParser
|
3
|
+
HEADER_DELIMITER = "\r\n"
|
4
|
+
|
5
|
+
def parse(raw_request)
|
6
|
+
@first_line = raw_request.gets
|
7
|
+
|
8
|
+
if @first_line.nil?
|
9
|
+
raise Kwipper::EmptyRequest, 'could not get first line'
|
10
|
+
else
|
11
|
+
Request.new do |r|
|
12
|
+
r.http_method = @first_line.split(' ').first
|
13
|
+
r.path = parse_path
|
14
|
+
r.query = parse_query
|
15
|
+
r.headers = parse_headers raw_request
|
16
|
+
r.post_data = parse_query_string raw_request.read(r.content_length) if r.post_data?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def parse_path
|
24
|
+
p = @first_line.split(' ')[1]
|
25
|
+
p ? p.split('?').first : '/'
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_query
|
29
|
+
p = @first_line.split(' ')[1]
|
30
|
+
q = p && !p['?'].nil? ? p.split('?').last.chomp : nil
|
31
|
+
parse_query_string q
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_headers(raw_request)
|
35
|
+
lines = []
|
36
|
+
|
37
|
+
while (line = raw_request.gets) != HEADER_DELIMITER
|
38
|
+
lines << line.chomp
|
39
|
+
end
|
40
|
+
|
41
|
+
lines.each_with_object RequestHeaders.new do |line, request_headers|
|
42
|
+
key, val = line.split(/:\s?/)
|
43
|
+
request_headers[key] = val
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_query_string(s)
|
48
|
+
Rack::Utils.parse_nested_query s.to_s
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Kwipper
|
2
|
+
class HttpServer < TCPServer
|
3
|
+
DEFAULT_PORT = 80
|
4
|
+
attr_reader :host
|
5
|
+
|
6
|
+
def self.run(bind = '127.0.0.1', port = 7335)
|
7
|
+
HttpServer.new(bind, port).serve
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(bind = '127.0.0.1', port = 7335)
|
11
|
+
@bind, @port = bind, port
|
12
|
+
@host = "#@bind#{":#@port" unless port.to_i == DEFAULT_PORT}"
|
13
|
+
log.debug "Starting server on #@host"
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def serve
|
18
|
+
load_models
|
19
|
+
load_controllers
|
20
|
+
parser = HttpParser.new
|
21
|
+
Kwipper.log_startup_time
|
22
|
+
|
23
|
+
while socket = accept
|
24
|
+
begin
|
25
|
+
request = parser.parse socket
|
26
|
+
|
27
|
+
log.info "#{request.info} #{request.params.inspect unless request.params.empty?}".strip.green
|
28
|
+
|
29
|
+
response = Application.new.respond_to request
|
30
|
+
socket.write response.to_http_response
|
31
|
+
|
32
|
+
rescue Errno::ECONNRESET, Errno::EPIPE => e
|
33
|
+
log.info "#{e.class} #{e.message}".yellow
|
34
|
+
rescue Kwipper::EmptyRequest => e
|
35
|
+
log.warn "#{e.class} #{e.message}".yellow
|
36
|
+
ensure
|
37
|
+
socket.close
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
rescue Interrupt
|
42
|
+
socket.close if socket && !socket.closed?
|
43
|
+
Model.db.close
|
44
|
+
log.debug "Ok bye."
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def load_models
|
50
|
+
Dir[File.join(Kwipper::ROOT, 'app/models/**/*.rb')].each do |model|
|
51
|
+
require model
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_controllers
|
56
|
+
Dir[File.join(Kwipper::ROOT, 'app/controllers/**/*.rb')].each do |controller|
|
57
|
+
require controller
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Kwipper
|
2
|
+
class Inflect < String
|
3
|
+
def self.plural(count, word)
|
4
|
+
"#{count} #{count > 1 ? new(word).pluralize : word}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def demodulize
|
8
|
+
Inflect.new split('::').last
|
9
|
+
end
|
10
|
+
|
11
|
+
def pluralize
|
12
|
+
Inflect.new chars.last == 's' ? self : "#{self}s"
|
13
|
+
end
|
14
|
+
|
15
|
+
def underscore
|
16
|
+
Inflect.new gsub(/[a-z]([A-Z])/) { |m| m.gsub $1, "_#{$1}" }.downcase
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module Kwipper
|
2
|
+
class Model
|
3
|
+
DB_NAME = 'kwipper'
|
4
|
+
DB_FILE_NAME = "#{DB_NAME}.db"
|
5
|
+
ID_COLUMN = 'id'
|
6
|
+
|
7
|
+
UnknownAttribute = Class.new ArgumentError
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def db
|
11
|
+
@db ||= SQLite3::Database.open File.join(Kwipper::ROOT, 'db', DB_FILE_NAME)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Declare columns in the model subclass in the same order the columns
|
15
|
+
# were created in the table. This lets us instantiate model objects
|
16
|
+
# from arrays of field values from the db. ID columns is defaulted.
|
17
|
+
def column(name, type)
|
18
|
+
@columns ||= { ID_COLUMN => :to_i }
|
19
|
+
@columns[name] = type
|
20
|
+
attr_accessor name
|
21
|
+
end
|
22
|
+
attr_reader :columns
|
23
|
+
|
24
|
+
# All SQL statements should be executed through this method
|
25
|
+
def sql(cmd)
|
26
|
+
start_time = Time.now.to_f
|
27
|
+
db.execute(cmd).tap do
|
28
|
+
log.debug "#{cmd.red} in #{sprintf '%.8f', Time.now.to_f - start_time}s"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get records from a single table and instantiate them
|
33
|
+
def all(statement = "SELECT * FROM #{table_name}")
|
34
|
+
sql(statement).each_with_object [] do |attrs, models|
|
35
|
+
models << new(attr_array_to_hash attrs)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def find(id)
|
40
|
+
where(id: id).first
|
41
|
+
end
|
42
|
+
|
43
|
+
def where(attrs)
|
44
|
+
all "SELECT * FROM #{table_name} WHERE #{hash_to_key_vals attrs}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def create(attrs)
|
48
|
+
db_attrs = attrs.map { |k, v| normalize_value_for_db v, columns[k] }
|
49
|
+
|
50
|
+
unless attrs.key? 'id'
|
51
|
+
id = generate_id
|
52
|
+
attrs['id'] = id
|
53
|
+
db_attrs = [id, *db_attrs]
|
54
|
+
end
|
55
|
+
|
56
|
+
sql "INSERT INTO #{table_name} VALUES(#{db_attrs.join ', '})"
|
57
|
+
new attrs
|
58
|
+
end
|
59
|
+
|
60
|
+
def update(id, attrs)
|
61
|
+
sql "UPDATE #{table_name} SET #{hash_to_key_vals attrs} WHERE id=#{id}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy(id)
|
65
|
+
sql "DELETE FROM #{table_name} WHERE id=#{id}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def exists?(id)
|
69
|
+
id = normalize_value_for_db id, columns['id']
|
70
|
+
result = sql "SELECT id FROM #{table_name} WHERE id = #{id} LIMIT 1"
|
71
|
+
result.first && result.first.any?
|
72
|
+
end
|
73
|
+
|
74
|
+
def count(statement = "SELECT COUNT(id) FROM #{table_name}")
|
75
|
+
sql(statement).first.first
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
attr_accessor :id
|
80
|
+
|
81
|
+
# Takes a hash of model attributes and sets them via accessors if they exists
|
82
|
+
def initialize(attrs = {})
|
83
|
+
attrs.keys.each do |name|
|
84
|
+
if self.class.columns.keys.include? name
|
85
|
+
type = self.class.columns[name]
|
86
|
+
send "#{name}=", attrs[name].send(type)
|
87
|
+
else
|
88
|
+
raise UnknownAttribute, "#{name} for #{self}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Saves model instance to the database
|
94
|
+
def save
|
95
|
+
if id
|
96
|
+
self.class.update id, attrs_for_db
|
97
|
+
else
|
98
|
+
self.class.create a = attrs_for_db
|
99
|
+
@id ||= a['id']
|
100
|
+
end
|
101
|
+
|
102
|
+
true
|
103
|
+
rescue SQLite3::SQLException => e
|
104
|
+
log.warn e.message
|
105
|
+
false
|
106
|
+
end
|
107
|
+
|
108
|
+
def update(attrs)
|
109
|
+
self.class.update id, attrs
|
110
|
+
true
|
111
|
+
rescue KeyError => e
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
def destroy(id)
|
116
|
+
self.class.destroy id
|
117
|
+
end
|
118
|
+
|
119
|
+
def sql(statement)
|
120
|
+
self.class.sql statement
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def attrs_for_db
|
126
|
+
self.class.columns.each_with_object({}) do |(name, _), attrs|
|
127
|
+
value = send name
|
128
|
+
value = generate_id if name == ID_COLUMN && value.nil?
|
129
|
+
attrs[name] = value unless value.nil?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def generate_id
|
134
|
+
self.class.generate_id
|
135
|
+
end
|
136
|
+
|
137
|
+
class << self
|
138
|
+
def table_name
|
139
|
+
Inflect.new(name).demodulize.pluralize.underscore
|
140
|
+
end
|
141
|
+
|
142
|
+
def generate_id
|
143
|
+
max_id_plus_1 = "SELECT (id + 1) as id FROM #{table_name} ORDER BY id DESC LIMIT 1"
|
144
|
+
result = sql(max_id_plus_1).first
|
145
|
+
result && result.first ? result.first : 1
|
146
|
+
end
|
147
|
+
|
148
|
+
def attr_array_to_hash(attrs)
|
149
|
+
attrs.each_with_index.inject({}) do |hash, (attr_val, i)|
|
150
|
+
hash.merge! @columns.keys[i] => attr_val
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Turn a hash of attributes into a comma separated string that's
|
155
|
+
# safe to use in a SQL statement (non int values are quoted).
|
156
|
+
# TODO: add SQL sanitation.
|
157
|
+
def hash_to_key_vals(hash)
|
158
|
+
hash.inject [] do |a, (k, v)|
|
159
|
+
v = normalize_value_for_db v, columns[k]
|
160
|
+
a << "#{k}=#{v}"
|
161
|
+
end.join ', '
|
162
|
+
end
|
163
|
+
|
164
|
+
# Non int values should be quoted when putting in a SQL statement
|
165
|
+
def normalize_value_for_db(value, type)
|
166
|
+
case type when :to_i
|
167
|
+
value.to_i
|
168
|
+
else
|
169
|
+
"\"#{value}\""
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|