compel 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -20
  3. data/lib/compel/builder/array.rb +31 -0
  4. data/lib/compel/builder/boolean.rb +7 -0
  5. data/lib/compel/builder/common.rb +2 -2
  6. data/lib/compel/builder/common_value.rb +4 -4
  7. data/lib/compel/builder/hash.rb +3 -1
  8. data/lib/compel/builder/methods.rb +5 -0
  9. data/lib/compel/builder/schema.rb +4 -0
  10. data/lib/compel/coercion/coercion.rb +47 -0
  11. data/lib/compel/coercion/nil_result.rb +13 -0
  12. data/lib/compel/coercion/result.rb +37 -0
  13. data/lib/compel/coercion/types/array.rb +15 -0
  14. data/lib/compel/coercion/{boolean.rb → types/boolean.rb} +1 -3
  15. data/lib/compel/coercion/types/date.rb +36 -0
  16. data/lib/compel/coercion/types/datetime.rb +36 -0
  17. data/lib/compel/coercion/{float.rb → types/float.rb} +2 -2
  18. data/lib/compel/coercion/{hash.rb → types/hash.rb} +2 -2
  19. data/lib/compel/coercion/{integer.rb → types/integer.rb} +2 -2
  20. data/lib/compel/coercion/{json.rb → types/json.rb} +2 -2
  21. data/lib/compel/coercion/{regexp.rb → types/regexp.rb} +2 -4
  22. data/lib/compel/coercion/{string.rb → types/string.rb} +3 -5
  23. data/lib/compel/coercion/types/time.rb +36 -0
  24. data/lib/compel/coercion/types/type.rb +29 -0
  25. data/lib/compel/contract.rb +14 -42
  26. data/lib/compel/errors.rb +13 -17
  27. data/lib/compel/exceptions/invalid_object_error.rb +9 -0
  28. data/lib/compel/result.rb +30 -0
  29. data/lib/compel/validation.rb +6 -4
  30. data/lib/compel/validators/array_validator.rb +107 -0
  31. data/lib/compel/validators/base.rb +11 -1
  32. data/lib/compel/validators/hash_validator.rb +77 -19
  33. data/lib/compel/validators/type_validator.rb +42 -11
  34. data/lib/compel/version.rb +1 -1
  35. data/lib/compel.rb +6 -15
  36. data/spec/compel/builder_spec.rb +354 -144
  37. data/spec/compel/coercion_spec.rb +16 -1
  38. data/spec/compel/compel_spec.rb +315 -302
  39. data/spec/compel/validation_spec.rb +97 -115
  40. data/spec/support/sinatra_app.rb +2 -2
  41. metadata +21 -15
  42. data/lib/compel/coercion/date.rb +0 -30
  43. data/lib/compel/coercion/datetime.rb +0 -30
  44. data/lib/compel/coercion/time.rb +0 -30
  45. data/lib/compel/coercion/type.rb +0 -17
  46. data/lib/compel/coercion.rb +0 -31
  47. data/lib/compel/exceptions/invalid_hash_error.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ca24b865ca3552e7fb66b3b5fa3cb54522c69f6
4
- data.tar.gz: 06342b5d60518ad7f05fc4fcd380ad6dce9b1017
3
+ metadata.gz: 258250ae836758b145cc384b581c628a696260b8
4
+ data.tar.gz: 1298e32dee4358430f051fcb378785e487b4123a
5
5
  SHA512:
6
- metadata.gz: eadd3a9cf227017eb8ed57431ba6b6b687ef2e76cee95de5dc89c9691201ff8cb2ce6afb96e7061314f07b2bc3fda5d0660bb3345e2784086f9e28d31ad90ad8
7
- data.tar.gz: d2adaa09f03406c98f87bdc0b816b30a3983a65bf306bab7ef8fc7cd6873302d024173d3cf3ca3122c0595d903962fa154a763e66e31c8f5e7c434927e214b08
6
+ metadata.gz: 69e0028f7dc235d8a8cf5e8a2bd3af74c59c11c7c37cf933a041447360444774e0590d38bad735ee8cf8463f602f48d38981394c7f109519ef604effc0da9d60
7
+ data.tar.gz: a580c0ff252daecc215142f323ca35f4d874cd62893da793ff5175f7f4c1d1491ba1272608043681dbed2ee87c9db17077407718c78b7556ba076ef5223404c9
data/README.md CHANGED
@@ -2,7 +2,7 @@ Compel
2
2
  ==========================
3
3
  ![](https://travis-ci.org/joaquimadraz/compel.svg)
4
4
  [![Code Climate](https://codeclimate.com/github/joaquimadraz/compel/badges/gpa.svg)](https://codeclimate.com/github/joaquimadraz/compel)
5
- [![Test Coverage](https://codeclimate.com/github/joaquimadraz/compel/badges/coverage.svg)](https://codeclimate.com/github/joaquimadraz/compel)
5
+ [![Test Coverage](https://codeclimate.com/github/joaquimadraz/compel/badges/coverage.svg)](https://codeclimate.com/github/joaquimadraz/compel/coverage)
6
6
 
7
7
  Ruby Hash Coercion and Validation
8
8
 
@@ -12,7 +12,7 @@ The motivation was to create an integration for [RestMyCase](https://github.com/
12
12
 
13
13
  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. The schema builder is based on [Joi](https://github.com/hapijs/joi).
14
14
 
15
- ### Example
15
+ ### Usage
16
16
 
17
17
  ```ruby
18
18
  object = {
@@ -67,23 +67,91 @@ There are 3 ways to run validations:
67
67
  Method | Behaviour
68
68
  ------------- | -------------
69
69
  `#run` | Validates and returns an Hash with coerced params plus a `:errors` key with a _Rails like_ Hash of errors if any.
70
- `#run!` | Validates and raises `Compel::InvalidHashError` exception with the coerced params and errors.
70
+ `#run!` | Validates and raises `Compel::InvalidObjectError` exception with the coerced params and errors.
71
71
  `#run?` | Validates and returns true or false.
72
+ `schema#validate` | Chech below
72
73
 
73
- ### Types
74
-
75
- - `#integer`
76
- - `#float`
77
- - `#string`
78
- - `#json`
79
- - ex: `"{\"a\":1,\"b\":2,\"c\":3}"`
80
- - `#hash`
81
- - ex: `{ a: 1, b: 2, c: 3 }`
82
- - `#date`
83
- - `#time`
84
- - `#datetime`
85
- - `#boolean`,
86
- - ex: `1`/`0`, `true`/`false`, `t`/`f`, `yes`/`no`, `y`/`n`
74
+ ==========================
75
+
76
+ ### Schema Builder API
77
+ - `Compel#array` *
78
+ - `is(``array``)`
79
+ - `#items(``schema``)`
80
+ ```ruby
81
+ * ex: [1, 2, 3], [{ a: 1, b: 2}, { a: 3, b: 4 }]
82
+ ```
83
+
84
+ - `Compel#hash` *
85
+ - `#keys(``schema_hash``)`
86
+ ```ruby
87
+ * ex: { a: 1, b: 2, c: 3 }
88
+ ```
89
+
90
+ - `Compel.date` *
91
+ - `#format(``ruby_date_format``)`
92
+ - `iso8601`, set format to: `%Y-%m-%d`
93
+
94
+ - `Compel.datetime & Compel.time` *
95
+ - `#format(``ruby_date_format``)`
96
+ - `#iso8601`, set format to: `%FT%T`
97
+
98
+ - `Compel#string` **
99
+ - `#format(``regexp``)`
100
+ - `#min_length(``integer``)`
101
+ - `#max_length(``integer``)`
102
+
103
+ - `Compel#json` *
104
+ ```ruby
105
+ * ex: "{\"a\":1,\"b\":2,\"c\":3}"
106
+ ```
107
+
108
+ - `Compel#boolean` *
109
+ ```ruby
110
+ * ex: 1/0, true/false, 't'/'f', 'yes'/'no', 'y'/'n'
111
+ ```
112
+
113
+ - `Compel#integer` **
114
+
115
+ - `Compel#float` **
116
+
117
+ (\*) **Common methods**
118
+ - `required`
119
+ - `default(``value``)`
120
+
121
+ (\*\*) **Common value methods**
122
+ - `is(``value``)`
123
+ - `in(``array``)`
124
+ - `length(``integer``)`
125
+ - `min(``value``)`
126
+ - `max(``value``)`
127
+
128
+
129
+ ### Schema Validate
130
+
131
+ For straight forward validations, you can call `#validate` on schema and it will return a `Compel::Result` object.
132
+
133
+ ```ruby
134
+ result = Compel.string
135
+ .format(/^\d{4}-\d{3}$/)
136
+ .required
137
+ .validate('1234')
138
+
139
+ puts result.errors
140
+ # => ["must match format ^\\d{4}-\\d{3}$"]
141
+ ```
142
+
143
+ #### Compel Result
144
+
145
+ Simple object that encapsulates a validation result.
146
+
147
+ Method | Behaviour
148
+ ------------- | -------------
149
+ `#value` | the coerced value or the input value is invalid
150
+ `#errors` | array of errors is any.
151
+ `#valid?` | `true` or `false`
152
+ `#raise?` | raises a `Compel::InvalidObjectError` if invalid, otherwise returns `#value`
153
+
154
+ ==========================
87
155
 
88
156
  ### Sinatra Integration
89
157
 
@@ -107,9 +175,9 @@ class App < Sinatra::Base
107
175
 
108
176
  end
109
177
 
110
- error Compel::InvalidHashError do |exception|
178
+ error Compel::InvalidObjectError do |exception|
111
179
  status 400
112
- { errors: exception.errors }.to_json
180
+ { errors: exception.object[:errors] }.to_json
113
181
  end
114
182
 
115
183
  configure :development do
@@ -143,7 +211,7 @@ And then execute:
143
211
 
144
212
  ### TODO
145
213
 
146
- - Write more Documentation (check specs for now ;)
214
+ - Write Advanced Documentation (check specs for now ;)
147
215
  - Rails integration
148
216
  - [RestMyCase](https://github.com/goncalvesjoao/rest_my_case) integration
149
217
 
@@ -0,0 +1,31 @@
1
+ module Compel
2
+ module Builder
3
+
4
+ class Array < Schema
5
+
6
+ def initialize
7
+ super(Coercion::Array)
8
+ end
9
+
10
+ def items(schema)
11
+ if !schema.is_a?(Schema)
12
+ raise Compel::TypeError, '#items must be a valid Schema'
13
+ end
14
+
15
+ options[:items] = schema
16
+ self
17
+ end
18
+
19
+ def is(value)
20
+ options[:is] = Coercion.coerce!(value, ::Array)
21
+ self
22
+ end
23
+
24
+ def validate(object)
25
+ Contract.new(object, self).validate
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+ end
@@ -7,6 +7,13 @@ module Compel
7
7
  super(Coercion::Boolean)
8
8
  end
9
9
 
10
+ def is(value)
11
+ Coercion.coerce!(value, Coercion::Boolean)
12
+ options[:is] = value
13
+
14
+ self
15
+ end
16
+
10
17
  end
11
18
 
12
19
  end
@@ -3,8 +3,8 @@ module Compel
3
3
 
4
4
  module Common
5
5
 
6
- def is(value)
7
- options[:is] = value
6
+ def length(value)
7
+ options[:length] = Coercion.coerce!(value, ::Integer)
8
8
  self
9
9
  end
10
10
 
@@ -3,13 +3,13 @@ module Compel
3
3
 
4
4
  module CommonValue
5
5
 
6
- def in(value)
7
- options[:in] = value
6
+ def is(value)
7
+ options[:is] = value
8
8
  self
9
9
  end
10
10
 
11
- def length(value)
12
- options[:length] = Coercion.coerce!(value, ::Integer)
11
+ def in(value)
12
+ options[:in] = value
13
13
  self
14
14
  end
15
15
 
@@ -5,6 +5,8 @@ module Compel
5
5
 
6
6
  def initialize
7
7
  super(Coercion::Hash)
8
+
9
+ options[:keys] = {}
8
10
  end
9
11
 
10
12
  def keys(hash)
@@ -13,7 +15,7 @@ module Compel
13
15
  end
14
16
 
15
17
  def validate(object)
16
- Contract.new(object, self).validate.serialize
18
+ Contract.new(object, self).validate
17
19
  end
18
20
 
19
21
  end
@@ -10,6 +10,7 @@ require 'compel/builder/datetime'
10
10
  require 'compel/builder/time'
11
11
  require 'compel/builder/date'
12
12
  require 'compel/builder/boolean'
13
+ require 'compel/builder/array'
13
14
 
14
15
  module Compel
15
16
  module Builder
@@ -52,6 +53,10 @@ module Compel
52
53
  Builder::Boolean.new
53
54
  end
54
55
 
56
+ def array
57
+ Builder::Array.new
58
+ end
59
+
55
60
  extend self
56
61
 
57
62
  end
@@ -21,6 +21,10 @@ module Compel
21
21
  options[:default]
22
22
  end
23
23
 
24
+ def validate(object)
25
+ Contract.new(object, self).validate
26
+ end
27
+
24
28
  end
25
29
 
26
30
  end
@@ -0,0 +1,47 @@
1
+ require 'compel/coercion/types/type'
2
+ require 'compel/coercion/types/integer'
3
+ require 'compel/coercion/types/float'
4
+ require 'compel/coercion/types/string'
5
+ require 'compel/coercion/types/date'
6
+ require 'compel/coercion/types/time'
7
+ require 'compel/coercion/types/datetime'
8
+ require 'compel/coercion/types/hash'
9
+ require 'compel/coercion/types/json'
10
+ require 'compel/coercion/types/boolean'
11
+ require 'compel/coercion/types/regexp'
12
+ require 'compel/coercion/types/array'
13
+
14
+ require 'compel/coercion/result'
15
+ require 'compel/coercion/nil_result'
16
+
17
+ module Compel
18
+
19
+ module Coercion
20
+
21
+ def valid?(value, type, options = {})
22
+ coerce(value, type, options).valid?
23
+ end
24
+
25
+ def coerce!(value, type, options = {})
26
+ result = coerce(value, type, options)
27
+
28
+ unless result.valid?
29
+ raise Compel::TypeError, result.error
30
+ end
31
+
32
+ result.coerced
33
+ end
34
+
35
+ def coerce(value, type, options = {})
36
+ return NilResult.new if value.nil?
37
+
38
+ coercion_klass = Compel::Coercion.const_get("#{type}")
39
+
40
+ coercion_klass.new(value, options).coerce
41
+ end
42
+
43
+ extend self
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,13 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class NilResult < Result
5
+
6
+ def initialize
7
+ super(nil, nil, nil)
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Result
5
+
6
+ attr_reader :coerced,
7
+ :value,
8
+ :klass,
9
+ :error
10
+
11
+ def initialize(coerced, value, klass, coercion_error = nil)
12
+ @coerced = coerced
13
+ @value = value
14
+ @klass = klass
15
+ @error = coercion_error.nil? ? standard_error : coercion_error
16
+ end
17
+
18
+ def valid?
19
+ @error.nil?
20
+ end
21
+
22
+ private
23
+
24
+ def standard_error
25
+ if !klass.nil? && coerced.nil?
26
+ "'#{value}' is not a valid #{klass_final_type}"
27
+ end
28
+ end
29
+
30
+ def klass_final_type
31
+ "#{klass}".split('::')[-1]
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Array < Type
5
+
6
+ def coerce_value
7
+ if value.is_a?(::Array)
8
+ value
9
+ end
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+ end
@@ -3,7 +3,7 @@ module Compel
3
3
 
4
4
  class Boolean < Type
5
5
 
6
- def coerce!
6
+ def coerce_value
7
7
  if /(false|f|no|n|0)$/i === "#{value}"
8
8
  return false
9
9
  end
@@ -11,8 +11,6 @@ module Compel
11
11
  if /(true|t|yes|y|1)$/i === "#{value}"
12
12
  return true
13
13
  end
14
-
15
- fail
16
14
  end
17
15
 
18
16
  end
@@ -0,0 +1,36 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Date < Type
5
+
6
+ attr_reader :format
7
+
8
+ def coerce_value
9
+ @format = options[:format] || '%Y-%m-%d'
10
+
11
+ if value.is_a?(::Date)
12
+ @value = value.strftime(format)
13
+ end
14
+
15
+ coerced = ::Date.strptime(value, format)
16
+
17
+ if coerced.strftime(format) == value
18
+ return coerced
19
+ end
20
+
21
+ build_error_result
22
+
23
+ rescue
24
+ build_error_result
25
+ end
26
+
27
+ def build_error_result
28
+ custom_error = "'#{value}' is not a parsable date with format: #{format}"
29
+
30
+ Result.new(nil, value, self.class, custom_error)
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class DateTime < Type
5
+
6
+ attr_reader :format
7
+
8
+ def coerce_value
9
+ @format = options[:format] || '%FT%T'
10
+
11
+ if value.is_a?(::DateTime)
12
+ @value = value.strftime(format)
13
+ end
14
+
15
+ coerced = ::DateTime.strptime(value, format)
16
+
17
+ if coerced.strftime(format) == value
18
+ return coerced
19
+ end
20
+
21
+ build_error_result
22
+
23
+ rescue
24
+ build_error_result
25
+ end
26
+
27
+ def build_error_result
28
+ custom_error = "'#{value}' is not a parsable datetime with format: #{format}"
29
+
30
+ Result.new(nil, value, self.class, custom_error)
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -3,8 +3,8 @@ module Compel
3
3
 
4
4
  class Float < Type
5
5
 
6
- def coerce!
7
- Float(value)
6
+ def coerce_value
7
+ Float(value) rescue nil
8
8
  end
9
9
 
10
10
  end
@@ -3,8 +3,8 @@ module Compel
3
3
 
4
4
  class Hash < Type
5
5
 
6
- def coerce!
7
- Hashie::Mash.new(value).to_hash
6
+ def coerce_value
7
+ Hashie::Mash.new(value).to_hash rescue nil
8
8
  end
9
9
 
10
10
  end
@@ -3,8 +3,8 @@ module Compel
3
3
 
4
4
  class Integer < Type
5
5
 
6
- def coerce!
7
- Integer(value)
6
+ def coerce_value
7
+ Integer(value) rescue nil
8
8
  end
9
9
 
10
10
  end
@@ -3,8 +3,8 @@ module Compel
3
3
 
4
4
  class JSON < Type
5
5
 
6
- def coerce!
7
- ::JSON.parse(value)
6
+ def coerce_value
7
+ ::JSON.parse(value) rescue nil
8
8
  end
9
9
 
10
10
  end
@@ -3,12 +3,10 @@ module Compel
3
3
 
4
4
  class Regexp < Type
5
5
 
6
- def coerce!
6
+ def coerce_value
7
7
  if value.is_a?(::Regexp)
8
- return value
8
+ value
9
9
  end
10
-
11
- fail
12
10
  end
13
11
 
14
12
  end
@@ -3,12 +3,10 @@ module Compel
3
3
 
4
4
  class String < Type
5
5
 
6
- def coerce!
7
- if !value.is_a?(::String)
8
- fail
6
+ def coerce_value
7
+ if value.is_a?(::String)
8
+ value
9
9
  end
10
-
11
- value
12
10
  end
13
11
 
14
12
  end
@@ -0,0 +1,36 @@
1
+ module Compel
2
+ module Coercion
3
+
4
+ class Time < Type
5
+
6
+ attr_reader :format
7
+
8
+ def coerce_value
9
+ @format = options[:format] || '%FT%T'
10
+
11
+ if value.is_a?(::Time)
12
+ @value = value.strftime(format)
13
+ end
14
+
15
+ coerced = ::Time.strptime(value, format)
16
+
17
+ if coerced.strftime(format) == value
18
+ return coerced
19
+ end
20
+
21
+ build_error_result
22
+
23
+ rescue
24
+ build_error_result
25
+ end
26
+
27
+ def build_error_result
28
+ custom_error = "'#{value}' is not a parsable time with format: #{format}"
29
+
30
+ Result.new(nil, value, self.class, custom_error)
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,29 @@
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 coerce
15
+ result = coerce_value
16
+
17
+ # There are some special cases that
18
+ # we need to build a custom error
19
+ if result.is_a?(Result)
20
+ return result
21
+ end
22
+
23
+ Result.new(result, value, self.class)
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+ end