Giter VIP home page Giter VIP logo

sinatra-param's Introduction

sinatra-param

Parameter Validation & Type Coercion for Sinatra

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.

However, when it comes to figuring out what parameters are expected... well, all bets are off.

This Sinatra extension takes a first step to solving this problem on the developer side

sinatra-param allows you to declare, validate, and transform endpoint parameters as you would in frameworks like ActiveModel or DataMapper.

Use sinatra-param in combination with Rack::PostBodyContentTypeParser and Rack::NestedParams to automatically parameterize JSON POST bodies and nested parameters.

Install

You can install sinatra-param from the command line with the following:

$ gem install sinatra-param

Alternatively, you can specify sinatra-param as a dependency in your Gemfile and run $ bundle install:

gem "sinatra-param", require: "sinatra/param"

Example

require 'sinatra/base'
require 'sinatra/param'
require 'json'

class App < Sinatra::Base
  helpers Sinatra::Param

  before do
    content_type :json
  end

  # GET /search?q=example
  # GET /search?q=example&categories=news
  # GET /search?q=example&sort=created_at&order=ASC
  get '/search' do
    param :q,           String, required: true
    param :categories,  Array
    param :sort,        String, default: "title"
    param :order,       String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC"
    param :price,       String, format: /[<\=>]\s*\$\d+/

    one_of :q, :categories

    {...}.to_json
  end
end

Parameter Types

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.

  • String
  • Integer
  • Float
  • Boolean ("1/0", "true/false", "t/f", "yes/no", "y/n")
  • Array ("1,2,3,4,5")
  • Hash (key1:value1,key2:value2)
  • Date, Time, & DateTime

Validations

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.

  • required
  • blank
  • is
  • in, within, range
  • min / max
  • min_length / max_length
  • format

Custom Error Messages

Passing a message option allows you to customize the message for any validation error that occurs.

param :spelling,
      format: /\b(?![a-z]*cie)[a-z]*(?:cei|ie)[a-z]*/i,
      message: "'i' before 'e', except after 'c'"

Defaults and Transformations

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:

param :attribution, String, default: "©"
param :year, Integer, default: lambda { Time.now.year }

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.

param :order, String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC"
param :offset, Integer, min: 0, transform: lambda {|n| n - (n % 10)}

One Of

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:

param :a, String
param :b, String
param :c, String

one_of :a, :b, :c

Any Of

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:

param :x, String
param :y, String

any_of :x, :y

All Or None Of

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:

param :x, String
param :y, String

all_or_none_of :x,:y

Exceptions

By default, when a parameter precondition fails, Sinatra::Param will halt 400 with an error message:

{
  "message": "Parameter must be within [\"ASC\", \"DESC\"]",
  "errors": {
    "order": "Parameter must be within [\"ASC\", \"DESC\"]"
  }
}

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

set :raise_sinatra_param_exceptions, true

error Sinatra::Param::InvalidParameterError do
    { error: "#{env['sinatra.error'].param} is invalid" }.to_json
end

Custom exception handling can also be enabled on an individual parameter basis, by passing the raise option:

param :order, String, in: ["ASC", "DESC"], raise: true

one_of :q, :categories, raise: true

Contact

Mattt (@mattt)

License

sinatra-param is released under an MIT license. See LICENSE for more information.

sinatra-param's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sinatra-param's Issues

Can't provide an empty hash as default value

First off, great idea for a gem! This will be useful for me all over the place.

I was trying to create a param rule with a default value of an empty hash as:

param :category, Hash, default: {}
puts params.inspect #=> {}

So this doesn't seem to work - I get no default value ("category" remains undefined in params). What I would expect it to do is fill params as: {"category" => {}}

If I put a placeholder value in the hash, then I can get it to work, which is fine for me now, but it's ugly, and against the grain for this gem's design principle I think?

param :category, Hash, default: {nil: {}}
puts params.inspect #=> {"category"=>{:nil=>{}}}

Maybe I'm doing something wrong or misunderstanding something? Thanks again for this very useful idea!

ArgumentError - invalid byte sequence in UTF-8

app_1      | 2019-03-29 15:55:38 - ArgumentError - invalid byte sequence in UTF-8:
app_1      |    /usr/local/bundle/gems/sinatra-param-1.6.0/lib/sinatra/param.rb:122:in `==='
app_1      |    /usr/local/bundle/gems/sinatra-param-1.6.0/lib/sinatra/param.rb:122:in `block in validate!'
app_1      |    /usr/local/bundle/gems/sinatra-param-1.6.0/lib/sinatra/param.rb:115:in `each'
app_1      |    /usr/local/bundle/gems/sinatra-param-1.6.0/lib/sinatra/param.rb:115:in `validate!'
app_1      |    /usr/local/bundle/gems/sinatra-param-1.6.0/lib/sinatra/param.rb:23:in `param'
app_1      |    /service/lib/routes/avatar.rb:19:in `block in <class:Application>'

The line in my code is:
param :email, String, blank: false

The request made to the server has a param of:
?email=%28t%B3odei%[email protected]

Params not triggering errors

I'm trying to make this work in my Sinatra app. I installed the gem, included the helpers, and copied the syntax directly from the examples, but the code runs silently no matter what param restrictions I set or params I pass through. Are there any known conflicts that would quiet it?

Here are the gems I'm using:

gem 'foreman'
gem 'sinatra'
gem 'sinatra-contrib'
gem 'sinatra-param'

gem 'dotenv'
gem 'erubis'
gem 'activesupport', require: 'active_support'

gem 'bcrypt'
gem 'rack-ssl'
gem 'sinatra_warden', github: 'drfeelngood/sinatra_warden', branch: 'settings'
gem 'rack-flash3'
gem 'rabl'

gem 'dedent'
gem 'color'

gem 'sequel_pg', require: 'sequel'
gem 'sinatra-sequel'
gem 'sequel_postgresql_triggers'
gem 'pg'

Output as Swagger?

First off, I love this gem. It's such a simple idea, and so useful. As I've been implementing with this gem, I've been thinking that I'm almost completely specifying my API with the [VERB] [PATH] Sinatra call (e.g. get '/api/element'), plus my sinatra-param statements.

So, with a bit of introspection, I've been wondering if sinatra-param might be induced to output Swagger syntax or similar if run in a "documenting" mode? I'm not quite sure how this would work, but I think we might do this by setting sinatra-param into "documentation == true" mode, and then running the Sinatra app in a test harness. Sinatra-param would create/hold an in-memory representation of every API call that is made to it (minus dupes)? Then at the end of the test suite, we could ask sinatra-param for that a data structure (json/hash?) of all the unique API calls it has experienced. From there converting to whatever format (e.g. Swagger) and dumping to a file? We'd have documentation for at least every API call that is tested which creates a nice incentive to test every API! :)

This might be overly complicated and/or uninteresting to you, but I wanted to raise the idea, and see what you thought. I'd be willing to pitch in, if you think there is something useful here.

Thanks again for the great gem, in all cases!

422 vs 400 status code

Typically, in Rails, Strong Parameters returns a 422 status code when a required param is missing, for this reason I would prefer to have the same behaviour as well in Sinatra. Could it be possible to set the status code via options or as a global configuration?

Use in Padrino?

Is it possible to use this in Padrino? I've added it to my gemfile, but it doesn't seem to recognize the use of "param" in the routes. I've used this in a straight Sinatra project and found it quite helpful, and would love to use it in the Padrino app I'm developing. Thanks!

"PG::InvalidTextRepresentation: ERROR: missing dimension value" for Array param

I am using an Array type parameter an run into the following error when writing to the database.

SEQUEL DEPRECATION WARNING: Automatically typecasting a hash or array to string for a string column is deprecated and will be removed in Sequel 4.0.  Either typecast the input manually or use the looser_typecasting extension.
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/database/misc.rb:498:in `typecast_value_string'
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/database/misc.rb:289:in `typecast_value'
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/model/base.rb:1988:in `typecast_value'
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/model/base.rb:1056:in `[]='
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/model/base.rb:769:in `tags='
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/model/base.rb:1922:in `block in set_restricted'
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/model/base.rb:1919:in `each'
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/model/base.rb:1919:in `set_restricted'
.../bundle/ruby/2.1.0/gems/sequel-3.48.0/lib/sequel/model/base.rb:1414:in `set'
.../bundle/ruby/2.1.0/bundler/gems/rack-push-notification-74486fdea05a/lib/rack/push-notification.rb:37:in `block in <class:PushNotification>'
Sequel::DatabaseError - PG::InvalidTextRepresentation: ERROR:  missing dimension value
LINE 1: ...ags", "timezone", "token") VALUES ('nl_NL', 'en', '["iPhone ...
                             ^

More specifically, to resolve a conflict I have updated the dependencies of 'rack-push-notification' and the 'tags' parameter gives me the error. For completeness sake, the action that causes the error looks like this:

put '/devices/:token/?' do
  param :languages, Array
  param :tags, Array
  ...

The database column has type 'text[]'.

Even though I smuggled with the dependencies, this looks like a pretty generic thing?

Using Symbol as param `name` argument results in having duplicate / non-matching attributes in parameters hash

Given a contract like param :q, String, transform: :upcase and a request parameters hash like {"q"=>"test"}, sinatra-param will create a problematic params hash such as follows:

{
   "q"=>"test",
   :q=>"TEST"
}

This can lead to bugs and confusion downstream, and complicates/inhibits predictable mass assignment.

An easy and straightforward way to resolve this would be to always transform the name argument of param to String since we can know that input param keys will always be strings. However this creates the side effect that you can write contracts like param :q, String in a request block but will have to reference that parameter as params['q'] when processing in that block. I think this is more desirable than the current behavior, but may not be ideal.

I'd love to hear other thoughts before patching.

Option to disable body errors display

Hi!
When I using param with format: regex there is nasty response returning with all that long regex.

For example:

      param :url, String, required: true,
                          format: URI::regexp,
                          message: "Valid url is required"

And the response:

=> {"message"=>"Valid url is required",
 "errors"=>
  {"url"=>
    "Parameter must match format (?x-mi:\n        ([a-zA-Z][\\-+.a-zA-Z\\d]*):                           (?# 1: scheme)\n        (?:\n           ((?:[\\-_.!~*'()a-zA-Z\\d;?:@&=+$,]|%[a-fA-F\\d]{2})(?:[\\-_.!~*'()a-zA-Z\\d;\\/?:@&=+$,\\[\\]]|%[a-fA-F\\d]{2})*)                    (?# 2: opaque)\n        |\n           (?:(?:\n             \\/\\/(?:\n                 (?:(?:((?:[\\-_.!~*'()a-zA-Z\\d;:&=+$,]|%[a-fA-F\\d]{2})*)@)?        (?# 3: userinfo)\n                   (?:((?:(?:[a-zA-Z0-9\\-.]|%\\h\\h)+|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|\\[(?:(?:[a-fA-F\\d]{1,4}:)*(?:[a-fA-F\\d]{1,4}|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|(?:(?:[a-fA-F\\d]{1,4}:)*[a-fA-F\\d]{1,4})?::(?:(?:[a-fA-F\\d]{1,4}:)*(?:[a-fA-F\\d]{1,4}|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}))?)\\]))(?::(\\d*))?))? (?# 4: host, 5: port)\n               |\n                 ((?:[\\-_.!~*'()a-zA-Z\\d$,;:@&=+]|%[a-fA-F\\d]{2})+)                 (?# 6: registry)\n               )\n             |\n             (?!\\/\\/))                           (?# XXX: '\\/\\/' is the mark for hostport)\n             (\\/(?:[\\-_.!~*'()a-zA-Z\\d:@&=+$,]|%[a-fA-F\\d]{2})*(?:;(?:[\\-_.!~*'()a-zA-Z\\d:@&=+$,]|%[a-fA-F\\d]{2})*)*(?:\\/(?:[\\-_.!~*'()a-zA-Z\\d:@&=+$,]|%[a-fA-F\\d]{2})*(?:;(?:[\\-_.!~*'()a-zA-Z\\d:@&=+$,]|%[a-fA-F\\d]{2})*)*)*)?                    (?# 7: path)\n           )(?:\\?((?:[\\-_.!~*'()a-zA-Z\\d;\\/?:@&=+$,\\[\\]]|%[a-fA-F\\d]{2})*))?                 (?# 8: query)\n        )\n        (?:\\#((?:[\\-_.!~*'()a-zA-Z\\d;\\/?:@&=+$,\\[\\]]|%[a-fA-F\\d]{2})*))?                  (?# 9: fragment)\n      )"}}

It doesn't look good. Is there any way to disable errors showing in the response?

False boolean param yields incorrect result when 'default' is not used

Line 18

params[name] = coerce(params[name], type, options) || options[:default]}

yields incorrect result if the first part of the condition is 'false' and 'options[:default]' is nil.

This is because 'false || nil' returns nil.
This also means that a boolean param always becomes true if default is true.

400 or 422?

Hello!

The documentation says that the app will return 400 if params are not valid. But status code 400 is only for syntax errors in the requests.
If syntax is OK and params are not valid because of the business logic (for example, email should match [email protected] and it doesn't), the app should return status code 422 Unprocessable Entity.
(http://www.restapitutorial.com/httpstatuscodes.html)

So, is there any difference between these two cases?

Custom Messages

It would be nice to have support for custom error messages. Maybe something like:

param :foo, required: true, is: 'super-secret-value', message: 'Invalid value for foo'

Otherwise, very handy gem. Thanks!

BigDecimal support

Are there any plans to support BigDecimal for validation, or should these be taken as String and handled manually?

License missing from gemspec

RubyGems.org doesn't report a license for your gem. This is because it is not specified in the gemspec of your last release.

via e.g.

  spec.license = 'MIT'
  # or
  spec.licenses = ['MIT', 'GPL-2']

Including a license in your gemspec is an easy way for rubygems.org and other tools to check how your gem is licensed. As you can imagine, scanning your repository for a LICENSE file or parsing the README, and then attempting to identify the license or licenses is much more difficult and more error prone. So, even for projects that already specify a license, including a license in your gemspec is a good practice. See, for example, how rubygems.org uses the gemspec to display the rails gem license.

There is even a License Finder gem to help companies/individuals ensure all gems they use meet their licensing needs. This tool depends on license information being available in the gemspec. This is an important enough issue that even Bundler now generates gems with a default 'MIT' license.

I hope you'll consider specifying a license in your gemspec. If not, please just close the issue with a nice message. In either case, I'll follow up. Thanks for your time!

Appendix:

If you need help choosing a license (sorry, I haven't checked your readme or looked for a license file), GitHub has created a license picker tool. Code without a license specified defaults to 'All rights reserved'-- denying others all rights to use of the code.
Here's a list of the license names I've found and their frequencies

p.s. In case you're wondering how I found you and why I made this issue, it's because I'm collecting stats on gems (I was originally looking for download data) and decided to collect license metadata,too, and make issues for gemspecs not specifying a license as a public service :). See the previous link or my blog post about this project for more information.

Please add an example of got to validate nested JSON body elements

So lets see this JSON body

{
	"configuration": {
		"type": "",
		"height": "apple",
		"width": false,
		"cost_in_usd": "not afforable"
	}
}

If this is posted in a POST request, and I am using

require 'rack' require 'rack/contrib'

and

use Rack::JSONBodyParser
helpers Sinatra::Param

I need to know how to validate the above mentioned JSON keys under configuration.

Type is empty, height and width and cost have wrongly provided values.

Param default behave anormaly with a Boolean type set to false

Hello,
When the default is set to false the param is not present in the request, we get a nil instead of the false.

This is an example of failing test

    it 'coerces default booleans to false when default is false and its not provided' do
      get('/default/boolean/false') do |response|
        expect(response.status).to eql 200
        expect(JSON.parse(response.body)['arg']).to be false # this test fail and we get a `nil`
        expect(JSON.parse(response.body)['arg']).to_not be_nil
      end
    end
  end

Support for nested params

At the moment, there isn't a way to validate nested params. Passing something like ?post[title]=Derp in Sinatra will convert the param to post = { title: 'Derp' }. It'd be very handy to validate it the way it's passed, like:

param 'post[title]', String, required: true

Boolean type parameter not working

If I try

patch '/:id' do |id|
    content_type :json
    param :foo, Boolean, required: true
  end

And try to do a patch passing as body:

{
  "foo": true
}

It raises 400 error. Am I doing anything wrong?

Add example of PostBodyContentTypeParser use for JSON validation

Could you please provide an example of the following?

Use sinatra-param in combination with Rack::PostBodyContentTypeParser and Rack::NestedParams to automatically parameterize JSON POST bodies and nested parameters.

Been looking all over for an example of this, and I can't find anything.

Thanks in advance!

Check if param exists in database

Wondering if it's possible to do data verification with this gem, by checking a parameter's value against a column to see if it exists?

Essentially, I want to move the verification out of activerecord into the route itself.

Uninitialized Constant Error

I get the following error when using the Boolean type like so:

param :archived, Boolean, :default => false

Error:
NameError - uninitialized constant Api::Boolean and the line number points to the above line.

Same result when running the example app and modifying the code to look like this:

get '/messages' do
param :sort, String, default: "name"
param :order, String, in: ["ASC", "DESC"], transform: :upcase, default: "ASC"
param :test, Boolean

{
sort: params[:sort],
order: params[:order]
}.to_json
end

Output:
NameError at /messages
uninitialized constant App::Boolean
file: app.rb location: block in class:App line: 13

Edit: Should probably mention that it's with the latest version of the Gem.

How to install

I am using bundler, so I put `gem 'sinatra-param', '~> 1.3' in my Gemfile. For those without bundler, it's 'gem install sinatra-param'. Having install details in the readme would probably be helpful!

README request - include example using URI.regexp

Since it seems like something that might come up here and there, maybe it would be good to show a format example with URI.regexp. It seems simple, but to a Ruby newb like me (coming from Python), this was a little A-ha! Nice! moment... I mean, it's not necessary but it is a common use case anyway (pretty common for someone to put in a URL into a form, linking accounts etc.) ... yeah, why not show an easy case.

Totally understand if this seems gratuitous just figured I'd suggest it and throw up a PR.

Return multiple errors

Currently, it seems to only return the first error found. So, e.g. API user gets an error, fixes the parameter, then gets an error on a different parameter. Rinse and repeat.

It would be useful to return multiple errors. To avoid the need for "rinse and repeat".

It begs the question, though, about "message" vs "errors". I'm not sure why they both exist in the first place? If it were to check and return errors for multiple parameters, what, logically, should "message be".

Multiple errors?

errors is an array, so there must have been some intent to return multiple errors. Is it just that that feature is not implemented, or am I doing something wrong?

Question about expected behaviour of `any_of`, `one_of` and `any_all_or_none_of`

Hello! Here's my use case:

Let's say I have a GET /users route that accepts 3 parameters to filter a list of users from a database. All parameters are not required:

  • param: name String
  • param: age Integer
  • param: is_active Boolean

But, I want to force the client to have a least 1 filter parameter, so I use:

any_of :name, :age, :is_active

The client wants to get all inactive users (of any name and age), so they send a request with a single parameter: is_active: false.

This will result in sinatra-param throwing an error like this:

    "status": "error",
    "details": "Invalid '[\"name\", \"age\", \"is_active\"]' param",
    "class": "Sinatra::Param::InvalidParameterError",
    "message": "One of parameters [name, age, is_active] is required",

This seems to occur because false does not respond to .empty?, in the definition for blank? in lib/sinatra/param.rb and therefor does not count as an existing parameter in the query:

...
    def validate_any_of!(params, names, options)
      raise InvalidParameterError, "One of parameters [#{names.join(', ')}] is required" if names.count{|name| present?(params[name])} < 1
    end
...
    # ActiveSupport #present? and #blank? without patching Object
    def present?(object)
      !blank?(object)
    end

    def blank?(object)
      object.respond_to?(:empty?) ? object.empty? : !object
    end
...

A boolean parameter set to false does not "count" as a valid parameter provided for the routes that use any_of, one_of and any_all_or_none_of. Is this the expected behaviour?

Am I looking at this the wrong way conceptually? Would it make sense to change the logic to accept false as a "valid" parameter value?

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.