jsonatra 1.0.2
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/.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
|
+
[](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
|