cloudkit 0.11.1 → 0.11.2

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/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