jsonatra 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ .DS_Store
3
+ tags
4
+ jsonatra-*.gem
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ - "2.0.0"
5
+ - jruby-19mode
6
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'sinatra'
4
+ gem 'webmock'
5
+ gem 'rack-test'
6
+ gem 'pry'
7
+ gem 'pry-nav'
8
+ gem 'rake'
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
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.pattern = ENV['TEST_PATTERN'] || "test/**/*_spec.rb"
5
+ end
6
+
7
+ task :default => :test
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
@@ -0,0 +1,3 @@
1
+ module Jsonatra
2
+ VERSION = '1.0.2'
3
+ 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