rackful 0.1.4 → 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.
@@ -1,81 +1,68 @@
1
- # Required for parsing:
2
- #require 'forwardable' # Used to be for ResourceFactoryWrapper.
3
-
4
- # Required for running:
5
-
6
1
  module Rackful
7
2
 
8
- =begin markdown
9
- Rack compliant server class for implementing RESTful web services.
10
- =end
3
+ # Rack compliant server class for implementing RESTful web services.
11
4
  class Server
12
5
 
13
6
 
14
- =begin markdown
15
- An object responding thread safely to method `#[]`.
16
-
17
- A {Server} has no knowledge, and makes no presumptions, about your URI namespace.
18
- It requires a _Resource Factory_ which produces {Resource Resources} given
19
- a certain absolute path.
20
-
21
- The Resource Factory you provide need only implement one method, with signature
22
- `Resource #[]( String path )`.
23
- This method will be called with a URI-encoded path string, and must return a
24
- {Resource}, or `nil` if there's no resource at the given path.
25
-
26
- For example, if a Rackful client
27
- tries to access a resource with URI {http://example.com/your/resource http://example.com/some/resource},
28
- then your Resource Factory can expect to be called like this:
29
-
30
- resource = resource_factory[ '/your/resource' ]
31
-
32
- If there's no resource at the given path, but you'd still like to respond to
33
- `POST` or `PUT` requests to this path, you must return an
34
- {Resource#empty? empty resource}.
35
- @return [#[]]
36
- @see #initialize
37
- =end
38
- attr_reader :resource_factory
7
+ # Constructor.
8
+ #
9
+ # This generic server class has no knowledge, and makes no presumptions,
10
+ # about your URI namespace. It depends on the code block you provide here
11
+ # to produce the {Resource} object which lives at a certain URI.
12
+ # This block will be called with a {URI::Generic#normalize! normalized}
13
+ # URI, and must return a {Resource}, or `nil` if there’s no
14
+ # resource at the given URI.
15
+ #
16
+ # If there’s no resource at the given URI, but you’d still like to respond to
17
+ # `POST` or `PUT` requests to this URI, you can return an
18
+ # {Resource#empty? empty resource}.
19
+ #
20
+ # The provided code block must be thread-safe and reentrant.
21
+ # @yieldparam uri [URI::Generic] The {URI::Generic::normalize! normalized}
22
+ # URI of the requested resource.
23
+ # @yieldreturn [Resource] A (possibly {Resource#empty? empty}) resource, or nil.
24
+ def initialize( &resource_registry )
25
+ @resource_registry = resource_registry
26
+ end
39
27
 
40
28
 
41
- =begin markdown
42
- @param resource_factory [#[]] see Server#resource_factory
43
- =end
44
- def initialize(resource_factory)
45
- super()
46
- @resource_factory = resource_factory
29
+ # Calls the code block passed to the {#initialize constructor}.
30
+ # @param uri [URI::HTTP, String]
31
+ # @return [Resource]
32
+ # @raise [HTTP404NotFound]
33
+ def resource_at(uri)
34
+ uri = URI(uri) unless uri.kind_of?( URI::Generic )
35
+ retval = @resource_registry.call( uri.normalize )
36
+ raise HTTP404NotFound unless retval
37
+ retval
47
38
  end
48
39
 
49
40
 
50
- =begin markdown
51
- As required by the Rack specification.
52
-
53
- For thread safety, this method clones `self`, which handles the request in
54
- {#call!}. A similar approach is taken by the Sinatra library.
55
- @return [Array<(status_code, response_headers, response_body)>]
56
- =end
57
- def call(p_env)
58
- start = Time.now
59
- retval = dup.call! p_env
60
- #$stderr.puts( p_env.inspect )
61
- retval
41
+ # As required by the Rack specification.
42
+ #
43
+ # For thread safety, this method clones `self`, which handles the request in
44
+ # {#call!}.
45
+ # For reentrancy, the clone is stored in the environment.
46
+ # @param env [{String => Mixed}]
47
+ # @return [(status_code, response_headers, response_body)]
48
+ def call( env )
49
+ ( env['rackful.server'] ||= self.dup ).call!( env )
62
50
  end
63
51
 
64
52
 
65
- =begin markdown
66
- @return [Array<(status_code, response_headers, response_body)>]
67
- =end
68
- def call!(p_env)
69
- request = Request.new( self.resource_factory, p_env )
70
- # See also Request::current():
71
- Thread.current[:rackful_request] = request
53
+ # @see #call
54
+ # @return [(status_code, response_headers, response_body)]
55
+ def call!( env )
56
+ request = Request.new( env )
72
57
  response = Rack::Response.new
73
58
  begin
74
- raise HTTP404NotFound, request.path \
75
- unless resource = self.resource_factory[Path.new(request.path)]
76
- unless resource.path == request.path
77
- response.header['Content-Location'] = resource.path
78
- request.content_path = resource.path
59
+ resource = resource_at( request.url )
60
+ request.canonical_uri = resource.uri
61
+ if request.url != request.canonical_uri.to_s
62
+ if %w{HEAD GET}.include?( request.request_method )
63
+ raise HTTP301MovedPermanently, request.canonical_uri
64
+ end
65
+ response.header['Content-Location'] = request.canonical_uri.to_s
79
66
  end
80
67
  request.assert_if_headers resource
81
68
  if %w{HEAD GET OPTIONS PUT DELETE}.include?( request.request_method )
@@ -84,32 +71,35 @@ For thread safety, this method clones `self`, which handles the request in
84
71
  resource.http_method request, response
85
72
  end
86
73
  rescue HTTPStatus => e
87
- # Already handled by HTTPStatus#initialize:
88
- #raise if $DEBUG && 500 <= e.status
89
- bct = e.class.best_content_type request.accept, false
90
- serializer = e.serializer(bct)
91
- response = Rack::Response.new serializer, e.status, e.headers
92
- ensure
93
- # The next line fixes a small peculiarity in RFC2616: the response body of
94
- # a `HEAD` request _must_ be empty, even for responses outside 2xx.
95
- if request.head?
96
- response.body = []
74
+ serializer = e.serializer(request)
75
+ response = Rack::Response.new
76
+ response['Content-Type'] = serializer.content_type
77
+ response.status = e.status
78
+ if serializer.respond_to? :headers
79
+ response.headers.merge!( serializer.headers )
97
80
  end
81
+ response.body = serializer
98
82
  end
99
- if 201 == response.status &&
100
- ( location = response['Location'] ) &&
101
- ( new_resource = request.resource_factory[location] ) &&
102
- ! new_resource.empty? \
103
- or ( (200...300) === response.status ||
104
- 304 == response.status ) &&
105
- ! response['Location'] &&
106
- ( new_resource = request.resource_factory[request.path] ) &&
107
- ! new_resource.empty?
108
- response.headers.merge! new_resource.default_headers
83
+ # The next line fixes a small peculiarity in RFC2616: the response body of
84
+ # a `HEAD` request _must_ be empty, even for responses outside 2xx.
85
+ if request.head?
86
+ response.body = []
87
+ end
88
+ begin
89
+ if 201 == response.status &&
90
+ ( location = response['Location'] ) &&
91
+ ( new_resource = resource_at( location ) ) &&
92
+ ! new_resource.empty? \
93
+ or ( (200...300) === response.status ||
94
+ 304 == response.status ) &&
95
+ ! response['Location'] &&
96
+ ( new_resource = resource_at( request.canonical_uri ) ) &&
97
+ ! new_resource.empty?
98
+ response.headers.merge! new_resource.default_headers
99
+ end
100
+ rescue HTTP404NotFound => e
109
101
  end
110
- r = response.finish
111
- #~ $stderr.puts r.inspect
112
- r
102
+ response.finish
113
103
  end
114
104
 
115
105
 
@@ -0,0 +1,150 @@
1
+ # Extension and monkeypatch of Ruby’s StdLib URI::Generic class.
2
+ class URI::Generic
3
+
4
+ unless method_defined? :uri_generic_normalize!
5
+ # Copy of the original StdLib
6
+ # [URI::Generic::normalize!](http://ruby-doc.org/stdlib/libdoc/uri/rdoc/URI/Generic.html#method-i-normalize-21)
7
+ # method.
8
+ alias_method :uri_generic_normalize!, :normalize!
9
+ end
10
+
11
+ unless method_defined? :uri_generic_normalize
12
+ # Copy of the original StdLib
13
+ # [URI::Generic::normalize!](http://ruby-doc.org/stdlib/libdoc/uri/rdoc/URI/Generic.html#method-i-normalize-21)
14
+ # method.
15
+ alias_method :uri_generic_normalize, :normalize
16
+ end
17
+
18
+ # (see #normalize!)
19
+ # @return [URI::Generic] a normalized copy of `self`.
20
+ def normalize
21
+ r = self.dup
22
+ r.normalize!
23
+ r
24
+ end
25
+
26
+ # Monkeypatch of [Ruby’s StdLib implementation](http://ruby-doc.org/stdlib/libdoc/uri/rdoc/URI/Generic.html#method-i-normalize-21).
27
+ # In addition to the default implementation, this implementation ensures that
28
+ #
29
+ # 1. _no_ unreserved characters are pct-encoded, and
30
+ # 2. all non-unreserved characters _are_ pct-encoded.
31
+ #
32
+ # Check [RFC3986](http://tools.ietf.org/html/rfc3986) syntax:
33
+ #
34
+ # abempty = *( "/" *(unreserved / pct-encoded / sub-delims / ":" / "@") )
35
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
36
+ # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
37
+ # @return [self, nil] `self` if the URI was modified, or `nil` of the uri was
38
+ # already in normal form.
39
+ def normalize!
40
+ #unless %r{\A(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*\z}i === self.path
41
+ # raise TypeError, "Can’t convert String #{self.path.inspect} to Rackful::Path"
42
+ #end
43
+ self.uri_generic_normalize!
44
+ path = '/' + self.segments( Encoding::BINARY ).collect do |segment|
45
+ segment.gsub(/([^a-zA-Z0-9\-._~]+)/n) {
46
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
47
+ }
48
+ end.join('/')
49
+ if path == self.path
50
+ nil
51
+ else
52
+ self.path = path
53
+ self
54
+ end
55
+ end
56
+
57
+
58
+ # @return [Path] a copy of `self`, with a trailing slash.
59
+ def slashify
60
+ r = self.dup
61
+ r.slashify!
62
+ r
63
+ end
64
+
65
+
66
+ # Adds a trailing slash to `self` if necessary.
67
+ # @return [self]
68
+ def slashify!
69
+ if '/' != self.path[-1,1]
70
+ self.path += '/'
71
+ self
72
+ else
73
+ nil
74
+ end
75
+ end
76
+
77
+
78
+ # @return [Path] a copy of `self` without trailing slashes.
79
+ def unslashify
80
+ r = self.dup
81
+ r.unslashify!
82
+ r
83
+ end
84
+
85
+
86
+ # Removes trailing slashes from `self`.
87
+ # @return [self]
88
+ def unslashify!
89
+ path = self.path.sub( %r{/+\z}, '' )
90
+ if path == self.path
91
+ nil
92
+ else
93
+ self.path = path
94
+ self
95
+ end
96
+ end
97
+
98
+
99
+ # @return [Array<String>] Unencoded segments
100
+ def segments( encoding = Encoding::UTF_8 )
101
+ r = self.path.split(%r{/+}).collect do |s|
102
+ Rack::Utils.unescape( s, encoding )
103
+ end
104
+ r.shift
105
+ r
106
+ end
107
+
108
+
109
+ # Turns a relative URI (starting with `/`) into a relative path (starting with `./` or `../`)
110
+ # @param base_path [Path]
111
+ # @return [String] a relative URI
112
+ # @deprecated
113
+ def relative_deprecated base_path
114
+ case self
115
+ when base_path
116
+ # RFC2396, Section 4.2
117
+ return ''
118
+ when %r{(?:\A|/)\.\.?(?:/|\z)}
119
+ # self has abnormal absolute path,
120
+ # like "/./", "/../", "/x/../", ...
121
+ return self.dup
122
+ end
123
+
124
+ src_path = base_path.scan(%r{(?:\A|[^/]+)/})
125
+ dst_path = self.scan(%r{(?:\A|[^/]+)/?})
126
+
127
+ # discard same parts
128
+ while !dst_path.empty? && dst_path.first == src_path.first
129
+ src_path.shift
130
+ dst_path.shift
131
+ end
132
+
133
+ tmp = dst_path.join
134
+
135
+ # calculate
136
+ if src_path.empty?
137
+ if tmp.empty?
138
+ return './'
139
+ elsif dst_path.first.include?(':') # (see RFC2396 Section 5)
140
+ return './' + tmp
141
+ else
142
+ return tmp
143
+ end
144
+ end
145
+
146
+ return '../' * src_path.size + tmp
147
+ end
148
+
149
+
150
+ end # class URI::Generic
data/mkdoc.sh CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/bin/bash
2
+ # Nice options: server --reload
2
3
 
3
- cd "`dirname "$0"`"
4
+ cd "`dirname "${BASH_SOURCE[0]}"`"
4
5
 
5
6
  rm -rf doc/* .yardoc/
6
- exec yard "$@"
7
+ yard --yardopts .yardopts.user
8
+ yard --yardopts .yardopts.devel
data/rackful.gemspec CHANGED
@@ -2,14 +2,14 @@ Gem::Specification.new do |s|
2
2
 
3
3
  # Required properties:
4
4
  s.name = 'rackful'
5
- s.version = '0.1.4'
5
+ s.version = '0.2.0'
6
6
  s.summary = "Library for building ReSTful web services with Rack"
7
7
  s.description = <<EOS
8
8
  Rackful provides a minimal interface for developing ReSTful web services with
9
- Rack and Ruby. Instead of writing HTTP method handlers, you'll implement
9
+ Rack and Ruby. Instead of writing HTTP method handlers, youll implement
10
10
  resource objects, which expose their state at URLs.
11
11
 
12
- This version is not backward compatible with v0.0.x.
12
+ This version is not backward compatible with v0.1.x.
13
13
  EOS
14
14
  s.files = Dir[ '{*.md,example/*.ru,lib/**/*.rb}' ] +
15
15
  %w( rackful.gemspec mkdoc.sh )
@@ -18,8 +18,9 @@ EOS
18
18
  s.author = 'Pieter van Beek'
19
19
  s.email = 'rackful@djinnit.com'
20
20
  s.license = 'Apache License 2.0'
21
- s.homepage = 'http://pieterb.github.com/Rackful/'
21
+ s.homepage = 'http://github.com/pieterb/Rackful'
22
22
 
23
- s.add_runtime_dependency 'rack', '>= 1.4'
23
+ s.add_runtime_dependency 'rack', '~> 1.5'
24
+ s.add_runtime_dependency 'nokogiri', '~> 1.6'
24
25
 
25
26
  end
metadata CHANGED
@@ -1,87 +1,95 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: rackful
3
- version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.1.4
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
6
5
  platform: ruby
7
- authors:
6
+ authors:
8
7
  - Pieter van Beek
9
- autorequire:
8
+ autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
-
13
- date: 2012-11-26 00:00:00 Z
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
11
+ date: 2014-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
16
14
  name: rack
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ~>
23
+ - !ruby/object:Gem::Version
24
+ version: '1.5'
25
+ prerelease: false
26
+ type: :runtime
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: '1.6'
17
39
  prerelease: false
18
- requirement: &id001 !ruby/object:Gem::Requirement
19
- none: false
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: "1.4"
24
40
  type: :runtime
25
- version_requirements: *id001
26
41
  description: |
27
42
  Rackful provides a minimal interface for developing ReSTful web services with
28
- Rack and Ruby. Instead of writing HTTP method handlers, you'll implement
43
+ Rack and Ruby. Instead of writing HTTP method handlers, youll implement
29
44
  resource objects, which expose their state at URLs.
30
-
31
- This version is not backward compatible with v0.0.x.
32
45
 
46
+ This version is not backward compatible with v0.1.x.
33
47
  email: rackful@djinnit.com
34
48
  executables: []
35
-
36
49
  extensions: []
37
-
38
50
  extra_rdoc_files: []
39
-
40
- files:
51
+ files:
52
+ - CHANGES.md
53
+ - LICENSE.md
41
54
  - RACKFUL.md
42
55
  - README.md
43
- - LICENSE.md
44
- - CHANGES.md
45
56
  - example/config.ru
57
+ - lib/rackful.rb
58
+ - lib/rackful/httpstatus.rb
59
+ - lib/rackful/middleware.rb
60
+ - lib/rackful/middleware/headerspoofing.rb
61
+ - lib/rackful/middleware/methodoverride.rb
62
+ - lib/rackful/parser.rb
63
+ - lib/rackful/request.rb
46
64
  - lib/rackful/resource.rb
47
- - lib/rackful/path.rb
48
- - lib/rackful/http_status.rb
49
65
  - lib/rackful/serializer.rb
50
66
  - lib/rackful/server.rb
51
- - lib/rackful/request.rb
52
- - lib/rackful/middleware.rb
53
- - lib/rackful/middleware/method_spoofing.rb
54
- - lib/rackful/middleware/header_spoofing.rb
55
- - lib/rackful/middleware/relative_location.rb
56
- - lib/rackful.rb
57
- - rackful.gemspec
67
+ - lib/rackful/uri.rb
58
68
  - mkdoc.sh
59
- homepage: http://pieterb.github.com/Rackful/
60
- licenses:
69
+ - rackful.gemspec
70
+ homepage: http://github.com/pieterb/Rackful
71
+ licenses:
61
72
  - Apache License 2.0
62
- post_install_message:
73
+ metadata: {}
74
+ post_install_message:
63
75
  rdoc_options: []
64
-
65
- require_paths:
76
+ require_paths:
66
77
  - lib
67
- required_ruby_version: !ruby/object:Gem::Requirement
68
- none: false
69
- requirements:
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: "0"
73
- required_rubygems_version: !ruby/object:Gem::Requirement
74
- none: false
75
- requirements:
76
- - - ">="
77
- - !ruby/object:Gem::Version
78
- version: "0"
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
79
88
  requirements: []
80
-
81
- rubyforge_project:
82
- rubygems_version: 1.8.24
83
- signing_key:
84
- specification_version: 3
89
+ rubyforge_project:
90
+ rubygems_version: 2.2.0
91
+ signing_key:
92
+ specification_version: 4
85
93
  summary: Library for building ReSTful web services with Rack
86
94
  test_files: []
87
-
95
+ has_rdoc: