hatetepe 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.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem "rake"
6
+ gem "awesome_print"
@@ -0,0 +1,13 @@
1
+ Builds and parses HTTP messages
2
+ ===============================
3
+
4
+ Hatetepe combines its own builder with http_parser.rb to make dealing with HTTP
5
+ messages as comfortable as possible.
6
+
7
+ TODO
8
+ ----
9
+
10
+ - Fix http_parser.rb's parsing of chunked bodies
11
+ - Does http_parser.rb recognize trailing headers?
12
+ - Support for pausing and resuming parsing/building
13
+ - Encoding support (see https://github.com/tmm1/http_parser.rb/pull/1)
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ Bundler.setup :default
3
+
4
+ task :default => :test
5
+
6
+ require "rake/testtask"
7
+ Rake::TestTask.new :test do |t|
8
+ t.test_files = FileList["test/*_test.rb"]
9
+ end
10
+
11
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,29 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ require "hatetepe"
5
+ require "awesome_print"
6
+
7
+ Hatetepe::Parser.parse do
8
+ [:request, :response, :header, :body_chunk, :complete, :error].each do |hook|
9
+ send :"on_#{hook}" do |*args|
10
+ puts "on_#{hook}: #{args.inspect}"
11
+ end
12
+ end
13
+
14
+ self << "HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nHallo Welt!"
15
+ end
16
+
17
+ Hatetepe::Builder.build do
18
+ on_write {|data| p data }
19
+ on_complete {|bytes_written| puts "Wrote #{bytes_written} bytes" }
20
+
21
+ response 200
22
+ header "Content-Type", "text/html", "utf-8"
23
+ raw_header "Content-Length: 25"
24
+ body "<p>Hallo Welt!</p>"
25
+
26
+ response 201
27
+ header "Location", "/new_entity"
28
+ complete
29
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "hatetepe/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "hatetepe"
7
+ s.version = Hatetepe::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Lars Gierth"]
10
+ s.email = ["lars.gierth@gmail.com"]
11
+ s.homepage = "https://github.com/lgierth/hatetepe"
12
+ s.summary = %q{Builds and parses HTTP messages}
13
+ s.description = %q{Hatetepe combines its own builder with http_parser.rb to make dealing with HTTP messages as comfortable as possible.}
14
+
15
+ s.add_dependency "http_parser.rb"
16
+
17
+ s.add_development_dependency "test-unit"
18
+
19
+ s.files = `git ls-files`.split("\n") - [".gitignore"]
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+ end
@@ -0,0 +1,3 @@
1
+ require "hatetepe/builder"
2
+ require "hatetepe/parser"
3
+ require "hatetepe/version"
@@ -0,0 +1,142 @@
1
+ require "hatetepe/status"
2
+
3
+ module Hatetepe
4
+ class BuilderError < StandardError; end
5
+
6
+ class Builder
7
+ def self.build(&block)
8
+ message = ""
9
+ builder = new do |b|
10
+ b.on_write {|data| message << data }
11
+ end
12
+
13
+ block.arity == 0 ? builder.instance_eval(&block) : block.call(builder)
14
+
15
+ builder.complete
16
+ return message.empty? ? nil : message
17
+ end
18
+
19
+ attr_reader :state, :bytes_written
20
+
21
+ def initialize(&block)
22
+ reset
23
+ @on_write, @on_complete, @on_error = [], [], []
24
+
25
+ if block
26
+ block.arity == 0 ? instance_eval(&block) : block.call(self)
27
+ end
28
+ end
29
+
30
+ def reset
31
+ @state = :ready
32
+ @chunked = true
33
+ @bytes_written = 0
34
+ end
35
+
36
+ [:write, :complete, :error].each do |hook|
37
+ define_method :"on_#{hook}" do |&block|
38
+ store = instance_variable_get(:"@on_#{hook}")
39
+ return store unless block
40
+ store << block
41
+ end
42
+ end
43
+
44
+ def ready?
45
+ state == :ready
46
+ end
47
+
48
+ def writing_headers?
49
+ state == :writing_headers
50
+ end
51
+
52
+ def writing_body?
53
+ state == :writing_body
54
+ end
55
+
56
+ def writing_trailing_headers?
57
+ state == :writing_trailing_headers
58
+ end
59
+
60
+ def chunked?
61
+ @chunked
62
+ end
63
+
64
+ def request(verb, uri, version = "1.1")
65
+ complete unless ready?
66
+ write "#{verb.upcase} #{uri} HTTP/#{version}\r\n"
67
+ @state = :writing_headers
68
+ end
69
+
70
+ def response(code, version = "1.1")
71
+ complete unless ready?
72
+ unless status = STATUS_CODES[code]
73
+ error "Unknown status code: #{code}"
74
+ end
75
+ write "HTTP/#{version} #{code} #{status}\r\n"
76
+ @state = :writing_headers
77
+ end
78
+
79
+ def header(name, value, charset = nil)
80
+ charset = charset ? "; charset=#{charset}" : ""
81
+ raw_header "#{name}: #{value}#{charset}"
82
+ end
83
+
84
+ def raw_header(header)
85
+ if ready?
86
+ error "A request or response line is required before writing headers"
87
+ elsif writing_body?
88
+ error "Trailing headers require chunked transfer encoding" unless chunked?
89
+ write "0\r\n"
90
+ @state = :writing_trailing_headers
91
+ end
92
+
93
+ if header[0..13] == "Content-Length"
94
+ @chunked = false
95
+ end
96
+
97
+ write "#{header}\r\n"
98
+ end
99
+
100
+ def body(chunk)
101
+ if ready?
102
+ error "A request or response line and headers are required before writing body"
103
+ elsif writing_trailing_headers?
104
+ error "Cannot write body after trailing headers"
105
+ elsif writing_headers?
106
+ write "\r\n"
107
+ @state = :writing_body
108
+ end
109
+
110
+ if chunked?
111
+ write "#{chunk.length.to_s(16)}\r\n#{chunk}\r\n"
112
+ else
113
+ write chunk
114
+ end
115
+ end
116
+
117
+ def complete
118
+ return if ready?
119
+
120
+ if writing_body? && chunked?
121
+ write "0\r\n\r\n"
122
+ elsif writing_headers? || writing_trailing_headers?
123
+ write "\r\n"
124
+ end
125
+
126
+ on_complete.each {|blk| blk.call(bytes_written) }
127
+ reset
128
+ end
129
+
130
+ def write(chunk)
131
+ @bytes_written += chunk.length
132
+ on_write.each {|blk| blk.call(chunk) }
133
+ end
134
+
135
+ def error(message)
136
+ exception = BuilderError.new(message)
137
+ exception.set_backtrace(caller[1..-1])
138
+ on_error.each {|blk| blk.call(exception) }
139
+ raise(exception)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,96 @@
1
+ require "http/parser"
2
+
3
+ module Hatetepe
4
+ class ParserError < StandardError; end
5
+
6
+ class Parser
7
+ def self.parse(data = [], &block)
8
+ message = {}
9
+ parser = new do |p|
10
+ p.on_request do |*args|
11
+ message[:http_method] = args[0]
12
+ message[:request_url] = args[1]
13
+ message[:http_version] = args[2]
14
+ end
15
+ p.on_response do |*args|
16
+ message[:status] = args[0]
17
+ message[:http_version] = args[1]
18
+ end
19
+ p.on_header do |name, value|
20
+ (message[:headers] ||= {})[name] = value
21
+ end
22
+ p.on_body_chunk do |chunk|
23
+ (message[:body] ||= "") << chunk
24
+ end
25
+ end
26
+
27
+ if block
28
+ block.arity == 0 ? parser.instance_eval(&block) : block.call(parser)
29
+ end
30
+
31
+ Array(data).each {|chunk| parser << chunk }
32
+ message
33
+ end
34
+
35
+ attr_reader :bytes_read
36
+
37
+ def initialize(&block)
38
+ @on_request, @on_response, @on_header = [], [], []
39
+ @on_body_chunk, @on_complete, @on_error = [], [], []
40
+ @parser = HTTP::Parser.new
41
+
42
+ @parser.on_headers_complete = proc do
43
+ if @parser.http_method
44
+ on_request.each do |r|
45
+ r.call(@parser.http_method, @parser.request_url, @parser.http_version.join("."))
46
+ end
47
+ else
48
+ on_response.each do |r|
49
+ r.call(@parser.status_code, @parser.http_version.join("."))
50
+ end
51
+ end
52
+
53
+ @parser.headers.each do |header|
54
+ on_header.each {|h| h.call(*header) }
55
+ end
56
+ end
57
+
58
+ @parser.on_body = proc do |chunk|
59
+ on_body_chunk.each {|b| b.call(chunk) }
60
+ end
61
+
62
+ @parser.on_message_complete = proc do
63
+ on_complete.each {|f| f.call(bytes_read) }
64
+ end
65
+
66
+ reset
67
+
68
+ if block
69
+ block.arity == 0 ? instance_eval(&block) : block.call(self)
70
+ end
71
+ end
72
+
73
+ def reset
74
+ @parser.reset!
75
+ @bytes_read = 0
76
+ end
77
+
78
+ [:request, :response, :header, :body_chunk, :complete, :error].each do |hook|
79
+ define_method :"on_#{hook}" do |&block|
80
+ store = instance_variable_get(:"@on_#{hook}")
81
+ return store unless block
82
+ store << block
83
+ end
84
+ end
85
+
86
+ def <<(data)
87
+ @bytes_read += data.length
88
+ @parser << data
89
+ rescue HTTP::Parser::Error => original_error
90
+ error = ParserError.new(original_error.message)
91
+ error.set_backtrace(original_error.backtrace)
92
+ on_error.each {|e| e.call(error) }
93
+ raise(error)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,42 @@
1
+ module Hatetepe
2
+ # @author Mongrel
3
+ STATUS_CODES = {
4
+ 100 => "Continue",
5
+ 101 => "Switching Protocols",
6
+ 200 => "OK",
7
+ 201 => "Created",
8
+ 202 => "Accepted",
9
+ 203 => "Non-Authoritative Information",
10
+ 204 => "No Content",
11
+ 205 => "Reset Content",
12
+ 206 => "Partial Content",
13
+ 300 => "Multiple Choices",
14
+ 301 => "Moved Permanently",
15
+ 302 => "Moved Temporarily",
16
+ 303 => "See Other",
17
+ 304 => "Not Modified",
18
+ 305 => "Use Proxy",
19
+ 400 => "Bad Request",
20
+ 401 => "Unauthorized",
21
+ 402 => "Payment Required",
22
+ 403 => "Forbidden",
23
+ 404 => "Not Found",
24
+ 405 => "Method Not Allowed",
25
+ 406 => "Not Acceptable",
26
+ 407 => "Proxy Authentication Required",
27
+ 408 => "Request Time-out",
28
+ 409 => "Conflict",
29
+ 410 => "Gone",
30
+ 411 => "Length Required",
31
+ 412 => "Precondition Failed",
32
+ 413 => "Request Entity Too Large",
33
+ 414 => "Request-URI Too Large",
34
+ 415 => "Unsupported Media Type",
35
+ 500 => "Internal Server Error",
36
+ 501 => "Not Implemented",
37
+ 502 => "Bad Gateway",
38
+ 503 => "Service Unavailable",
39
+ 504 => "Gateway Time-out",
40
+ 505 => "HTTP Version Not Supported"
41
+ }
42
+ end
@@ -0,0 +1,3 @@
1
+ module Hatetepe
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path("..", __FILE__) + "/test_helper"
2
+
3
+ class BuilderTest < Test::Unit::TestCase
4
+ def test_bar
5
+ assert true
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path("..", __FILE__) + "/test_helper"
2
+
3
+ class ParserTest < Test::Unit::TestCase
4
+ def test_foo
5
+ assert true
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require "bundler"
2
+ Bundler.setup :default, :development
3
+
4
+ require "hatetepe"
5
+
6
+ require "test/unit"
7
+ require "awesome_print"
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hatetepe
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Lars Gierth
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-06-03 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: http_parser.rb
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: test-unit
28
+ requirement: &id002 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *id002
37
+ description: Hatetepe combines its own builder with http_parser.rb to make dealing with HTTP messages as comfortable as possible.
38
+ email:
39
+ - lars.gierth@gmail.com
40
+ executables: []
41
+
42
+ extensions: []
43
+
44
+ extra_rdoc_files: []
45
+
46
+ files:
47
+ - Gemfile
48
+ - README.md
49
+ - Rakefile
50
+ - example.rb
51
+ - hatetepe.gemspec
52
+ - lib/hatetepe.rb
53
+ - lib/hatetepe/builder.rb
54
+ - lib/hatetepe/parser.rb
55
+ - lib/hatetepe/status.rb
56
+ - lib/hatetepe/version.rb
57
+ - test/builder_test.rb
58
+ - test/parser_test.rb
59
+ - test/test_helper.rb
60
+ homepage: https://github.com/lgierth/hatetepe
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options: []
65
+
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ hash: 678111023
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ hash: 678111023
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ requirements: []
87
+
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.2
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Builds and parses HTTP messages
93
+ test_files: []
94
+