kwipper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +16 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +33 -0
  7. data/Rakefile +2 -0
  8. data/app/controllers/comments_controller.rb +32 -0
  9. data/app/controllers/post_favorites_controller.rb +17 -0
  10. data/app/controllers/posts_controller.rb +41 -0
  11. data/app/controllers/sessions_controller.rb +32 -0
  12. data/app/controllers/users_controller.rb +70 -0
  13. data/app/models/comment.rb +20 -0
  14. data/app/models/post.rb +31 -0
  15. data/app/models/post_favorite.rb +6 -0
  16. data/app/models/session.rb +12 -0
  17. data/app/models/user.rb +21 -0
  18. data/app/views/edit_user.erb +26 -0
  19. data/app/views/fave_button.erb +8 -0
  20. data/app/views/home.erb +126 -0
  21. data/app/views/layout.erb +110 -0
  22. data/app/views/login_user.erb +24 -0
  23. data/app/views/new_comment.erb +18 -0
  24. data/app/views/new_post.erb +11 -0
  25. data/app/views/new_user.erb +26 -0
  26. data/app/views/not_found.erb +11 -0
  27. data/app/views/pagination.erb +29 -0
  28. data/app/views/posts.erb +7 -0
  29. data/app/views/posts_list.erb +44 -0
  30. data/app/views/reply_button.erb +4 -0
  31. data/app/views/server_error.erb +17 -0
  32. data/app/views/show_post.erb +37 -0
  33. data/app/views/show_user.erb +11 -0
  34. data/app/views/users.erb +38 -0
  35. data/db/.DS_Store +0 -0
  36. data/kwipper.gemspec +27 -0
  37. data/lib/kwipper.rb +46 -0
  38. data/lib/kwipper/application.rb +87 -0
  39. data/lib/kwipper/controller.rb +36 -0
  40. data/lib/kwipper/controller_helpers.rb +22 -0
  41. data/lib/kwipper/errors.rb +5 -0
  42. data/lib/kwipper/http_parser.rb +51 -0
  43. data/lib/kwipper/http_server.rb +61 -0
  44. data/lib/kwipper/inflect.rb +19 -0
  45. data/lib/kwipper/model.rb +174 -0
  46. data/lib/kwipper/paginator.rb +71 -0
  47. data/lib/kwipper/renders_views.rb +18 -0
  48. data/lib/kwipper/request.rb +35 -0
  49. data/lib/kwipper/request_headers.rb +36 -0
  50. data/lib/kwipper/response.rb +85 -0
  51. data/lib/kwipper/version.rb +3 -0
  52. data/public/css/bootstrap-lumen.min.css +7 -0
  53. data/public/css/styles.css +48 -0
  54. data/public/fonts/glyphicons-halflings-regular.eot +0 -0
  55. data/public/fonts/glyphicons-halflings-regular.svg +229 -0
  56. data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  57. data/public/fonts/glyphicons-halflings-regular.woff +0 -0
  58. data/public/js/bootstrap.min.js +7 -0
  59. 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,5 @@
1
+ module Kwipper
2
+ AuthenticationRequired = Class.new RuntimeError
3
+ NotFoundError = Class.new RuntimeError
4
+ EmptyRequest = Class.new RuntimeError
5
+ 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