rackjson 0.3.2 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -53,6 +53,8 @@ When creating resources with either the public or private resources the specifie
53
53
 
54
54
  ### REST API
55
55
 
56
+ #### Collections
57
+
56
58
  To see what actions are available on the notes resource:
57
59
 
58
60
  curl -i -XOPTIONS http://localhost:9292/notes
@@ -148,6 +150,98 @@ Finally a resource can be deleted using a DELETE request
148
150
 
149
151
  {"ok": "true"}
150
152
 
153
+ #### Nested Documents
154
+
155
+ Rack::JSON fully supports nested documents. Any element within a document can be accessed directly regardless of how deeply it is nested. For example if the following document exists at the location `/notes/1`
156
+
157
+ {
158
+ "_id": 1,
159
+ "title": "Nested Document",
160
+ "author": {
161
+ "name": "Bob",
162
+ "contacts": {
163
+ "email": "bob@mail.com"
164
+ }
165
+ },
166
+ "viewed_by": [1, 5, 12, 87],
167
+ "comments": [{
168
+ "user_id": 1
169
+ "text": "awesome!"
170
+ }]
171
+ }
172
+
173
+ To get just all the comments we can make a get request to `/notes/1/comments`
174
+
175
+ curl -i http://localhost:9292/notes/1/comments
176
+
177
+ HTTP/1.1 200 OK
178
+ Connection: close
179
+ Date: Sun, 29 Aug 2010 19:43:09 GMT
180
+ Content-Type: application/json
181
+ Content-Length: 33
182
+
183
+ [{"text":"awesome!","user_id":1}]
184
+
185
+ We can also get just the first comment by passing in the index of that comment in the array, to get the first comment make a GET request to `/notes/1/comments/0`
186
+
187
+ curl -i http://localhost:9292/notes/1/comments/0
188
+
189
+ HTTP/1.1 200 OK
190
+ Connection: close
191
+ Date: Sun, 29 Aug 2010 19:45:28 GMT
192
+ Content-Type: application/json
193
+ Content-Length: 31
194
+
195
+ {"text":"awesome!","user_id":1}
196
+
197
+ If we try and get a comment that doesn't exist in the array a 404 is returned.
198
+
199
+ curl -i http://localhost:9292/notes/1/comments/1
200
+
201
+ HTTP/1.1 404 Not Found
202
+ Connection: close
203
+ Date: Sun, 29 Aug 2010 19:46:46 GMT
204
+ Content-Type: text/plain
205
+ Content-Length: 15
206
+
207
+ field not found
208
+
209
+ Any field within the document is accessable in this way, just append the field name or the index of the item within an array to the url.
210
+
211
+ As well as providing read access to any field within a document Rack::JSON also allows you to modify or remove any field within a document. To change the value of a field make a PUT request to the fields url and pass the value you want as the body.
212
+
213
+ Both simple values, numbers and strings, or JSON structures can be set in this way, however if you want to set a field to contain a JSON structure (array or object) you must set the correct content type for the request, application/json.
214
+
215
+ #### Array Modifiers
216
+
217
+ Fields within a document that are arrays also support atomic push and pulls for adding and removing items from an array.
218
+
219
+ To push a new item onto an array we make a post request to _push
220
+
221
+ curl -i -XPOST -d'101' http://localhost:9292/notes/1/viewed_by/_push
222
+
223
+ The above will push the value 101 onto the viewed by array within the note with _id 1.
224
+
225
+ Similarly an item can be pulled from an array using _pull
226
+
227
+ curl -i -XPOST -d'101' http://localhost:9292/notes/1/viewed_by/_pull
228
+
229
+ This will remove the value 101 from the viewed_by array if it already exists.
230
+ To remove or add more than one item from an array we can use either _pull_all or _push_all passing in an array each time.
231
+
232
+ Arrays within documents can also be treated like sets and only add items that do not currently exists by using the add_to_set command like below
233
+
234
+ curl -i -XPOST -d'101' http://localhost:9292/notes/1/viewed_by/_add_to_set
235
+
236
+ This will only add the value 101 to the viewed_by array if it doesn't already exist.
237
+
238
+ #### Incrementing & Decrementing
239
+
240
+ RackJSON provides a simple means of incrementing and decrementing counters within a document, simply make a post request to either _increment or _decrement as shown below
241
+
242
+ curl -i -XPOST http://localhost:9292/notes/1/views/_increment
243
+ curl -i -XPOST http://localhost:9292/notes/1/views/_decrement
244
+
151
245
  ### JSON Query
152
246
 
153
247
  RackJSON supports querying of the resources using JSONQuery style syntax. Pass the JSONQuery as query string parameters when making a get request.
data/Rakefile CHANGED
@@ -10,7 +10,7 @@ begin
10
10
  gem.email = "oliver.n@new-bamboo.co.uk"
11
11
  gem.homepage = "http://github.com/olivernn/rackjson"
12
12
  gem.authors = ["Oliver Nightingale"]
13
- gem.add_dependency('mongo', '>=1.0.0')
13
+ gem.add_dependency('mongo', '>=1.1.0')
14
14
  gem.add_dependency('mongo_ext', '>=0.19.1')
15
15
  gem.add_dependency('json', '=1.2.3')
16
16
  gem.add_dependency('rack', '>=1.0.1')
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.4.1
@@ -1,5 +1,10 @@
1
+ require 'enumerator'
2
+
1
3
  module Rack::JSON
2
4
  class Collection
5
+
6
+ class Rack::JSON::Collection::DataTypeError < TypeError ; end
7
+
3
8
  def initialize(collection)
4
9
  @collection = collection
5
10
  end
@@ -8,6 +13,10 @@ module Rack::JSON
8
13
  @collection.remove(prepared(selector))
9
14
  end
10
15
 
16
+ def delete_field(selector, field)
17
+ _update(prepared(selector), { "$unset" => { dot_notate(field) => 1 }, "$set" => { :updated_at => Time.now }})
18
+ end
19
+
11
20
  def exists?(selector)
12
21
  !@collection.find(prepared(selector)).to_a.empty?
13
22
  end
@@ -16,8 +25,28 @@ module Rack::JSON
16
25
  @collection.find(selector, options).inject([]) {|documents, row| documents << Rack::JSON::Document.create(row)}
17
26
  end
18
27
 
28
+ def find_field(selector, fields, options={})
29
+ document = find_one(prepared(selector))
30
+ document ? document.field(fields) : nil
31
+ end
32
+
19
33
  def find_one(selector, options={})
20
- find(prepared(selector), options).first
34
+ find(prepared(selector), options.merge(:limit => 0)).first
35
+ end
36
+
37
+ def decrement(selector, field, value=1)
38
+ _update(prepared(selector), { "$inc" => { dot_notate(field) => -1 * (value || 1) }, "$set" => { :updated_at => Time.now }})
39
+ end
40
+
41
+ def increment(selector, field, value=1)
42
+ _update(prepared(selector), { "$inc" => { dot_notate(field) => value || 1 }, "$set" => { :updated_at => Time.now }})
43
+ end
44
+
45
+ [:pull, :pull_all, :push, :push_all, :add_to_set].each do |method_name|
46
+ define_method method_name do |selector, field, value|
47
+ modifier = "$#{method_name.to_s.split('_').to_enum.each_with_index.map { |w, i| i == 0 ? w : w.capitalize }.join}"
48
+ _update(prepared(selector), { modifier => { dot_notate(field) => value }, "$set" => { :updated_at => Time.now }})
49
+ end
21
50
  end
22
51
 
23
52
  def save(document)
@@ -26,16 +55,28 @@ module Rack::JSON
26
55
 
27
56
  def update(selector, document, query={})
28
57
  if exists?(prepared(selector).merge(query))
29
- @collection.update(prepared(selector).merge(query), document.to_h, :upsert => false)
58
+ _update(prepared(selector).merge(query), document.to_h, :upsert => false)
30
59
  else
31
60
  false
32
61
  end
33
62
  end
34
63
 
64
+ def update_field(selector, field, value)
65
+ _update(prepared(selector), { "$set" => { dot_notate(field) => value, :updated_at => Time.now }})
66
+ end
67
+
35
68
  private
36
69
 
70
+ def dot_notate field
71
+ field.is_a?(Array) ? field.join(".") : field
72
+ end
73
+
37
74
  def prepared selector
38
75
  selector.is_a?(Hash) ? selector : {:_id => selector}
39
76
  end
77
+
78
+ def _update(query, hash, options={})
79
+ @collection.update(query, hash, options)
80
+ end
40
81
  end
41
82
  end
@@ -15,11 +15,22 @@ module Rack::JSON
15
15
  end
16
16
  end
17
17
 
18
-
19
18
  def add_attributes(pair)
20
19
  attributes.merge!(pair)
21
20
  end
22
21
 
22
+ def field(field_names)
23
+ attrs = attributes
24
+ Array.wrap(field_names).each do |field_name|
25
+ if attrs.is_a? Array
26
+ attrs = attrs[field_name.to_i]
27
+ else
28
+ attrs = attrs[field_name]
29
+ end
30
+ end
31
+ attrs
32
+ end
33
+
23
34
  def set_id(val)
24
35
  add_attributes('_id' => val) unless attributes.keys.include? '_id'
25
36
  end
@@ -3,6 +3,10 @@ module Rack::JSON
3
3
 
4
4
  private
5
5
 
6
+ def bad_request error
7
+ error_response error, 400
8
+ end
9
+
6
10
  def bypass? request
7
11
  request.collection.empty? || !(@collections.include? request.collection.to_sym)
8
12
  end
@@ -15,8 +19,12 @@ module Rack::JSON
15
19
  !@methods.include?(request.request_method.downcase.to_sym)
16
20
  end
17
21
 
22
+ def error_response error, status_code
23
+ render (error.class.to_s + " :" + error.message), :status => status_code
24
+ end
25
+
18
26
  def invalid_json error
19
- render (error.class.to_s + " :" + error.message), :status => 422
27
+ error_response error, 422
20
28
  end
21
29
 
22
30
  def method_not_allowed? request
@@ -1,5 +1,5 @@
1
1
  module BSON
2
- class ObjectID
2
+ class ObjectId
3
3
  def to_json
4
4
  to_s
5
5
  end
@@ -0,0 +1,12 @@
1
+ class Array
2
+ def self.wrap(object)
3
+ case object
4
+ when nil
5
+ []
6
+ when self
7
+ object
8
+ else
9
+ [object]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ class String
2
+ def numeric?
3
+ true if Float(self) rescue false
4
+ end
5
+
6
+ def to_number
7
+ if numeric?
8
+ f = to_f
9
+ i = to_i
10
+ f == i ? i : f
11
+ end
12
+ end
13
+ end
@@ -17,8 +17,8 @@ module Rack::JSON
17
17
  end
18
18
 
19
19
  def set_attribute_ids
20
- attributes["_id"] = BSON::ObjectID.from_string(attributes["_id"].to_s)
21
- rescue BSON::InvalidObjectID
20
+ attributes["_id"] = BSON::ObjectId.from_string(attributes["_id"].to_s)
21
+ rescue BSON::InvalidObjectId
22
22
  return false
23
23
  end
24
24
 
@@ -1,9 +1,10 @@
1
1
  module Rack::JSON
2
2
  class JSONQuery
3
3
 
4
- attr_accessor :options, :selector
4
+ attr_accessor :options, :selector, :resource_id
5
5
 
6
- def initialize(query_string)
6
+ def initialize(query_string, options={})
7
+ @resource_id = options[:resource_id] || nil
7
8
  @query_string = query_string
8
9
  @conditions = @query_string.split(/\[|\]/).compact.reject {|s| s.empty? }
9
10
  @options = {}
@@ -21,6 +22,7 @@ module Rack::JSON
21
22
  end
22
23
  end
23
24
  end
25
+ add_query_document_id
24
26
  end
25
27
 
26
28
  def comparison(symbol)
@@ -32,6 +34,12 @@ module Rack::JSON
32
34
  }[symbol]
33
35
  end
34
36
 
37
+ def add_query_document_id
38
+ if resource_id
39
+ @selector[:_id] = resource_id
40
+ end
41
+ end
42
+
35
43
  def set_query_fields(condition)
36
44
  if condition.match /^=\w+$/
37
45
  @options[:fields] = condition.sub('=', '').split(',')
@@ -9,7 +9,7 @@ module Rack::JSON
9
9
  private
10
10
 
11
11
  def set_attribute_ids
12
- attributes["_id"] = attributes["_id"].to_s if (attributes["_id"].is_a? BSON::ObjectID)
12
+ attributes["_id"] = attributes["_id"].to_s if (attributes["_id"].is_a? BSON::ObjectId)
13
13
  end
14
14
  end
15
15
  end
@@ -1,5 +1,8 @@
1
1
  module Rack::JSON
2
2
  class Request < Rack::Request
3
+
4
+ class Rack::JSON::Request::UnrecognisedPathTypeError < StandardError ; end
5
+
3
6
  include Rack::Utils
4
7
 
5
8
  attr_reader :env
@@ -21,25 +24,82 @@ module Rack::JSON
21
24
  self.path_info.match /^\/[\w-]+$/
22
25
  end
23
26
 
27
+ def field
28
+ path_info.split('/')[3] || ""
29
+ end
30
+
31
+ def fields
32
+ path_info.split('/').slice(3..-1).reject { |f| f.match(/(_increment|_decrement|_push|_pull|_push_all|_pull_all|_add_to_set)/)} || []
33
+ end
34
+
35
+ def field_path?
36
+ path_info.match(/^\/[\w-]+\/[\w-]+\/[\w-]+(\/[\w-]+)*$/) && !modifier_path?
37
+ end
38
+
24
39
  def member_path?
25
- self.path_info.match /^\/\w+\/[\w-]+$/
40
+ self.path_info.match /^\/[\w-]+\/[\w-]+$/
41
+ end
42
+
43
+ def modifier
44
+ modifier_path? ? path_info.split('/').last : nil
45
+ end
46
+
47
+ def modifier_path?
48
+ path_info.match /^\/[\w-]+\/[\w-]+\/[\w-]+(\/[\w-]+)*\/(_increment|_decrement|_push|_pull|_push_all|_pull_all|_add_to_set)$/
49
+ end
50
+
51
+ def path_type
52
+ if member_path?
53
+ :member
54
+ elsif field_path?
55
+ :field
56
+ elsif collection_path?
57
+ :collection
58
+ else
59
+ raise UnrecognisedPathTypeError
60
+ end
61
+ end
62
+
63
+ def payload
64
+ if content_type == 'application/json'
65
+ JSON.parse(raw_body)
66
+ elsif raw_body.empty?
67
+ nil
68
+ else
69
+ raw_body.numeric? ? raw_body.to_number : raw_body
70
+ end
71
+ end
72
+
73
+ def property
74
+ property = path_info.split('/')[4]
75
+ if property
76
+ property.match(/^\d+$/)? property.to_i : property
77
+ else
78
+ nil
79
+ end
26
80
  end
27
81
 
28
82
  def json
29
- self.body.rewind
30
- self.body.read
83
+ raw_body
31
84
  end
32
85
 
33
86
  def query
34
- @query ||= Rack::JSON::JSONQuery.new(unescape(query_string))
87
+ @query ||= Rack::JSON::JSONQuery.new(unescape(query_string), :resource_id => resource_id)
88
+ end
89
+
90
+ def raw_body
91
+ self.body.rewind
92
+ self.body.read
35
93
  end
36
94
 
37
95
  def resource_id
38
- id_string = self.path_info.split('/').last.to_s
39
- begin
40
- BSON::ObjectID.from_string(id_string)
41
- rescue BSON::InvalidObjectID
42
- id_string.match(/^\d+$/) ? id_string.to_i : id_string
96
+ unless collection_path?
97
+ id_string = self.path_info.split('/')[2].to_s
98
+ begin
99
+ BSON::ObjectId.from_string(id_string)
100
+ rescue BSON::InvalidObjectId
101
+ id_string.match(/^\d+$/) ? id_string.to_i : id_string
102
+ end
43
103
  end
44
104
  end
45
105
 
@@ -1,7 +1,7 @@
1
1
  module Rack::JSON
2
2
  class Resource
3
3
  include Rack::JSON::EndPoint
4
- HTTP_METHODS = [:get, :post, :put, :delete]
4
+ HTTP_METHODS = [:get, :post, :put, :delete, :options]
5
5
 
6
6
  def initialize(app, options)
7
7
  @app = app
@@ -24,11 +24,19 @@ module Rack::JSON
24
24
 
25
25
  private
26
26
 
27
+ def create(request)
28
+ document = Rack::JSON::Document.create(request.json)
29
+ @collection.save(document)
30
+ render document, :status => 201
31
+ end
32
+
27
33
  def delete(request)
28
- if request.member_path?
29
- if @collection.delete({:_id => request.resource_id})
30
- render "{'ok': true}"
31
- end
34
+ if request.field_path?
35
+ @collection.delete_field(request.query.selector, request.fields)
36
+ render "", :status => 204
37
+ elsif request.member_path?
38
+ @collection.delete(request.query.selector)
39
+ render "", :status => 204
32
40
  else
33
41
  render "", :status => 405
34
42
  end
@@ -36,12 +44,28 @@ module Rack::JSON
36
44
 
37
45
  [:get, :head].each do |method|
38
46
  define_method method do |request|
39
- request.member_path? ? get_member(request, method) : get_collection(request, method)
47
+ begin
48
+ send("get_#{request.path_type}", request, method)
49
+ rescue Rack::JSON::Request::UnrecognisedPathTypeError => error
50
+ bad_request error
51
+ end
52
+ end
53
+ end
54
+
55
+ def get_collection(request, method)
56
+ render @collection.find(request.query.selector, request.query.options)
57
+ end
58
+
59
+ def get_field(request, method)
60
+ field = @collection.find_field(request.query.selector, request.fields, request.query.options)
61
+ if field
62
+ render field, :head => (method == :head)
63
+ else
64
+ render "field not found", :status => 404, :head => (method == :head)
40
65
  end
41
66
  end
42
67
 
43
68
  def get_member(request, method)
44
- request.query.selector.merge!({:_id => request.resource_id})
45
69
  document = @collection.find_one(request.query.selector, request.query.options)
46
70
  if document
47
71
  render document, :head => (method == :head)
@@ -50,37 +74,46 @@ module Rack::JSON
50
74
  end
51
75
  end
52
76
 
53
- def get_collection(request, method)
54
- render @collection.find(request.query.selector, request.query.options)
55
- end
56
-
57
- def not_allowed?(request)
58
-
59
- end
60
-
61
77
  def options(request)
62
78
  if request.collection_path?
63
79
  headers = { "Allow" => "GET, POST" }
64
80
  elsif request.member_path?
65
81
  headers = { "Allow" => "GET, PUT, DELETE" }
82
+ elsif request.field_path?
83
+ headers = { "Allow" => "GET, PUT, DELETE" }
84
+ elsif request.modifier_path?
85
+ headers = { "Allow" => "POST" }
66
86
  end
67
87
  render "", :headers => headers
68
88
  end
69
89
 
70
90
  def post(request)
71
- document = Rack::JSON::Document.create(request.json)
72
- @collection.save(document)
73
- render document, :status => 201
91
+ if request.collection_path?
92
+ create(request)
93
+ elsif request.modifier_path?
94
+ @collection.exists?(request.resource_id) ? modify(request) : render("document not found", :status => 404)
95
+ else
96
+ render "", :status => 405
97
+ end
74
98
  rescue JSON::ParserError => error
75
99
  invalid_json error
76
100
  end
77
101
 
78
102
  def put(request)
79
- @collection.exists?(request.resource_id) ? update(request) : upsert(request)
103
+ if request.field_path?
104
+ @collection.exists?(request.resource_id) ? update_field(request) : render("document not found", :status => 404)
105
+ else
106
+ @collection.exists?(request.resource_id) ? update(request) : upsert(request)
107
+ end
80
108
  rescue JSON::ParserError => error
81
109
  invalid_json error
82
110
  end
83
111
 
112
+ def modify(request)
113
+ @collection.send(request.modifier[1..-1], request.query.selector, request.fields, request.payload)
114
+ render "OK", :status => 200
115
+ end
116
+
84
117
  def update(request)
85
118
  document = Rack::JSON::Document.create(request.json)
86
119
  document.set_id(request.resource_id)
@@ -91,6 +124,11 @@ module Rack::JSON
91
124
  end
92
125
  end
93
126
 
127
+ def update_field(request)
128
+ @collection.update_field(request.query.selector, request.fields, request.payload)
129
+ render "OK", :status => 200
130
+ end
131
+
94
132
  def upsert(request)
95
133
  document = Rack::JSON::Document.create(request.json)
96
134
  document.set_id(request.resource_id)
@@ -25,11 +25,11 @@ module Rack::JSON
25
25
  end
26
26
 
27
27
  def parse_body(body)
28
- if body.is_a?(Rack::JSON::Document) || body.is_a?(Array)
28
+ if body.is_a?(Rack::JSON::Document) || body.is_a?(Array) || body.is_a?(Hash)
29
29
  @body = body.to_json
30
30
  @headers["Content-Type"] = "application/json"
31
- elsif body.is_a? String
32
- @body = body
31
+ elsif body.is_a?(String) || body.is_a?(Fixnum)
32
+ @body = body.to_s
33
33
  @headers["Content-Type"] = "text/plain"
34
34
  else
35
35
  raise Rack::JSON::Response::BodyFormatError
data/lib/rackjson.rb CHANGED
@@ -4,6 +4,8 @@ require 'rack'
4
4
  require 'mongo'
5
5
  require 'time'
6
6
  require 'rackjson/rack/builder'
7
+ require 'rackjson/extensions/core/array'
8
+ require 'rackjson/extensions/core/string'
7
9
 
8
10
  module Rack::JSON
9
11
 
@@ -19,7 +21,7 @@ module Rack::JSON
19
21
  autoload :Request, 'rackjson/request'
20
22
  autoload :Resource, 'rackjson/resource'
21
23
  autoload :Response, 'rackjson/response'
22
- autoload :ObjectID, 'rackjson/extensions/bson/object_id'
24
+ autoload :ObjectId, 'rackjson/extensions/bson/object_id'
23
25
  autoload :OrderedHash, 'rackjson/extensions/bson/ordered_hash'
24
26
 
25
27
  end
data/rackjson.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{rackjson}
8
- s.version = "0.3.2"
8
+ s.version = "0.4.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Oliver Nightingale"]
12
- s.date = %q{2010-08-17}
12
+ s.date = %q{2010-10-06}
13
13
  s.description = %q{A rack end point for storing json documents.}
14
14
  s.email = %q{oliver.n@new-bamboo.co.uk}
15
15
  s.extra_rdoc_files = [
@@ -29,6 +29,8 @@ Gem::Specification.new do |s|
29
29
  "lib/rackjson/end_point.rb",
30
30
  "lib/rackjson/extensions/BSON/object_id.rb",
31
31
  "lib/rackjson/extensions/BSON/ordered_hash.rb",
32
+ "lib/rackjson/extensions/core/array.rb",
33
+ "lib/rackjson/extensions/core/string.rb",
32
34
  "lib/rackjson/filter.rb",
33
35
  "lib/rackjson/json_document.rb",
34
36
  "lib/rackjson/json_query.rb",
@@ -40,6 +42,7 @@ Gem::Specification.new do |s|
40
42
  "rackjson.gemspec",
41
43
  "test/helper.rb",
42
44
  "test/suite.rb",
45
+ "test/test_collection.rb",
43
46
  "test/test_document.rb",
44
47
  "test/test_filter.rb",
45
48
  "test/test_json_document.rb",
@@ -57,6 +60,7 @@ Gem::Specification.new do |s|
57
60
  s.test_files = [
58
61
  "test/helper.rb",
59
62
  "test/suite.rb",
63
+ "test/test_collection.rb",
60
64
  "test/test_document.rb",
61
65
  "test/test_filter.rb",
62
66
  "test/test_json_document.rb",
@@ -64,7 +68,6 @@ Gem::Specification.new do |s|
64
68
  "test/test_mongo_document.rb",
65
69
  "test/test_rack_builder.rb",
66
70
  "test/test_resource.rb",
67
- "test/test_resource_modifier.rb",
68
71
  "test/test_response.rb"
69
72
  ]
70
73
 
@@ -73,18 +76,18 @@ Gem::Specification.new do |s|
73
76
  s.specification_version = 3
74
77
 
75
78
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
76
- s.add_runtime_dependency(%q<mongo>, [">= 1.0.0"])
79
+ s.add_runtime_dependency(%q<mongo>, [">= 1.1.0"])
77
80
  s.add_runtime_dependency(%q<mongo_ext>, [">= 0.19.1"])
78
81
  s.add_runtime_dependency(%q<json>, ["= 1.2.3"])
79
82
  s.add_runtime_dependency(%q<rack>, [">= 1.0.1"])
80
83
  else
81
- s.add_dependency(%q<mongo>, [">= 1.0.0"])
84
+ s.add_dependency(%q<mongo>, [">= 1.1.0"])
82
85
  s.add_dependency(%q<mongo_ext>, [">= 0.19.1"])
83
86
  s.add_dependency(%q<json>, ["= 1.2.3"])
84
87
  s.add_dependency(%q<rack>, [">= 1.0.1"])
85
88
  end
86
89
  else
87
- s.add_dependency(%q<mongo>, [">= 1.0.0"])
90
+ s.add_dependency(%q<mongo>, [">= 1.1.0"])
88
91
  s.add_dependency(%q<mongo_ext>, [">= 0.19.1"])
89
92
  s.add_dependency(%q<json>, ["= 1.2.3"])
90
93
  s.add_dependency(%q<rack>, [">= 1.0.1"])
@@ -0,0 +1,125 @@
1
+ require 'helper'
2
+
3
+ class CollectionTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @db = Mongo::Connection.new.db("test")
7
+ @doc = @db['resource_test'].insert({:_id => 1, :count => 1, :field => 'foo', :array => [1,2,3,4], :obj => { :field => 'baz'}})
8
+ @collection = Rack::JSON::Collection.new(@db['resource_test'])
9
+ end
10
+
11
+ def teardown
12
+ @db['resource_test'].drop
13
+ end
14
+
15
+ test "should be able to retrieve a specific element from a document in the collection" do
16
+ assert_equal('foo', @collection.find_field(1, ['field']))
17
+ end
18
+
19
+ test "should return nil if there is no matching field" do
20
+ assert_nil(@collection.find_field(1, ['non-existant-field']))
21
+ end
22
+
23
+ test "should be able to retrieve a specific element form an array" do
24
+ assert_equal(2, @collection.find_field(1, ['array', 1]))
25
+ end
26
+
27
+ test "should return nil if asking for an array element that doesn't exist" do
28
+ assert_nil(@collection.find_field(1, ['array', 100]))
29
+ end
30
+
31
+ test "should be able to retrieve a specific element from an embedded object" do
32
+ assert_equal('baz', @collection.find_field(1, ['obj', 'field']))
33
+ end
34
+
35
+ test "should return nil if asking for an element that doesn't exist on the embeded object" do
36
+ assert_nil(@collection.find_field(1, ['obj', 'nonexistant']))
37
+ end
38
+
39
+ test "attomic increment" do
40
+ @collection.increment(1, 'count')
41
+ assert_equal(2, @collection.find_field(1, 'count'))
42
+ end
43
+
44
+ test "incrementing by more than 1" do
45
+ @collection.increment(1, 'count', 2)
46
+ assert_equal(3, @collection.find_field(1, 'count'))
47
+ end
48
+
49
+ test "attomic decrement" do
50
+ @collection.decrement(1, 'count')
51
+ assert_equal(0, @collection.find_field(1, 'count'))
52
+ end
53
+
54
+ test "decrementing by more than 1" do
55
+ @collection.decrement(1, 'count', 2)
56
+ assert_equal(-1, @collection.find_field(1, 'count'))
57
+ end
58
+
59
+ test "attomic push" do
60
+ @collection.push(1, 'array', 'pushed value')
61
+ assert_equal([1,2,3,4,'pushed value'], @collection.find_field(1, 'array'))
62
+ end
63
+
64
+ test "attomic push on a new list" do
65
+ @collection.push(1, 'new-array', 'pushed value')
66
+ assert_equal(['pushed value'], @collection.find_field(1, 'new-array'))
67
+ end
68
+
69
+ # mongo isn't throwing an error when doing this, may need to upgrade
70
+ # still not throwing an error in 1.6
71
+ # test "attomic push on a non list field should raise a DataTypeError" do
72
+ # assert_raises Rack::JSON::Collection::DataTypeError do
73
+ # @collection.push(1, 'count', 'pushed value')
74
+ # end
75
+ # end
76
+
77
+ test "attomic push all on an existing list" do
78
+ @collection.push_all(1, 'array', ["a", "b", "c"])
79
+ assert_equal([1,2,3,4,'a','b','c'], @collection.find_field(1, 'array'))
80
+ end
81
+
82
+ test "attomic push all on to create a new list" do
83
+ @collection.push_all(1, 'new-array', ["a", "b", "c"])
84
+ assert_equal(["a", "b", "c"], @collection.find_field(1, 'new-array'))
85
+ end
86
+
87
+ test "attomic pull item form a list" do
88
+ @collection.pull(1, 'array', 4)
89
+ assert_equal([1,2,3], @collection.find_field(1, 'array'))
90
+ end
91
+
92
+ test "attomic pull all to remove more than one item from a list" do
93
+ @collection.pull_all(1, 'array', [1,2,3])
94
+ assert_equal([4], @collection.find_field(1, 'array'))
95
+ end
96
+
97
+ # currently failing because add to set is supported in mongo v1.3+
98
+ test "adding an element to an array only if it doesn't already exist in the array" do
99
+ @collection.add_to_set(1, 'array', 'pushed value')
100
+ assert_equal([1,2,3,4,'pushed value'], @collection.find_field(1, 'array'))
101
+ @collection.add_to_set(1, 'array', 1)
102
+ assert_equal([1,2,3,4,'pushed value'], @collection.find_field(1, 'array'))
103
+ end
104
+
105
+ test "updating a specific field of a document" do
106
+ @collection.update_field(1, 'field', 'bar')
107
+ assert_equal('bar', @collection.find_field(1, 'field'))
108
+ end
109
+
110
+ test "updating a field that doesn't currently exist in a document" do
111
+ @collection.update_field(1, 'new-field', 'bar')
112
+ assert_equal('bar', @collection.find_field(1, 'new-field'))
113
+ end
114
+
115
+ test "updating a nested field in a document" do
116
+ @collection.update_field(1, ['obj', 'field'], 'blah')
117
+ assert_equal('blah', @collection.find_field(1, ['obj', 'field']))
118
+ end
119
+
120
+ test "deleting a specific field from a document" do
121
+ @collection.delete_field(1, 'field')
122
+ assert_nil(@collection.find_field(1, 'field'))
123
+ end
124
+
125
+ end
@@ -29,7 +29,7 @@ class DocumentTest < Test::Unit::TestCase
29
29
  def test_creating_from_json_with_id
30
30
  json = '{"_id": "4b9f783ba040140525000001", "test":"hello"}'
31
31
  document = Rack::JSON::Document.create(json)
32
- assert_equal(BSON::ObjectID.from_string('4b9f783ba040140525000001'), document.attributes["_id"])
32
+ assert_equal(BSON::ObjectId.from_string('4b9f783ba040140525000001'), document.attributes["_id"])
33
33
  assert_equal("hello", document.attributes["test"])
34
34
  assert_equal(Time.now.to_s, document.attributes["created_at"].to_s)
35
35
  assert_equal(Time.now.to_s, document.attributes["updated_at"].to_s)
@@ -82,4 +82,11 @@ class DocumentTest < Test::Unit::TestCase
82
82
  assert_equal("01/01/2010", document.attributes["created_at"])
83
83
  assert_equal(Time.now.to_s, document.attributes["updated_at"].to_s)
84
84
  end
85
+
86
+ test "accessing getting a single attribute from a document" do
87
+ json = '{"test":"hello", "nested":{"foo":"bar"}}'
88
+ document = Rack::JSON::Document.create(json)
89
+ assert_equal "hello", document.field(["test"])
90
+ assert_equal "bar", document.field(["nested", "foo"])
91
+ end
85
92
  end
@@ -24,7 +24,7 @@ class JSONDocumentTest < Test::Unit::TestCase
24
24
  def test_parsing_mongo_object_id
25
25
  hash = { "_id" => "4ba7e82ca04014011c000001" }
26
26
  doc = JSON.generate hash
27
- assert_equal(BSON::ObjectID.from_string("4ba7e82ca04014011c000001"), Rack::JSON::JSONDocument.new(doc).attributes["_id"])
27
+ assert_equal(BSON::ObjectId.from_string("4ba7e82ca04014011c000001"), Rack::JSON::JSONDocument.new(doc).attributes["_id"])
28
28
  end
29
29
 
30
30
  def test_parsing_non_mongo_object_ids
@@ -61,6 +61,12 @@ class QueryTest < Test::Unit::TestCase
61
61
  assert_equal({:price => {'$lt' => 10}}, query.selector)
62
62
  end
63
63
 
64
+ test "automatically merging in the _id from the resource id" do
65
+ json_query = '[?name=bob!]'
66
+ query = Rack::JSON::JSONQuery.new(json_query, :resource_id => 1)
67
+ assert_equal({:_id => 1, :name => 'bob!'}, query.selector)
68
+ end
69
+
64
70
  # def test_single_greater_than_or_equal_condition
65
71
  # json_query = '[?price=<10]'
66
72
  # query = Rack::JSON::JSONQuery.new(json_query)
@@ -3,7 +3,7 @@ require 'helper'
3
3
  class MongoDocumentTest < Test::Unit::TestCase
4
4
 
5
5
  def test_stringifying_mongo_object_ids
6
- hash = {"_id" => BSON::ObjectID.from_string("4ba7e82ca04014011c000001")}
6
+ hash = {"_id" => BSON::ObjectId.from_string("4ba7e82ca04014011c000001")}
7
7
  doc = Rack::JSON::MongoDocument.new(hash).attributes
8
8
  assert_equal("4ba7e82ca04014011c000001", doc["_id"])
9
9
  end
@@ -94,6 +94,55 @@ class ResourceTest < Test::Unit::TestCase
94
94
  assert_equal "document not found", last_response.body
95
95
  end
96
96
 
97
+ test "finding a field within a specific document" do
98
+ @collection.save({:testing => true, :rating => 5, :title => 'testing', :_id => 1})
99
+ get '/testing/1/title'
100
+ assert last_response.ok?
101
+ assert_equal "testing", last_response.body
102
+ end
103
+
104
+ test "trying to find a field within a non-existant document" do
105
+ get '/testing/1/title'
106
+ assert_equal 404, last_response.status
107
+ end
108
+
109
+ test "finding an array inside a document" do
110
+ @collection.save({:obj => { :hello => "world"}, :ratings => [5,2], :title => 'testing', :_id => 1})
111
+ get '/testing/1/ratings'
112
+ assert last_response.ok?
113
+ expected = [5,2]
114
+ assert_equal expected, JSON.parse(last_response.body)
115
+ end
116
+
117
+ test "finding an element of an array from a specific document" do
118
+ @collection.save({:testing => true, :ratings => [5,2], :title => 'testing', :_id => 1})
119
+ get '/testing/1/ratings/0'
120
+ assert last_response.ok?
121
+ assert_equal "5", last_response.body
122
+ end
123
+
124
+ test "finding an embedded document" do
125
+ @collection.save({:obj => { :hello => "world"}, :ratings => [5,2], :title => 'testing', :_id => 1})
126
+ get '/testing/1/obj'
127
+ assert last_response.ok?
128
+ expected = { "hello" => "world" }
129
+ assert_equal expected, JSON.parse(last_response.body)
130
+ end
131
+
132
+ test "finding a property of an embedded document" do
133
+ @collection.save({:obj => { :hello => "world"}, :ratings => [5,2], :title => 'testing', :_id => 1})
134
+ get '/testing/1/obj/hello'
135
+ assert last_response.ok?
136
+ assert_equal "world", last_response.body
137
+ end
138
+
139
+ test "incrementing a property of an embedded document" do
140
+ @collection.save({:obj => { :counter => 1}, :_id => 1})
141
+ post '/testing/1/obj/counter/_increment'
142
+ assert last_response.ok?
143
+ assert_equal 2, @collection.find_one(:_id => 1)["obj"]["counter"]
144
+ end
145
+
97
146
  test "index method with query parameters" do
98
147
  @collection.save({:testing => true, :rating => 5, :title => 'testing'})
99
148
  get '/testing?[?title=testing]'
@@ -144,14 +193,128 @@ class ResourceTest < Test::Unit::TestCase
144
193
  assert_nil @collection.find_one(:_id => 1, :user_id => 1)
145
194
  end
146
195
 
196
+ test "updating a field within a document" do
197
+ @collection.save({:title => 'testing', :_id => 1})
198
+ put '/testing/1/title', "updated"
199
+ assert last_response.ok?
200
+ assert_equal "updated", @collection.find_one(:_id => 1)['title']
201
+ end
202
+
203
+ test "creating a new field within an existing documennt" do
204
+ @collection.save({:title => 'testing', :_id => 1})
205
+ put '/testing/1/new_field', "created"
206
+ assert last_response.ok?
207
+ assert_equal "created", @collection.find_one(:_id => 1)['new_field']
208
+ end
209
+
210
+ test "trying to create a new field within a non-existant document" do
211
+ @collection.save({:title => 'testing', :_id => 1})
212
+ put '/testing/2/title', "updated"
213
+ assert_equal 404, last_response.status
214
+ end
215
+
216
+ test "incrementing a value within a document" do
217
+ @collection.save({:counter => 1, :_id => 1})
218
+ post '/testing/1/counter/_increment'
219
+ assert last_response.ok?
220
+ assert_equal 2, @collection.find_one(:_id => 1)["counter"]
221
+ end
222
+
223
+ test "incrementing a non existant field" do
224
+ @collection.save({:_id => 1})
225
+ post '/testing/1/counter/_increment'
226
+ assert last_response.ok?
227
+ assert_equal 1, @collection.find_one(:_id => 1)["counter"]
228
+ end
229
+
230
+ test "incrementing a value within a document by a custom amount" do
231
+ @collection.save({:counter => 1, :_id => 1})
232
+ post '/testing/1/counter/_increment', '10'
233
+ assert last_response.ok?
234
+ assert_equal 11, @collection.find_one(:_id => 1)["counter"]
235
+ end
236
+
237
+ test "incrementing a value on a non existent document" do
238
+ post '/testing/1/counter/_increment'
239
+ assert_equal 404, last_response.status
240
+ end
241
+
242
+ test "decrementing a value within a document" do
243
+ @collection.save({:counter => 1, :_id => 1})
244
+ post '/testing/1/counter/_decrement'
245
+ assert last_response.ok?
246
+ assert_equal 0, @collection.find_one(:_id => 1)["counter"]
247
+ end
248
+
249
+ test "push a simple value onto an array within a document" do
250
+ @collection.save({:list => [1,2,3], :_id => 1})
251
+ post '/testing/1/list/_push', '4'
252
+ assert last_response.ok?
253
+ assert_equal [1,2,3,4], @collection.find_one(:_id => 1)['list']
254
+ end
255
+
256
+ test "push an object onto an array within a document" do
257
+ @collection.save({:list => [1,2,3], :_id => 1})
258
+ header 'Content-Type', 'application/json'
259
+ post '/testing/1/list/_push', '{"foo": "bar"}'
260
+ assert last_response.ok?
261
+ assert_equal [1,2,3,{"foo" => "bar"}], @collection.find_one(:_id => 1)['list']
262
+ end
263
+
264
+ test "push more than one item onto an array within a document" do
265
+ @collection.save({:list => [1,2,3], :_id => 1})
266
+ header 'Content-Type', 'application/json'
267
+ post '/testing/1/list/_push_all', '[4,5,6,7]'
268
+ assert last_response.ok?
269
+ assert_equal [1,2,3,4,5,6,7], @collection.find_one(:_id => 1)['list']
270
+ end
271
+
272
+ test "pull a simple value from an array within a document" do
273
+ @collection.save({:list => [1,2,3], :_id => 1})
274
+ post '/testing/1/list/_pull', '2'
275
+ assert last_response.ok?
276
+ assert_equal [1,3], @collection.find_one(:_id => 1)['list']
277
+ end
278
+
279
+ test "pull an object from an array within a document" do
280
+ @collection.save({:list => [1,2,3,{"foo" => "bar"}], :_id => 1})
281
+ header 'Content-Type', 'application/json'
282
+ post '/testing/1/list/_pull', '{ "foo": "bar" }'
283
+ assert last_response.ok?
284
+ assert_equal [1,2,3], @collection.find_one(:_id => 1)['list']
285
+ end
286
+
287
+ test "pull more than one item from an array within a document" do
288
+ @collection.save({:list => [1,2,3,4,5,6,7], :_id => 1})
289
+ header 'Content-Type', 'application/json'
290
+ post '/testing/1/list/_pull_all', '[4,5,6,7]'
291
+ assert last_response.ok?
292
+ assert_equal [1,2,3], @collection.find_one(:_id => 1)['list']
293
+ end
294
+
295
+ test "adding an item to a set" do
296
+ @collection.save({:list => [1,2,3], :_id => 1})
297
+ post '/testing/1/list/_add_to_set', '4'
298
+ assert last_response.ok?
299
+ assert_equal [1,2,3,4], @collection.find_one(:_id => 1)['list']
300
+ end
301
+
147
302
  test "deleting a document" do
148
303
  @collection.save({:title => 'testing', :_id => 1})
149
304
  assert @collection.find_one({:_id => 1})
150
305
  delete '/testing/1'
151
- assert last_response.ok?
306
+ assert_equal 204, last_response.status
152
307
  assert_nil @collection.find_one({:_id => 1})
153
308
  end
154
309
 
310
+ test "deleting a field within a document" do
311
+ @collection.save({:title => 'testing', :_id => 1})
312
+ assert @collection.find_one({:_id => 1})
313
+ delete '/testing/1/title'
314
+ assert_equal 204, last_response.status
315
+ assert_nil @collection.find_one({:_id => 1})['title']
316
+ end
317
+
155
318
  test "deleting only with member path" do
156
319
  delete '/testing'
157
320
  assert_equal 405, last_response.status
@@ -38,6 +38,11 @@ class ResponseTest < Test::Unit::TestCase
38
38
  assert_match(JSON.parse(response.body)['title'], 'Hello')
39
39
  end
40
40
 
41
+ test "sending a number" do
42
+ response = Rack::JSON::Response.new(1)
43
+ assert_equal "1", response.body
44
+ end
45
+
41
46
  def test_head_response
42
47
  response = Rack::JSON::Response.new("test", :head => true)
43
48
  assert_equal([""], response.to_a[2])
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 3
8
- - 2
9
- version: 0.3.2
7
+ - 4
8
+ - 1
9
+ version: 0.4.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Oliver Nightingale
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-08-17 00:00:00 +01:00
17
+ date: 2010-10-06 00:00:00 +01:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -26,9 +26,9 @@ dependencies:
26
26
  - !ruby/object:Gem::Version
27
27
  segments:
28
28
  - 1
29
+ - 1
29
30
  - 0
30
- - 0
31
- version: 1.0.0
31
+ version: 1.1.0
32
32
  type: :runtime
33
33
  version_requirements: *id001
34
34
  - !ruby/object:Gem::Dependency
@@ -95,6 +95,8 @@ files:
95
95
  - lib/rackjson/end_point.rb
96
96
  - lib/rackjson/extensions/BSON/object_id.rb
97
97
  - lib/rackjson/extensions/BSON/ordered_hash.rb
98
+ - lib/rackjson/extensions/core/array.rb
99
+ - lib/rackjson/extensions/core/string.rb
98
100
  - lib/rackjson/filter.rb
99
101
  - lib/rackjson/json_document.rb
100
102
  - lib/rackjson/json_query.rb
@@ -106,6 +108,7 @@ files:
106
108
  - rackjson.gemspec
107
109
  - test/helper.rb
108
110
  - test/suite.rb
111
+ - test/test_collection.rb
109
112
  - test/test_document.rb
110
113
  - test/test_filter.rb
111
114
  - test/test_json_document.rb
@@ -147,6 +150,7 @@ summary: A rack end point for storing json documents.
147
150
  test_files:
148
151
  - test/helper.rb
149
152
  - test/suite.rb
153
+ - test/test_collection.rb
150
154
  - test/test_document.rb
151
155
  - test/test_filter.rb
152
156
  - test/test_json_document.rb
@@ -154,5 +158,4 @@ test_files:
154
158
  - test/test_mongo_document.rb
155
159
  - test/test_rack_builder.rb
156
160
  - test/test_resource.rb
157
- - test/test_resource_modifier.rb
158
161
  - test/test_response.rb
@@ -1,73 +0,0 @@
1
- # require 'helper'
2
- #
3
- # class ResourceModifierTest < Test::Unit::TestCase
4
- # include Rack::Test::Methods
5
- # include Rack::Utils
6
- #
7
- # def setup
8
- # @db = Mongo::Connection.new.db("test")
9
- # @collection = @db['testing']
10
- # @doc = @collection.save(:_id => 1, :count => 0, :likers => [])
11
- # end
12
- #
13
- # def teardown
14
- # @collection.drop
15
- # end
16
- #
17
- # def app
18
- # Rack::JSON::ResourceModifier.new lambda { |env|
19
- # [404, {'Content-Length' => '9', 'Content-Type' => 'text/plain'}, ["Not Found"]]
20
- # }, :collections => [:testing], :db => @db
21
- # end
22
- #
23
- # test "ignore non matched resources" do
24
- # put '/foo/1'
25
- # assert_status_not_found last_response
26
- # end
27
- #
28
- # test "ignore non modifier resources" do
29
- # put '/testing/1'
30
- # assert_status_not_found last_response
31
- # end
32
- #
33
- # test "ignoring modifier requests with the wrong method" do
34
- # post '/testing/1/inc?field=count'
35
- # assert_status_not_found last_response
36
- # end
37
- #
38
- # test "incrementing a value" do
39
- # put '/testing/1/inc?field=count'
40
- # assert_status_ok last_response
41
- # assert_equal 1, @collection.find_one({:_id => 1})["count"]
42
- # end
43
- #
44
- # test "decrementing a value" do
45
- # put '/testing/1/dec?field=count'
46
- # assert_status_ok last_response
47
- # assert_equal -1, @collection.find_one({:_id => 1})["count"]
48
- # end
49
- #
50
- # test "incrementing a non-existant field" do
51
- # put '/testing/1/inc?field=non_existant'
52
- # assert_status_ok last_response
53
- # assert_equal 0, @collection.find_one({:_id => 1})["count"]
54
- # assert_equal 1, @collection.find_one({:_id => 1})["non_existant"]
55
- # end
56
- #
57
- # test "decrementing a non-existent field" do
58
- # put '/testing/1/dec?field=non_existant'
59
- # assert_status_ok last_response
60
- # assert_equal 0, @collection.find_one({:_id => 1})["count"]
61
- # assert_equal -1, @collection.find_one({:_id => 1})["non_existant"]
62
- # end
63
- #
64
- # test "pushing a value onto the end of an array" do
65
- # testing/1/counter/_inc
66
- # testing/1/counter/_dec
67
- # testing/1/likers?operation
68
- # put '/testing/1/push?field=asdfa' '{"value": [123] }'
69
- # put '/testing/1/push', '{"field": "likers", "value": 1}'
70
- # assert_status_ok last_response
71
- # assert_equal [1], @collection.find_one({:_id => 1})['likers']
72
- # end
73
- # end