kwipper 0.0.1
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/.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
|