rackjson 0.3.2 → 0.4.1

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