compel 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +131 -0
- data/Rakefile +5 -0
- data/compel.gemspec +25 -0
- data/lib/compel/coercion/boolean.rb +21 -0
- data/lib/compel/coercion/date.rb +13 -0
- data/lib/compel/coercion/datetime.rb +13 -0
- data/lib/compel/coercion/float.rb +13 -0
- data/lib/compel/coercion/hash.rb +13 -0
- data/lib/compel/coercion/integer.rb +13 -0
- data/lib/compel/coercion/json.rb +13 -0
- data/lib/compel/coercion/string.rb +17 -0
- data/lib/compel/coercion/time.rb +13 -0
- data/lib/compel/coercion/type.rb +30 -0
- data/lib/compel/coercion.rb +33 -0
- data/lib/compel/contract.rb +101 -0
- data/lib/compel/errors.rb +53 -0
- data/lib/compel/invalid_params_error.rb +9 -0
- data/lib/compel/param.rb +46 -0
- data/lib/compel/param_type_error.rb +7 -0
- data/lib/compel/param_validation_error.rb +7 -0
- data/lib/compel/validation.rb +72 -0
- data/lib/compel/version.rb +3 -0
- data/lib/compel.rb +42 -0
- data/spec/compel/coercion_spec.rb +186 -0
- data/spec/compel/compel_spec.rb +279 -0
- data/spec/compel/contract_spec.rb +36 -0
- data/spec/compel/errors_spec.rb +48 -0
- data/spec/compel/param_spec.rb +25 -0
- data/spec/compel/validation_spec.rb +73 -0
- data/spec/spec_helper.rb +1 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 20943c4ee705f920a1d316a9028b0ddc7e26cfc7
|
4
|
+
data.tar.gz: e9071ee624b81b8efe30b1747eacc3d00426339b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 638f760fe8ea31a6545101cf48923ed2d835cf4f449d4f29bbf64610945eb78013d4aaedb5ea0724946846235b490741712b12d7dbd8cc09ac2c60127734aa0f
|
7
|
+
data.tar.gz: cf70bca72fc84dbc3e4c3434a16bb49569a659188e1be6049e20d058027ac33be2f0361b12f240a9d1b5e0a3b6572a7b5ae295735445cf29628f09f28558d1b9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Joaquim Adráz
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
Compel
|
2
|
+
==========================
|
3
|
+

|
4
|
+
[](https://codeclimate.com/github/joaquimadraz/compel)
|
5
|
+
|
6
|
+
Ruby Hash Coercion and Validation
|
7
|
+
|
8
|
+
This is a straight forward way to validate a Ruby Hash: just give an object and the schema.
|
9
|
+
|
10
|
+
The motivation was to create an integration for [RestMyCase](https://github.com/goncalvesjoao/rest_my_case) and have validations before any business logic execution.
|
11
|
+
|
12
|
+
Based on the same principle from [Grape](https://github.com/ruby-grape/grape) framework and [sinatra-param](https://github.com/mattt/sinatra-param) gem to validate request params.
|
13
|
+
|
14
|
+
###Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'compel'
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
### Example
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
params= {
|
28
|
+
first_name: 'Joaquim',
|
29
|
+
birth_date: '1989-0',
|
30
|
+
address: {
|
31
|
+
line_one: 'Lisboa',
|
32
|
+
post_code: '1100',
|
33
|
+
country: 'PT'
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
Compel.run(params) do
|
38
|
+
param :first_name, String, required: true
|
39
|
+
param :last_name, String, required: true
|
40
|
+
param :birth_date, DateTime
|
41
|
+
param :address, Hash do
|
42
|
+
param :line_one, String, required: true
|
43
|
+
param :line_two, String, default: '-'
|
44
|
+
param :post_code, String, required: true, format: /^\d{4}-\d{3}$/
|
45
|
+
param :country_code, String, in: ['PT', 'GB'], default: 'PT'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
Will return an [Hashie::Mash](https://github.com/intridea/hashie) object:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
{
|
54
|
+
"first_name" => "Joaquim",
|
55
|
+
"address" => {
|
56
|
+
"line_one" => "Lisboa",
|
57
|
+
"line_two" => "-", # default value
|
58
|
+
"post_code_pfx" => 1100, # Already an Integer
|
59
|
+
"country_code"=> "PT"
|
60
|
+
},
|
61
|
+
"errors" => {
|
62
|
+
"last_name" => ["is required"],
|
63
|
+
"birth_date" => ["'1989-0' is not a valid DateTime"],
|
64
|
+
"address" => {
|
65
|
+
"post_code_sfx" => ["is required"]
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
```
|
70
|
+
|
71
|
+
There are 3 ways run validations:
|
72
|
+
|
73
|
+
- `#run`
|
74
|
+
- Validates and returns an Hash with coerced params plus a `:errors` key with a _Rails like_ Hash of errors if any.
|
75
|
+
- `#run!`
|
76
|
+
- Validates and raises `Compel::InvalidParamsError` exception with the coerced params and errors.
|
77
|
+
- `#run?`
|
78
|
+
- Validates and returns true or false.
|
79
|
+
|
80
|
+
|
81
|
+
### Types
|
82
|
+
|
83
|
+
- `Integer`
|
84
|
+
- `Float`
|
85
|
+
- `String`
|
86
|
+
- `JSON`
|
87
|
+
- ex: `"{\"a\":1,\"b\":2,\"c\":3}"`
|
88
|
+
- `Hash`
|
89
|
+
- ex: `{ a: 1, b: 2, c: 3 }`
|
90
|
+
- `Date`
|
91
|
+
- `Time`
|
92
|
+
- `DateTime`
|
93
|
+
- `Compel::Boolean`,
|
94
|
+
- ex: `1`/`0`, `true`/`false`, `t`/`f`, `yes`/`no`, `y`/`n`
|
95
|
+
|
96
|
+
### Sinatra Integration
|
97
|
+
|
98
|
+
If you want to use with `Sinatra`, just add the following code to your Sinatra app:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class App < Sinatra::Base
|
102
|
+
|
103
|
+
...
|
104
|
+
|
105
|
+
def compel(&block)
|
106
|
+
Compel::run!(params, &block)
|
107
|
+
end
|
108
|
+
|
109
|
+
error Compel::InvalidParamsError do |exception|
|
110
|
+
status 400
|
111
|
+
json errors: exception.errors
|
112
|
+
end
|
113
|
+
|
114
|
+
configure :development do
|
115
|
+
set :show_exceptions, false
|
116
|
+
set :raise_errors, true
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
### TODO
|
123
|
+
|
124
|
+
- Write more Documentation (check specs for now ;)
|
125
|
+
- Rails integration
|
126
|
+
- [RestMyCase](https://github.com/goncalvesjoao/rest_my_case) integration
|
127
|
+
|
128
|
+
|
129
|
+
### Get in touch
|
130
|
+
If you have any questions, write an issue or get in touch [@joaquimadraz](https://twitter.com/joaquimadraz)
|
131
|
+
|
data/Rakefile
ADDED
data/compel.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'compel/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |gem|
|
8
|
+
gem.name = 'compel'
|
9
|
+
gem.version = Compel::VERSION
|
10
|
+
gem.authors = ['Joaquim Adráz']
|
11
|
+
gem.email = ['joaquim.adraz@gmail.com']
|
12
|
+
gem.description = %q{Compel}
|
13
|
+
gem.summary = %q{Ruby Hash Coercion and Validation}
|
14
|
+
gem.homepage = 'https://github.com/joaquimadraz/compel'
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ['lib']
|
20
|
+
|
21
|
+
gem.add_runtime_dependency 'hashie', '~> 3.4'
|
22
|
+
gem.add_development_dependency 'rspec', '~> 3.2'
|
23
|
+
gem.add_development_dependency 'rake', '~> 0'
|
24
|
+
gem.add_development_dependency 'pry', '~> 0'
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Compel
|
2
|
+
module Coercion
|
3
|
+
|
4
|
+
class Boolean < Type
|
5
|
+
|
6
|
+
def coerce
|
7
|
+
if /(false|f|no|n|0)$/i === "#{value}"
|
8
|
+
return false
|
9
|
+
end
|
10
|
+
|
11
|
+
if /(true|t|yes|y|1)$/i === "#{value}"
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
fail
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Compel
|
2
|
+
module Coercion
|
3
|
+
|
4
|
+
class Type
|
5
|
+
|
6
|
+
attr_accessor :value,
|
7
|
+
:options
|
8
|
+
|
9
|
+
def initialize(value, options = {})
|
10
|
+
@value = value
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse
|
15
|
+
@value = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def serialize
|
19
|
+
raise '#serialize? should be implemented'
|
20
|
+
end
|
21
|
+
|
22
|
+
def coerce!
|
23
|
+
parse
|
24
|
+
coerce
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Compel
|
2
|
+
|
3
|
+
module Coercion
|
4
|
+
|
5
|
+
def valid?(value, type, options = {})
|
6
|
+
!!coerce!(value, type, options) rescue false
|
7
|
+
end
|
8
|
+
|
9
|
+
def coerce!(value, type, options = {})
|
10
|
+
return nil if value.nil?
|
11
|
+
|
12
|
+
begin
|
13
|
+
klass = compel_type?(type) ? type : get_compel_type_klass(type)
|
14
|
+
|
15
|
+
return klass.new(value, options).coerce!
|
16
|
+
rescue
|
17
|
+
raise ParamTypeError, "'#{value}' is not a valid #{type}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def compel_type?(type)
|
22
|
+
type.to_s.split('::')[0..1].join('::') == 'Compel::Coercion'
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_compel_type_klass(type)
|
26
|
+
const_get("Compel::Coercion::#{type}")
|
27
|
+
end
|
28
|
+
|
29
|
+
extend self
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Compel
|
2
|
+
|
3
|
+
class Contract
|
4
|
+
|
5
|
+
attr_reader :errors,
|
6
|
+
:conditions,
|
7
|
+
:coerced_params,
|
8
|
+
:serialized_errors
|
9
|
+
|
10
|
+
def initialize(params, &block)
|
11
|
+
if params.nil? || !Coercion.valid?(params, Hash)
|
12
|
+
raise ParamTypeError, 'params must be an Hash'
|
13
|
+
end
|
14
|
+
|
15
|
+
@errors = Errors.new
|
16
|
+
@params = Hashie::Mash.new(params)
|
17
|
+
@conditions = Hashie::Mash.new
|
18
|
+
@coerced_params = Hashie::Mash.new
|
19
|
+
|
20
|
+
instance_eval(&block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate
|
24
|
+
@conditions.values.each do |param|
|
25
|
+
begin
|
26
|
+
# If it is an Hash and it was given conditions for that Hash,
|
27
|
+
# build a new Compel::Contract form inner conditions
|
28
|
+
if (param.hash? && param.conditions?)
|
29
|
+
|
30
|
+
# If this param is required, build the Compel::Contract,
|
31
|
+
# otherwise, only build it if is given a value for the param
|
32
|
+
if param.required? || !param.value.nil?
|
33
|
+
contract = Contract.new(param.value, ¶m.conditions).validate
|
34
|
+
|
35
|
+
@errors.add(param.name, contract.errors)
|
36
|
+
|
37
|
+
# Update the param value with coerced values to use later
|
38
|
+
# when coercing param parent
|
39
|
+
@coerced_params[param.name] = contract.coerced_params
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# All values must coerce before going through validation,
|
44
|
+
# raise exception to avoid validation
|
45
|
+
|
46
|
+
# If the param value has already been coerced from digging into child Hash
|
47
|
+
# use that value instead, so we don't loose the previous coerced values
|
48
|
+
|
49
|
+
coerced_value = Coercion.coerce! \
|
50
|
+
(@coerced_params[param.name].nil? ? param.value : @coerced_params[param.name]), param.type, param.options
|
51
|
+
|
52
|
+
# Only add to coerced values if not nil
|
53
|
+
if !coerced_value.nil?
|
54
|
+
@coerced_params[param.name] = coerced_value
|
55
|
+
end
|
56
|
+
|
57
|
+
@errors.add \
|
58
|
+
param.name, Validation.validate(param.value, param.options)
|
59
|
+
|
60
|
+
rescue Compel::ParamTypeError => exception
|
61
|
+
@errors.add(param.name, exception.message)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def param(name, type, options = {}, &block)
|
69
|
+
@conditions[name] = \
|
70
|
+
Param.new(name, type, @params[name], options, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
def serialize
|
74
|
+
coerced_params.tap do |hash|
|
75
|
+
if !valid?
|
76
|
+
hash[:errors] = serialized_errors
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def valid?
|
82
|
+
@errors.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
def serialized_errors
|
86
|
+
@errors.to_hash
|
87
|
+
end
|
88
|
+
|
89
|
+
def raise?
|
90
|
+
if !valid?
|
91
|
+
exception = InvalidParamsError.new
|
92
|
+
exception.params = coerced_params
|
93
|
+
exception.errors = serialized_errors
|
94
|
+
|
95
|
+
raise exception, 'params are invalid'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Compel
|
2
|
+
|
3
|
+
class Errors
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@errors = Hashie::Mash.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(key, error)
|
10
|
+
if error.is_a?(Compel::Errors)
|
11
|
+
if error.empty?
|
12
|
+
return
|
13
|
+
end
|
14
|
+
|
15
|
+
if @errors[key].nil?
|
16
|
+
@errors[key] = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
@errors[key].merge!(error.to_hash)
|
20
|
+
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
if !error.is_a?(Array)
|
25
|
+
error = [error]
|
26
|
+
end
|
27
|
+
|
28
|
+
if error.empty?
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
if @errors[key].nil?
|
33
|
+
@errors[key] = []
|
34
|
+
end
|
35
|
+
|
36
|
+
@errors[key].concat(error)
|
37
|
+
end
|
38
|
+
|
39
|
+
def length
|
40
|
+
@errors.keys.length
|
41
|
+
end
|
42
|
+
|
43
|
+
def empty?
|
44
|
+
length == 0
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_hash
|
48
|
+
@errors
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/lib/compel/param.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Compel
|
2
|
+
|
3
|
+
class Param
|
4
|
+
|
5
|
+
attr_reader :name,
|
6
|
+
:type,
|
7
|
+
:options,
|
8
|
+
:conditions
|
9
|
+
|
10
|
+
def initialize(name, type, value, options = {}, &conditions)
|
11
|
+
@name = name
|
12
|
+
@type = type
|
13
|
+
@value = value
|
14
|
+
@options = options
|
15
|
+
@conditions = conditions
|
16
|
+
end
|
17
|
+
|
18
|
+
def value
|
19
|
+
default_value = if options[:default].is_a?(Proc)
|
20
|
+
options[:default].call
|
21
|
+
else
|
22
|
+
options[:default]
|
23
|
+
end
|
24
|
+
|
25
|
+
@value || default_value
|
26
|
+
end
|
27
|
+
|
28
|
+
def value=(value)
|
29
|
+
@value = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def required?
|
33
|
+
!!@options[:required]
|
34
|
+
end
|
35
|
+
|
36
|
+
def hash?
|
37
|
+
@type == Hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def conditions?
|
41
|
+
!!@conditions
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|