compel 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .rvmrc
7
+ .ruby-version
8
+ .ruby-gemset
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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
+ ![](https://travis-ci.org/joaquimadraz/compel.svg)
4
+ [![Code Climate](https://codeclimate.com/github/joaquimadraz/compel/badges/gpa.svg)](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
@@ -0,0 +1,5 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ task :default => :test
5
+ RSpec::Core::RakeTask.new(:test)
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,13 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Date < Type
5
+
6
+ def coerce
7
+ ::Date.parse(value)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class DateTime < Type
5
+
6
+ def coerce
7
+ ::DateTime.parse(value)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Float < Type
5
+
6
+ def coerce
7
+ Float(value)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Hash < Type
5
+
6
+ def coerce
7
+ Hashie::Mash.new(value).to_hash
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Integer < Type
5
+
6
+ def coerce
7
+ Integer(value)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class JSON < Type
5
+
6
+ def coerce
7
+ ::JSON.parse(value)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class String < Type
5
+
6
+ def coerce
7
+ if !value.is_a?(::String)
8
+ fail
9
+ end
10
+
11
+ value
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Time < Type
5
+
6
+ def coerce
7
+ ::Time.parse(value)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ 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, &param.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
@@ -0,0 +1,9 @@
1
+ module Compel
2
+
3
+ class InvalidParamsError < StandardError
4
+
5
+ attr_accessor :params, :errors
6
+
7
+ end
8
+
9
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ module Compel
2
+
3
+ class ParamTypeError < StandardError
4
+
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ module Compel
2
+
3
+ class ParamValidationError < StandardError
4
+
5
+ end
6
+
7
+ end