jdx-sinatra-param 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 25b304c125e3f1409ede06a86888a15bb4ec4e3184242213c944f6e2022facee
4
+ data.tar.gz: edd31eedf1724a9f7e67741ce445b99ca5bbfa4bec01e65c86494e4a2ff99616
5
+ SHA512:
6
+ metadata.gz: 56cbcf65a1de6583bd380ae27967401c24036b2f76255d3cd9dceccc3467b355d7310edfe71144e19b0df94d6701500dcaeabb8b0bbfa3a75a6f864398e620b9
7
+ data.tar.gz: 93680828a46da46330bc7604f87b912c295391a2c98538cff231b837bbdf3ee734b9b8ca91b56e3eb05a7529a6f34efa2abba44b6f1df51decc446cc54da11ec
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
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.
@@ -0,0 +1,182 @@
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.
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,4 @@
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'
@@ -0,0 +1 @@
1
+ web: bundle exec rackup --port $PORT
@@ -0,0 +1,52 @@
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
@@ -0,0 +1,6 @@
1
+ require 'bundler'
2
+ Bundler.require
3
+
4
+ require './app'
5
+
6
+ run App
@@ -0,0 +1,199 @@
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(name, 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], 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 = {error: error}.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_name, 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, "Parameter #{param_name} '#{param}' is not a valid #{type}"
132
+ end
133
+ end
134
+
135
+ def validate!(param, param_name, options)
136
+ options.each do |key, value|
137
+ case key
138
+ when :required
139
+ raise InvalidParameterError, "Parameter #{param_name} is required" if value && param.nil?
140
+ when :blank
141
+ raise InvalidParameterError, "Parameter #{param_name} 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 #{param_name} must be a string if using the format validation" unless param.kind_of?(String)
151
+ raise InvalidParameterError, "Parameter #{param_name} must match format #{value}" unless param =~ value
152
+ when :is
153
+ raise InvalidParameterError, "Parameter #{param_name} must be #{value}" unless param === value
154
+ when :in, :within, :range
155
+ raise InvalidParameterError, "Parameter #{param_name} must be within #{value}" unless param.nil? || case value
156
+ when Range
157
+ value.include?(param)
158
+ else
159
+ Array.wrap(value).any? {|v| Array.wrap(param).include?(v)}
160
+ end
161
+ when :min
162
+ raise InvalidParameterError, "Parameter #{param_name} cannot be less than #{value}" unless param.nil? || value <= param
163
+ when :max
164
+ raise InvalidParameterError, "Parameter #{param_name} cannot be greater than #{value}" unless param.nil? || value >= param
165
+ when :min_length
166
+ raise InvalidParameterError, "Parameter #{param_name} cannot have length less than #{value}" unless param.nil? || value <= param.length
167
+ when :max_length
168
+ raise InvalidParameterError, "Parameter #{param_name} cannot have length greater than #{value}" unless param.nil? || value >= param.length
169
+ when :custom
170
+ raise InvalidParameterError, "Parameter #{param_name} is not valid" unless value.to_proc.call(param)
171
+ end
172
+ end
173
+ end
174
+
175
+ # def validate_one_of!(params, names, options)
176
+ # raise InvalidParameterError, "Only one of [#{names.join(', ')}] is allowed" if names.count{|name| present?(params[name])} > 1
177
+ # end
178
+
179
+ # def validate_any_of!(params, names, options)
180
+ # raise InvalidParameterError, "One of parameters [#{names.join(', ')}] is required" if names.count{|name| present?(params[name])} < 1
181
+ # end
182
+
183
+ # def validate_all_or_none_of!(params, names, options)
184
+ # present_count = names.count{|name| present?(params[name])}
185
+ # raise InvalidParameterError, "All or none of parameters [#{names.join(', ')}] are required" if present_count > 0 and present_count != names.length
186
+ # end
187
+
188
+ # ActiveSupport #present? and #blank? without patching Object
189
+ # def present?(object)
190
+ # !blank?(object)
191
+ # end
192
+
193
+ # def blank?(object)
194
+ # object.respond_to?(:empty?) ? object.empty? : !object
195
+ # end
196
+ end
197
+
198
+ helpers Param
199
+ end