cloudkit 0.11.1 → 0.11.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,3 +1,11 @@
1
+ 0.11.2
2
+ - Added Location header for 201s
3
+ - Fixed JSON response for DELETE operations
4
+ - Updated MD5 dependency for Ruby 1.9 (Andreas Haller)
5
+ - Updated spec setup for RSpec 1.2 (Andreas Haller)
6
+ - Updated gem dependencies for Rack, OpenID, and JSON
7
+ - Other minor fixes
8
+
1
9
  0.11.1
2
10
  - Added a block option for configuring OpenID bypassed routes (Devlin Daley)
3
11
  - Added write locks for Tokyo Tyrant Tables
data/README CHANGED
@@ -17,7 +17,7 @@ In a rackup file called config.ru:
17
17
  require 'cloudkit'
18
18
  expose :notes, :todos
19
19
 
20
- The above creates a versioned HTTP service using JSON to represent two types of resource collections -- Notes and ToDos.
20
+ The above creates a versioned HTTP/REST service using JSON to represent two types of resource collections -- Notes and ToDos.
21
21
 
22
22
  In a different rackup file:
23
23
 
data/TODO CHANGED
@@ -2,18 +2,16 @@
2
2
  - openid sreg
3
3
  - oauth token management
4
4
  - oauth consumer registration
5
- - acls i.e. allowed methods per user per uri
6
- - jsonpath
7
5
  - jsonquery
8
6
  - jsonschema
9
7
  - user lookup via a block for integration with existing stores
10
8
  - custom templates for openid / oauth
11
9
 
12
10
  Backlog
11
+ - acls i.e. allowed methods per user per uri
13
12
  - method filtering on collections
14
13
  - js functions as observers (validation, mapping, etc.)
15
14
  - complete user data export
16
- - batch updates (post, put, delete)
17
15
  - expect header/100-continue
18
16
  - deployable gem + admin app
19
17
  - cappuccino adapter
@@ -2,8 +2,8 @@ Gem::Specification.new do |s|
2
2
  s.specification_version = 2 if s.respond_to? :specification_version=
3
3
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
4
  s.name = "cloudkit"
5
- s.version = "0.11.1"
6
- s.date = "2008-03-24"
5
+ s.version = "0.11.2"
6
+ s.date = "2008-05-03"
7
7
  s.summary = "An Open Web JSON Appliance."
8
8
  s.description = "An Open Web JSON Appliance."
9
9
  s.authors = ["Jon Crosby"]
@@ -77,9 +77,9 @@ Gem::Specification.new do |s|
77
77
  s.test_files = s.files.select {|path| path =~ /^spec\/.*_spec.rb/}
78
78
  s.rubyforge_project = "cloudkit"
79
79
  s.rubygems_version = "1.1.1"
80
- s.add_dependency 'rack', '~> 0.9'
80
+ s.add_dependency 'rack', '>= 1.0'
81
81
  s.add_dependency 'uuid', '= 2.0.1'
82
82
  s.add_dependency 'oauth', '~> 0.3'
83
- s.add_dependency 'ruby-openid', '= 2.1.2'
84
- s.add_dependency 'json', '= 1.1.3'
83
+ s.add_dependency 'ruby-openid', '~> 2.1'
84
+ s.add_dependency 'json', '~> 1.1'
85
85
  end
@@ -38,13 +38,13 @@ If you haven't already installed the gem:
38
38
  </p>
39
39
 
40
40
  <p>
41
- If you already have the gem, make sure you're running the latest version (0.11.1):
41
+ If you already have the gem, make sure you're running the latest version (0.11.2):
42
42
  <div class="code">
43
43
  $ gem list cloudkit<br/>
44
44
  cloudkit (0.10.0) &lt;-- need to upgrade<br/>
45
45
  $ gem update cloudkit<br/>
46
46
  $ gem list cloudkit<br/>
47
- cloudkit (0.11.1, 0.10.0) &lt;-- 0.11.1 is now in the list
47
+ cloudkit (0.11.2, 0.10.0) &lt;-- 0.11.2 is now in the list
48
48
  </div>
49
49
  </p>
50
50
 
@@ -120,6 +120,7 @@ Let's move on, creating a note using <a href="http://tools.ietf.org/html/rfc2616
120
120
  $ curl -i -XPOST -d'{"title":"projects"}' http://localhost:9292/notes<br/>
121
121
  HTTP/1.1 201 Created<br/>
122
122
  Cache-Control: no-cache<br/>
123
+ Location: http://localhost:9292/notes/0dda06f0-b134-012b-a2d8-0017f2c62348<br/>
123
124
  Content-Type: application/json<br/>
124
125
  Content-Length: 159<br/><br/>
125
126
 
@@ -139,6 +140,7 @@ so that we can specify its location:
139
140
  $ curl -i -XPUT -d'{"title":"reminders"}' http://localhost:9292/notes/abc<br/>
140
141
  HTTP/1.1 201 Created<br/>
141
142
  Cache-Control: no-cache<br/>
143
+ Location: http://localhost:9292/notes/abc<br/>
142
144
  Content-Type: application/json<br/>
143
145
  Content-Length: 126<br/><br/>
144
146
 
@@ -24,7 +24,7 @@
24
24
  </div>
25
25
  <div class="meta">
26
26
  <p class="wrapper">
27
- Version 0.11.1 released with Tokyo Cabinet support. Install with <em>gem install cloudkit</em>.
27
+ Version 0.11.2 released with Tokyo Cabinet support. Install with <em>gem install cloudkit</em>.
28
28
  </p>
29
29
  </div>
30
30
  <div class="wrapper intro-row">
@@ -1,7 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'erb'
3
3
  require 'json'
4
- require 'md5'
4
+ require 'digest/md5'
5
5
  require 'openid'
6
6
  require 'time'
7
7
  require 'uuid'
@@ -34,7 +34,7 @@ require 'cloudkit/user_store'
34
34
  include CloudKit::Constants
35
35
 
36
36
  module CloudKit
37
- VERSION = '0.11.1'
37
+ VERSION = '0.11.2'
38
38
 
39
39
  # Sets up the storage adapter. Defaults to development-time
40
40
  # CloudKit::MemoryTable. Also supports Rufus Tokyo Table instances. See the
@@ -9,7 +9,7 @@ module CloudKit
9
9
  # /oauth/authorized_request_tokens/{id}
10
10
  # /oauth/access_tokens
11
11
  #
12
- # Responds to the following URIs are part of OAuth Discovery:
12
+ # Responds to the following URIs as part of OAuth Discovery:
13
13
  # /oauth
14
14
  # /oauth/meta
15
15
  #
@@ -6,8 +6,17 @@ module CloudKit
6
6
  # The root URI, "/", is always bypassed. More URIs can also be bypassed using
7
7
  # the :allow option:
8
8
  #
9
+ # use Rack::Session::Pool
9
10
  # use OpenIDFilter, :allow => ['/foo', '/bar']
10
11
  #
12
+ # In addition to the :allow option, a block can also be used for more complex
13
+ # decisions:
14
+ #
15
+ # use Rack::Session::Pool
16
+ # use OpenIDFilter, :allow => ['/foo'] do |url|
17
+ # bar(url) # some method returning true or false
18
+ # end
19
+ #
11
20
  # Responds to the following URIs:
12
21
  # /login
13
22
  # /logout
@@ -4,7 +4,7 @@ module Rack #:nodoc:
4
4
 
5
5
  # Extends Rack::Builder's to_app method to detect if the last piece of
6
6
  # middleware in the stack is a CloudKit shortcut (contain or expose), adding
7
- # adding a default developer page at the root and a 404 everywhere else.
7
+ # a default developer page at the root and a 404 everywhere else.
8
8
  def to_app
9
9
  default_app = lambda do |env|
10
10
  if (env['PATH_INFO'] == '/')
@@ -168,5 +168,10 @@ module CloudKit
168
168
  def flash
169
169
  session[CLOUDKIT_FLASH] ||= CloudKit::FlashSession.new
170
170
  end
171
+
172
+ # Return the host and scheme
173
+ def domain_root
174
+ "#{scheme}://#{@env['HTTP_HOST']}"
175
+ end
171
176
  end
172
177
  end
@@ -1,7 +1,7 @@
1
1
  module CloudKit
2
2
 
3
3
  # A CloudKit Service is Rack middleware providing a REST/HTTP 1.1 interface to
4
- # a Store. Its primary purpose is initialize and adapt a Store for use in a
4
+ # a Store. Its primary purpose is to initialize and adapt a Store for use in a
5
5
  # Rack middleware stack.
6
6
  #
7
7
  # ==Examples
@@ -68,18 +68,22 @@ module CloudKit
68
68
  if tunnel_methods.include?(request['_method'].try(:upcase))
69
69
  return send(request['_method'].downcase, request)
70
70
  end
71
- @store.post(
71
+ response = @store.post(
72
72
  request.uri,
73
73
  {:json => request.json}.filter_merge!(
74
- :remote_user => request.current_user)).to_rack
74
+ :remote_user => request.current_user))
75
+ update_location_header(request, response)
76
+ response.to_rack
75
77
  end
76
78
 
77
79
  def put(request)
78
- @store.put(
80
+ response = @store.put(
79
81
  request.uri,
80
82
  {:json => request.json}.filter_merge!(
81
83
  :remote_user => request.current_user,
82
- :etag => request.if_match)).to_rack
84
+ :etag => request.if_match))
85
+ update_location_header(request, response)
86
+ response.to_rack
83
87
  end
84
88
 
85
89
  def delete(request)
@@ -114,21 +118,26 @@ module CloudKit
114
118
  end
115
119
 
116
120
  def versions_link_header(request)
117
- base_url = "#{request.scheme}://#{request.env['HTTP_HOST']}#{request.path_info}"
121
+ base_url = "#{request.domain_root}#{request.path_info}"
118
122
  "<#{base_url}/versions>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/versions\""
119
123
  end
120
124
 
121
125
  def resolved_link_header(request)
122
- base_url = "#{request.scheme}://#{request.env['HTTP_HOST']}#{request.path_info}"
126
+ base_url = "#{request.domain_root}#{request.path_info}"
123
127
  "<#{base_url}/_resolved>; rel=\"http://joncrosby.me/cloudkit/1.0/rel/resolved\""
124
128
  end
125
129
 
126
130
  def index_link_header(request)
127
131
  index_path = request.path_info.sub(/\/_resolved(\/)*$/, '')
128
- base_url = "#{request.scheme}://#{request.env['HTTP_HOST']}#{index_path}"
132
+ base_url = "#{request.domain_root}#{index_path}"
129
133
  "<#{base_url}>; rel=\"index\""
130
134
  end
131
135
 
136
+ def update_location_header(request, response)
137
+ return unless response['Location']
138
+ response['Location'] = "#{request.domain_root}#{response['Location']}"
139
+ end
140
+
132
141
  def auth_missing?(request)
133
142
  request.current_user == nil
134
143
  end
@@ -117,7 +117,8 @@ module CloudKit
117
117
  return status_412 if resource.etag != options[:etag]
118
118
 
119
119
  resource.delete
120
- return json_meta_response(200, resource.uri.string, resource.etag, resource.last_modified)
120
+ archived_resource = resource.previous_version
121
+ return json_meta_response(archived_resource.uri.string, archived_resource.etag, resource.last_modified)
121
122
  end
122
123
 
123
124
  # Build a response containing the allowed methods for a given URI.
@@ -276,7 +277,7 @@ module CloudKit
276
277
  def create_resource(uri, options)
277
278
  JSON.parse(options[:json]) rescue (return status_422)
278
279
  resource = CloudKit::Resource.create(uri, options[:json], options[:remote_user])
279
- json_meta_response(201, resource.uri.string, resource.etag, resource.last_modified)
280
+ json_create_response(resource.uri.string, resource.etag, resource.last_modified)
280
281
  end
281
282
 
282
283
  # Update the resource at the specified URI. Requires the :etag option.
@@ -288,7 +289,7 @@ module CloudKit
288
289
  return etag_required unless options[:etag]
289
290
  return status_412 unless options[:etag] == resource.etag
290
291
  resource.update(options[:json])
291
- return json_meta_response(200, uri.string, resource.etag, resource.last_modified)
292
+ return json_meta_response(uri.string, resource.etag, resource.last_modified)
292
293
  end
293
294
 
294
295
  # Bundle a collection of results as a list of URIs for the response.
@@ -337,7 +338,7 @@ module CloudKit
337
338
  # optimization for GETs. This method is used for collections of resources
338
339
  # where the optimization is not practical.
339
340
  def build_etag(data)
340
- MD5::md5(data.to_s).hexdigest
341
+ Digest::MD5.hexdigest(data.to_s)
341
342
  end
342
343
 
343
344
  # Returns true if the collection type is valid for this Store.
@@ -1,8 +1,8 @@
1
1
  module CloudKit
2
2
 
3
3
  # A CloudKit::Resource represents a "resource" in the REST/HTTP sense of the
4
- # word. It encapsulates a JSON document and its metadata such as it URI, ETag,
5
- # Last-Modified date, remote user, and its historical versions.
4
+ # word. It encapsulates a JSON document and its metadata such as its URI, ETag,
5
+ # Last-Modified date, remote user, and historical versions.
6
6
  class Resource
7
7
 
8
8
  attr_reader :uri, :etag, :last_modified, :json, :remote_user
@@ -60,7 +60,7 @@ module CloudKit
60
60
  end
61
61
 
62
62
  # Delete the given resource. This is a soft delete, archiving the previous
63
- # resource and inserted a deleted resource placeholder at the old URI.
63
+ # resource and inserting a deleted resource placeholder at the old URI.
64
64
  # Raises HistoricalIntegrityViolation for attempts to delete resources that
65
65
  # are not current.
66
66
  def delete
@@ -46,21 +46,32 @@ module CloudKit::ResponseHelpers
46
46
 
47
47
  def response(status, content='', etag=nil, last_modified=nil, options={})
48
48
  cache_control = options[:cache] == false ? 'no-cache' : 'proxy-revalidate'
49
- headers = {
49
+ etag = "\"#{etag}\"" if etag
50
+ headers = {}.filter_merge!(
50
51
  'Content-Type' => 'application/json',
51
- 'Cache-Control' => cache_control}
52
- headers.merge!('ETag' => "\"#{etag}\"") if etag
53
- headers.merge!('Last-Modified' => last_modified) if last_modified
52
+ 'Cache-Control' => cache_control,
53
+ 'Last-Modified' => last_modified,
54
+ 'Location' => options[:location],
55
+ 'ETag' => etag)
54
56
  CloudKit::Response.new(status, headers, content)
55
57
  end
56
58
 
57
- def json_meta_response(status, uri, etag, last_modified)
58
- json = JSON.generate(
59
+ def json_meta_response(uri, etag, last_modified)
60
+ json = json_metadata(uri, etag, last_modified)
61
+ response(200, json, nil, nil, :cache => false)
62
+ end
63
+
64
+ def json_create_response(uri, etag, last_modified)
65
+ json = json_metadata(uri, etag, last_modified)
66
+ response(201, json, nil, nil, {:cache => false, :location => uri})
67
+ end
68
+
69
+ def json_metadata(uri, etag, last_modified)
70
+ JSON.generate(
59
71
  :ok => true,
60
72
  :uri => uri,
61
73
  :etag => etag,
62
74
  :last_modified => last_modified)
63
- response(status, json, nil, nil, :cache => false)
64
75
  end
65
76
 
66
77
  def json_error(message)
@@ -1,6 +1,6 @@
1
1
  module CloudKit
2
2
 
3
- # A CloudKit::URI wraps a URI string, added methods useful for routing
3
+ # A CloudKit::URI wraps a URI string, adding methods useful for routing
4
4
  # in CloudKit as well as caching URI components for future comparisons.
5
5
  class URI
6
6
 
@@ -42,7 +42,7 @@ module CloudKit
42
42
 
43
43
  # Returns true if URI matches /{collection}
44
44
  def resource_collection_uri?
45
- return components.size == 1
45
+ return components.size == 1 && components[0] != 'cloudkit-meta'
46
46
  end
47
47
 
48
48
  # Returns true if URI matches /{collection}/_resolved
@@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/spec_helper'
2
2
 
3
3
  describe "A FlashSession" do
4
4
 
5
- setup do
5
+ before do
6
6
  @flash = CloudKit::FlashSession.new
7
7
  end
8
8
 
@@ -34,6 +34,8 @@ describe "An OpenIDFilter" do
34
34
  request = Rack::MockRequest.new(openid_app)
35
35
  response = request.get('/bar')
36
36
  response.status.should == 200
37
+ response = request.get('/foo')
38
+ response.status.should == 200
37
39
  end
38
40
 
39
41
  it "should redirect to the login page if authorization is required" do
@@ -182,4 +182,10 @@ describe "A Request" do
182
182
  request.last_path_element.should == 'def'
183
183
  end
184
184
 
185
+ it "should know its domain root" do
186
+ request = CloudKit::Request.new(Rack::MockRequest.env_for(
187
+ '/', 'HTTP_HOST' => 'example.com'))
188
+ request.domain_root.should == "http://example.com"
189
+ end
190
+
185
191
  end
@@ -570,7 +570,7 @@ describe "A CloudKit::Service" do
570
570
  before(:each) do
571
571
  json = JSON.generate(:this => 'that')
572
572
  @response = @request.post(
573
- '/items', {:input => json}.merge(VALID_TEST_AUTH))
573
+ '/items', {:input => json, 'HTTP_HOST' => 'example.org'}.merge(VALID_TEST_AUTH))
574
574
  @body = JSON.parse(@response.body)
575
575
  end
576
576
 
@@ -600,6 +600,10 @@ describe "A CloudKit::Service" do
600
600
  @response['Last-Modified'].should be_nil
601
601
  end
602
602
 
603
+ it "should return a Location header" do
604
+ @response['Location'].should match(/http:\/\/example.org#{@body['uri']}/)
605
+ end
606
+
603
607
  it "should return a 422 if parsing fails" do
604
608
  response = @request.post('/items', {:input => 'fail'}.merge(VALID_TEST_AUTH))
605
609
  response.status.should == 422
@@ -607,7 +611,7 @@ describe "A CloudKit::Service" do
607
611
 
608
612
  end
609
613
 
610
- describe "on PUT /:collection/:id" do
614
+ describe "on PUT /:collection/:id" do
611
615
 
612
616
  before(:each) do
613
617
  json = JSON.generate(:this => 'that')
@@ -626,11 +630,15 @@ describe "A CloudKit::Service" do
626
630
  it "should create a document if it does not already exist" do
627
631
  json = JSON.generate(:this => 'thing')
628
632
  response = @request.put(
629
- '/items/xyz', {:input => json}.merge(VALID_TEST_AUTH))
633
+ '/items/xyz', {
634
+ :input => json,
635
+ 'HTTP_HOST' => 'example.org'}.merge(VALID_TEST_AUTH))
630
636
  response.status.should == 201
631
637
  result = @request.get('/items/xyz', VALID_TEST_AUTH)
632
638
  result.status.should == 200
633
- JSON.parse(result.body)['this'].should == 'thing'
639
+ parsed_json = JSON.parse(result.body)['this']
640
+ parsed_json.should == 'thing'
641
+ response['Location'].should match(/http:\/\/example.org#{parsed_json['uri']}/)
634
642
  end
635
643
 
636
644
  it "should not create new resources using deleted resource URIs" do
@@ -782,6 +790,24 @@ describe "A CloudKit::Service" do
782
790
  json.keys.sort.should == ['etag', 'last_modified', 'ok', 'uri']
783
791
  end
784
792
 
793
+ it "should use the archived URI in the JSON result" do
794
+ response = @request.delete(
795
+ '/items/abc',
796
+ 'HTTP_IF_MATCH' => @etag,
797
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
798
+ json = JSON.parse(response.body)
799
+ json['uri'].should match(/\/items\/abc\/versions\/.+/)
800
+ end
801
+
802
+ it "should not change the ETag for the archived version" do
803
+ response = @request.delete(
804
+ '/items/abc',
805
+ 'HTTP_IF_MATCH' => @etag,
806
+ CLOUDKIT_AUTH_KEY => TEST_REMOTE_USER)
807
+ json = JSON.parse(response.body)
808
+ json['etag'].should == @etag
809
+ end
810
+
785
811
  it "should set the Content-Type header" do
786
812
  response = @request.delete(
787
813
  '/items/abc',
@@ -36,7 +36,7 @@ describe "A URI" do
36
36
  end
37
37
 
38
38
  it "should know if it is a resource collection URI" do
39
- ['/items/123', '/items/123/versions', '/items/123/versions/abc'].each { |uri|
39
+ ['/cloudkit-meta', '/items/123', '/items/123/versions', '/items/123/versions/abc'].each { |uri|
40
40
  CloudKit::URI.new(uri).should_not be_resource_collection_uri
41
41
  }
42
42
  CloudKit::URI.new('/items').should be_resource_collection_uri
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloudkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Crosby
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-03-24 00:00:00 -07:00
12
+ date: 2008-05-03 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -18,9 +18,9 @@ dependencies:
18
18
  version_requirement:
19
19
  version_requirements: !ruby/object:Gem::Requirement
20
20
  requirements:
21
- - - ~>
21
+ - - ">="
22
22
  - !ruby/object:Gem::Version
23
- version: "0.9"
23
+ version: "1.0"
24
24
  version:
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: uuid
@@ -48,9 +48,9 @@ dependencies:
48
48
  version_requirement:
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "="
51
+ - - ~>
52
52
  - !ruby/object:Gem::Version
53
- version: 2.1.2
53
+ version: "2.1"
54
54
  version:
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: json
@@ -58,9 +58,9 @@ dependencies:
58
58
  version_requirement:
59
59
  version_requirements: !ruby/object:Gem::Requirement
60
60
  requirements:
61
- - - "="
61
+ - - ~>
62
62
  - !ruby/object:Gem::Version
63
- version: 1.1.3
63
+ version: "1.1"
64
64
  version:
65
65
  description: An Open Web JSON Appliance.
66
66
  email: jon@joncrosby.me