├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── example ├── Gemfile ├── Procfile ├── app.rb └── config.ru ├── lib └── sinatra │ ├── param.rb │ └── param │ └── version.rb ├── sinatra-param.gemspec └── spec ├── dummy └── app.rb ├── parameter_conjunctivity_spec.rb ├── parameter_exclusivity_spec.rb ├── parameter_inclusivity_spec.rb ├── parameter_raise_spec.rb ├── parameter_spec.rb ├── parameter_transformations_spec.rb ├── parameter_type_coercion_spec.rb ├── parameter_validations_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | *.gem 3 | *.rbc 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /spec/reports/ 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Documentation 14 | /.yardoc/ 15 | /_yardoc/ 16 | /doc/ 17 | /rdoc/ 18 | 19 | # Environment 20 | /.bundle/ 21 | /lib/bundler/man/ 22 | 23 | # Library 24 | Gemfile.lock 25 | .ruby-version 26 | .ruby-gemset 27 | 28 | # RVM 29 | .rvmrc 30 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012–2018 Mattt (http://mat.tt/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sinatra-param 2 | 3 | _Parameter Validation & Type Coercion for Sinatra_ 4 | 5 | REST conventions take the guesswork out of designing and consuming web APIs. Simply `GET`, `POST`, `PATCH`, or `DELETE` resource endpoints, and you get what you'd expect. 6 | 7 | However, when it comes to figuring out what parameters are expected... well, all bets are off. 8 | 9 | This Sinatra extension takes a first step to solving this problem on the developer side 10 | 11 | **`sinatra-param` allows you to declare, validate, and transform endpoint parameters as you would in frameworks like [ActiveModel](http://rubydoc.info/gems/activemodel/3.2.3/frames) or [DataMapper](http://datamapper.org/).** 12 | 13 | > Use `sinatra-param` in combination with [`Rack::PostBodyContentTypeParser` and `Rack::NestedParams`](https://github.com/rack/rack-contrib) to automatically parameterize JSON `POST` bodies and nested parameters. 14 | 15 | ## Install 16 | 17 | You can install `sinatra-param` from the command line with the following: 18 | 19 | ```bash 20 | $ gem install sinatra-param 21 | ``` 22 | 23 | Alternatively, you can specify `sinatra-param` as a dependency in your `Gemfile` and run `$ bundle install`: 24 | 25 | ```ruby 26 | gem "sinatra-param", require: "sinatra/param" 27 | ``` 28 | 29 | ## Example 30 | 31 | ```ruby 32 | require 'sinatra/base' 33 | require 'sinatra/param' 34 | require 'json' 35 | 36 | class App < Sinatra::Base 37 | helpers Sinatra::Param 38 | 39 | before do 40 | content_type :json 41 | end 42 | 43 | # GET /search?q=example 44 | # GET /search?q=example&categories=news 45 | # GET /search?q=example&sort=created_at&order=ASC 46 | get '/search' do 47 | param :q, String, required: true 48 | param :categories, Array 49 | param :sort, String, default: "title" 50 | param :order, String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC" 51 | param :price, String, format: /[<\=>]\s*\$\d+/ 52 | 53 | one_of :q, :categories 54 | 55 | {...}.to_json 56 | end 57 | end 58 | ``` 59 | 60 | ### Parameter Types 61 | 62 | By declaring parameter types, incoming parameters will automatically be transformed into an object of that type. For instance, if a param is `Boolean`, values of `'1'`, `'true'`, `'t'`, `'yes'`, and `'y'` will be automatically transformed into `true`. 63 | 64 | * `String` 65 | * `Integer` 66 | * `Float` 67 | * `Boolean` _("1/0", "true/false", "t/f", "yes/no", "y/n")_ 68 | * `Array` _("1,2,3,4,5")_ 69 | * `Hash` _(key1:value1,key2:value2)_ 70 | * `Date`, `Time`, & `DateTime` 71 | 72 | ### Validations 73 | 74 | Encapsulate business logic in a consistent way with validations. If a parameter does not satisfy a particular condition, a `400` error is returned with a message explaining the failure. 75 | 76 | * `required` 77 | * `blank` 78 | * `is` 79 | * `in`, `within`, `range` 80 | * `min` / `max` 81 | * `min_length` / `max_length` 82 | * `format` 83 | 84 | ### Custom Error Messages 85 | 86 | Passing a `message` option allows you to customize the message 87 | for any validation error that occurs. 88 | 89 | ```ruby 90 | param :spelling, 91 | format: /\b(?![a-z]*cie)[a-z]*(?:cei|ie)[a-z]*/i, 92 | message: "'i' before 'e', except after 'c'" 93 | ``` 94 | 95 | ### Defaults and Transformations 96 | 97 | Passing a `default` option will provide a default value for a parameter if none is passed. A `default` can defined as either a default or as a `Proc`: 98 | 99 | ```ruby 100 | param :attribution, String, default: "©" 101 | param :year, Integer, default: lambda { Time.now.year } 102 | ``` 103 | 104 | Use the `transform` option to take even more of the business logic of parameter I/O out of your code. Anything that responds to `to_proc` (including `Proc` and symbols) will do. 105 | 106 | ```ruby 107 | param :order, String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC" 108 | param :offset, Integer, min: 0, transform: lambda {|n| n - (n % 10)} 109 | ``` 110 | 111 | ## One Of 112 | 113 | Using `one_of`, routes can specify two or more parameters to be mutually exclusive, and fail if _more than one_ of those parameters is provided: 114 | 115 | ```ruby 116 | param :a, String 117 | param :b, String 118 | param :c, String 119 | 120 | one_of :a, :b, :c 121 | ``` 122 | 123 | ## Any Of 124 | 125 | Using `any_of`, a route can specify that _at least one of_ two or more parameters are required, and fail if _none of them_ are provided: 126 | 127 | ```ruby 128 | param :x, String 129 | param :y, String 130 | 131 | any_of :x, :y 132 | ``` 133 | 134 | ## All Or None Of 135 | 136 | Using `all_or_none_of`, a router can specify that _all_ or _none_ of a set of parameters are required, and fail if _some_ are provided: 137 | 138 | ```ruby 139 | param :x, String 140 | param :y, String 141 | 142 | all_or_none_of :x,:y 143 | ``` 144 | 145 | ### Exceptions 146 | 147 | By default, when a parameter precondition fails, `Sinatra::Param` will `halt 400` with an error message: 148 | 149 | ```json 150 | { 151 | "message": "Parameter must be within [\"ASC\", \"DESC\"]", 152 | "errors": { 153 | "order": "Parameter must be within [\"ASC\", \"DESC\"]" 154 | } 155 | } 156 | ``` 157 | 158 | To change this, you can set `:raise_sinatra_param_exceptions` to `true`, and intercept `Sinatra::Param::InvalidParameterError` with a Sinatra `error do...end` block. (To make this work in development, set `:show_exceptions` to `false` and `:raise_errors` to `true`): 159 | 160 | ```ruby 161 | set :raise_sinatra_param_exceptions, true 162 | 163 | error Sinatra::Param::InvalidParameterError do 164 | { error: "#{env['sinatra.error'].param} is invalid" }.to_json 165 | end 166 | ``` 167 | 168 | Custom exception handling can also be enabled on an individual parameter basis, by passing the `raise` option: 169 | 170 | ```ruby 171 | param :order, String, in: ["ASC", "DESC"], raise: true 172 | 173 | one_of :q, :categories, raise: true 174 | ``` 175 | 176 | ## Contact 177 | 178 | Mattt ([@mattt](http://twitter.com/mattt)) 179 | 180 | ## License 181 | 182 | sinatra-param is released under an MIT license. See LICENSE for more information. 183 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.setup 3 | 4 | gemspec = eval(File.read("sinatra-param.gemspec")) 5 | 6 | task :build => "#{gemspec.full_name}.gem" 7 | 8 | file "#{gemspec.full_name}.gem" => gemspec.files + ["sinatra-param.gemspec"] do 9 | system "gem build sinatra-param.gemspec" 10 | end 11 | 12 | begin 13 | require 'rspec/core/rake_task' 14 | RSpec::Core::RakeTask.new(:spec) 15 | 16 | task :default => :spec 17 | rescue LoadError 18 | end 19 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'sinatra', '~> 2.0', require: 'sinatra/base' 4 | gem 'sinatra-param', path: File.join(__FILE__, "../.."), require: 'sinatra/param' 5 | -------------------------------------------------------------------------------- /example/Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rackup --port $PORT 2 | -------------------------------------------------------------------------------- /example/app.rb: -------------------------------------------------------------------------------- 1 | class App < Sinatra::Base 2 | helpers Sinatra::Param 3 | 4 | before do 5 | content_type :json 6 | end 7 | 8 | # GET /messages 9 | # GET /messages?sort=name&order=ASC 10 | get '/messages' do 11 | param :sort, String, default: "name" 12 | param :order, String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC" 13 | 14 | { 15 | sort: params[:sort], 16 | order: params[:order] 17 | }.to_json 18 | end 19 | 20 | # GET /messages/1,2,3,4,5 21 | get '/messages/:ids' do 22 | param :ids, Array, required: true 23 | 24 | { 25 | ids: params[:ids] 26 | }.to_json 27 | end 28 | 29 | # POST /messages/1/response 30 | post '/messages/:id/response' do 31 | param :message, String, max: 1024, required: true 32 | 33 | { 34 | message: params[:message] 35 | }.to_json 36 | end 37 | 38 | # GET /choice?a=foo 39 | # GET /choice?b=bar 40 | # GET /choice?c=baz 41 | get '/choice' do 42 | param :a, String 43 | param :b, String 44 | param :c, String 45 | 46 | one_of(:a, :b, :c) 47 | 48 | { 49 | message: 'OK' 50 | }.to_json 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.require 3 | 4 | require './app' 5 | 6 | run App 7 | -------------------------------------------------------------------------------- /lib/sinatra/param.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'sinatra/param/version' 3 | require 'date' 4 | require 'time' 5 | 6 | module Sinatra 7 | module Param 8 | Boolean = :boolean 9 | 10 | class InvalidParameterError < StandardError 11 | attr_accessor :param, :options 12 | end 13 | 14 | def param(name, type, options = {}) 15 | name = name.to_s 16 | 17 | return unless params.member?(name) or options.has_key?(:default) or options[:required] 18 | 19 | begin 20 | params[name] = coerce(params[name], type, options) 21 | params[name] = (options[:default].call if options[:default].respond_to?(:call)) || options[:default] if params[name].nil? and options.has_key?(:default) 22 | params[name] = options[:transform].to_proc.call(params[name]) if params[name] and options[:transform] 23 | validate!(params[name], options) 24 | params[name] 25 | rescue InvalidParameterError => exception 26 | if options[:raise] or (settings.raise_sinatra_param_exceptions rescue false) 27 | exception.param, exception.options = name, options 28 | raise exception 29 | end 30 | 31 | error = options[:message] || exception.to_s 32 | 33 | if content_type and content_type.match(mime_type(:json)) 34 | error = {message: error, errors: {name => exception.message}}.to_json 35 | else 36 | content_type 'text/plain' 37 | end 38 | 39 | halt 400, error 40 | end 41 | end 42 | 43 | def one_of(*args) 44 | options = args.last.is_a?(Hash) ? args.pop : {} 45 | names = args.collect(&:to_s) 46 | 47 | return unless names.length >= 2 48 | 49 | begin 50 | validate_one_of!(params, names, options) 51 | rescue InvalidParameterError => exception 52 | if options[:raise] or (settings.raise_sinatra_param_exceptions rescue false) 53 | exception.param, exception.options = names, options 54 | raise exception 55 | end 56 | 57 | error = "Invalid parameters [#{names.join(', ')}]" 58 | if content_type and content_type.match(mime_type(:json)) 59 | error = {message: error, errors: {names => exception.message}}.to_json 60 | end 61 | 62 | halt 400, error 63 | end 64 | end 65 | 66 | def any_of(*args) 67 | options = args.last.is_a?(Hash) ? args.pop : {} 68 | names = args.collect(&:to_s) 69 | 70 | return unless names.length >= 2 71 | 72 | begin 73 | validate_any_of!(params, names, options) 74 | rescue InvalidParameterError => exception 75 | if options[:raise] or (settings.raise_sinatra_param_exceptions rescue false) 76 | exception.param, exception.options = names, options 77 | raise exception 78 | end 79 | 80 | error = "Invalid parameters [#{names.join(', ')}]" 81 | if content_type and content_type.match(mime_type(:json)) 82 | error = {message: error, errors: {names => exception.message}}.to_json 83 | end 84 | 85 | halt 400, error 86 | end 87 | end 88 | 89 | def all_or_none_of(*args) 90 | options = args.last.is_a?(Hash) ? args.pop : {} 91 | names = args.collect(&:to_s) 92 | 93 | begin 94 | validate_all_or_none_of!(params, names, options) 95 | rescue InvalidParameterError => exception 96 | if options[:raise] or (settings.raise_sinatra_param_exceptions rescue false) 97 | exception.param, exception.options = names, options 98 | raise exception 99 | end 100 | 101 | error = "Invalid parameters [#{names.join(', ')}]" 102 | if content_type and content_type.match(mime_type(:json)) 103 | error = {message: error, errors: {names => exception.message}}.to_json 104 | end 105 | 106 | halt 400, error 107 | end 108 | end 109 | 110 | private 111 | 112 | def coerce(param, type, options = {}) 113 | begin 114 | return nil if param.nil? 115 | return param if (param.is_a?(type) rescue false) 116 | return Integer(param, 10) if type == Integer 117 | return Float(param) if type == Float 118 | return String(param) if type == String 119 | return Date.parse(param) if type == Date 120 | return Time.parse(param) if type == Time 121 | return DateTime.parse(param) if type == DateTime 122 | return Array(param.split(options[:delimiter] || ",")) if type == Array 123 | return Hash[param.split(options[:delimiter] || ",").map{|c| c.split(options[:separator] || ":")}] if type == Hash 124 | if [TrueClass, FalseClass, Boolean].include? type 125 | coerced = /^(false|f|no|n|0)$/i === param.to_s ? false : /^(true|t|yes|y|1)$/i === param.to_s ? true : nil 126 | raise ArgumentError if coerced.nil? 127 | return coerced 128 | end 129 | return nil 130 | rescue ArgumentError 131 | raise InvalidParameterError, "'#{param}' is not a valid #{type}" 132 | end 133 | end 134 | 135 | def validate!(param, options) 136 | options.each do |key, value| 137 | case key 138 | when :required 139 | raise InvalidParameterError, "Parameter is required" if value && param.nil? 140 | when :blank 141 | raise InvalidParameterError, "Parameter cannot be blank" if !value && case param 142 | when String 143 | !(/\S/ === param) 144 | when Array, Hash 145 | param.empty? 146 | else 147 | param.nil? 148 | end 149 | when :format 150 | raise InvalidParameterError, "Parameter must be a string if using the format validation" unless param.kind_of?(String) 151 | raise InvalidParameterError, "Parameter must match format #{value}" unless param =~ value 152 | when :is 153 | raise InvalidParameterError, "Parameter must be #{value}" unless param === value 154 | when :in, :within, :range 155 | raise InvalidParameterError, "Parameter must be within #{value}" unless param.nil? || case value 156 | when Range 157 | value.include?(param) 158 | else 159 | Array(value).include?(param) 160 | end 161 | when :min 162 | raise InvalidParameterError, "Parameter cannot be less than #{value}" unless param.nil? || value <= param 163 | when :max 164 | raise InvalidParameterError, "Parameter cannot be greater than #{value}" unless param.nil? || value >= param 165 | when :min_length 166 | raise InvalidParameterError, "Parameter cannot have length less than #{value}" unless param.nil? || value <= param.length 167 | when :max_length 168 | raise InvalidParameterError, "Parameter cannot have length greater than #{value}" unless param.nil? || value >= param.length 169 | end 170 | end 171 | end 172 | 173 | def validate_one_of!(params, names, options) 174 | raise InvalidParameterError, "Only one of [#{names.join(', ')}] is allowed" if names.count{|name| present?(params[name])} > 1 175 | end 176 | 177 | def validate_any_of!(params, names, options) 178 | raise InvalidParameterError, "One of parameters [#{names.join(', ')}] is required" if names.count{|name| present?(params[name])} < 1 179 | end 180 | 181 | def validate_all_or_none_of!(params, names, options) 182 | present_count = names.count{|name| present?(params[name])} 183 | raise InvalidParameterError, "All or none of parameters [#{names.join(', ')}] are required" if present_count > 0 and present_count != names.length 184 | end 185 | 186 | # ActiveSupport #present? and #blank? without patching Object 187 | def present?(object) 188 | !blank?(object) 189 | end 190 | 191 | def blank?(object) 192 | object.respond_to?(:empty?) ? object.empty? : !object 193 | end 194 | end 195 | 196 | helpers Param 197 | end 198 | -------------------------------------------------------------------------------- /lib/sinatra/param/version.rb: -------------------------------------------------------------------------------- 1 | module Sinatra 2 | module Param 3 | VERSION = '1.6.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /sinatra-param.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/sinatra/param/version', __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "sinatra-param" 6 | s.license = "MIT" 7 | s.authors = ["Mattt"] 8 | s.email = "mattt@me.com" 9 | s.homepage = "https://github.com/mattt/sinatra-param" 10 | s.version = Sinatra::Param::VERSION 11 | s.platform = Gem::Platform::RUBY 12 | s.summary = "Parameter Validation & Type Coercion for Sinatra." 13 | s.description = "sinatra-param allows you to declare, validate, and transform endpoint parameters as you would in frameworks like ActiveModel or DataMapper." 14 | 15 | s.add_dependency "sinatra", ">= 1.3" 16 | 17 | s.add_development_dependency "rake" 18 | s.add_development_dependency "rspec" 19 | s.add_development_dependency "rack-test" 20 | s.add_development_dependency "simplecov" 21 | 22 | s.files = Dir["./**/*"].reject { |file| file =~ /\.\/(bin|log|pkg|script|spec|test|vendor)/ } 23 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 24 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 25 | s.require_paths = ["lib"] 26 | end 27 | -------------------------------------------------------------------------------- /spec/dummy/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'sinatra/param' 3 | require 'date' 4 | require 'time' 5 | require 'json' 6 | 7 | class App < Sinatra::Base 8 | helpers Sinatra::Param 9 | 10 | set :show_exceptions, false 11 | set :raise_errors, true 12 | 13 | before do 14 | content_type :json 15 | end 16 | 17 | get '/' do 18 | param :a, String 19 | param :b, String, required: true 20 | param :c, String, default: 'test' 21 | param :d, String 22 | 23 | params.to_json 24 | end 25 | 26 | get '/keys/stringify' do 27 | param :q, String, transform: :upcase 28 | 29 | params['q'] 30 | end 31 | 32 | get '/coerce/string' do 33 | params['arg'] = params['arg'].to_i 34 | param :arg, String 35 | params.to_json 36 | end 37 | 38 | get '/coerce/integer' do 39 | param :arg, Integer 40 | params.to_json 41 | end 42 | 43 | get '/coerce/float' do 44 | param :arg, Float 45 | params.to_json 46 | end 47 | 48 | get '/coerce/time' do 49 | param :arg, Time 50 | params.to_json 51 | end 52 | 53 | get '/coerce/date' do 54 | param :arg, Date 55 | params.to_json 56 | end 57 | 58 | get '/coerce/datetime' do 59 | param :arg, DateTime 60 | params.to_json 61 | end 62 | 63 | get '/coerce/array' do 64 | param :arg, Array 65 | params.to_json 66 | end 67 | 68 | get '/coerce/hash' do 69 | param :arg, Hash 70 | params.to_json 71 | end 72 | 73 | get '/coerce/boolean' do 74 | param :arg, Boolean 75 | params.to_json 76 | end 77 | 78 | get '/default' do 79 | param :sort, String, default: "title" 80 | params.to_json 81 | end 82 | 83 | get '/default/hash' do 84 | param :attributes, Hash, default: {} 85 | params.to_json 86 | end 87 | 88 | get '/default/proc' do 89 | param :year, Integer, default: proc { 2014 } 90 | params.to_json 91 | end 92 | 93 | get '/default/boolean/true' do 94 | param :arg, Boolean, default: true 95 | params.to_json 96 | end 97 | 98 | get '/default/boolean/false' do 99 | param :arg, Boolean, default: false 100 | params.to_json 101 | end 102 | 103 | get '/transform' do 104 | param :order, String, transform: :upcase 105 | params.to_json 106 | end 107 | 108 | get '/transform/required' do 109 | param :order, String, required: true, transform: :upcase 110 | params.to_json 111 | end 112 | 113 | get '/validation/required' do 114 | param :arg, String, required: true 115 | params.to_json 116 | end 117 | 118 | get '/validation/blank/string' do 119 | param :arg, String, blank: false 120 | end 121 | 122 | get '/validation/blank/array' do 123 | param :arg, Array, blank: false 124 | end 125 | 126 | get '/validation/blank/hash' do 127 | param :arg, Hash, blank: false 128 | end 129 | 130 | get '/validation/blank/other' do 131 | param :arg, Class, blank: false 132 | end 133 | 134 | get '/validation/nonblank/string' do 135 | param :arg, String, blank: true 136 | end 137 | 138 | get '/validation/format/9000' do 139 | param :arg, Integer, format: /9000/ 140 | params.to_json 141 | end 142 | 143 | get '/validation/format/hello' do 144 | param :arg, String, format: /hello/ 145 | params.to_json 146 | end 147 | 148 | get '/validation/is' do 149 | param :arg, String, is: 'foo' 150 | params.to_json 151 | end 152 | 153 | get '/validation/in' do 154 | param :arg, String, in: ['ASC', 'DESC'] 155 | params.to_json 156 | end 157 | 158 | get '/validation/within' do 159 | param :arg, Integer, within: 1..10 160 | params.to_json 161 | end 162 | 163 | get '/validation/range' do 164 | param :arg, Integer, range: 1..10 165 | params.to_json 166 | end 167 | 168 | get '/validation/min' do 169 | param :arg, Integer, min: 12 170 | params.to_json 171 | end 172 | 173 | get '/validation/max' do 174 | param :arg, Integer, max: 20 175 | params.to_json 176 | end 177 | 178 | get '/validation/min_length' do 179 | param :arg, String, min_length: 5 180 | params.to_json 181 | end 182 | 183 | get '/validation/max_length' do 184 | param :arg, String, max_length: 10 185 | params.to_json 186 | end 187 | 188 | get '/one_of/1' do 189 | param :a, String 190 | param :b, String 191 | param :c, String 192 | 193 | one_of :a 194 | 195 | { 196 | message: 'OK' 197 | }.to_json 198 | end 199 | 200 | get '/one_of/2' do 201 | param :a, String 202 | param :b, String 203 | param :c, String 204 | 205 | one_of :a, :b 206 | 207 | { 208 | message: 'OK' 209 | }.to_json 210 | end 211 | 212 | get '/one_of/3' do 213 | param :a, String 214 | param :b, String 215 | param :c, String 216 | 217 | one_of :a, :b, :c 218 | 219 | { 220 | message: 'OK' 221 | }.to_json 222 | end 223 | 224 | get '/any_of' do 225 | param :a, String 226 | param :b, String 227 | param :c, String 228 | 229 | any_of :a, :b, :c 230 | 231 | { 232 | message: 'OK' 233 | }.to_json 234 | end 235 | 236 | get '/raise/validation/required' do 237 | param :arg, String, required: true, raise: true 238 | params.to_json 239 | end 240 | 241 | get '/raise/one_of/3' do 242 | param :a, String 243 | param :b, String 244 | param :c, String 245 | 246 | one_of :a, :b, :c, raise: true 247 | 248 | { 249 | message: 'OK' 250 | }.to_json 251 | end 252 | 253 | get '/raise/any_of' do 254 | param :a, String 255 | param :b, String 256 | param :c, String 257 | 258 | any_of :a, :b, :c, raise: true 259 | 260 | { 261 | message: 'OK' 262 | }.to_json 263 | end 264 | 265 | get '/xml' do 266 | content_type :xml 267 | param :a, Integer, within: 1..10, required: true 268 | end 269 | 270 | get '/custommessage' do 271 | param :a, Integer, within: 1..10, required: true, message: "'a' must be less than 10" 272 | end 273 | 274 | get '/all_or_none_of' do 275 | param :a, String 276 | param :b, String 277 | param :c, String 278 | 279 | all_or_none_of :a, :b, :c 280 | 281 | { 282 | message: 'OK' 283 | }.to_json 284 | end 285 | end 286 | -------------------------------------------------------------------------------- /spec/parameter_conjunctivity_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Parameter Sets' do 4 | describe 'all_or_none_of' do 5 | it 'returns 400 on requests that have some but not all required parameters' do 6 | params = [ 7 | {a: 1}, 8 | {b: 2, c: 3}, 9 | {a: 1, c: 3}, 10 | ] 11 | 12 | params.each do |param| 13 | get('/all_or_none_of', param) do |response| 14 | expect(response.status).to eql 400 15 | expect(JSON.parse(response.body)['message']).to match(/^Invalid parameters/) 16 | end 17 | end 18 | end 19 | 20 | it 'returns successfully for requests that have all parameters' do 21 | param = {a: 1, b: 2, c: 3} 22 | 23 | response = get("/all_or_none_of", param) 24 | expect(response.status).to eql 200 25 | expect(JSON.parse(response.body)['message']).to match(/OK/) 26 | end 27 | 28 | it 'returns successfully for requests that have none of the parameters' do 29 | response = get("/all_or_none_of") 30 | expect(response.status).to eql 200 31 | expect(JSON.parse(response.body)['message']).to match(/OK/) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/parameter_exclusivity_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Parameter Sets' do 4 | describe 'one_of' do 5 | it 'returns 400 on requests that contain more than one mutually exclusive parameter' do 6 | params = [ 7 | {a: 1, b: 2}, 8 | {b: 2, c: 3}, 9 | {a: 1, b: 2, c: 3} 10 | ] 11 | 12 | params.each do |param| 13 | get('/one_of/3', param) do |response| 14 | expect(response.status).to eql 400 15 | expect(JSON.parse(response.body)['message']).to match(/^Invalid parameters/) 16 | end 17 | end 18 | end 19 | 20 | it 'returns 400 on requests that contain more than one mutually exclusive parameter' do 21 | params = {a: 1, b: 2} 22 | 23 | get('/one_of/2', params) do |response| 24 | expect(response.status).to eql 400 25 | expect(JSON.parse(response.body)['message']).to match(/^Invalid parameters/) 26 | end 27 | end 28 | 29 | it 'returns successfully for requests that have one parameter' do 30 | params = [ 31 | {a: 1}, 32 | {b: 2}, 33 | {c: 3} 34 | ] 35 | 36 | (1..3).each do |n| 37 | params.each do |param| 38 | get("/one_of/#{n}", param) do |response| 39 | expect(response.status).to eql 200 40 | expect(JSON.parse(response.body)['message']).to match(/OK/) 41 | end 42 | end 43 | end 44 | end 45 | 46 | it 'returns successfully for requests that have no parameter' do 47 | (1..3).each do |n| 48 | get("/one_of/#{n}") do |response| 49 | expect(response.status).to eql 200 50 | expect(JSON.parse(response.body)['message']).to match(/OK/) 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/parameter_inclusivity_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Parameter Sets' do 4 | describe 'any_of' do 5 | it 'returns 400 on requests that contain fewer than one required parameter' do 6 | get('/any_of', {}) do |response| 7 | expect(response.status).to eql 400 8 | expect(JSON.parse(response.body)['message']).to match(/Invalid parameters/) 9 | end 10 | end 11 | 12 | it 'returns successfully for requests that have at least one parameter' do 13 | params = [ 14 | {a: 1}, 15 | {b: 2}, 16 | {c: 3}, 17 | {a: 1, b: 2}, 18 | {b: 2, c: 3}, 19 | {a: 1, b: 2, c: 3} 20 | ] 21 | 22 | params.each do |param| 23 | get("/any_of", param) do |response| 24 | expect(response.status).to eql 200 25 | expect(JSON.parse(response.body)['message']).to match(/OK/) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/parameter_raise_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Exception' do 4 | describe 'raise' do 5 | it 'should raise error when option is specified' do 6 | expect { 7 | get('/raise/validation/required') 8 | }.to raise_error Sinatra::Param::InvalidParameterError 9 | end 10 | end 11 | 12 | it 'should raise error when more than one parameter is specified' do 13 | params = {a: 1, b: 2, c: 3} 14 | expect { 15 | get('/raise/one_of/3', params) 16 | }.to raise_error Sinatra::Param::InvalidParameterError 17 | end 18 | 19 | it 'should raise error when no parameters are specified' do 20 | params = {} 21 | expect { 22 | get('/raise/any_of', params) 23 | }.to raise_error Sinatra::Param::InvalidParameterError 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/parameter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Parameter' do 4 | it 'only sets parameters present in request or with a default value' do 5 | get('/', a: 'a', b: 'b') do |response| 6 | response_body = JSON.parse(response.body) 7 | expect(response_body).to be_member('a') 8 | expect(response_body).to be_member('b') 9 | expect(response_body).to be_member('c') 10 | expect(response_body).to_not be_member('d') 11 | end 12 | end 13 | 14 | it 'stringifies parameters' do 15 | get('/keys/stringify', q: 'test') do |response| 16 | expect(response.body).to eq 'TEST' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/parameter_transformations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Parameter Transformations' do 4 | describe 'default' do 5 | it 'sets a default value when none is given' do 6 | get('/default') do |response| 7 | expect(response.status).to eql 200 8 | expect(JSON.parse(response.body)['sort']).to eql 'title' 9 | end 10 | end 11 | 12 | it 'sets a default value from an empty hash' do 13 | get('/default/hash') do |response| 14 | expect(response.status).to eql 200 15 | expect(JSON.parse(response.body)['attributes']).to eql Hash.new 16 | end 17 | end 18 | 19 | it 'sets a default value from a proc' do 20 | get('/default/proc') do |response| 21 | expect(response.status).to eql 200 22 | expect(JSON.parse(response.body)['year']).to eql 2014 23 | end 24 | end 25 | end 26 | 27 | describe 'transform' do 28 | it 'transforms the input using to_proc' do 29 | get('/transform', order: 'asc') do |response| 30 | expect(response.status).to eql 200 31 | expect(JSON.parse(response.body)['order']).to eql 'ASC' 32 | end 33 | end 34 | 35 | it 'skips transformations when the value is nil' do 36 | get('/transform/required') do |response| 37 | expect(response.status).to eql 400 38 | expect(JSON.parse(response.body)['message']).to eq("Parameter is required") 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/parameter_type_coercion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Parameter Types' do 4 | describe 'String' do 5 | it 'coerces strings' do 6 | get('/coerce/string', arg: '1234') do |response| 7 | expect(response.status).to eql 200 8 | expect(JSON.parse(response.body)['arg']).to eq('1234') 9 | end 10 | end 11 | end 12 | 13 | describe 'Integer' do 14 | it 'coerces integers' do 15 | get('/coerce/integer', arg: '1234') do |response| 16 | expect(response.status).to eql 200 17 | expect(JSON.parse(response.body)['arg']).to eq(1234) 18 | end 19 | end 20 | 21 | it 'returns 400 on requests when integer is invalid' do 22 | get('/coerce/integer', arg: '123abc') do |response| 23 | expect(response.status).to eql 400 24 | expect(JSON.parse(response.body)['message']).to eq("'123abc' is not a valid Integer") 25 | end 26 | end 27 | end 28 | 29 | describe 'Float' do 30 | it 'coerces floats' do 31 | get('/coerce/float', arg: '1234') do |response| 32 | expect(response.status).to eql 200 33 | expect(JSON.parse(response.body)['arg']).to eq(1234.0) 34 | end 35 | end 36 | 37 | it 'returns 400 on requests when float is invalid' do 38 | get('/coerce/float', arg: '123abc') do |response| 39 | expect(response.status).to eql 400 40 | expect(JSON.parse(response.body)['message']).to eq("'123abc' is not a valid Float") 41 | end 42 | end 43 | end 44 | 45 | describe 'Time' do 46 | it 'coerces time' do 47 | get('/coerce/time', arg: '20130117') do |response| 48 | expect(response.status).to eql 200 49 | expect(JSON.parse(response.body)['arg']).to match(/2013-01-17 00:00:00/) 50 | end 51 | end 52 | 53 | it 'returns 400 on requests when time is invalid' do 54 | get('/coerce/time', arg: 'noon') do |response| 55 | expect(response.status).to eql 400 56 | expect(JSON.parse(response.body)['message']).to eq("'noon' is not a valid Time") 57 | end 58 | end 59 | end 60 | 61 | describe 'Date' do 62 | it 'coerces date' do 63 | get('/coerce/date', arg: '20130117') do |response| 64 | expect(response.status).to eql 200 65 | expect(JSON.parse(response.body)['arg']).to eq('2013-01-17') 66 | end 67 | end 68 | 69 | it 'returns 400 on requests when date is invalid' do 70 | get('/coerce/date', arg: 'abc') do |response| 71 | expect(response.status).to eql 400 72 | expect(JSON.parse(response.body)['message']).to eq("'abc' is not a valid Date") 73 | end 74 | end 75 | end 76 | 77 | describe 'DateTime' do 78 | it 'coerces datetimes' do 79 | get('/coerce/datetime', arg: '20130117') do |response| 80 | expect(response.status).to eql 200 81 | expect(JSON.parse(response.body)['arg']).to eq('2013-01-17T00:00:00+00:00') 82 | end 83 | end 84 | 85 | it 'returns 400 on requests when datetime is invalid' do 86 | get('/coerce/datetime', arg: 'abc') do |response| 87 | expect(response.status).to eql 400 88 | expect(JSON.parse(response.body)['message']).to eq("'abc' is not a valid DateTime") 89 | end 90 | end 91 | end 92 | 93 | describe 'Array' do 94 | it 'coerces arrays' do 95 | get('/coerce/array', arg: '1,2,3,4,5') do |response| 96 | expect(response.status).to eql 200 97 | parsed_body = JSON.parse(response.body) 98 | expect(parsed_body['arg']).to be_an(Array) 99 | expect(parsed_body['arg']).to eq(%w(1 2 3 4 5)) 100 | end 101 | end 102 | 103 | it 'coerces arrays of size 1' do 104 | get('/coerce/array', arg: '1') do |response| 105 | expect(response.status).to eql 200 106 | parsed_body = JSON.parse(response.body) 107 | expect(parsed_body['arg']).to be_an(Array) 108 | expect(parsed_body['arg']).to eq(%w(1)) 109 | end 110 | end 111 | 112 | it 'coerces arrays with arg[] style' do 113 | get('/coerce/array', 'arg[]' => ['1','2','3','4','5']) do |response| 114 | expect(response.status).to eql 200 115 | parsed_body = JSON.parse(response.body) 116 | expect(parsed_body['arg']).to be_an(Array) 117 | expect(parsed_body['arg']).to eq(%w(1 2 3 4 5)) 118 | end 119 | end 120 | end 121 | 122 | describe 'Hash' do 123 | it 'coerces hashes' do 124 | get('/coerce/hash', arg: 'a:b,c:d') do |response| 125 | expect(response.status).to eql 200 126 | parsed_body = JSON.parse(response.body) 127 | expect(parsed_body['arg']).to be_an(Hash) 128 | expect(parsed_body['arg']).to eq({ 'a' => 'b', 'c' => 'd'}) 129 | end 130 | end 131 | end 132 | 133 | describe 'Boolean' do 134 | it 'coerces truthy booleans to true' do 135 | %w(1 true t yes y).each do |bool| 136 | get('/coerce/boolean', arg: bool) do |response| 137 | expect(response.status).to eql 200 138 | expect(JSON.parse(response.body)['arg']).to be true 139 | end 140 | end 141 | end 142 | 143 | it 'coerces falsey booleans to false' do 144 | %w(0 false f no n).each do |bool| 145 | get('/coerce/boolean', arg: bool) do |response| 146 | expect(response.status).to eql 200 147 | expect(JSON.parse(response.body)['arg']).to be false 148 | expect(JSON.parse(response.body)['arg']).to_not be_nil 149 | end 150 | end 151 | end 152 | 153 | it 'coerces truthy booleans to true when default is false' do 154 | %w(1 true t yes y).each do |bool| 155 | get('/default/boolean/false', arg: bool) do |response| 156 | expect(response.status).to eql 200 157 | expect(JSON.parse(response.body)['arg']).to be true 158 | end 159 | end 160 | end 161 | 162 | it 'coerces falsey booleans to false when default is true' do 163 | %w(0 false f no n).each do |bool| 164 | get('/default/boolean/true', arg: bool) do |response| 165 | expect(response.status).to eql 200 166 | expect(JSON.parse(response.body)['arg']).to be false 167 | expect(JSON.parse(response.body)['arg']).to_not be_nil 168 | end 169 | end 170 | end 171 | 172 | it 'coerces default booleans to true when default is true and its not provided' do 173 | get('/default/boolean/true') do |response| 174 | expect(response.status).to eql 200 175 | expect(JSON.parse(response.body)['arg']).to be true 176 | expect(JSON.parse(response.body)['arg']).to_not be_nil 177 | end 178 | end 179 | 180 | it 'coerces default booleans to false when default is false and its not provided' do 181 | get('/default/boolean/false') do |response| 182 | expect(response.status).to eql 200 183 | expect(JSON.parse(response.body)['arg']).to be false 184 | expect(JSON.parse(response.body)['arg']).to_not be_nil 185 | end 186 | end 187 | 188 | it 'returns 400 on requests when true is not a truthy value' do 189 | get('/default/boolean/true', arg: 'abc') do |response| 190 | expect(response.status).to eql 400 191 | expect(JSON.parse(response.body)['message']).to eq("'abc' is not a valid boolean") 192 | end 193 | end 194 | 195 | it 'returns 400 on requests when false is not a falsey value' do 196 | get('/default/boolean/false', arg: 'abc') do |response| 197 | expect(response.status).to eql 400 198 | expect(JSON.parse(response.body)['message']).to eq("'abc' is not a valid boolean") 199 | end 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /spec/parameter_validations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Parameter Validations' do 4 | describe 'required' do 5 | it 'returns 400 on requests without required fields' do 6 | get('/validation/required') do |response| 7 | expect(response.status).to eq(400) 8 | expect(JSON.parse(response.body)['message']).to eq("Parameter is required") 9 | end 10 | end 11 | 12 | it 'returns 200 on requests when required field present' do 13 | get('/validation/required', arg: 'foo') do |response| 14 | expect(response.status).to eq(200) 15 | end 16 | end 17 | end 18 | 19 | describe 'blank' do 20 | it 'returns 400 on requests when string is blank' do 21 | get('/validation/blank/string', arg: '') do |response| 22 | expect(response.status).to eq(400) 23 | expect(JSON.parse(response.body)['message']).to eq("Parameter cannot be blank") 24 | end 25 | end 26 | 27 | it 'returns 400 on requests when array is blank' do 28 | get('/validation/blank/array', arg: '') do |response| 29 | expect(response.status).to eq(400) 30 | expect(JSON.parse(response.body)['message']).to eq("Parameter cannot be blank") 31 | end 32 | end 33 | 34 | it 'returns 400 on requests when hash is blank' do 35 | get('/validation/blank/hash', arg: '') do |response| 36 | expect(response.status).to eq(400) 37 | expect(JSON.parse(response.body)['message']).to eq("Parameter cannot be blank") 38 | end 39 | end 40 | 41 | it 'returns 400 on requests when hash is blank' do 42 | get('/validation/blank/other', arg: '') do |response| 43 | expect(response.status).to eq(400) 44 | expect(JSON.parse(response.body)['message']).to eq("Parameter cannot be blank") 45 | end 46 | end 47 | 48 | it 'returns 200 on request when blank is true' do 49 | get('/validation/nonblank/string', arg: '') do |response| 50 | expect(response.status).to eq(200) 51 | end 52 | end 53 | end 54 | 55 | describe 'format' do 56 | it 'returns 200 on requests when value matches the param regex' do 57 | get('/validation/format/hello', arg: 'hello world') do |response| 58 | expect(response.status).to eq(200) 59 | end 60 | end 61 | 62 | it 'returns 400 on requests when value does not match the param regex' do 63 | get('/validation/format/hello', arg: 'world') do |response| 64 | expect(response.status).to eq(400) 65 | end 66 | end 67 | 68 | it 'returns 400 on requests when value is not a string' do 69 | get('/validation/format/9000', arg: 9000) do |response| 70 | expect(response.status).to eq(400) 71 | end 72 | end 73 | end 74 | 75 | describe 'is' do 76 | it 'returns 400 on requests when value is other than defined' do 77 | get('/validation/is', arg: 'bar') do |response| 78 | expect(response.status).to eq(400) 79 | expect(JSON.parse(response.body)['message']).to eq("Parameter must be foo") 80 | end 81 | end 82 | 83 | it 'returns 200 on requests with a value is other than defined' do 84 | get('/validation/is', arg: 'foo') do |response| 85 | expect(response.status).to eq(200) 86 | end 87 | end 88 | end 89 | 90 | describe 'in' do 91 | it 'returns 400 on requests with a value not in the set' do 92 | get('/validation/in', arg: 'MISC') do |response| 93 | expect(response.status).to eq(400) 94 | expect(JSON.parse(response.body)['message']).to eq("Parameter must be within [\"ASC\", \"DESC\"]") 95 | end 96 | end 97 | 98 | it 'returns 200 on requests with a value in the set' do 99 | get('/validation/in', arg: 'ASC') do |response| 100 | expect(response.status).to eq(200) 101 | end 102 | end 103 | end 104 | 105 | describe 'within' do 106 | it 'returns 400 on requests with a value outside the range' do 107 | get('/validation/within', arg: 20) do |response| 108 | expect(response.status).to eq(400) 109 | expect(JSON.parse(response.body)['message']).to eq("Parameter must be within 1..10") 110 | end 111 | end 112 | 113 | it 'returns 200 on requests with a value within the range' do 114 | get('/validation/within', arg: 5) do |response| 115 | expect(response.status).to eq(200) 116 | end 117 | end 118 | end 119 | 120 | describe 'range' do 121 | it 'returns 400 on requests with a value outside the range' do 122 | get('/validation/range', arg: 20) do |response| 123 | expect(response.status).to eq(400) 124 | expect(JSON.parse(response.body)['message']).to eq("Parameter must be within 1..10") 125 | end 126 | end 127 | 128 | it 'returns 200 on requests within the range' do 129 | get('/validation/range', arg: 10) do |response| 130 | expect(response.status).to eq(200) 131 | end 132 | end 133 | end 134 | 135 | describe 'min' do 136 | it 'returns 400 on requests with a value smaller than min' do 137 | get('/validation/min', arg: 5) do |response| 138 | expect(response.status).to eq(400) 139 | expect(JSON.parse(response.body)['message']).to eq("Parameter cannot be less than 12") 140 | end 141 | end 142 | 143 | it 'returns 200 on requests with a value larger than min' do 144 | get('/validation/min', arg: 200) do |response| 145 | expect(response.status).to eq(200) 146 | end 147 | end 148 | end 149 | 150 | describe 'max' do 151 | it 'returns 400 on requests with a value larger than max' do 152 | get('/validation/max', arg: 100) do |response| 153 | expect(response.status).to eq(400) 154 | expect(JSON.parse(response.body)['message']).to eq("Parameter cannot be greater than 20") 155 | end 156 | end 157 | 158 | it 'returns 200 on requests with a value smaller than max' do 159 | get('/validation/max', arg: 2) do |response| 160 | expect(response.status).to eq(200) 161 | end 162 | end 163 | end 164 | 165 | describe 'min_length' do 166 | it 'returns 400 on requests with a string shorter than min_length' do 167 | get('/validation/min_length', arg: 'hi') do |response| 168 | expect(response.status).to eq(400) 169 | expect(JSON.parse(response.body)['message']).to eq("Parameter cannot have length less than 5") 170 | end 171 | end 172 | 173 | it 'returns 200 on requests with a string longer than min_length' do 174 | get('/validation/max_length', arg: 'longer') do |response| 175 | expect(response.status).to eq(200) 176 | end 177 | end 178 | end 179 | 180 | describe 'max_length' do 181 | it 'returns 400 on requests with a string longer than max_length' do 182 | get('/validation/max_length', arg: 'reallylongstringlongerthanmax') do |response| 183 | expect(response.status).to eq(400) 184 | expect(JSON.parse(response.body)['message']).to eq("Parameter cannot have length greater than 10") 185 | end 186 | end 187 | 188 | it 'returns 200 on requests with a string shorter than max_length' do 189 | get('/validation/max_length', arg: 'short') do |response| 190 | expect(response.status).to eq(200) 191 | end 192 | end 193 | end 194 | 195 | context 'content-type header' do 196 | it 'returns application/json for JSON APIs' do 197 | get('/validation/max_length', arg: 'reallylongstringlongerthanmax') do |response| 198 | expect(response.headers['Content-Type']).to eq('application/json') 199 | end 200 | end 201 | 202 | it 'returns text/plain for non-JSON APIs' do 203 | get('/xml', arg: 'reallylongstringlongerthanmax') do |response| 204 | expect(response.headers['Content-Type']).to include('text/plain') 205 | end 206 | end 207 | end 208 | 209 | context 'custom message ' do 210 | it 'returns a custom message when configured' do 211 | get('/custommessage') do |response| 212 | expect(JSON.parse(response.body)['message']).to eq("'a' must be less than 10") 213 | end 214 | end 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | unless ENV['CI'] 2 | require 'simplecov' 3 | SimpleCov.start do 4 | add_filter 'spec' 5 | add_filter '.bundle' 6 | end 7 | end 8 | 9 | require 'sinatra/param' 10 | 11 | require 'rspec' 12 | require 'rack/test' 13 | 14 | require 'dummy/app' 15 | 16 | def app 17 | App 18 | end 19 | 20 | include Rack::Test::Methods 21 | --------------------------------------------------------------------------------