jsonatra 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.travis.yml +6 -0
- data/Gemfile +8 -0
- data/LICENSE +13 -0
- data/README.md +197 -0
- data/Rakefile +7 -0
- data/jsonatra.gemspec +16 -0
- data/lib/jsonatra/helpers/error.rb +18 -0
- data/lib/jsonatra/helpers/params.rb +85 -0
- data/lib/jsonatra/response.rb +85 -0
- data/lib/jsonatra/version.rb +3 -0
- data/lib/jsonatra.rb +149 -0
- data/test/helper.rb +61 -0
- data/test/helpers/error_spec.rb +121 -0
- data/test/helpers/params_spec.rb +72 -0
- data/test/jsonatra_spec.rb +133 -0
- metadata +82 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2013 Esri, Inc
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
jsonatra
|
2
|
+
========
|
3
|
+
|
4
|
+
[![Build Status](https://travis-ci.org/esripdx/jsonatra.png?branch=master)](https://travis-ci.org/esripdx/jsonatra)
|
5
|
+
|
6
|
+
This is a very opinionated gem. Some of its strongly held views:
|
7
|
+
|
8
|
+
* [Sinatra](http://sinatrarb.com) is the BEST.
|
9
|
+
* JSON is awesome!
|
10
|
+
* REST is fo' suckas.
|
11
|
+
* HTTP status codes are for **transport**!
|
12
|
+
|
13
|
+
To that end, this gem subclasses `Sinatra::Base` and `Sinatra::Response` adding
|
14
|
+
some helpful defaults to achieve the following goals:
|
15
|
+
|
16
|
+
* always respond with JSON, route blocks' return hashes are automatically converted
|
17
|
+
* all GET routes also respond to POST for large query values
|
18
|
+
* accept form encoded **OR** JSON POST body parameters
|
19
|
+
* always supply CORS headers
|
20
|
+
* short-circuit OPTIONS requests
|
21
|
+
* *application* errors (i.e. param validation) should still 200, respond with error object
|
22
|
+
* 404s still 404 with a JSON body
|
23
|
+
* have handy error helpers
|
24
|
+
|
25
|
+
### Settings
|
26
|
+
|
27
|
+
##### :arrayified_params
|
28
|
+
|
29
|
+
Accepts all the following formats for the given param names, always turning it into an
|
30
|
+
`Array` in the `params` hash.
|
31
|
+
|
32
|
+
`set :arrayified_params, [:foo, :bar]`
|
33
|
+
|
34
|
+
* formencoded name
|
35
|
+
`(ex: tags[]=foo&tags[]=bar => ['foo', 'bar'])`
|
36
|
+
|
37
|
+
* JSON POST body Array type
|
38
|
+
`(ex: { "tags": ["foo", "bar"] } => ['foo', 'bar'])`
|
39
|
+
|
40
|
+
* formencoded comma-separated
|
41
|
+
`(ex: tags=foo%2Cbar => ['foo', 'bar'])`
|
42
|
+
|
43
|
+
* JSON POST body comma-separated
|
44
|
+
`(ex: { "tags": "foo,bar"] } => ['foo', 'bar'])`
|
45
|
+
|
46
|
+
|
47
|
+
##### :camelcase_error_types
|
48
|
+
|
49
|
+
For whatever reason, you may want to have camelCase goin' on. Here's to working with
|
50
|
+
legacy systems, eh?
|
51
|
+
|
52
|
+
`enable :camelcase_error_types`
|
53
|
+
|
54
|
+
### Customizing Access Headers
|
55
|
+
|
56
|
+
Standard [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) headers
|
57
|
+
not enough? Customize everything about them with:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
class Example < Jsonatra::Base
|
61
|
+
|
62
|
+
def access_control_headers
|
63
|
+
if crazy_header_mode?
|
64
|
+
{
|
65
|
+
"these" => "headers",
|
66
|
+
"are" => "CRAZY"
|
67
|
+
}
|
68
|
+
else
|
69
|
+
Jsonatra::ACCESS_CONTROL_HEADERS
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
### Example
|
77
|
+
|
78
|
+
example `config.ru`:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
require 'jsonatra'
|
82
|
+
|
83
|
+
class Foo < Jsonatra::Base
|
84
|
+
|
85
|
+
configure do
|
86
|
+
set :arrayified_params, [:foos]
|
87
|
+
end
|
88
|
+
|
89
|
+
get '/hi' do
|
90
|
+
{ hello: "there", foos: params[:foos] }
|
91
|
+
end
|
92
|
+
|
93
|
+
get '/error' do
|
94
|
+
param_error :foo, 'type', 'message' unless params[:foo]
|
95
|
+
{ you: "shouldn't", see: "this", unless: "foo" }
|
96
|
+
end
|
97
|
+
|
98
|
+
get '/halt_on_error' do
|
99
|
+
param_error :foo, 'type', 'message' unless params[:foo]
|
100
|
+
param_error :bar, 'type', 'message' unless params[:bar]
|
101
|
+
|
102
|
+
# since errors in the response always take precendence,
|
103
|
+
# halt if you need to just stop now
|
104
|
+
#
|
105
|
+
halt if response.error?
|
106
|
+
|
107
|
+
{ you: "shouldn't", see: "this", unless: "foo", and: "bar" }
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
map '/' do
|
113
|
+
run Foo
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
The above would respond like this:
|
118
|
+
|
119
|
+
#### http://localhost:9292/hi?foos=bars,bats
|
120
|
+
|
121
|
+
```
|
122
|
+
< HTTP/1.1 200 OK
|
123
|
+
< Content-Type: application/json;charset=utf-8
|
124
|
+
< Access-Control-Allow-Origin: *
|
125
|
+
< Access-Control-Allow-Methods: GET, POST
|
126
|
+
< Access-Control-Allow-Headers: Accept, Authorization, Content-Type, Origin
|
127
|
+
|
128
|
+
# ...
|
129
|
+
|
130
|
+
{"hello":"there","foos":["bars","bats"]}
|
131
|
+
```
|
132
|
+
|
133
|
+
#### http://localhost:9292/hi?foos[]=bars&foos[]=bats
|
134
|
+
|
135
|
+
```javascript
|
136
|
+
{
|
137
|
+
"hello": "there",
|
138
|
+
"foos": [
|
139
|
+
"bars",
|
140
|
+
"bats"
|
141
|
+
]
|
142
|
+
}
|
143
|
+
```
|
144
|
+
|
145
|
+
#### http://localhost:9292/error
|
146
|
+
|
147
|
+
```javascript
|
148
|
+
{
|
149
|
+
"error": {
|
150
|
+
"type": "invalidInput",
|
151
|
+
"message": "invalid parameter or parameter value",
|
152
|
+
"parameters": {
|
153
|
+
"foo": [
|
154
|
+
{
|
155
|
+
"type": "type",
|
156
|
+
"message": "message"
|
157
|
+
}
|
158
|
+
]
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
162
|
+
```
|
163
|
+
|
164
|
+
#### http://localhost:9292/error?foo=bar
|
165
|
+
|
166
|
+
```javascript
|
167
|
+
{
|
168
|
+
"you": "shouldn't",
|
169
|
+
"see": "this",
|
170
|
+
"unless": "foo"
|
171
|
+
}
|
172
|
+
```
|
173
|
+
|
174
|
+
#### http://localhost:9292/halt_on_error
|
175
|
+
|
176
|
+
```javascript
|
177
|
+
{
|
178
|
+
"error": {
|
179
|
+
"type": "invalidInput",
|
180
|
+
"message": "invalid parameter or parameter value",
|
181
|
+
"parameters": {
|
182
|
+
"foo": [
|
183
|
+
{
|
184
|
+
"type": "type",
|
185
|
+
"message": "message"
|
186
|
+
}
|
187
|
+
],
|
188
|
+
"bar": [
|
189
|
+
{
|
190
|
+
"type": "type",
|
191
|
+
"message": "message"
|
192
|
+
}
|
193
|
+
]
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}
|
197
|
+
```
|
data/Rakefile
ADDED
data/jsonatra.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/jsonatra/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Kenichi Nakamura"]
|
6
|
+
gem.email = ["kenichi.nakamura@gmail.com"]
|
7
|
+
gem.description = gem.summary = "An opinionated, non-ReST, JSON API extension for Sinatra"
|
8
|
+
gem.homepage = "https://github.com/esripdx/jsonatra"
|
9
|
+
gem.files = `git ls-files | grep -Ev '^(myapp|examples)'`.split("\n")
|
10
|
+
gem.test_files = `git ls-files -- test/*`.split("\n")
|
11
|
+
gem.name = "jsonatra"
|
12
|
+
gem.require_paths = ["lib"]
|
13
|
+
gem.version = Jsonatra::VERSION
|
14
|
+
gem.license = 'apache'
|
15
|
+
gem.add_dependency 'sinatra'
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Jsonatra
|
2
|
+
module ErrorHelpers
|
3
|
+
|
4
|
+
# sets a default root error type and message if not present, and appends this
|
5
|
+
# error to the list for this parameter
|
6
|
+
#
|
7
|
+
def param_error parameter, type, message, &block
|
8
|
+
response.add_parameter_error parameter.to_sym, type, message, &block
|
9
|
+
end
|
10
|
+
|
11
|
+
# sets a default root error type and message if not present, and appends this
|
12
|
+
# error to the list for this header
|
13
|
+
#
|
14
|
+
def header_error header, type, message, &block
|
15
|
+
response.add_header_error header, type, message, &block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Jsonatra
|
2
|
+
module ParamsHelpers
|
3
|
+
|
4
|
+
JSON_CONTENT_TYPE = 'application/json'.freeze
|
5
|
+
|
6
|
+
# merges JSON POST body data over query params if provided
|
7
|
+
#
|
8
|
+
def params
|
9
|
+
unless @_params_hash
|
10
|
+
@_params_hash = super
|
11
|
+
|
12
|
+
# if we see what looks like JSON data, but have no Content-Type header...
|
13
|
+
#
|
14
|
+
if request.content_type.nil? or request.content_type == ''
|
15
|
+
check_for_content_type_mismatch
|
16
|
+
else
|
17
|
+
content_type_header = request.content_type.split ';'
|
18
|
+
if content_type_header.include? JSON_CONTENT_TYPE
|
19
|
+
begin
|
20
|
+
json_params_hash = JSON.parse request.body.read
|
21
|
+
@_params_hash.merge! json_params_hash unless json_params_hash.nil?
|
22
|
+
rescue JSON::ParserError => e
|
23
|
+
begin
|
24
|
+
msg = e.message.match(/\d+: (.+)/).captures.first
|
25
|
+
rescue NoMethodError => noe
|
26
|
+
msg = e.message
|
27
|
+
end
|
28
|
+
response.error = {
|
29
|
+
type: 'json_parse_error',
|
30
|
+
message: "could not process JSON: #{msg}"
|
31
|
+
}
|
32
|
+
halt
|
33
|
+
end
|
34
|
+
|
35
|
+
request.body.rewind
|
36
|
+
else
|
37
|
+
check_for_content_type_mismatch
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if settings.respond_to? :arrayified_params and settings.arrayified_params
|
42
|
+
settings.arrayified_params.each do |param_name|
|
43
|
+
array_or_comma_sep_param param_name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
@_params_hash
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# convert param value to `Array` if String
|
54
|
+
#
|
55
|
+
# * formencoded name
|
56
|
+
# (ex: tags[]=foo&tags[]=bar => ['foo', 'bar'])
|
57
|
+
# * JSON POST body Array type
|
58
|
+
# (ex: { "tags": ["foo", "bar"] } => ['foo', 'bar'])
|
59
|
+
# * formencoded comma-separated
|
60
|
+
# (ex: tags=foo%2Cbar => ['foo', 'bar'])
|
61
|
+
# * JSON POST body comma-separated
|
62
|
+
# (ex: { "tags": "foo,bar"] } => ['foo', 'bar'])
|
63
|
+
#
|
64
|
+
def array_or_comma_sep_param param_name
|
65
|
+
if @_params_hash[param_name] and String === @_params_hash[param_name]
|
66
|
+
@_params_hash[param_name] = @_params_hash[param_name].split ','
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# halt with a contentTypeMismatch error
|
71
|
+
#
|
72
|
+
def check_for_content_type_mismatch
|
73
|
+
body = request.body.read
|
74
|
+
request.body.rewind
|
75
|
+
if body =~ /^[\{\[].*[\}\]]$/
|
76
|
+
response.error = {
|
77
|
+
type: 'content_type_mismatch',
|
78
|
+
message: 'Request looks like JSON but Content-Type header was not set to application/json'
|
79
|
+
}
|
80
|
+
halt
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Jsonatra
|
2
|
+
class Response < Sinatra::Response
|
3
|
+
|
4
|
+
# do a `response.override_processing = true` in your route if you need to
|
5
|
+
#
|
6
|
+
@override_processing = false
|
7
|
+
attr_accessor :override_processing
|
8
|
+
|
9
|
+
# set this and the `content_type` in the `before` filter
|
10
|
+
#
|
11
|
+
attr_writer :jsonp_callback
|
12
|
+
|
13
|
+
# new #finish method to handle error reporting and json(p)-ification
|
14
|
+
#
|
15
|
+
alias sinatra_finish finish
|
16
|
+
def finish
|
17
|
+
unless @override_processing
|
18
|
+
if self.error?
|
19
|
+
self.body = {error: @error.delete_if {|k,v| v.nil?}}.to_json
|
20
|
+
else
|
21
|
+
|
22
|
+
# TODO what if there are more elements in the array?
|
23
|
+
#
|
24
|
+
if Array === self.body
|
25
|
+
self.body = self.body[0]
|
26
|
+
|
27
|
+
# JSON is not valid unless it's "{}" or "[]"
|
28
|
+
#
|
29
|
+
self.body ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
if Hash === self.body
|
33
|
+
json_body = self.body.to_json
|
34
|
+
if @jsonp_callback
|
35
|
+
self.body = "#{@jsonp_callback}(#{json_body});"
|
36
|
+
else
|
37
|
+
self.body = json_body
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
sinatra_finish
|
43
|
+
end
|
44
|
+
|
45
|
+
# new methods for adding and appending errors
|
46
|
+
#
|
47
|
+
attr_writer :error
|
48
|
+
|
49
|
+
def error
|
50
|
+
@error ||= {}
|
51
|
+
@error
|
52
|
+
end
|
53
|
+
|
54
|
+
def error?; !error.empty?; end
|
55
|
+
|
56
|
+
def add_parameter_error parameter, type, message
|
57
|
+
error[:type] ||= 'invalid_input'
|
58
|
+
error[:message] ||= 'invalid parameter or parameter value'
|
59
|
+
error[:parameters] ||= {}
|
60
|
+
error[:parameters][parameter.to_sym] ||= []
|
61
|
+
yield error if block_given?
|
62
|
+
error[:parameters][parameter.to_sym] << {type: type, message: message}
|
63
|
+
end
|
64
|
+
|
65
|
+
def add_header_error header, type, message
|
66
|
+
error[:type] ||= 'invalid_header'
|
67
|
+
error[:message] ||= 'invalid header or header value'
|
68
|
+
error[:headers] ||= {}
|
69
|
+
error[:headers][header.to_sym] ||= []
|
70
|
+
yield error if block_given?
|
71
|
+
error[:headers][header.to_sym] << {type: type, message: message}
|
72
|
+
end
|
73
|
+
|
74
|
+
def camelcase_error_types
|
75
|
+
error[:type] = error[:type].camelcase
|
76
|
+
if error[:headers]
|
77
|
+
error[:headers].each {|k,v| v.each {|he| he[:type] = he[:type].camelcase}}
|
78
|
+
end
|
79
|
+
if error[:parameters]
|
80
|
+
error[:parameters].each {|k,v| v.each {|pe| pe[:type] = pe[:type].camelcase}}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
data/lib/jsonatra.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'jsonatra/response'
|
5
|
+
require 'jsonatra/helpers/error'
|
6
|
+
require 'jsonatra/helpers/params'
|
7
|
+
|
8
|
+
module Jsonatra
|
9
|
+
|
10
|
+
ACCESS_CONTROL_HEADERS = {
|
11
|
+
'Access-Control-Allow-Origin' => '*',
|
12
|
+
'Access-Control-Allow-Methods' => 'GET, POST',
|
13
|
+
'Access-Control-Allow-Headers' => 'Accept, Authorization, Content-Type, Origin'
|
14
|
+
}
|
15
|
+
|
16
|
+
class Base < Sinatra::Base
|
17
|
+
|
18
|
+
helpers ErrorHelpers
|
19
|
+
helpers ParamsHelpers
|
20
|
+
|
21
|
+
# copied here so we can override Response.new with Jsonatra::Response.new
|
22
|
+
#
|
23
|
+
# https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L880
|
24
|
+
#
|
25
|
+
def call!(env) # :nodoc:
|
26
|
+
@env = env
|
27
|
+
@request = ::Sinatra::Request.new(env)
|
28
|
+
@response = ::Jsonatra::Response.new
|
29
|
+
@params = indifferent_params(@request.params)
|
30
|
+
template_cache.clear if settings.reload_templates
|
31
|
+
force_encoding(@params)
|
32
|
+
|
33
|
+
@response['Content-Type'] = nil
|
34
|
+
invoke { dispatch! }
|
35
|
+
invoke { error_block!(response.status) } unless @env['sinatra.error']
|
36
|
+
|
37
|
+
unless @response['Content-Type']
|
38
|
+
if Array === body and body[0].respond_to? :content_type
|
39
|
+
content_type body[0].content_type
|
40
|
+
else
|
41
|
+
content_type :html
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
@response.finish
|
46
|
+
end
|
47
|
+
|
48
|
+
configure do
|
49
|
+
disable :show_exceptions
|
50
|
+
disable :protection
|
51
|
+
end
|
52
|
+
|
53
|
+
before do
|
54
|
+
|
55
|
+
# default to Content-Type to JSON, or javascript if request is JSONP
|
56
|
+
#
|
57
|
+
content_type :json
|
58
|
+
unless params[:callback].nil? or params[:callback] == ''
|
59
|
+
halt param_error(:callback, :invalid, 'invalid callback') if params[:callback].index('"')
|
60
|
+
response.jsonp_callback = params[:callback]
|
61
|
+
content_type :js
|
62
|
+
end
|
63
|
+
|
64
|
+
# grok access control headers
|
65
|
+
#
|
66
|
+
achs = begin
|
67
|
+
self.access_control_headers
|
68
|
+
rescue NoMethodError
|
69
|
+
ACCESS_CONTROL_HEADERS
|
70
|
+
end
|
71
|
+
|
72
|
+
# immediately return on OPTIONS
|
73
|
+
#
|
74
|
+
if request.request_method == 'OPTIONS'
|
75
|
+
halt [200, achs, '']
|
76
|
+
end
|
77
|
+
|
78
|
+
# allow origin, oauth from everywhere
|
79
|
+
#
|
80
|
+
achs.each {|k,v| headers[k] = v}
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
after do
|
85
|
+
if settings.respond_to? :camelcase_error_types? and settings.camelcase_error_types?
|
86
|
+
response.camelcase_error_types if response.error?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
error do
|
91
|
+
response.error = {
|
92
|
+
type: :unexpected,
|
93
|
+
message: 'An unexpected error has occured, please try your request again later'
|
94
|
+
}
|
95
|
+
end
|
96
|
+
|
97
|
+
# sinatra installs special handlers during development
|
98
|
+
# this runs the "real" `not_found` block instead
|
99
|
+
#
|
100
|
+
error(Sinatra::NotFound){ not_found } if development?
|
101
|
+
|
102
|
+
# set error values for JSON 404 response body
|
103
|
+
#
|
104
|
+
not_found do
|
105
|
+
response.error = {
|
106
|
+
type: :not_found,
|
107
|
+
message: "The requested path was not found: #{request.path}"
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
class << self
|
112
|
+
|
113
|
+
# because some parameters can be too large for normal GET query strings,
|
114
|
+
# all GET routes also accept a POST with body data, with the same parameter
|
115
|
+
# names and behavior
|
116
|
+
#
|
117
|
+
alias_method :sinatra_get, :get
|
118
|
+
def get(*args, &block)
|
119
|
+
sinatra_get *args, &block
|
120
|
+
post *args, &block
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
class String
|
130
|
+
# ripped from:
|
131
|
+
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb
|
132
|
+
#
|
133
|
+
unless instance_methods.include? :camelcase
|
134
|
+
def camelcase
|
135
|
+
string = self.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase }
|
136
|
+
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
|
137
|
+
string.gsub!('/', '::')
|
138
|
+
string
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class Symbol
|
144
|
+
unless instance_methods.include? :camelcase
|
145
|
+
def camelcase
|
146
|
+
self.to_s.camelcase.to_sym
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
|
3
|
+
require 'rack/test'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'minitest/pride'
|
6
|
+
require 'pry'
|
7
|
+
require 'pry-nav'
|
8
|
+
|
9
|
+
$:.push File.expand_path '../../lib', __FILE__
|
10
|
+
require 'jsonatra'
|
11
|
+
|
12
|
+
include Rack::Test::Methods
|
13
|
+
|
14
|
+
def app
|
15
|
+
@controller
|
16
|
+
end
|
17
|
+
|
18
|
+
def mock_app &block
|
19
|
+
@controller = Sinatra.new Jsonatra::Base, &block
|
20
|
+
end
|
21
|
+
|
22
|
+
def r
|
23
|
+
JSON.parse last_response.body
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_and_post *args, &block
|
27
|
+
get *args
|
28
|
+
yield
|
29
|
+
setup
|
30
|
+
post *args
|
31
|
+
yield
|
32
|
+
end
|
33
|
+
|
34
|
+
JSON_CT = { 'CONTENT_TYPE' => 'application/json' }
|
35
|
+
def post_json path, params = {}, headers = {}
|
36
|
+
post path, params.to_json, headers.merge(JSON_CT)
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_and_post_and_post_json path, params = {}, headers = {}, &block
|
40
|
+
get_and_post path, params, headers, &block
|
41
|
+
post_json path, params, headers, &block
|
42
|
+
end
|
43
|
+
alias gapapj get_and_post_and_post_json
|
44
|
+
|
45
|
+
def must_have_parameter_error_for parameter, type = :invalid, error_type = :invalid_input
|
46
|
+
last_response.status.must_equal 200
|
47
|
+
r['error'].wont_be_nil
|
48
|
+
r['error']['type'].must_equal error_type.to_s
|
49
|
+
r['error']['parameters'].wont_be_nil
|
50
|
+
r['error']['parameters'][parameter.to_s].wont_be_empty
|
51
|
+
r['error']['parameters'][parameter.to_s].first['type'].must_equal type.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def must_have_header_error_for header, type = :invalid, error_type = :invalid_header
|
55
|
+
last_response.status.must_equal 200
|
56
|
+
r['error'].wont_be_nil
|
57
|
+
r['error']['type'].must_equal error_type.to_s
|
58
|
+
r['error']['headers'].wont_be_nil
|
59
|
+
r['error']['headers'][header.to_s].wont_be_empty
|
60
|
+
r['error']['headers'][header.to_s].first['type'].must_equal type.to_s
|
61
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
|
3
|
+
describe Jsonatra::ErrorHelpers do
|
4
|
+
|
5
|
+
before do
|
6
|
+
mock_app do
|
7
|
+
get '/pe' do
|
8
|
+
param_error :foo, :invalid, 'foo bar'
|
9
|
+
end
|
10
|
+
get '/he' do
|
11
|
+
header_error :foo, :invalid, 'foo bar'
|
12
|
+
end
|
13
|
+
|
14
|
+
get '/pec' do
|
15
|
+
param_error :foo, :invalid, 'foo bar' do |e|
|
16
|
+
e[:code] = 401
|
17
|
+
end
|
18
|
+
end
|
19
|
+
get '/hec' do
|
20
|
+
header_error :foo, :invalid, 'foo bar' do |e|
|
21
|
+
e[:code] = 498
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '404 handler' do
|
28
|
+
|
29
|
+
it 'includes code: 404 in the json body of the response' do
|
30
|
+
get '/not_found'
|
31
|
+
last_response.status.must_equal 404
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'param_error' do
|
37
|
+
|
38
|
+
it 'creates error messages properly' do
|
39
|
+
gapapj '/pe' do
|
40
|
+
must_have_parameter_error_for :foo
|
41
|
+
r['error']['parameters']['foo'].first['message'].must_equal 'foo bar'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'can arbitrarily add to error object' do
|
46
|
+
gapapj '/pec' do
|
47
|
+
must_have_parameter_error_for :foo
|
48
|
+
r['error']['code'].must_equal 401
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'header_error' do
|
55
|
+
|
56
|
+
it 'creates error messages properly' do
|
57
|
+
gapapj '/he' do
|
58
|
+
must_have_header_error_for :foo
|
59
|
+
r['error']['headers']['foo'].first['message'].must_equal 'foo bar'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'can arbitrarily add to error object' do
|
64
|
+
gapapj '/hec' do
|
65
|
+
must_have_header_error_for :foo
|
66
|
+
r['error']['code'].must_equal 498
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
describe 'camelcase_error_tyeps' do
|
73
|
+
|
74
|
+
before do
|
75
|
+
mock_app do
|
76
|
+
configure do
|
77
|
+
enable :camelcase_error_types
|
78
|
+
end
|
79
|
+
get '/pe' do
|
80
|
+
param_error :foo, :snakey_snake, 'foo bar'
|
81
|
+
end
|
82
|
+
get '/he' do
|
83
|
+
header_error :foo, :snakey_snake, 'foo bar'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'camelcases parameter error types' do
|
89
|
+
gapapj '/pe' do
|
90
|
+
must_have_parameter_error_for :foo, :snakeySnake, :invalidInput
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'camelcases header error types' do
|
95
|
+
gapapj '/he' do
|
96
|
+
must_have_header_error_for :foo, :snakeySnake, :invalidHeader
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'contentTypeMismatch' do
|
101
|
+
|
102
|
+
before do
|
103
|
+
mock_app do
|
104
|
+
configure do
|
105
|
+
enable :camelcase_error_types
|
106
|
+
end
|
107
|
+
get '/params' do
|
108
|
+
params
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'returns an informative error if it notices JSON content without correct header' do
|
114
|
+
post '/params', {foo: 'bar', baz: 42, bat: ['a', 1, 5.75, true]}.to_json
|
115
|
+
r['error'].wont_be_nil
|
116
|
+
r['error']['type'].must_equal "contentTypeMismatch"
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require_relative '../helper'
|
2
|
+
|
3
|
+
describe Jsonatra::ParamsHelpers do
|
4
|
+
|
5
|
+
before do
|
6
|
+
mock_app do
|
7
|
+
get '/params' do
|
8
|
+
params
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'returns request params same as default for GET requests' do
|
14
|
+
get '/params', foo: 'bar', baz: 42, bat: ['a', 1, 5.75, true]
|
15
|
+
r['foo'].must_equal 'bar'
|
16
|
+
r['baz'].must_equal '42'
|
17
|
+
r['bat'][0].must_equal 'a'
|
18
|
+
r['bat'][1].must_equal '1'
|
19
|
+
r['bat'][2].must_equal '5.75'
|
20
|
+
r['bat'][3].must_equal 'true'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns request form encoded params same as default for POST requests' do
|
24
|
+
post '/params', foo: 'bar', baz: 42, bat: ['a', 1, 5.75, true]
|
25
|
+
r['foo'].must_equal 'bar'
|
26
|
+
r['baz'].must_equal '42'
|
27
|
+
r['bat'][0].must_equal 'a'
|
28
|
+
r['bat'][1].must_equal '1'
|
29
|
+
r['bat'][2].must_equal '5.75'
|
30
|
+
r['bat'][3].must_equal 'true'
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'parses and returns JSON data POSTed in the request body' do
|
34
|
+
post_json '/params', foo: 'bar', baz: 42, bat: ['a', 1, 5.75, true]
|
35
|
+
r['foo'].must_equal 'bar'
|
36
|
+
r['baz'].must_equal 42
|
37
|
+
r['bat'][0].must_equal 'a'
|
38
|
+
r['bat'][1].must_equal 1
|
39
|
+
r['bat'][2].must_equal 5.75
|
40
|
+
r['bat'][3].must_equal true
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'JSON data POSTed in the request body overrides default params' do
|
44
|
+
qs = Rack::Utils.escape({ foo: 'baR', baz: 43, bat: ['A', 2, 5.76, false] })
|
45
|
+
post_json "/params?#{qs}", foo: 'bar', baz: 42, bat: ['a', 1, 5.75, true]
|
46
|
+
r['foo'].must_equal 'bar'
|
47
|
+
r['baz'].must_equal 42
|
48
|
+
r['bat'][0].must_equal 'a'
|
49
|
+
r['bat'][1].must_equal 1
|
50
|
+
r['bat'][2].must_equal 5.75
|
51
|
+
r['bat'][3].must_equal true
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'returns an informative error if it notices JSON content without correct header' do
|
55
|
+
post '/params', {foo: 'bar', baz: 42, bat: ['a', 1, 5.75, true]}.to_json
|
56
|
+
r['error'].wont_be_nil
|
57
|
+
r['error']['type'].must_equal "content_type_mismatch"
|
58
|
+
|
59
|
+
post '/params', ['a', 1, 5.75, true].to_json
|
60
|
+
r['error'].wont_be_nil
|
61
|
+
r['error']['type'].must_equal "content_type_mismatch"
|
62
|
+
|
63
|
+
post '/params', {foo: 'bar', baz: 42, bat: ['a', 1, 5.75, true]}.to_json, {'Content-Type' => 'text/plain'}
|
64
|
+
r['error'].wont_be_nil
|
65
|
+
r['error']['type'].must_equal "content_type_mismatch"
|
66
|
+
|
67
|
+
post '/params', ['a', 1, 5.75, true].to_json, {'Content-Type' => 'text/plain'}
|
68
|
+
r['error'].wont_be_nil
|
69
|
+
r['error']['type'].must_equal "content_type_mismatch"
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require_relative './helper'
|
2
|
+
|
3
|
+
describe Jsonatra::Base do
|
4
|
+
|
5
|
+
HI = {
|
6
|
+
'hello' => 'there',
|
7
|
+
'scinot' => -122.67641415934295823498407,
|
8
|
+
'foo' => 42,
|
9
|
+
'bar' => true,
|
10
|
+
'baz' => false
|
11
|
+
}
|
12
|
+
|
13
|
+
before do
|
14
|
+
mock_app do
|
15
|
+
configure do
|
16
|
+
set :arrayified_params, ['foos']
|
17
|
+
end
|
18
|
+
get('/'){}
|
19
|
+
get('/hi'){ HI }
|
20
|
+
get('/aps'){ {foos: params[:foos]} }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'basics' do
|
25
|
+
|
26
|
+
it 'delivers empty object for nil returning routes' do
|
27
|
+
gapapj '/' do
|
28
|
+
r.must_equal({})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'sets content type to json' do
|
33
|
+
gapapj '/' do
|
34
|
+
ct = last_response.content_type.split(';')
|
35
|
+
ct.must_include Rack::Mime::MIME_TYPES['.json']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'sets content type to js if callback param' do
|
40
|
+
gapapj '/', { callback: 'foo' } do
|
41
|
+
ct = last_response.content_type.split(';')
|
42
|
+
ct.must_include Rack::Mime::MIME_TYPES['.js']
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'returns json from routes that return hashes' do
|
47
|
+
gapapj '/hi' do
|
48
|
+
r.must_equal HI
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'jsonp' do
|
55
|
+
|
56
|
+
it 'wraps response object in func call if callback param' do
|
57
|
+
gapapj '/', callback: 'foo' do
|
58
|
+
last_response.body.must_equal 'foo({});'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'errors if callback contains double quote char' do
|
63
|
+
get '/', callback: 'foo"bar"baz'
|
64
|
+
must_have_parameter_error_for :callback
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'arrayified params' do
|
70
|
+
|
71
|
+
it 'returns an array when an array is given' do
|
72
|
+
a = ['foo', 'bar']
|
73
|
+
get '/aps', foos: a
|
74
|
+
r['foos'].must_equal a
|
75
|
+
post '/aps', foos: a
|
76
|
+
r['foos'].must_equal a
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'returns an array when a comma-separated string is given' do
|
80
|
+
a = ['foo', 'bar']
|
81
|
+
get '/aps', foos: a.join(',')
|
82
|
+
r['foos'].must_equal a
|
83
|
+
post '/aps', foos: a.join(',')
|
84
|
+
r['foos'].must_equal a
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'returns nil if nothing was given' do
|
88
|
+
get '/aps'
|
89
|
+
r['foos'].must_be_nil
|
90
|
+
post '/aps'
|
91
|
+
r['foos'].must_be_nil
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'CORS header' do
|
97
|
+
|
98
|
+
it 'responds with headers containing an open allow origin policy' do
|
99
|
+
[:get, :post, :options].each do |meth|
|
100
|
+
__send__ meth, '/'
|
101
|
+
last_response.headers.keys.must_include 'Access-Control-Allow-Origin'
|
102
|
+
last_response.headers['Access-Control-Allow-Origin'].must_equal '*'
|
103
|
+
last_response.headers.keys.must_include 'Access-Control-Allow-Headers'
|
104
|
+
last_response.headers['Access-Control-Allow-Headers'].must_equal 'Accept, Authorization, Content-Type, Origin'
|
105
|
+
last_response.headers.keys.must_include 'Access-Control-Allow-Methods'
|
106
|
+
last_response.headers['Access-Control-Allow-Methods'].must_equal 'GET, POST'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'access_control_headers' do
|
113
|
+
|
114
|
+
before do
|
115
|
+
mock_app do
|
116
|
+
def access_control_headers
|
117
|
+
{ 'header_fu' => 'value_fu' }
|
118
|
+
end
|
119
|
+
get('/'){}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'responds with headers customized by app' do
|
124
|
+
[:get, :post, :options].each do |meth|
|
125
|
+
__send__ meth, '/'
|
126
|
+
last_response.headers.keys.must_include 'header_fu'
|
127
|
+
last_response.headers['header_fu'].must_equal 'value_fu'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jsonatra
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kenichi Nakamura
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sinatra
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: An opinionated, non-ReST, JSON API extension for Sinatra
|
31
|
+
email:
|
32
|
+
- kenichi.nakamura@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- .travis.yml
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- jsonatra.gemspec
|
44
|
+
- lib/jsonatra.rb
|
45
|
+
- lib/jsonatra/helpers/error.rb
|
46
|
+
- lib/jsonatra/helpers/params.rb
|
47
|
+
- lib/jsonatra/response.rb
|
48
|
+
- lib/jsonatra/version.rb
|
49
|
+
- test/helper.rb
|
50
|
+
- test/helpers/error_spec.rb
|
51
|
+
- test/helpers/params_spec.rb
|
52
|
+
- test/jsonatra_spec.rb
|
53
|
+
homepage: https://github.com/esripdx/jsonatra
|
54
|
+
licenses:
|
55
|
+
- apache
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
requirements: []
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.8.23
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: An opinionated, non-ReST, JSON API extension for Sinatra
|
78
|
+
test_files:
|
79
|
+
- test/helper.rb
|
80
|
+
- test/helpers/error_spec.rb
|
81
|
+
- test/helpers/params_spec.rb
|
82
|
+
- test/jsonatra_spec.rb
|