webbed 0.1.1 → 0.2.0

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 (46) hide show
  1. data/.gitignore +22 -0
  2. data/.travis.yml +7 -0
  3. data/.yardopts +6 -0
  4. data/Gemfile +5 -13
  5. data/Guardfile +5 -0
  6. data/README.md +36 -31
  7. data/Rakefile +7 -14
  8. data/lib/webbed.rb +28 -6
  9. data/lib/webbed/generic_message.rb +27 -15
  10. data/lib/webbed/headers.rb +2 -0
  11. data/lib/webbed/helpers/entity_headers_helper.rb +62 -0
  12. data/lib/webbed/helpers/method_helper.rb +83 -0
  13. data/lib/webbed/helpers/rack_request_helper.rb +86 -0
  14. data/lib/webbed/helpers/rack_response_helper.rb +56 -0
  15. data/lib/webbed/helpers/request_headers_helper.rb +62 -0
  16. data/lib/webbed/helpers/request_uri_helper.rb +34 -0
  17. data/lib/webbed/helpers/response_headers_helper.rb +48 -0
  18. data/lib/webbed/helpers/scheme_helper.rb +26 -0
  19. data/lib/webbed/http_version.rb +88 -33
  20. data/lib/webbed/media_type.rb +160 -0
  21. data/lib/webbed/method.rb +63 -33
  22. data/lib/webbed/request.rb +65 -21
  23. data/lib/webbed/response.rb +65 -24
  24. data/lib/webbed/status_code.rb +84 -17
  25. data/lib/webbed/version.rb +1 -1
  26. data/test/support/assertions.rb +17 -0
  27. data/test/support/runner.rb +326 -0
  28. data/test/test_helper.rb +13 -0
  29. data/test/webbed/generic_message_test.rb +44 -0
  30. data/test/webbed/headers_test.rb +31 -0
  31. data/test/webbed/helpers/entity_headers_helper_test.rb +68 -0
  32. data/test/webbed/helpers/method_helper_test.rb +151 -0
  33. data/test/webbed/helpers/rack_request_helper_test.rb +108 -0
  34. data/test/webbed/helpers/rack_response_helper_test.rb +33 -0
  35. data/test/webbed/helpers/request_headers_helper_test.rb +57 -0
  36. data/test/webbed/helpers/request_uri_helper_test.rb +32 -0
  37. data/test/webbed/helpers/response_headers_helper_test.rb +46 -0
  38. data/test/webbed/helpers/scheme_helper_test.rb +28 -0
  39. data/test/webbed/http_version_test.rb +52 -0
  40. data/test/webbed/media_type_test.rb +100 -0
  41. data/test/webbed/method_test.rb +160 -0
  42. data/test/webbed/request_test.rb +74 -0
  43. data/test/webbed/response_test.rb +86 -0
  44. data/test/webbed/status_code_test.rb +105 -0
  45. data/webbed.gemspec +31 -0
  46. metadata +128 -41
@@ -0,0 +1,56 @@
1
+ module Webbed
2
+ module Helpers
3
+ # Response helper for converting Responses into Rack response arrays.
4
+ module RackResponseHelper
5
+ module ClassMethods
6
+ # Converts a Rack response array to a Response.
7
+ #
8
+ # The array has the same format as that defined in the Rack specification.
9
+ #
10
+ # @param [Array] rack_array
11
+ # @return [Response]
12
+ def from_rack(rack_array)
13
+ Response.new(*rack_array)
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ # Converts the Response to a Rack response array.
19
+ #
20
+ # The array has the same format as that defined in the Rack specification.
21
+ #
22
+ # @return [Array]
23
+ # @note Due to limitations in Rack, Reason Phrases are not included in the
24
+ # array.
25
+ def to_rack
26
+ if entity_body.respond_to?(:each)
27
+ [status_code.to_i, headers, entity_body]
28
+ else
29
+ [status_code.to_i, headers, [entity_body]]
30
+ end
31
+ end
32
+
33
+ # Converts the Response to an array.
34
+ #
35
+ # The array has a similar format to the format defined in the Rack specification with some modifications:
36
+ #
37
+ # 1. The Reason Phrase is included with the Status Code.
38
+ # 2. The Entity Body does not need to respond to `#each`.
39
+ #
40
+ # @return [Array]
41
+ # @note This will *not* return a Rack-compatible response array.
42
+ # @see Response#initialize
43
+ def to_a
44
+ ["#{status_code} #{reason_phrase}", headers, entity_body]
45
+ end
46
+ end
47
+
48
+ # @see ClassMethods
49
+ # @see InstanceMethods
50
+ def self.included(base)
51
+ base.send :include, InstanceMethods
52
+ base.send :extend, ClassMethods
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,62 @@
1
+ module Webbed
2
+ module Helpers
3
+ # Request helper for Request Headers
4
+ module RequestHeadersHelper
5
+ # The Host of the Request (as defined in the Host Header)
6
+ #
7
+ # @return [String, nil]
8
+ def host
9
+ headers['Host']
10
+ end
11
+
12
+ # Sets the Host of the Request (as defined in the Host Header)
13
+ #
14
+ # @param [String] host
15
+ def host=(host)
16
+ headers['Host'] = host
17
+ end
18
+
19
+ # The From email of the Request (as defined in the From Header)
20
+ #
21
+ # @return [String, nil]
22
+ def from
23
+ headers['From']
24
+ end
25
+
26
+ # Sets the From email of the Request (as defined in the From Header)
27
+ #
28
+ # @param [String] from
29
+ def from=(from)
30
+ headers['From'] = from
31
+ end
32
+
33
+ # The Max-Forwards of the Request (as defined in the Max-Forwards Header)
34
+ #
35
+ # @return [Fixnum, nil]
36
+ def max_forwards
37
+ headers['Max-Forwards'] ? headers['Max-Forwards'].to_i : nil
38
+ end
39
+
40
+ # Sets the Max-Forwards of the Request (as defined in the Max-Forwards Header)
41
+ #
42
+ # @param [#to_s] max_forwards
43
+ def max_forwards=(max_forwards)
44
+ headers['Max-Forwards'] = max_forwards.to_s
45
+ end
46
+
47
+ # The Referer of the Request (as defined in the Referer Header)
48
+ #
49
+ # @return [Addressable::URI, nil]
50
+ def referer
51
+ headers['Referer'] ? Addressable::URI.parse(headers['Referer']) : nil
52
+ end
53
+
54
+ # Sets the Referer of the Request (as defined in the Referer Header)
55
+ #
56
+ # @param [#to_s] referer
57
+ def referer=(referer)
58
+ headers['Referer'] = referer.to_s
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,34 @@
1
+ module Webbed
2
+ module Helpers
3
+ # Request helper for the Request-URI
4
+ module RequestURIHelper
5
+ # Aliases the {Request#request_uri} method to `#request_url`.
6
+ def self.included(base)
7
+ base.class_eval do
8
+ alias :request_url :request_uri
9
+ end
10
+ end
11
+
12
+ # The URI of the actual location of the requested resource.
13
+ #
14
+ # If the Request-URI is an absolute URI or there is no Host header, it
15
+ # returns the Request-URI.
16
+ #
17
+ # If the Request-URI is a relative URI, it combines the Host header with
18
+ # the Request-URI.
19
+ #
20
+ # @return [Addressable::URI]
21
+ def uri
22
+ if host && !request_uri.host
23
+ request_uri.dup.tap do |uri|
24
+ uri.scheme = 'http'
25
+ uri.host = host
26
+ end
27
+ else
28
+ request_uri
29
+ end
30
+ end
31
+ alias :url :uri
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ module Webbed
2
+ module Helpers
3
+ # Response helper for Response Headers
4
+ module ResponseHeadersHelper
5
+ # The ETag of the Response (as defined in the ETag Header)
6
+ #
7
+ # @return [String, nil]
8
+ def etag
9
+ headers['ETag']
10
+ end
11
+
12
+ # Sets the ETag of the Response (as defined in the ETag Header)
13
+ #
14
+ # @param [String] etag
15
+ def etag=(etag)
16
+ headers['ETag'] = etag
17
+ end
18
+
19
+ # The Age of the Response (as defined in the Age Header)
20
+ #
21
+ # @return [Fixnum, nil]
22
+ def age
23
+ headers['Age'] ? headers['Age'].to_i : nil
24
+ end
25
+
26
+ # Sets the Age of the Response (as defined in the Age Header)
27
+ #
28
+ # @param [#to_s] age
29
+ def age=(age)
30
+ headers['Age'] = age.to_s
31
+ end
32
+
33
+ # The Location of the Response (as defined in the Location Header)
34
+ #
35
+ # @return [Addressable::URI, nil]
36
+ def location
37
+ headers['Location'] ? Addressable::URI.parse(headers['Location']) : nil
38
+ end
39
+
40
+ # Sets the Location of the Response (as defined in the Location Header)
41
+ #
42
+ # @param [#to_s] location
43
+ def location=(location)
44
+ headers['Location'] = location.to_s
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,26 @@
1
+ module Webbed
2
+ module Helpers
3
+ # Request helper for the schemes
4
+ module SchemeHelper
5
+ # Whether or not the request was sent securely (using HTTPS)
6
+ #
7
+ # @return [Boolean]
8
+ def secure?
9
+ scheme == 'https'
10
+ end
11
+
12
+ # The default port for the designated scheme
13
+ #
14
+ # This method returns `80` if you're using HTTP and `443` if you're using
15
+ # HTTPS.
16
+ #
17
+ # @return [Fixnum, nil]
18
+ def default_port
19
+ case scheme
20
+ when 'http' then 80
21
+ when 'https' then 443
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,55 +1,110 @@
1
1
  module Webbed
2
+ # Representation of an HTTP HTTP Version.
3
+ #
4
+ # Webbed supports both primary versions of HTTP, HTTP/1.0 and HTTP/1.1.
5
+ # Although the use of HTTP/1.1 has been strongly encouraged since its creation
6
+ # in 1999, it remains relatively common for older command line tools (such as
7
+ # wget) and some search engines. Webbed can also be extended in the future to
8
+ # support new versions of HTTP, should one ever come into existence.
9
+ #
10
+ # {HTTPVersion} is a small abstraction on top of the HTTP-Version as defined
11
+ # in RFC 2616. According to the RFC, its simple format is:
12
+ #
13
+ # HTTP-Version = "HTTP" "/" 1*DIGIT "." 1*DIGIT
14
+ #
15
+ # While this is perhaps the simplest of all the abstractions in Webbed, it
16
+ # does offer some nice helper methods for treating the version string more
17
+ # Ruby-like.
18
+ #
19
+ # HTTP/1.0 and HTTP/1.1 {HTTPVersion}'s are cached. In every case I can think
20
+ # of, you will not have to create a new {HTTPVersion}, just use the constants
21
+ # {ONE_POINT_OH} and {ONE_POINT_ONE} when creating messages.
2
22
  class HTTPVersion
3
-
4
23
  include Comparable
5
- REGEX = /^HTTP\/(\d+\.\d+)$/
6
-
7
- def self.new(http_version, dup = false)
8
- unless dup
9
- if ['HTTP/1.1', 1.1].include? http_version
10
- return ONE_POINT_ONE
11
- end
12
-
13
- if ['HTTP/1.0', 1.0].include? http_version
14
- return ONE_POINT_OH
15
- end
16
- end
17
-
18
- super(http_version)
19
- end
20
24
 
25
+ REGEX = /^HTTP\/(\d+)\.(\d+)$/
26
+
27
+ # Creates a new HTTP-Version.
28
+ #
29
+ # Only HTTP/1.0 and HTTP/1.1 versions are cached. All other versions will be
30
+ # created at runtime each time this method is called.
31
+ #
32
+ # @example
33
+ # Webbed::HTTPVersion.new(1.1)
34
+ # Webbed::HTTPVersion.new('HTTP/1.1')
35
+ #
36
+ # @param [#to_s] http_version the HTTP-Version to create
21
37
  def initialize(http_version)
22
- @http_version = to_f(http_version)
38
+ if REGEX =~ http_version.to_s
39
+ @http_version = http_version.to_s
40
+ else
41
+ @http_version = "HTTP/#{http_version}"
42
+ end
23
43
  end
24
44
 
45
+ # Converts the HTTP-Version to a string according to RFC 2616.
46
+ #
47
+ # @example
48
+ # version = Webbed::HTTPVersion.new(1.1)
49
+ # version.to_s # => 'HTTP/1.1'
50
+ #
51
+ # @return [String]
25
52
  def to_s
26
- "HTTP/#{to_f}"
53
+ @http_version
27
54
  end
28
- alias :inspect :to_s
29
55
 
30
- # This feels like a hack. Oh well.
31
- def to_f(http_version = @http_version)
32
- if http_version.respond_to? :match
33
- REGEX.match(http_version)[1].to_f
34
- else
35
- http_version.to_f
36
- end
56
+ # Converts the HTTP-Version to a float.
57
+ #
58
+ # @example
59
+ # version = Webbed::HTTPVersion.new('HTTP/1.1')
60
+ # version.to_f # => 1.1
61
+ #
62
+ # @return [Float]
63
+ def to_f
64
+ REGEX =~ @http_version
65
+ "#{$1}.#{$2}".to_f
37
66
  end
38
67
 
39
- def <=>(other_version)
40
- to_f <=> to_f(other_version)
68
+ # Compares the HTTP-Version to another HTTP-Version.
69
+ #
70
+ # @example
71
+ # version_1_1 = Webbed::HTTPVersion.new(1.1)
72
+ # version_5_0 = Webbed::HTTPVersion.new('HTTP/5.0')
73
+ # version_1_1 == version_5_0 # => false
74
+ # version_5_0 < version_5_0 # => false
75
+ # version_5_0 > version_1_1 # => true
76
+ #
77
+ # @param [#to_f] other_http_version the other HTTP-Version
78
+ # @return [Fixnum] the sign of the comparison (either `1`, `0`, or `-1`)
79
+ def <=>(other_http_version)
80
+ to_f <=> other_http_version.to_f
41
81
  end
42
82
 
83
+ # The major HTTP-Version number.
84
+ #
85
+ # @example
86
+ # version = Webbed::HTTPVersion.new('HTTP/6.9')
87
+ # version.major # => 6
88
+ #
89
+ # @return [Fixnum]
43
90
  def major
44
- to_f.floor
91
+ REGEX =~ @http_version
92
+ $1.to_i
45
93
  end
46
94
 
47
- # TODO: Fix this ugly hack! :x
95
+ # The minor HTTP-Version number.
96
+ #
97
+ # @example
98
+ # version = Webbed::HTTPVersion.new('HTTP/4.2')
99
+ # version.minor # => 2
100
+ #
101
+ # @return [Fixnum]
48
102
  def minor
49
- (to_f - to_f.floor).to_s[2..-1].to_i
103
+ REGEX =~ @http_version
104
+ $2.to_i
50
105
  end
51
106
 
52
- ONE_POINT_ONE = HTTPVersion.new(1.1, true)
53
- ONE_POINT_OH = HTTPVersion.new(1.0, true)
107
+ ONE_POINT_ONE = HTTPVersion.new(1.1)
108
+ ONE_POINT_OH = HTTPVersion.new(1.0)
54
109
  end
55
110
  end
@@ -0,0 +1,160 @@
1
+ module Webbed
2
+ # Representation of an HTTP Media Type.
3
+ class MediaType
4
+ MIME_TYPE_REGEX = /^([-\w.+]+)\/([-\w.+]*)$/
5
+ PARAMETERS_REGEX = /=|\s*;\s*/
6
+
7
+ # The type of the MIME type.
8
+ #
9
+ # According to RFC 2616, this is the part *before* the slash.
10
+ #
11
+ # @example
12
+ # media_type = Webbed::MediaType.new('text/html')
13
+ # media_type.type # => 'text'
14
+ #
15
+ # @return [String]
16
+ attr_accessor :type
17
+
18
+ # The subtype of the MIME type.
19
+ #
20
+ # According to RFC 2616, this is the *after* before the slash.
21
+ #
22
+ # @example
23
+ # media_type = Webbed::MediaType.new('text/html')
24
+ # media_type.type # => 'html'
25
+ #
26
+ # @return [String]
27
+ attr_accessor :subtype
28
+
29
+ # The parameters of the Media Type.
30
+ #
31
+ # According to RFC 2616, parameters are separated from the MIME type and
32
+ # each other using a semicolon.
33
+ #
34
+ # @example
35
+ # media_type = Webbed::MediaType.new('text/html; q=1.0')
36
+ # media_type.parameters # => { 'q' => '1.0' }
37
+ #
38
+ # @return [Hash{String => String}]
39
+ attr_accessor :parameters
40
+
41
+ # Creates a new Media Type.
42
+ #
43
+ # @example Create a MediaType without parameters
44
+ # media_type = Webbed::MediaType.new('text/html')
45
+ # media_type.mime_type # => 'text/html'
46
+ # media_type.parameters # => {}
47
+ #
48
+ # @example Create a MediaType with parameters
49
+ # media_type = Webbed::MediaType.new('text/html; q=1.0')
50
+ # media_type.mime_type # => 'text/html'
51
+ # media_type.parameters # => { 'q' => '1.0' }
52
+ #
53
+ # @param [String] media_type the Media Type to create as defined in RFC 2616
54
+ def initialize(media_type)
55
+ self.mime_type, *parameters = media_type.split(PARAMETERS_REGEX)
56
+ self.parameters = Hash[*parameters] || {}
57
+ end
58
+
59
+ # The MIME type of the Media Type.
60
+ #
61
+ # @return [String]
62
+ def mime_type
63
+ "#{type}/#{subtype}"
64
+ end
65
+
66
+ # Sets the MIME type of the Media Type.
67
+ #
68
+ # @param [String] mime_type
69
+ def mime_type=(mime_type)
70
+ MIME_TYPE_REGEX =~ mime_type
71
+ self.type = $1
72
+ self.subtype = $2
73
+ end
74
+
75
+ # Converts the Media Type to a string.
76
+ #
77
+ # @return [String]
78
+ def to_s
79
+ if parameters.empty?
80
+ mime_type
81
+ else
82
+ parameters = self.parameters.map { |k, v| "#{k}=#{v}" }.join('; ')
83
+ "#{mime_type}; #{parameters}"
84
+ end
85
+ end
86
+
87
+ # Whether or not the Media Type is vendor-specific.
88
+ #
89
+ # The method uses the `vnd.` prefix convention to determine whether or not
90
+ # it was created for a specific vendor.
91
+ #
92
+ # @example
93
+ # media_type = Webbed::MediaType.new('application/json')
94
+ # media_type.vendor_specific? # => false
95
+ #
96
+ # media_type = Webbed::MediaType.new('application/vnd.my-special-type')
97
+ # media_type.vendor_specific? # => true
98
+ #
99
+ # @return [Boolean]
100
+ def vendor_specific?
101
+ subtype[0..3] == 'vnd.'
102
+ end
103
+
104
+ # The suffix of the MIME type.
105
+ #
106
+ # Suffixes follow the convention set forth by the Atom specification:
107
+ # separated from the rest of the MIME Type by a `+`.
108
+ #
109
+ # @example
110
+ # media_type = Webbed::MediaType.new('application/xml')
111
+ # media_type.suffix # => nil
112
+ #
113
+ # media_type = Webbed::MediaType.new('application/atom+xml')
114
+ # media_type.suffix # => 'xml'
115
+ #
116
+ # @return [String, nil]
117
+ def suffix
118
+ suffix = subtype.split('+')[-1]
119
+
120
+ if suffix != subtype
121
+ suffix
122
+ else
123
+ nil
124
+ end
125
+ end
126
+
127
+ # Compares the Media Type to another Media Type.
128
+ #
129
+ # Two Media Types are equal if their `#mime_type`'s are equal.
130
+ #
131
+ # @param [#mime_type] other_media_type the other Media Type
132
+ # @return [Boolean]
133
+ def ==(other_media_type)
134
+ mime_type == other_media_type.mime_type
135
+ end
136
+
137
+ # The MIME types that the Media Type can be interpreted as.
138
+ #
139
+ # This uses the suffix to generate a list of MIME types that can be used to
140
+ # interpret the Media Type. It's useful if you need to be able to parse XML
141
+ # or JSON Media Types that may or may not have suffixes using general XML
142
+ # or JSON parsers.
143
+ #
144
+ # @example
145
+ # media_type = Webbed::MediaType.new('application/xml')
146
+ # media_type.interpretable_as # ['application/xml']
147
+ #
148
+ # media_type = Webbed::MediaType.new('application/atom+xml')
149
+ # media_type.interpretable_as # => ['application/atom+xml', 'application/xml']
150
+ #
151
+ # @return [Array<String>]
152
+ def interpretable_as
153
+ if suffix
154
+ [mime_type, "#{type}/#{suffix}"]
155
+ else
156
+ [mime_type]
157
+ end
158
+ end
159
+ end
160
+ end