rackful 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: