rackjson 0.1.0

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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Oliver Nightingale
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,144 @@
1
+ # RackJSON
2
+
3
+ Simple Rack middleware for persisting JSON documents.
4
+
5
+ ## Installation
6
+
7
+ `gem install rackjson`
8
+
9
+ RackJSON uses MongoDB for persistence so you also need to install MongoDB.
10
+
11
+ ## Usage
12
+
13
+ In your rackup file:
14
+
15
+ `require mongo
16
+ require rackjson`
17
+
18
+ `use Rack::JSON::Resource, :collections => [:notes], :db => Mongo::Connection.new.db("mydb")`
19
+
20
+ `run lambda { |env|
21
+ [404, {'Content-Length' => '9', 'Content-Type' => 'text/plain'}, "Not Found"]
22
+ }`
23
+
24
+ This will set up a RESTful resource called 'notes' at /notes which will store any JSON document.
25
+
26
+ ### REST API
27
+
28
+ To see what actions are available on the notes resource:
29
+
30
+ `curl -i -XOPTIONS http://localhost:9292/notes`
31
+
32
+ `HTTP/1.1 200 OK`
33
+ `Connection: close`
34
+ `Date: Sun, 11 Apr 2010 11:09:40 GMT`
35
+ `Content-Type: text/plain`
36
+ `Content-Length: 0`
37
+ `Allow: GET, POST`
38
+
39
+ Listing the existing notes:
40
+
41
+ `curl -i http://localhost:9292/notes`
42
+
43
+ `HTTP/1.1 200 OK
44
+ Connection: close
45
+ Date: Sun, 11 Apr 2010 11:12:04 GMT
46
+ Content-Type: application/json
47
+ Content-Length: 2`
48
+
49
+ `[]`
50
+
51
+ Currently there are no notes, create one with a post request:
52
+
53
+ `curl -i -XPOST -d'{"title":"hello world!"}' http://localhost:9292/notes`
54
+
55
+ `HTTP/1.1 201 Created
56
+ Connection: close
57
+ Date: Sun, 11 Apr 2010 11:14:17 GMT
58
+ Content-Type: application/json
59
+ Content-Length: 149`
60
+
61
+ `{"updated_at":"Sun Apr 11 12:14:17 +0100 2010","title":"hello world!","_id":"4bc1af0934701204fd000001","created_at":"Sun Apr 11 12:14:17 +0100 2010"}`
62
+
63
+ RackJSON will assign an id to this resource as _id. This can be used to access this resource directly
64
+
65
+ `curl -i http://localhost:9292/notes/4bc1af0934701204fd000001`
66
+
67
+ `HTTP/1.1 200 OK
68
+ Connection: close
69
+ Date: Sun, 11 Apr 2010 11:16:30 GMT
70
+ Content-Type: application/json
71
+ Content-Length: 147`
72
+
73
+ `[{"updated_at":"Sun Apr 11 11:14:17 UTC 2010","title":"hello world!","_id":"4bc1af0934701204fd000001","created_at":"Sun Apr 11 11:14:17 UTC 2010"}]`
74
+
75
+ This resource will also appear in the index of notes resources
76
+
77
+ `curl -i http://localhost:9292/notes`
78
+
79
+ `HTTP/1.1 200 OK
80
+ Connection: close
81
+ Date: Sun, 11 Apr 2010 11:17:27 GMT
82
+ Content-Type: application/json
83
+ Content-Length: 147`
84
+
85
+ `[{"updated_at":"Sun Apr 11 11:14:17 UTC 2010","title":"hello world!","_id":"4bc1af0934701204fd000001","created_at":"Sun Apr 11 11:14:17 UTC 2010"}]`
86
+
87
+ Update a resource using a PUT request
88
+
89
+ `curl -i -XPUT -d'{"title":"updated"}' http://localhost:9292/notes/4bc1af0934701204fd000001`
90
+
91
+ `HTTP/1.1 200 OK
92
+ Connection: close
93
+ Date: Sun, 11 Apr 2010 11:25:04 GMT
94
+ Content-Type: application/json
95
+ Content-Length: 144`
96
+
97
+ `{"updated_at":"Sun Apr 11 12:25:04 +0100 2010","title":"updated","_id":"4bc1af0934701204fd000001","created_at":"Sun Apr 11 12:25:04 +0100 2010"}`
98
+
99
+ A PUT request can also be used to create a resource at the given location:
100
+
101
+ `curl -i -XPUT -d'{"foo":"bar"}' http://localhost:9292/notes/1`
102
+
103
+ `HTTP/1.1 200 OK
104
+ Connection: close
105
+ Date: Sun, 11 Apr 2010 11:27:13 GMT
106
+ Content-Type: application/json
107
+ Content-Length: 113`
108
+
109
+ `{"updated_at":"Sun Apr 11 12:27:13 +0100 2010","_id":1,"foo":"bar","created_at":"Sun Apr 11 12:27:13 +0100 2010"}`
110
+
111
+ Finally a resource can be deleted using a DELETE request
112
+
113
+ `curl -i -XDELETE http://localhost:9292/notes/1`
114
+
115
+ `HTTP/1.1 200 OK
116
+ Connection: close
117
+ Date: Sun, 11 Apr 2010 11:29:14 GMT
118
+ Content-Type: application/json
119
+ Content-Length: 12
120
+ `
121
+
122
+ `{"ok": "true"}`
123
+
124
+ ### JSON Query
125
+
126
+ RackJSON supports querying of the resources using JSONQuery style syntax. Pass the JSONQuery as query string parameters when making a get request.
127
+
128
+ `curl http://localhost:9292/notes?[?title=foo]`
129
+
130
+ This will get all resources which have a title attribute = 'foo'. Greater than and less than are also supported:
131
+
132
+ `curl http://localhost:9292/notes?[?rating>5][?position=<10]`
133
+
134
+ The resources can be ordered using the query syntax also, to get the notes ordered by rating:
135
+
136
+ `curl http://localhost:9292/notes?[\rating]`
137
+
138
+ You can limit the number of resources that are returned, to get the first 10 resources:
139
+
140
+ `curl http://localhost:9292/notes?[0:10]`
141
+
142
+ To only select certain properties of a document you can specify the fields you want, to get just the titles
143
+
144
+ `curl http://localhost:9292/notes?[=title]`
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rackjson"
8
+ gem.summary = %Q{A rack end point for storing json documents.}
9
+ gem.description = %Q{A rack end point for storing json documents.}
10
+ gem.email = "oliver.n@new-bamboo.co.uk"
11
+ gem.homepage = "http://github.com/olivernn/rackjson"
12
+ gem.authors = ["Oliver Nightingale"]
13
+ gem.add_dependency('mongo', '>=0.19.1')
14
+ gem.add_dependency('mongo_ext', '>=0.19.1')
15
+ gem.add_dependency('json', '>=1.2.3')
16
+ gem.add_dependency('rack', '>=1.0.1')
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/rdoctask'
49
+ Rake::RDocTask.new do |rdoc|
50
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "rackjson #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/rackjson.rb ADDED
@@ -0,0 +1,57 @@
1
+ module Rack #:nodoc:
2
+ class Builder
3
+
4
+ # Setup resource collections hosted behind OAuth and OpenID auth filters.
5
+ #
6
+ # ===Example
7
+ # contain :notes, :projects
8
+ #
9
+ # def contain(*args)
10
+ # @ins << lambda do |app|
11
+ # Rack::Session::Pool.new(
12
+ # CloudKit::OAuthFilter.new(
13
+ # CloudKit::OpenIDFilter.new(
14
+ # CloudKit::Service.new(app, :collections => args.to_a))))
15
+ # end
16
+ # @last_cloudkit_id = @ins.last.object_id
17
+ # end
18
+
19
+ def private_resource
20
+ @ins << lambda do |app|
21
+ Rack::JSON::UserFilter.new(
22
+ Rack::JSON::Resource.new(app, args.first),
23
+ args.first
24
+ )
25
+ end
26
+ end
27
+
28
+ # Setup resource collections without authentication.
29
+ #
30
+ # ===Example
31
+ # expose_resources :collections => [:notes, :projects], :db => Mongo::Connection.new.db("my_mongodb")
32
+ #
33
+ def expose_resource(*args)
34
+ @ins << lambda do |app|
35
+ Rack::JSON::Resource.new(app, args.first)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ require 'rubygems'
42
+ require 'json'
43
+ require 'rack'
44
+ require 'mongo'
45
+ require 'time'
46
+
47
+ module Rack::JSON
48
+
49
+ autoload :Document, 'rackjson/document'
50
+ autoload :JSONDocument, 'rackjson/json_document'
51
+ autoload :JSONQuery, 'rackjson/json_query'
52
+ autoload :MongoDocument, 'rackjson/mongo_document'
53
+ autoload :Request, 'rackjson/request'
54
+ autoload :Resource, 'rackjson/resource'
55
+ autoload :Response, 'rackjson/response'
56
+
57
+ end
@@ -0,0 +1,29 @@
1
+ module Rack::JSON
2
+ class Document
3
+
4
+ def initialize(doc)
5
+ if doc.is_a? String
6
+ @document = Rack::JSON::JSONDocument.new(doc)
7
+ else doc.is_a? OrderedHash
8
+ @document = Rack::JSON::MongoDocument.new(doc)
9
+ end
10
+ end
11
+
12
+ def method_missing(name, *args)
13
+ @document.send(name, *args)
14
+ end
15
+
16
+ def to_json
17
+ unless @json
18
+ gen_attrs = @document.attributes
19
+ gen_attrs.each_pair do |key, value|
20
+
21
+ if value.is_a? Mongo::ObjectID
22
+ gen_attrs[key] = gen_attrs[key].to_s
23
+ end
24
+ end
25
+ end
26
+ @json ||= JSON.generate gen_attrs
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ module Rack::JSON
2
+ class JSONDocument
3
+
4
+ attr_reader :attributes
5
+
6
+ def initialize(doc)
7
+ @attributes = JSON.parse(doc)
8
+ set_attributes
9
+ end
10
+
11
+ def add_id(id)
12
+ @attributes["_id"] = id unless @attributes["_id"]
13
+ end
14
+
15
+ private
16
+
17
+ def set_attribute_created_at
18
+ @attributes["created_at"] = Time.now unless @attributes["created_at"]
19
+ end
20
+
21
+ def set_attribute_dates
22
+ @attributes.each_pair do |key, value|
23
+ if value.class == String && value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/)
24
+ @attributes[key] = Time.parse(value)
25
+ end
26
+ end
27
+ end
28
+
29
+ def set_attribute_ids
30
+ @attributes["_id"] = Mongo::ObjectID.from_string(@attributes["_id"].to_s)
31
+ rescue Mongo::InvalidObjectID
32
+ return false
33
+ end
34
+
35
+ def set_attribute_updated_at
36
+ @attributes["updated_at"] = Time.now
37
+ end
38
+
39
+ def set_attributes
40
+ private_methods.each do |method|
41
+ if method.match /^set_attribute_\w*$/
42
+ send method
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,73 @@
1
+ module Rack::JSON
2
+ class JSONQuery
3
+
4
+ attr_accessor :options, :selector
5
+
6
+ def initialize(query_string)
7
+ @query_string = query_string
8
+ @conditions = @query_string.split(/\[|\]/).compact.reject {|s| s.empty? }
9
+ @options = {}
10
+ @selector = {}
11
+ build
12
+ end
13
+
14
+ private
15
+
16
+ def build
17
+ @conditions.each do |condition|
18
+ private_methods.each do |method|
19
+ if method.match /^set_query_\w*$/
20
+ send method, condition
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ def comparison(symbol)
27
+ {
28
+ '>' => '$gt',
29
+ '<' => '$lt',
30
+ '=<' => '$lte',
31
+ '>=' => '$gte'
32
+ }[symbol]
33
+ end
34
+
35
+ def set_query_fields(condition)
36
+ if condition.match /^=\w+$/
37
+ @options[:fields] = condition.sub('=', '').split(',')
38
+ end
39
+ end
40
+
41
+ def set_query_selector_equality(condition)
42
+ if condition.match /^\?\w+=.+$/
43
+ field = condition.sub('?', '').split('=').first.to_sym
44
+ value = (condition.sub('?', '').split('=').last.match(/^\d+$/) ? condition.sub('?', '').split('=').last.to_f : condition.sub('?', '').split('=').last.to_s)
45
+ @selector[field] = value
46
+ end
47
+ end
48
+
49
+ def set_query_selector(condition)
50
+ if condition.match /^\?\w+>=|=<|<|>\d+$/
51
+ field = condition.sub('?', '').split(/>=|=<|<|>/).first.to_sym
52
+ value = condition.sub('?', '').split(/>=|=<|<|>/).last.to_f
53
+ @selector[field] = { comparison(condition.slice(/>=|=<|<|>/)) => value }
54
+ end
55
+ end
56
+
57
+ def set_query_skip_limit(condition)
58
+ if condition.match /^\d+:\d+$/
59
+ @options[:skip] = condition.split(':').first.to_i
60
+ @options[:limit] = condition.split(':').last.to_i
61
+ end
62
+ end
63
+
64
+ def set_query_sort(condition)
65
+ condition.split(/,\s?/).each do |part|
66
+ if part.match /^[\/|\\]\w*$/
67
+ @options[:sort] ||= []
68
+ @options[:sort] << [part.sub(/[\/|\\]/, '').to_sym, (part.match(/\//) ? :asc : :desc)]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,32 @@
1
+ module Rack::JSON
2
+ class MongoDocument
3
+
4
+ attr_accessor :attributes
5
+
6
+ def initialize(row)
7
+ @attributes = row
8
+ set_created_at
9
+ set_attributes
10
+ end
11
+
12
+ private
13
+
14
+ def set_attribute_ids
15
+ @attributes["_id"] = @attributes["_id"].to_s if (@attributes["_id"].is_a? Mongo::ObjectID)
16
+ end
17
+
18
+ def set_created_at
19
+ if @attributes["_id"].is_a? Mongo::ObjectID
20
+ @attributes["created_at"] = @attributes["_id"].generation_time unless @attributes["created_at"]
21
+ end
22
+ end
23
+
24
+ def set_attributes
25
+ private_methods.each do |method|
26
+ if method.match /^set_attribute_\w*$/
27
+ send method
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ module Rack::JSON
2
+ class Request < Rack::Request
3
+ include Rack::Utils
4
+ def initialize(env)
5
+ super(env)
6
+ end
7
+
8
+ def collection
9
+ self.path_info.split('/')[1] || ""
10
+ end
11
+
12
+ def collection_path?
13
+ self.path_info.match /^\/\w+$/
14
+ end
15
+
16
+ def member_path?
17
+ self.path_info.match /^\/\w+\/\w+$/
18
+ end
19
+
20
+ def json
21
+ self.body.rewind
22
+ self.body.read
23
+ end
24
+
25
+ def query
26
+ @query ||= Rack::JSON::JSONQuery.new(unescape(query_string))
27
+ end
28
+
29
+ def resource_id
30
+ id_string = self.path_info.split('/').last.to_s
31
+ begin
32
+ Mongo::ObjectID.from_string(id_string)
33
+ rescue Mongo::InvalidObjectID
34
+ id_string.match(/^\d+$/) ? id_string.to_i : id_string
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,87 @@
1
+ module Rack::JSON
2
+ class Resource
3
+ METHODS_NOT_ALLOWED = [:trace, :connect]
4
+
5
+ def initialize(app, options)
6
+ @app = app
7
+ @collections = options[:collections]
8
+ @db = options[:db]
9
+ end
10
+
11
+ def call(env)
12
+ request = Rack::JSON::Request.new(env)
13
+ if bypass? request
14
+ @app.call(env)
15
+ else
16
+ @collection = @db[request.collection]
17
+ send(request.request_method.downcase, request)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def bypass?(request)
24
+ !(@collections.include? request.collection.to_sym)
25
+ end
26
+
27
+ def delete(request)
28
+ if request.member_path?
29
+ if @collection.remove({:_id => request.resource_id})
30
+ render "{'ok': true}"
31
+ end
32
+ else
33
+ render "", :status => 405
34
+ end
35
+ end
36
+
37
+ [:get, :head].each do |method|
38
+ define_method method do |request|
39
+ request.query.selector.merge!({:_id => request.resource_id}) if request.member_path?
40
+ rows = []
41
+ @collection.find(request.query.selector, request.query.options).each { |row| rows << Rack::JSON::Document.new(row).attributes }
42
+ if rows.empty? && request.member_path?
43
+ render "document not found", :status => 404, :head => (method == :head)
44
+ else
45
+ render JSON.generate(rows), :head => (method == :head)
46
+ end
47
+ end
48
+ end
49
+
50
+ def options(request)
51
+ if request.collection_path?
52
+ headers = { "Allow" => "GET, POST" }
53
+ elsif request.member_path?
54
+ headers = { "Allow" => "GET, PUT, DELETE" }
55
+ end
56
+ render "", :headers => headers
57
+ end
58
+
59
+ def post(request)
60
+ document = Rack::JSON::Document.new(request.json)
61
+ @collection.insert(document.attributes)
62
+ render document.to_json, :status => 201
63
+ rescue JSON::ParserError => error
64
+ render (error.class.to_s + " :" + error.message), :status => 422
65
+ end
66
+
67
+ def put(request)
68
+ @collection.find_one(:_id => request.resource_id) ? status = 200 : status = 201
69
+ document = Rack::JSON::Document.new(request.json)
70
+ document.add_id(request.resource_id)
71
+ @collection.save(document.attributes)
72
+ render document.to_json, :status => status
73
+ rescue JSON::ParserError => error
74
+ render (error.class.to_s + " :" + error.message), :status => 422
75
+ end
76
+
77
+ def render(body, options={})
78
+ Rack::JSON::Response.new(body, options).to_a
79
+ end
80
+
81
+ METHODS_NOT_ALLOWED.each do |method|
82
+ define_method method do
83
+ render "", :status => 405
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,33 @@
1
+ module Rack::JSON
2
+ class Response
3
+ def initialize(body, options={})
4
+ @status = options[:status] || 200
5
+ @body = body
6
+ @head = options[:head] || false
7
+ @headers = options[:headers] || {}
8
+ set_headers
9
+ head_response if @head
10
+ end
11
+
12
+ def to_a
13
+ [@status, @headers, @body]
14
+ end
15
+
16
+ private
17
+
18
+ def head_response
19
+ @body = ""
20
+ end
21
+
22
+ def set_headers
23
+ @headers["Content-Length"] = @body.length.to_s
24
+ begin
25
+ JSON.parse(@body)
26
+ @headers["Content-Type"] = "application/json"
27
+ rescue JSON::ParserError => error
28
+ # the response will only ever be either json or plain text
29
+ @headers["Content-Type"] = "text/plain"
30
+ end
31
+ end
32
+ end
33
+ end
data/rackjson.gemspec ADDED
@@ -0,0 +1,83 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{rackjson}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Oliver Nightingale"]
12
+ s.date = %q{2010-04-25}
13
+ s.description = %q{A rack end point for storing json documents.}
14
+ s.email = %q{oliver.n@new-bamboo.co.uk}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.markdown"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.markdown",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/rackjson.rb",
27
+ "lib/rackjson/document.rb",
28
+ "lib/rackjson/json_document.rb",
29
+ "lib/rackjson/json_query.rb",
30
+ "lib/rackjson/mongo_document.rb",
31
+ "lib/rackjson/request.rb",
32
+ "lib/rackjson/resource.rb",
33
+ "lib/rackjson/response.rb",
34
+ "rackjson.gemspec",
35
+ "test/helper.rb",
36
+ "test/suite.rb",
37
+ "test/test_document.rb",
38
+ "test/test_json_document.rb",
39
+ "test/test_json_query.rb",
40
+ "test/test_mongo_document.rb",
41
+ "test/test_resource.rb",
42
+ "test/test_response.rb"
43
+ ]
44
+ s.homepage = %q{http://github.com/olivernn/rackjson}
45
+ s.rdoc_options = ["--charset=UTF-8"]
46
+ s.require_paths = ["lib"]
47
+ s.rubygems_version = %q{1.3.5}
48
+ s.summary = %q{A rack end point for storing json documents.}
49
+ s.test_files = [
50
+ "test/helper.rb",
51
+ "test/suite.rb",
52
+ "test/test_builder.rb",
53
+ "test/test_document.rb",
54
+ "test/test_json_document.rb",
55
+ "test/test_json_query.rb",
56
+ "test/test_mongo_document.rb",
57
+ "test/test_resource.rb",
58
+ "test/test_response.rb"
59
+ ]
60
+
61
+ if s.respond_to? :specification_version then
62
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
63
+ s.specification_version = 3
64
+
65
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
66
+ s.add_runtime_dependency(%q<mongo>, [">= 0.19.1"])
67
+ s.add_runtime_dependency(%q<mongo_ext>, [">= 0.19.1"])
68
+ s.add_runtime_dependency(%q<json>, [">= 1.2.3"])
69
+ s.add_runtime_dependency(%q<rack>, [">= 1.0.1"])
70
+ else
71
+ s.add_dependency(%q<mongo>, [">= 0.19.1"])
72
+ s.add_dependency(%q<mongo_ext>, [">= 0.19.1"])
73
+ s.add_dependency(%q<json>, [">= 1.2.3"])
74
+ s.add_dependency(%q<rack>, [">= 1.0.1"])
75
+ end
76
+ else
77
+ s.add_dependency(%q<mongo>, [">= 0.19.1"])
78
+ s.add_dependency(%q<mongo_ext>, [">= 0.19.1"])
79
+ s.add_dependency(%q<json>, [">= 1.2.3"])
80
+ s.add_dependency(%q<rack>, [">= 1.0.1"])
81
+ end
82
+ end
83
+
data/test/helper.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'rack'
4
+ require 'rack/test'
5
+ require 'timecop'
6
+
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+ require 'rackjson'
10
+
11
+ class Test::Unit::TestCase
12
+ end
data/test/suite.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'test_document'
2
+ require 'test_json_document'
3
+ require 'test_json_query'
4
+ require 'test_mongo_document'
5
+ require 'test_resource'
@@ -0,0 +1,17 @@
1
+ require 'helper'
2
+ require 'rack/builder'
3
+
4
+ class BuilderTest < Test::Unit::TestCase
5
+
6
+ def test_expose_resource_shortcut
7
+ app = Rack::Builder.new do
8
+ expose_resource :collections => [:testing], :db => Mongo::Connection.new.db("test")
9
+ run lambda { |env|
10
+ [404, {'Content-Length' => '9', 'Content-Type' => 'text/plain'}, "Not Found"]
11
+ }
12
+ end
13
+
14
+ response = Rack::MockRequest.new(app).get('/testing')
15
+ assert response.ok?
16
+ end
17
+ end
@@ -0,0 +1,63 @@
1
+ require 'helper'
2
+
3
+ class DocumentTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @db = Mongo::Connection.new.db("test")
7
+ @collection = @db['resource_test']
8
+ end
9
+
10
+ def teardown
11
+ @collection.drop
12
+ end
13
+
14
+ def test_creating_from_json
15
+ json = '{"test":"hello"}'
16
+ document = Rack::JSON::Document.new(json)
17
+ assert_equal("hello", document.attributes["test"])
18
+ assert_equal(Time.now.to_s, document.attributes["created_at"].to_s)
19
+ assert_equal(Time.now.to_s, document.attributes["updated_at"].to_s)
20
+ end
21
+
22
+ def test_creating_from_json_with_id
23
+ json = '{"_id": "4b9f783ba040140525000001", "test":"hello"}'
24
+ document = Rack::JSON::Document.new(json)
25
+ assert_equal(Mongo::ObjectID.from_string('4b9f783ba040140525000001'), document.attributes["_id"])
26
+ assert_equal("hello", document.attributes["test"])
27
+ assert_equal(Time.now.to_s, document.attributes["created_at"].to_s)
28
+ assert_equal(Time.now.to_s, document.attributes["updated_at"].to_s)
29
+ end
30
+
31
+ def test_creating_from_json_with_non_object_id
32
+ json = '{"_id": 1, "test":"hello"}'
33
+ document = Rack::JSON::Document.new(json)
34
+ assert_equal(1, document.attributes["_id"])
35
+ assert_equal("hello", document.attributes["test"])
36
+ assert_equal(Time.now.to_s, document.attributes["created_at"].to_s)
37
+ assert_equal(Time.now.to_s, document.attributes["updated_at"].to_s)
38
+ end
39
+
40
+ def test_adding_id
41
+ json = '{"test":"hello"}'
42
+ document = Rack::JSON::Document.new(json)
43
+ id = @collection.insert(document.attributes)
44
+ document.add_id(id)
45
+ assert_equal(id.to_s, document.attributes[:_id].to_s)
46
+ end
47
+
48
+ def test_creating_from_row
49
+ @collection.insert({"test"=>"hello"})
50
+ rows = []
51
+ @collection.find.each { |r| rows << r }
52
+ document = Rack::JSON::Document.new(rows.first)
53
+ assert_equal("hello", document.attributes["test"])
54
+ end
55
+
56
+ def test_document_created_at
57
+ json = '{"test":"hello", "created_at": "01/01/2010"}'
58
+ document = Rack::JSON::Document.new(json)
59
+ assert_equal("hello", document.attributes["test"])
60
+ assert_equal("01/01/2010", document.attributes["created_at"])
61
+ assert_equal(Time.now.to_s, document.attributes["updated_at"].to_s)
62
+ end
63
+ end
@@ -0,0 +1,73 @@
1
+ require 'helper'
2
+
3
+ class JSONDocumentTest < Test::Unit::TestCase
4
+
5
+ def test_parsing_simple_json_structure
6
+ hash = { "title" => "testing" }
7
+ doc = JSON.generate hash
8
+ assert_equal hash["title"], Rack::JSON::JSONDocument.new(doc).attributes["title"]
9
+ end
10
+
11
+ def test_parsing_nested_json_structure
12
+ hash = { "title" => "nested", "nest" => { "note" => "im nested" } }
13
+ doc = JSON.generate hash
14
+ assert_equal hash["nest"], Rack::JSON::JSONDocument.new(doc).attributes["nest"]
15
+ assert_equal hash["title"], Rack::JSON::JSONDocument.new(doc).attributes["title"]
16
+ end
17
+
18
+ def test_parsing_dates_from_json
19
+ hash = { "date" => "2010-04-10T14:20:12Z" }
20
+ doc = JSON.generate hash
21
+ assert_equal( Time.parse("2010-04-10T14:20:12Z") , Rack::JSON::JSONDocument.new(doc).attributes["date"])
22
+ end
23
+
24
+ def test_parsing_mongo_object_id
25
+ hash = { "_id" => "4ba7e82ca04014011c000001" }
26
+ doc = JSON.generate hash
27
+ assert_equal(Mongo::ObjectID.from_string("4ba7e82ca04014011c000001"), Rack::JSON::JSONDocument.new(doc).attributes["_id"])
28
+ end
29
+
30
+ def test_parsing_non_mongo_object_ids
31
+ hash = { "_id" => 1 }
32
+ doc = JSON.generate hash
33
+ assert_equal(hash["_id"], Rack::JSON::JSONDocument.new(doc).attributes["_id"])
34
+ end
35
+
36
+ def test_adding_an_id
37
+ hash = { "test" => "I don't have an ID" }
38
+ doc = JSON.generate hash
39
+ json_doc = Rack::JSON::JSONDocument.new(doc)
40
+ json_doc.add_id(1)
41
+ assert_equal(1, json_doc.attributes["_id"])
42
+ end
43
+
44
+ def test_not_overriding_an_id
45
+ hash = { "test" => "I do have an ID", "_id" => 2 }
46
+ doc = JSON.generate hash
47
+ json_doc = Rack::JSON::JSONDocument.new(doc)
48
+ json_doc.add_id(1)
49
+ assert_equal(2 , json_doc.attributes["_id"])
50
+ end
51
+
52
+ def test_adding_timestamps
53
+ hash = { "_id" => 1 }
54
+ doc = JSON.generate hash
55
+ t = Time.now
56
+ assert_equal(t.to_s, Rack::JSON::JSONDocument.new(doc).attributes["created_at"].to_s)
57
+ assert_equal(t.to_s, Rack::JSON::JSONDocument.new(doc).attributes["updated_at"].to_s)
58
+ end
59
+
60
+ def test_not_always_updating_created_at
61
+ hash = { "_id" => 1 }
62
+ doc = JSON.generate hash
63
+ t1 = Time.now
64
+ assert_equal(t1.to_s, Rack::JSON::JSONDocument.new(doc).attributes["created_at"].to_s)
65
+ assert_equal(t1.to_s, Rack::JSON::JSONDocument.new(doc).attributes["updated_at"].to_s)
66
+ sleep 1
67
+ hash2 = { "_id" => 1, "created_at" => t1.to_s }
68
+ doc = JSON.generate hash2
69
+ t2 = Time.now
70
+ assert_equal(t1.to_s, Rack::JSON::JSONDocument.new(doc).attributes["created_at"].to_s)
71
+ assert_equal(t2.to_s, Rack::JSON::JSONDocument.new(doc).attributes["updated_at"].to_s)
72
+ end
73
+ end
@@ -0,0 +1,69 @@
1
+ require 'helper'
2
+
3
+ class QueryTest < Test::Unit::TestCase
4
+ def test_ascending_sort
5
+ json_query = "[/price]"
6
+ query = Rack::JSON::JSONQuery.new(json_query)
7
+ assert_equal({:sort => [[:price, :asc]]}, query.options)
8
+ end
9
+
10
+ def test_descending_sort
11
+ json_query = '[\price]'
12
+ query = Rack::JSON::JSONQuery.new(json_query)
13
+ assert_equal({:sort => [[:price, :desc]]}, query.options)
14
+ end
15
+
16
+ def test_multiple_sort_fields
17
+ json_query = '[/price, \rating]'
18
+ query = Rack::JSON::JSONQuery.new(json_query)
19
+ assert_equal({:sort => [[:price, :asc], [:rating, :desc]]}, query.options)
20
+ end
21
+
22
+ def test_skips_and_limits
23
+ json_query = '[0:10]'
24
+ query = Rack::JSON::JSONQuery.new(json_query)
25
+ assert_equal({:skip => 0, :limit => 10}, query.options)
26
+ end
27
+
28
+ def test_map_query
29
+ json_query = '[=name]'
30
+ query = Rack::JSON::JSONQuery.new(json_query)
31
+ assert_equal({:fields => ['name']}, query.options)
32
+ end
33
+
34
+ def test_single_equality_condition_with_number
35
+ json_query = '[?price=10]'
36
+ query = Rack::JSON::JSONQuery.new(json_query)
37
+ assert_equal({:price => 10}, query.selector)
38
+ end
39
+
40
+ def test_single_equality_condition_with_string
41
+ json_query = '[?name=bob!]'
42
+ query = Rack::JSON::JSONQuery.new(json_query)
43
+ assert_equal({:name => 'bob!'}, query.selector)
44
+ end
45
+
46
+ def test_single_greater_than_condition
47
+ json_query = '[?price>10]'
48
+ query = Rack::JSON::JSONQuery.new(json_query)
49
+ assert_equal({:price => {'$gt' => 10}}, query.selector)
50
+ end
51
+
52
+ def test_single_greater_than_or_equal_condition
53
+ json_query = '[?price>=10]'
54
+ query = Rack::JSON::JSONQuery.new(json_query)
55
+ assert_equal({:price => { '$gte' => 10}}, query.selector)
56
+ end
57
+
58
+ def test_single_less_than_condition
59
+ json_query = '[?price<10]'
60
+ query = Rack::JSON::JSONQuery.new(json_query)
61
+ assert_equal({:price => {'$lt' => 10}}, query.selector)
62
+ end
63
+
64
+ # def test_single_greater_than_or_equal_condition
65
+ # json_query = '[?price=<10]'
66
+ # query = Rack::JSON::JSONQuery.new(json_query)
67
+ # assert_equal({:price => { '$lte' => 10}}, query.selector)
68
+ # end
69
+ end
@@ -0,0 +1,24 @@
1
+ require 'helper'
2
+
3
+ class MongoDocumentTest < Test::Unit::TestCase
4
+
5
+ def test_stringifying_mongo_object_ids
6
+ hash = { "_id" => Mongo::ObjectID.from_string("4ba7e82ca04014011c000001") }
7
+ doc = Rack::JSON::MongoDocument.new(hash).attributes
8
+ assert_equal("4ba7e82ca04014011c000001", doc["_id"])
9
+ end
10
+
11
+ def test_not_stringifying_non_mongo_object_ids
12
+ hash = { "_id" => 1 }
13
+ doc = Rack::JSON::MongoDocument.new(hash).attributes
14
+ assert_equal({ "_id" => 1 }, doc)
15
+ end
16
+
17
+ def test_setting_the_created_at_stamp
18
+ hash = { "_id" => Mongo::ObjectID.from_string("4ba7e82ca04014011c000001") }
19
+ doc = Rack::JSON::MongoDocument.new(hash).attributes
20
+ assert_equal({ "_id" => "4ba7e82ca04014011c000001",
21
+ "created_at" => Mongo::ObjectID.from_string("4ba7e82ca04014011c000001").generation_time
22
+ }, doc)
23
+ end
24
+ end
@@ -0,0 +1,108 @@
1
+ require 'helper'
2
+
3
+ class ResourceTest < Test::Unit::TestCase
4
+ include Rack::Test::Methods
5
+ include Rack::Utils
6
+ def setup
7
+ @db = Mongo::Connection.new.db("test")
8
+ @collection = @db['testing']
9
+ end
10
+
11
+ def teardown
12
+ @collection.drop
13
+ end
14
+
15
+ def app
16
+ Rack::JSON::Resource.new lambda { |env|
17
+ [404, {'Content-Length' => '9', 'Content-Type' => 'text/plain'}, "Not Found"]
18
+ }, :collections => [:testing], :db => @db
19
+ end
20
+
21
+ def test_non_existing_resource
22
+ get '/blah'
23
+ assert_equal 404, last_response.status
24
+ end
25
+
26
+ def test_index_method
27
+ @collection.save({:testing => true})
28
+ get '/testing'
29
+ assert last_response.ok?
30
+ assert_match /"testing":true/, last_response.body
31
+ end
32
+
33
+ def test_creating_a_document
34
+ put '/testing/1', '{"title": "testing"}'
35
+ assert_equal 201, last_response.status
36
+ assert_match /"_id":1/, last_response.body
37
+ assert_match /"title":"testing"/, last_response.body
38
+ end
39
+
40
+ def test_show_a_single_document
41
+ put '/testing/1', '{"title": "testing first"}'
42
+ put '/testing/2', '{"title": "testing second"}'
43
+ get '/testing/1'
44
+ assert last_response.ok?
45
+ assert_match /"title":"testing first"/, last_response.body
46
+ assert_no_match /"title":"testing second"/, last_response.body
47
+ end
48
+
49
+ def test_not_finding_a_specific_document
50
+ get '/testing/1'
51
+ assert_equal 404, last_response.status
52
+ assert_equal "document not found", last_response.body
53
+ end
54
+
55
+ def test_index_method_with_query_parameters
56
+ @collection.save({:testing => true, :rating => 5, :title => 'testing'})
57
+ get '/testing?[?title=testing]'
58
+ assert last_response.ok?
59
+ assert_match /"title":"testing"/, last_response.body
60
+ get '/testing?[?rating=5]'
61
+ assert last_response.ok?
62
+ assert_match /"title":"testing"/, last_response.body
63
+ end
64
+
65
+ def test_index_method_with_sort
66
+ @collection.save({:testing => true, :rating => 5, :title => 'testing'})
67
+ get '/testing?[/title]'
68
+ assert last_response.ok?
69
+ assert_match /"title":"testing"/, last_response.body
70
+ get '/testing?[?rating=5]'
71
+ assert last_response.ok?
72
+ assert_match /"title":"testing"/, last_response.body
73
+ end
74
+
75
+ def test_putting_a_new_document
76
+ put '/testing/1', '{"title": "testing update"}'
77
+ assert_equal 201, last_response.status
78
+ assert_match /"_id":1/, last_response.body
79
+ assert_match /"title":"testing update"/, last_response.body
80
+ end
81
+
82
+ def test_updating_a_document
83
+ @collection.save({:title => 'testing', :_id => 1})
84
+ put '/testing/1', '{"title": "testing update"}'
85
+ assert last_response.ok?
86
+ assert_match /"_id":1/, last_response.body
87
+ assert_match /"title":"testing update"/, last_response.body
88
+ end
89
+
90
+ def test_deleting_a_document
91
+ @collection.save({:title => 'testing', :_id => 1})
92
+ assert @collection.find_one({:_id => 1})
93
+ delete '/testing/1'
94
+ assert last_response.ok?
95
+ assert_nil @collection.find_one({:_id => 1})
96
+ end
97
+
98
+ def test_deleting_only_with_member_path
99
+ delete '/testing'
100
+ assert_equal 405, last_response.status
101
+ end
102
+
103
+ def test_posting_a_document
104
+ post '/testing', '{"title": "testing"}'
105
+ assert last_response.status == 201
106
+ assert_match /"title":"testing"/, last_response.body
107
+ end
108
+ end
@@ -0,0 +1,45 @@
1
+ require 'helper'
2
+
3
+ class ResponseTest < Test::Unit::TestCase
4
+ def test_setting_the_content
5
+ response = Rack::JSON::Response.new("test")
6
+ assert_equal(3, response.to_a.length)
7
+ end
8
+
9
+ def test_default_http_status_to_200
10
+ response = Rack::JSON::Response.new("test")
11
+ assert_equal(200, response.to_a[0])
12
+ end
13
+
14
+ def test_setting_http_status_code
15
+ response = Rack::JSON::Response.new("test", :status => 422)
16
+ assert_equal(422, response.to_a[0])
17
+ end
18
+
19
+ def test_response_body
20
+ response = Rack::JSON::Response.new("test")
21
+ assert_equal("test", response.to_a[2])
22
+ end
23
+
24
+ def test_setting_the_content_length
25
+ response = Rack::JSON::Response.new("test")
26
+ assert_equal("4", response.to_a[1]["Content-Length"])
27
+ end
28
+
29
+ def test_setting_the_content_type
30
+ response = Rack::JSON::Response.new("test")
31
+ assert_equal("text/plain", response.to_a[1]["Content-Type"])
32
+ end
33
+
34
+ def test_sending_json
35
+ response = Rack::JSON::Response.new("{'title': 'hello'}")
36
+ assert_equal("{'title': 'hello'}", response.to_a[2])
37
+ end
38
+
39
+ def test_head_response
40
+ response = Rack::JSON::Response.new("test", :head => true)
41
+ assert_equal("", response.to_a[2])
42
+ assert_equal("4", response.to_a[1]["Content-Length"])
43
+ assert_equal("text/plain", response.to_a[1]["Content-Type"])
44
+ end
45
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rackjson
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Oliver Nightingale
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-04-25 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mongo
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.19.1
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: mongo_ext
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.19.1
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: json
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 1.2.3
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: rack
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.1
54
+ version:
55
+ description: A rack end point for storing json documents.
56
+ email: oliver.n@new-bamboo.co.uk
57
+ executables: []
58
+
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - LICENSE
63
+ - README.markdown
64
+ files:
65
+ - .document
66
+ - .gitignore
67
+ - LICENSE
68
+ - README.markdown
69
+ - Rakefile
70
+ - VERSION
71
+ - lib/rackjson.rb
72
+ - lib/rackjson/document.rb
73
+ - lib/rackjson/json_document.rb
74
+ - lib/rackjson/json_query.rb
75
+ - lib/rackjson/mongo_document.rb
76
+ - lib/rackjson/request.rb
77
+ - lib/rackjson/resource.rb
78
+ - lib/rackjson/response.rb
79
+ - rackjson.gemspec
80
+ - test/helper.rb
81
+ - test/suite.rb
82
+ - test/test_document.rb
83
+ - test/test_json_document.rb
84
+ - test/test_json_query.rb
85
+ - test/test_mongo_document.rb
86
+ - test/test_resource.rb
87
+ - test/test_response.rb
88
+ has_rdoc: true
89
+ homepage: http://github.com/olivernn/rackjson
90
+ licenses: []
91
+
92
+ post_install_message:
93
+ rdoc_options:
94
+ - --charset=UTF-8
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: "0"
102
+ version:
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: "0"
108
+ version:
109
+ requirements: []
110
+
111
+ rubyforge_project:
112
+ rubygems_version: 1.3.5
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: A rack end point for storing json documents.
116
+ test_files:
117
+ - test/helper.rb
118
+ - test/suite.rb
119
+ - test/test_builder.rb
120
+ - test/test_document.rb
121
+ - test/test_json_document.rb
122
+ - test/test_json_query.rb
123
+ - test/test_mongo_document.rb
124
+ - test/test_resource.rb
125
+ - test/test_response.rb