errapi 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ module Errapi
2
+
3
+ class SingleValidator
4
+
5
+ def self.configure *args, &block
6
+
7
+ options = args.last.kind_of?(Hash) ? args.pop : {}
8
+ config = options[:config] || Errapi.config
9
+ config = Errapi.config config if config.kind_of? Symbol
10
+
11
+ @errapi_validator = ObjectValidator.new config, options, &block
12
+ end
13
+
14
+ def self.validate *args, &block
15
+ raise "Validator has not yet been configured. You must call #configure before calling #validate." unless @errapi_validator
16
+ @errapi_validator.validate *args, &block
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module Errapi::Utils
2
+
3
+ def self.camelize string, uppercase_first_letter = false
4
+ parts = string.split '_'
5
+ return string if parts.length < 2
6
+ parts[0] + parts[1, parts.length - 1].collect(&:capitalize).join
7
+ end
8
+
9
+ def self.underscore string
10
+ string.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr("-", "_").downcase
11
+ end
12
+ end
@@ -0,0 +1,49 @@
1
+ require 'ostruct'
2
+
3
+ class Errapi::ValidationContext
4
+ attr_reader :data
5
+ attr_reader :errors
6
+ attr_reader :config
7
+
8
+ def initialize options = {}
9
+ @errors = []
10
+ @data = OpenStruct.new options[:data] || {}
11
+ @config = options[:config]
12
+ end
13
+
14
+ def add_error options = {}, &block
15
+
16
+ error = options.kind_of?(Errapi::ValidationError) ? options : @config.new_error(options)
17
+ yield error if block_given?
18
+ @config.build_error error, self
19
+
20
+ @errors << error
21
+ self
22
+ end
23
+
24
+ def errors? criteria = {}, &block
25
+ return !@errors.empty? if criteria.empty? && !block
26
+ block ? @errors.any?{ |err| err.matches?(criteria) && block.call(err) } : @errors.any?{ |err| err.matches?(criteria) }
27
+ end
28
+
29
+ def valid?
30
+ !errors?
31
+ end
32
+
33
+ def clear
34
+ @errors.clear
35
+ @data = OpenStruct.new
36
+ end
37
+
38
+ # TODO: add custom serialization options
39
+ def serialize
40
+ # TODO: add hook for plugins to serialize context
41
+ { errors: [] }.tap do |h|
42
+ @errors.each do |error|
43
+ serialized = {}
44
+ @config.serialize_error error, serialized
45
+ h[:errors] << serialized
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,43 @@
1
+ require 'ostruct'
2
+
3
+ class Errapi::ValidationError
4
+ attr_accessor :reason
5
+ attr_accessor :check_value
6
+ attr_accessor :checked_value
7
+ attr_accessor :validation
8
+ attr_accessor :constraints
9
+ attr_accessor :location
10
+
11
+ def initialize options = {}
12
+ ATTRIBUTES.each do |attr|
13
+ instance_variable_set "@#{attr}", options[attr] if options.key? attr
14
+ end
15
+ end
16
+
17
+ def matches? criteria = {}
18
+ unknown_criteria = criteria.keys - ATTRIBUTES
19
+ raise "Unknown error attributes: #{unknown_criteria.join(', ')}." if unknown_criteria.any?
20
+ ATTRIBUTES.all?{ |attr| criterion_matches? criteria, attr }
21
+ end
22
+
23
+ private
24
+
25
+ ATTRIBUTES = %i(reason location check_value checked_value validation)
26
+
27
+ def criterion_matches? criteria, attr
28
+ return true unless criteria.key? attr
29
+
30
+ value = send attr
31
+ criterion = criteria[attr]
32
+
33
+ if criterion.kind_of? Regexp
34
+ !!criterion.match(value.to_s)
35
+ elsif criterion.kind_of? String
36
+ criterion == value.to_s
37
+ elsif criterion.respond_to? :===
38
+ criterion === value
39
+ else
40
+ criterion == value
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ module Errapi::Validations
2
+ module Clusivity
3
+ private
4
+
5
+ DELIMITER_METHOD_CHECKS = %i(include? call to_sym).freeze
6
+
7
+ def check_delimiter! option_desc
8
+ unless @delimiter.respond_to?(:include?) || callable_option_value?(@delimiter)
9
+ raise callable_option_type_error option_desc, "an object with the #include? method", @delimiter
10
+ end
11
+ end
12
+
13
+ def members option_desc, options = {}
14
+ enumerable = actual_option_value @delimiter, options
15
+
16
+ unless enumerable.respond_to? :include?
17
+ raise callable_option_value_error option_desc, "an object with the #include? method", @delimiter
18
+ end
19
+
20
+ enumerable
21
+ end
22
+
23
+ def include? members, value
24
+ members.send inclusion_method(members), value
25
+ end
26
+
27
+ # From rails/activemodel/lib/active_model/validations/clusivity.rb:
28
+ # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
29
+ # possible values in the range for equality, which is slower but more accurate.
30
+ # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
31
+ # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges.
32
+ def inclusion_method enumerable
33
+ if enumerable.is_a? Range
34
+ case enumerable.first
35
+ when Numeric, Time, DateTime
36
+ :cover?
37
+ else
38
+ :include?
39
+ end
40
+ else
41
+ :include?
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ require File.join(File.dirname(__FILE__), 'clusivity.rb')
2
+
3
+ module Errapi::Validations
4
+ class Exclusion < Base
5
+ include Clusivity
6
+
7
+ def initialize options = {}
8
+ unless key = exactly_one_option?(OPTIONS, options)
9
+ raise ArgumentError, "Either :from or :in or :within must be supplied (but only one of them)."
10
+ end
11
+
12
+ @delimiter = options[key]
13
+ check_delimiter! OPTIONS_DESCRIPTION
14
+ end
15
+
16
+ def validate value, context, options = {}
17
+ excluded_values = members OPTIONS_DESCRIPTION, options
18
+ if include? excluded_values, value
19
+ context.add_error reason: :excluded, check_value: excluded_values
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ OPTIONS = %i(from in within)
26
+ OPTIONS_DESCRIPTION = ":from (or :in or :within)"
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ module Errapi::Validations
2
+ class Format < Base
3
+
4
+ def initialize options = {}
5
+ unless key = exactly_one_option?(OPTIONS, options)
6
+ raise ArgumentError, "Either :with or :without must be supplied (but not both)."
7
+ end
8
+
9
+ @format = options[key]
10
+ @should_match = key == :with
11
+
12
+ unless @format.kind_of?(Regexp) or callable_option_value?(@format)
13
+ raise callable_option_type_error ":with (or :without)", "a regular expression", @format
14
+ end
15
+ end
16
+
17
+ def validate value, context, options = {}
18
+
19
+ regexp = actual_option_value @format, options
20
+ unless regexp.kind_of? Regexp
21
+ raise callable_option_value_error ":with (or :without)", "a regular expression", regexp
22
+ end
23
+
24
+ if !regexp.match(value.to_s) == @should_match
25
+ context.add_error reason: :invalid_format, check_value: regexp
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ OPTIONS = %i(with without)
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ require File.join(File.dirname(__FILE__), 'clusivity.rb')
2
+
3
+ module Errapi::Validations
4
+ class Inclusion < Base
5
+ include Clusivity
6
+
7
+ def initialize options = {}
8
+ unless key = exactly_one_option?(OPTIONS, options)
9
+ raise ArgumentError, "Either :in or :within must be supplied (but not both)."
10
+ end
11
+
12
+ @delimiter = options[key]
13
+ check_delimiter! OPTIONS_DESCRIPTION
14
+ end
15
+
16
+ def validate value, context, options = {}
17
+ allowed_values = members OPTIONS_DESCRIPTION, options
18
+ unless include? allowed_values, value
19
+ context.add_error reason: :not_included, check_value: allowed_values
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ OPTIONS = %i(in within)
26
+ OPTIONS_DESCRIPTION = ":in (or :within)"
27
+ end
28
+ end
@@ -0,0 +1,66 @@
1
+ module Errapi::Validations
2
+ class Length < Base
3
+ CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
4
+ REASONS = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
5
+
6
+ def initialize options = {}
7
+
8
+ constraints = options.select{ |k,v| OPTIONS.include? k }
9
+ if constraints.empty?
10
+ raise ArgumentError, "The :is, :minimum/:maximum or :within options must be supplied (but only :minimum and :maximum can be used together)."
11
+ elsif options.key?(:is) && constraints.length != 1
12
+ raise ArgumentError, "The :is option cannot be combined with :minimum, :maximum or :within."
13
+ elsif options.key?(:is)
14
+ check_numeric! options[:is]
15
+ elsif options.key?(:within)
16
+ if options.key?(:minimum) || options.key?(:maximum)
17
+ raise ArgumentError, "The :within option cannot be combined with :minimum or :maximum."
18
+ else
19
+ check_range! options[:within]
20
+ end
21
+ else
22
+ check_numeric! options[:minimum] if options.key? :minimum
23
+ check_numeric! options[:maximum] if options.key? :maximum
24
+ end
25
+
26
+ @constraints = actual_constraints constraints
27
+ end
28
+
29
+ def validate value, context, options = {}
30
+ return unless value.respond_to? :length
31
+ actual_length = value.length
32
+
33
+ CHECKS.each_pair do |key,check|
34
+ next unless check_value = @constraints[key]
35
+ next if actual_length.send check, check_value
36
+ context.add_error reason: REASONS[key], check_value: check_value, checked_value: actual_length, constraints: @constraints
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ OPTIONS = %i(is minimum maximum within)
43
+
44
+ def actual_constraints options = {}
45
+ if range = options[:within]
46
+ { minimum: range.min, maximum: range.max }
47
+ else
48
+ options
49
+ end
50
+ end
51
+
52
+ def check_numeric! bound
53
+ unless bound.kind_of? Numeric
54
+ raise ArgumentError, "The :is, :minimum or :maximum option must be a numeric value, but a #{bound.class.name} was given."
55
+ end
56
+ end
57
+
58
+ def check_range! range
59
+ if !range.kind_of?(Range)
60
+ raise ArgumentError, "The :within option must be a numeric range, but a #{range.class.name} was given."
61
+ elsif !(t = range.first).kind_of?(Numeric)
62
+ raise ArgumentError, "The :within option must be a numeric range, but a #{t.class.name} range was given."
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,39 @@
1
+ module Errapi::Validations
2
+ class Presence < Factory
3
+ class Implementation < Base
4
+
5
+ def validate value, context, options = {}
6
+ if reason = check(value, options.fetch(:value_set, true))
7
+ context.add_error reason: reason
8
+ end
9
+ end
10
+
11
+ private
12
+
13
+ BLANK_REGEXP = /\A[[:space:]]*\z/
14
+
15
+ def check value, value_set
16
+ # TODO: allow customization (e.g. values that are not required, booleans, etc)
17
+ if !value_set
18
+ :missing
19
+ elsif value.nil?
20
+ :null
21
+ elsif value.respond_to?(:empty?) && value.empty?
22
+ :empty
23
+ elsif value_blank? value
24
+ :blank
25
+ end
26
+ end
27
+
28
+ def value_blank? value
29
+ if value.respond_to? :blank?
30
+ value.blank?
31
+ elsif value.kind_of? String
32
+ BLANK_REGEXP === value
33
+ else
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,10 @@
1
+ module Errapi::Validations
2
+ class Trim < Base
3
+
4
+ def validate value, context, options = {}
5
+ if value.kind_of?(String) && /(?:\A\s|\s\Z)/.match(value)
6
+ context.add_error reason: :untrimmed
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,54 @@
1
+ module Errapi::Validations
2
+ class Type < Base
3
+
4
+ def initialize options = {}
5
+ unless key = exactly_one_option?(OPTIONS, options)
6
+ raise ArgumentError, "One option among :instance_of, :kind_of, :is_a or :is_an must be supplied (but only one)."
7
+ end
8
+
9
+ if key == :instance_of
10
+ @instance_of = check_types! options[key]
11
+ raise ArgumentError, "Type aliases cannot be used with the :instance_of option. Use :kind_of, :is_a or :is_an." if options[key].kind_of? Symbol
12
+ else
13
+ @kind_of = check_types! options[key]
14
+ end
15
+ end
16
+
17
+ def validate value, context, options = {}
18
+ if @instance_of && @instance_of.none?{ |type| value.instance_of? type }
19
+ context.add_error reason: :wrong_type, check_value: @instance_of, checked_value: value.class
20
+ elsif @kind_of && @kind_of.none?{ |type| value.kind_of? type }
21
+ context.add_error reason: :wrong_type, check_value: @kind_of, checked_value: value.class
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def check_types! types
28
+ if !types.kind_of?(Array)
29
+ types = [ types ]
30
+ elsif types.empty?
31
+ raise ArgumentError, "At least one class or module is required, but an empty array was given."
32
+ end
33
+
34
+ types.each do |type|
35
+ unless TYPE_ALIASES.key?(type) || type.class == Class || type.class == Module
36
+ raise ArgumentError, "A class or module (or an array of classes or modules, or a type alias) is required, but a #{type.class} was given."
37
+ end
38
+ end
39
+
40
+ types.collect{ |type| TYPE_ALIASES[type] || type }.flatten.uniq
41
+ end
42
+
43
+ OPTIONS = %i(instance_of kind_of is_a is_an)
44
+ TYPE_ALIASES = {
45
+ string: [ String ],
46
+ number: [ Numeric ],
47
+ integer: [ Integer ],
48
+ boolean: [ TrueClass, FalseClass ],
49
+ object: [ Hash ],
50
+ array: [ Array ],
51
+ null: [ NilClass ]
52
+ }
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ module Errapi::Validations
2
+
3
+ class Base
4
+
5
+ def initialize options = {}
6
+ end
7
+
8
+ def actual_option_value supplied_value, options
9
+ if supplied_value.respond_to? :call
10
+ supplied_value.call options[:source]
11
+ elsif supplied_value.respond_to? :to_sym
12
+ unless options[:source].respond_to? supplied_value
13
+ raise ArgumentError, "The validation source (#{options[:source].class.name}) does not respond to :#{supplied_value}."
14
+ else
15
+ options[:source].send supplied_value
16
+ end
17
+ else
18
+ supplied_value
19
+ end
20
+ end
21
+
22
+ def callable_option_value? supplied_value
23
+ supplied_value.respond_to?(:call) || supplied_value.respond_to?(:to_sym)
24
+ end
25
+
26
+ def exactly_one_option? keys, options
27
+ found_keys = options.keys.select{ |k| keys.include? k }
28
+ found_keys.length == 1 ? found_keys.first : false
29
+ end
30
+
31
+ def callable_option_type_error key_desc, value_desc, supplied_value
32
+ ArgumentError.new "The #{key_desc} option must be #{value_desc}, a proc, a lambda or a symbol, but a #{supplied_value.class.name} was given."
33
+ end
34
+
35
+ def callable_option_value_error key_desc, type_desc, supplied_value
36
+ ArgumentError.new "The call supplied to #{key_desc} must return #{type_desc}, but a #{supplied_value.class.name} was returned."
37
+ end
38
+ end
39
+
40
+ class Factory
41
+
42
+ def config= config
43
+ raise "A configuration has already been set for this factory." if @config
44
+ @config = config
45
+ end
46
+
47
+ def validation options = {}
48
+ self.class.const_get('Implementation').new options
49
+ end
50
+
51
+ def to_s
52
+ Errapi::Utils.underscore self.class.name.sub(/.*::/, '')
53
+ end
54
+ end
55
+ end
56
+
57
+ Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib }
@@ -0,0 +1,21 @@
1
+ module Errapi
2
+
3
+ class ValidatorProxy
4
+ instance_methods.each{ |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
5
+
6
+ def initialize object, validator
7
+ @object = object
8
+ @validator = validator
9
+ end
10
+
11
+ def validate context, options = {}
12
+ @validator.validate @object, context, options
13
+ end
14
+
15
+ protected
16
+
17
+ def method_missing name, *args, &block
18
+ @validator.send name, *args, &block
19
+ end
20
+ end
21
+ end
data/lib/errapi.rb CHANGED
@@ -1,3 +1,47 @@
1
1
  module Errapi
2
- VERSION = '0.1.0'
2
+ VERSION = '0.1.2'
3
+ end
4
+
5
+ Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib }
6
+
7
+ module Errapi
8
+
9
+ def self.configure name = nil, &block
10
+
11
+ init_configs
12
+ name ||= :default
13
+
14
+ if @configs[name]
15
+ @configs[name].configure &block
16
+ else
17
+ @configs[name] = Configuration.new &block
18
+ end
19
+ end
20
+
21
+ def self.config name = nil
22
+ init_configs[name || :default]
23
+ end
24
+
25
+ private
26
+
27
+ def self.init_configs
28
+ @configs ? @configs : @configs = { default: default_config }
29
+ end
30
+
31
+ def self.default_config
32
+ Configuration.new.tap do |config|
33
+ config.plugin Errapi::Plugins::I18nMessages
34
+ config.plugin Errapi::Plugins::Reason
35
+ config.plugin Errapi::Plugins::Location
36
+ config.validation_factory Errapi::Validations::Exclusion
37
+ config.validation_factory Errapi::Validations::Format
38
+ config.validation_factory Errapi::Validations::Inclusion
39
+ config.validation_factory Errapi::Validations::Length
40
+ config.validation_factory Errapi::Validations::Presence.new
41
+ config.validation_factory Errapi::Validations::Trim
42
+ config.validation_factory Errapi::Validations::Type
43
+ config.register_condition Errapi::Condition::SimpleCheck
44
+ config.register_condition Errapi::Condition::ErrorCheck
45
+ end
46
+ end
3
47
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: errapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Oulevay
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-09 00:00:00.000000000 Z
11
+ date: 2015-01-26 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: i18n
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.7.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.7.0
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rake
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +52,20 @@ dependencies:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
54
  version: '3.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec-collection_matchers
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
41
69
  - !ruby/object:Gem::Dependency
42
70
  name: jeweler
43
71
  requirement: !ruby/object:Gem::Requirement
@@ -72,28 +100,28 @@ dependencies:
72
100
  requirements:
73
101
  - - "~>"
74
102
  - !ruby/object:Gem::Version
75
- version: '0.9'
103
+ version: 0.9.1
76
104
  type: :development
77
105
  prerelease: false
78
106
  version_requirements: !ruby/object:Gem::Requirement
79
107
  requirements:
80
108
  - - "~>"
81
109
  - !ruby/object:Gem::Version
82
- version: '0.9'
110
+ version: 0.9.1
83
111
  - !ruby/object:Gem::Dependency
84
112
  name: coveralls
85
113
  requirement: !ruby/object:Gem::Requirement
86
114
  requirements:
87
115
  - - "~>"
88
116
  - !ruby/object:Gem::Version
89
- version: '0.7'
117
+ version: 0.7.3
90
118
  type: :development
91
119
  prerelease: false
92
120
  version_requirements: !ruby/object:Gem::Requirement
93
121
  requirements:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
- version: '0.7'
124
+ version: 0.7.3
97
125
  description: Utilities to validate data and serialize errors.
98
126
  email: git@alphahydrae.com
99
127
  executables: []
@@ -107,6 +135,34 @@ files:
107
135
  - README.md
108
136
  - VERSION
109
137
  - lib/errapi.rb
138
+ - lib/errapi/condition.rb
139
+ - lib/errapi/configuration.rb
140
+ - lib/errapi/errors.rb
141
+ - lib/errapi/location_builders.rb
142
+ - lib/errapi/locations.rb
143
+ - lib/errapi/locations/dotted.rb
144
+ - lib/errapi/locations/json.rb
145
+ - lib/errapi/locations/none.rb
146
+ - lib/errapi/model.rb
147
+ - lib/errapi/object_validator.rb
148
+ - lib/errapi/plugins.rb
149
+ - lib/errapi/plugins/i18n_messages.rb
150
+ - lib/errapi/plugins/location.rb
151
+ - lib/errapi/plugins/reason.rb
152
+ - lib/errapi/single_validator.rb
153
+ - lib/errapi/utils.rb
154
+ - lib/errapi/validation_context.rb
155
+ - lib/errapi/validation_error.rb
156
+ - lib/errapi/validations.rb
157
+ - lib/errapi/validations/clusivity.rb
158
+ - lib/errapi/validations/exclusion.rb
159
+ - lib/errapi/validations/format.rb
160
+ - lib/errapi/validations/inclusion.rb
161
+ - lib/errapi/validations/length.rb
162
+ - lib/errapi/validations/presence.rb
163
+ - lib/errapi/validations/trim.rb
164
+ - lib/errapi/validations/type.rb
165
+ - lib/errapi/validator_proxy.rb
110
166
  homepage: http://github.com/AlphaHydrae/errapi
111
167
  licenses:
112
168
  - MIT
@@ -127,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
183
  version: '0'
128
184
  requirements: []
129
185
  rubyforge_project:
130
- rubygems_version: 2.2.2
186
+ rubygems_version: 2.4.3
131
187
  signing_key:
132
188
  specification_version: 4
133
189
  summary: An extensible API-oriented validation library.