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