rackjson 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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