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 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