errapi 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ab7149f96d1f210d259a3ca6aae590426b61709
4
- data.tar.gz: d39bce05918e393860e7782e6facaaf7bbb6a4ed
3
+ metadata.gz: dcae888fc36cd422f1fda237dd5381aa10e776d1
4
+ data.tar.gz: 757ad4e09cea3666e7279d59d544ff2e99c6bab4
5
5
  SHA512:
6
- metadata.gz: c66e6b2430d880b18956f1e0d7c2a6adcd6a834926868c611c9db1f7afbc7eea94f0cf4cf4e4a83e7ff515e0a1a8fb7f2807d0a2b50236ac169d447df9708edf
7
- data.tar.gz: 0fb04d4565c105414eb21f519f1b74afa3434543ada8c3b6eb09ce486d31e8f93aa367009b820aee612697bcf6ae1b8cb2be92591ddfa8d4e165bb5f5c8479d3
6
+ metadata.gz: 5fe74be1e6ef55493f04accf713edfc0ce497c982dc84d68e92262bc7183f4d4076fd872d53f15b27a782e7f613054dc76fd8f26c9f2ef95842cfc9bb020c5a3
7
+ data.tar.gz: 3ff0b1fee2094513a11daf6ef7d8eb12155a23cadcb4d318843d1bb57dbae2e6c4cf570dac8e1015343a50acf928ccaa4c91da8ad3800b4a92075369124d9c2b
data/Gemfile CHANGED
@@ -1,10 +1,12 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  group :development do
4
+ gem 'i18n', '~> 0.7.0'
4
5
  gem 'rake', '~> 10.3'
5
6
  gem 'rspec', '~> 3.1'
7
+ gem 'rspec-collection_matchers', '~> 1.1'
6
8
  gem 'jeweler', '~> 2.0'
7
9
  gem 'rake-version', '~> 0.4'
8
- gem 'simplecov', '~> 0.9'
9
- gem 'coveralls', '~> 0.7', require: false
10
+ gem 'simplecov', '~> 0.9.1'
11
+ gem 'coveralls', '~> 0.7.3', require: false
10
12
  end
data/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  **An extensible API-oriented validation library.**
4
4
 
5
- [![Gem Version](https://badge.fury.io/rb/errapi.png)](http://badge.fury.io/rb/errapi)
5
+ [![Gem Version](https://badge.fury.io/rb/errapi.svg)](http://badge.fury.io/rb/errapi)
6
6
  [![Dependency Status](https://gemnasium.com/AlphaHydrae/errapi.png)](https://gemnasium.com/AlphaHydrae/errapi)
7
7
  [![Build Status](https://secure.travis-ci.org/AlphaHydrae/errapi.png)](http://travis-ci.org/AlphaHydrae/errapi)
8
- [![Coverage Status](https://coveralls.io/repos/AlphaHydrae/errapi/badge.png?branch=master)](https://coveralls.io/r/AlphaHydrae/errapi?branch=master)
8
+ [![Coverage Status](https://coveralls.io/repos/AlphaHydrae/errapi/badge.svg)](https://coveralls.io/r/AlphaHydrae/errapi?branch=master)
9
9
 
10
10
  ## Installation
11
11
 
@@ -17,6 +17,10 @@ gem 'errapi', '~> 0.1.0'
17
17
 
18
18
  Then run `bundle install`.
19
19
 
20
+ ### Requirements
21
+
22
+ * Ruby 2+
23
+
20
24
  ## Meta
21
25
 
22
26
  * **Author:** Simon Oulevay (Alpha Hydrae)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.1.2
@@ -0,0 +1,71 @@
1
+ class Errapi::Condition
2
+ ALLOWED_CONDITIONALS = %i(if unless).freeze
3
+
4
+ def self.conditionals
5
+ h = const_get('CONDITIONALS')
6
+ raise LoadError, "The CONDITIONALS constant in class #{self} is of the wrong type (#{h.class}). Either make it a Hash or override #{self}.conditionals to return a list of symbols." unless h.kind_of? Hash
7
+ h.keys
8
+ end
9
+
10
+ def initialize conditional, predicate, options = {}
11
+
12
+ @conditional = resolve_conditional conditional
13
+ raise ArgumentError, "Conditional must be either :if or :unless" unless ALLOWED_CONDITIONALS.include? @conditional
14
+
15
+ @predicate = predicate
16
+ end
17
+
18
+ def fulfilled? *args
19
+ result = check @predicate, *args
20
+ result = !result if @conditional == :unless
21
+ result
22
+ end
23
+
24
+ def resolve_conditional conditional
25
+ conditional
26
+ end
27
+
28
+ def check predicate, value, context, options = {}
29
+ raise NotImplementedError, "Subclasses should implement the #check method to check whether the value matches the predicate of the condition"
30
+ end
31
+
32
+ class SimpleCheck < Errapi::Condition
33
+
34
+ CONDITIONALS = {
35
+ if: :if,
36
+ unless: :unless
37
+ }.freeze
38
+
39
+ def check predicate, value, context, options = {}
40
+ if @predicate.kind_of?(Symbol) || @predicate.kind_of?(String)
41
+ value.respond_to?(:[]) ? value[@predicate] : value.send(@predicate)
42
+ elsif @predicate.respond_to? :call
43
+ @predicate.call value, context, options
44
+ else
45
+ @predicate
46
+ end
47
+ end
48
+ end
49
+
50
+ class ErrorCheck < Errapi::Condition
51
+
52
+ CONDITIONALS = {
53
+ if_error: :if,
54
+ unless_error: :unless
55
+ }.freeze
56
+
57
+ def resolve_conditional conditional
58
+ CONDITIONALS[conditional]
59
+ end
60
+
61
+ def check predicate, value, context, options = {}
62
+ if @predicate.respond_to? :call
63
+ context.errors? &@predicate
64
+ elsif @predicate.kind_of? Hash
65
+ context.errors? @predicate
66
+ else
67
+ @predicate ? context.errors? : !context.errors?
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,79 @@
1
+ require File.join(File.dirname(__FILE__), 'utils.rb')
2
+
3
+ module Errapi
4
+
5
+ class Configuration
6
+ attr_reader :options
7
+ attr_reader :plugins
8
+
9
+ def initialize
10
+ @options = OpenStruct.new
11
+ @plugins = OpenStruct.new
12
+ @validation_factories = {}
13
+ @condition_factories = {}
14
+ @location_factories = {}
15
+ end
16
+
17
+ def configure
18
+ yield self
19
+ end
20
+
21
+ def new_error options = {}
22
+ Errapi::ValidationError.new options
23
+ end
24
+
25
+ def build_error error, context
26
+ apply_plugins :build_error, error, context
27
+ end
28
+
29
+ def serialize_error error, serialized
30
+ apply_plugins :serialize_error, error, serialized
31
+ end
32
+
33
+ def new_context
34
+ Errapi::ValidationContext.new config: self
35
+ end
36
+
37
+ def plugin impl, options = {}
38
+ name = options[:name] || Utils.underscore(impl.to_s.sub(/.*::/, '')).to_sym
39
+ impl.config = self if impl.respond_to? :config=
40
+ @plugins[name] = impl
41
+ end
42
+
43
+ def validation_factory factory, options = {}
44
+ name = options[:name] || Utils.underscore(factory.to_s.sub(/.*::/, '')).to_sym
45
+ factory.config = self if factory.respond_to? :config=
46
+ @validation_factories[name] = factory
47
+ end
48
+
49
+ def validation name, options = {}
50
+ raise ArgumentError, "No validation factory registered for name #{name.inspect}" unless @validation_factories.key? name
51
+ factory = @validation_factories[name]
52
+ factory.respond_to?(:validation) ? factory.validation(options) : factory.new(options)
53
+ end
54
+
55
+ def register_condition factory
56
+ factory.conditionals.each do |conditional|
57
+ raise ArgumentError, "Conditional #{conditional} should start with 'if' or 'unless'." unless conditional.to_s.match /^(if|unless)/
58
+ @condition_factories[conditional] = factory
59
+ end
60
+ end
61
+
62
+ def extract_conditions! source, options = {}
63
+ [].tap do |conditions|
64
+ @condition_factories.each_pair do |conditional,factory|
65
+ next unless source.key? conditional
66
+ conditions << factory.new(conditional, source.delete(conditional), options)
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def apply_plugins operation, *args
74
+ @plugins.each_pair do |name,plugin|
75
+ plugin.send operation, *args if plugin.respond_to? operation
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,15 @@
1
+ module Errapi
2
+ # TODO: check all "raise" statements and use custom errors
3
+ class Error < StandardError; end
4
+ class ValidationErrorInvalid < Error; end
5
+ class ValidationDefinitionInvalid < Error; end
6
+
7
+ class ValidationFailed < Error
8
+ attr_reader :context
9
+
10
+ def initialize context
11
+ super "#{context.errors.length} errors were found during validation."
12
+ @context = context
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Errapi
2
+
3
+ module LocationBuilders
4
+
5
+ def json_location string = nil
6
+ Locations::Json.new string
7
+ end
8
+
9
+ def dotted_location string = nil
10
+ Locations::Dotted.new string
11
+ end
12
+
13
+ def no_location
14
+ Locations::None.instance
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ module Errapi
2
+
3
+ class Locations::Dotted
4
+
5
+ def initialize location = nil
6
+ @location = location.to_s.sub /^\./, '' unless location.nil?
7
+ end
8
+
9
+ def relative parts
10
+ if @location.nil?
11
+ self.class.new parts
12
+ else
13
+ self.class.new "#{@location}.#{parts.to_s.sub(/^\./, '')}"
14
+ end
15
+ end
16
+
17
+ def location_type
18
+ :dotted
19
+ end
20
+
21
+ def serialize
22
+ @location.nil? ? nil : @location
23
+ end
24
+
25
+ def === location
26
+ @location.to_s == location.to_s
27
+ end
28
+
29
+ def to_s
30
+ @location.to_s
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ module Errapi
2
+
3
+ class Locations::Json
4
+
5
+ def initialize location = nil
6
+ @location = location.nil? ? '' : "/#{location.to_s.sub(/^\//, '').sub(/\/$/, '')}"
7
+ end
8
+
9
+ def relative parts
10
+ if @location.nil?
11
+ self.class.new parts
12
+ else
13
+ self.class.new "#{@location}/#{parts.to_s.sub(/^\./, '').sub(/\/$/, '')}"
14
+ end
15
+ end
16
+
17
+ def location_type
18
+ :json
19
+ end
20
+
21
+ def serialize
22
+ @location
23
+ end
24
+
25
+ def === location
26
+ @location.to_s == location.to_s
27
+ end
28
+
29
+ def to_s
30
+ @location
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ require 'singleton'
2
+
3
+ module Errapi
4
+
5
+ class Locations::None
6
+ include Singleton
7
+
8
+ def relative parts
9
+ self
10
+ end
11
+
12
+ def serialize
13
+ nil
14
+ end
15
+
16
+ def === location
17
+ location.nil? || self == location
18
+ end
19
+
20
+ def to_s
21
+ LOCATION_STRING
22
+ end
23
+
24
+ private
25
+
26
+ LOCATION_STRING = ''.freeze
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+ module Errapi::Locations
2
+ end
3
+
4
+ Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib }
@@ -0,0 +1,30 @@
1
+ module Errapi
2
+
3
+ module Model
4
+
5
+ def errapi name = :default
6
+ validator = self.class.errapi name
7
+ ValidatorProxy.new self, validator
8
+ end
9
+
10
+ def self.included mod
11
+ mod.extend ClassMethods
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def errapi *args, &block
17
+
18
+ options = args.last.kind_of?(Hash) ? args.pop : {}
19
+ config = options[:config] || Errapi.config
20
+ config = Errapi.config config if config.kind_of? Symbol
21
+
22
+ name = args.shift || :default
23
+
24
+ @errapi_validators ||= {}
25
+ @errapi_validators[name] = Errapi::ObjectValidator.new(config, &block) if block
26
+ @errapi_validators[name]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,230 @@
1
+ require File.join(File.dirname(__FILE__), 'location_builders.rb')
2
+
3
+ module Errapi
4
+
5
+ class ObjectValidator
6
+ include LocationBuilders
7
+
8
+ def initialize config, options = {}, &block
9
+ # TODO: remove these options or if used, pass them to new validators instantiated in #register_validations
10
+ @config = config
11
+ @validations = []
12
+ instance_eval &block if block
13
+ end
14
+
15
+ def validates *args, &block
16
+ register_validations *args, &block
17
+ end
18
+
19
+ def validates_each *args, &block
20
+
21
+ options = args.last.kind_of?(Hash) ? args.pop : {}
22
+ options[:each] = args.shift
23
+ options[:each_options] = options.delete(:each_options) || {}
24
+ args << options
25
+
26
+ validates *args, &block
27
+ end
28
+
29
+ def validate value, context, options = {}
30
+ # TODO: skip validation by default if previous errors at current location
31
+ # TODO: add support for previous value and skip validation by default if value is unchanged
32
+
33
+ return context.valid? unless @validations
34
+
35
+ location = if options[:location]
36
+ options[:location]
37
+ elsif options[:location_type]
38
+ builder = "#{options[:location_type]}_location"
39
+ raise "Unknown location type #{options[:location_type].inspect}" unless respond_to? builder
40
+ send builder
41
+ else
42
+ no_location
43
+ end
44
+
45
+ source = options[:source]
46
+ context_proxy = ContextProxy.new context, self, location
47
+
48
+ @validations.each do |validation_definition|
49
+
50
+ each = validation_definition[:each]
51
+
52
+ each_values = nil
53
+ each_values_set = nil
54
+
55
+ if each
56
+ each_values = extract(value, each[:target], options)[:value]
57
+ each_values_set = each_values.collect{ |v| true } if each_values.kind_of? Array
58
+ each_values = [] unless each_values.kind_of? Array
59
+ each_sources = each_values.collect{ |v| source }
60
+ else
61
+ each_values = [ value ]
62
+ each_values_set = [ options.fetch(:value_set, true) ]
63
+ each_sources = [ source ]
64
+ end
65
+
66
+ each_location = each ? location.relative(each[:options][:as] || each[:target]) : location
67
+
68
+ each_values.each.with_index do |each_value,i|
69
+
70
+ each_index_location = each ? each_location.relative(i) : location
71
+ context_proxy.current_location = each_index_location
72
+
73
+ next if validation_definition[:conditions].any?{ |condition| !condition.fulfilled?(each_value, context_proxy) }
74
+
75
+ validation_definition[:validations].each do |validation|
76
+
77
+ next if validation[:conditions] && validation[:conditions].any?{ |condition| !condition.fulfilled?(each_value, context_proxy) }
78
+
79
+ validation_definition[:targets].each do |target|
80
+
81
+ target_value_info = extract each_value, target, value_set: each_values_set[i], source: source
82
+
83
+ validation_location = target ? each_index_location.relative(validation[:target_alias] || target) : each_index_location
84
+ context_proxy.current_location = validation_location
85
+
86
+ error_options = {
87
+ value: target_value_info[:value],
88
+ source: target_value_info[:source],
89
+ value_set: target_value_info[:value_set],
90
+ constraints: validation[:validation_options]
91
+ }
92
+
93
+ error_options[:location] = validation_location unless validation_location.kind_of? Errapi::Locations::None
94
+
95
+ error_options[:validation] = validation[:validation_name] if validation[:validation_name]
96
+
97
+ context_proxy.with_error_options error_options do
98
+ validation_options = { location: validation_location, value_set: target_value_info[:value_set] }
99
+ validation[:validation].validate target_value_info[:value], context_proxy, validation_options
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ # TODO: add config option to raise error by default
107
+ raise Errapi::ValidationFailed.new(context) if options[:raise_error] && context.errors?
108
+
109
+ context_proxy.valid?
110
+ end
111
+
112
+ def relative_location location
113
+ RelativeLocation.new location
114
+ end
115
+
116
+ private
117
+
118
+ class RelativeLocation
119
+ attr_reader :location
120
+
121
+ def initialize location
122
+ @location = location
123
+ end
124
+ end
125
+
126
+ def extract value, target, options = {}
127
+
128
+ source = options[:source]
129
+ value_set = options.fetch :value_set, true
130
+
131
+ if target.nil?
132
+ { value: value, value_set: value_set, source: source }
133
+ elsif target.respond_to? :call
134
+ { value: target.call(value), value_set: value_set, source: value }
135
+ elsif value.kind_of? Hash
136
+ { value: value[target], value_set: value.key?(target), source: value }
137
+ elsif value.respond_to?(target)
138
+ { value: value.send(target), value_set: value_set, source: value }
139
+ else
140
+ { value_set: false, source: value }
141
+ end
142
+ end
143
+
144
+ def register_validations *args, &block
145
+ # TODO: allow to set custom error options (e.g. reason) when registering validation
146
+
147
+ options = args.last.kind_of?(Hash) ? args.pop : {}
148
+ target_alias = options.delete :as
149
+
150
+ validations_definition = {
151
+ validations: []
152
+ }
153
+
154
+ # FIXME: register all validations (from :with, from block and from hash) in the order they are given
155
+ if options[:with]
156
+ validations_definition[:validations] += [*options.delete(:with)].collect{ |with| { validation: with, validation_options: {}, target_alias: target_alias } }
157
+ end
158
+
159
+ if block
160
+ validations_definition[:validations] << { validation: self.class.new(@config, &block), validation_options: {}, target_alias: target_alias }
161
+ end
162
+
163
+ if options[:each]
164
+ validations_definition[:each] = {
165
+ target: options.delete(:each),
166
+ options: options.delete(:each_options)
167
+ }
168
+ end
169
+
170
+ validations_definition[:conditions] = @config.extract_conditions! options
171
+
172
+ validations = options
173
+ raise Errapi::ValidationDefinitionInvalid, "No validation was defined. Use registered validations (e.g. `presence: true`), the :with option, or a block to define validations." if validations_definition[:validations].empty? && validations.empty?
174
+
175
+ validations.each do |validation_name,options|
176
+ next unless options
177
+ validation_options = options.kind_of?(Hash) ? options : {}
178
+ validation_target_alias = validation_options.delete(:as) || target_alias
179
+ conditions = @config.extract_conditions! validation_options
180
+ validation = @config.validation validation_name, validation_options
181
+ validations_definition[:validations] << { validation: validation, validation_name: validation_name, validation_options: validation_options, target_alias: validation_target_alias, conditions: conditions }
182
+ end
183
+
184
+ validations_definition[:targets] = args.empty? ? [ nil ] : args
185
+
186
+ @validations << validations_definition
187
+ end
188
+
189
+ class ContextProxy
190
+ instance_methods.each{ |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
191
+ attr_accessor :current_location
192
+
193
+ def initialize context, validator, location
194
+ @context = context
195
+ @validator = validator
196
+ @current_location = location
197
+ @error_options = {}
198
+ end
199
+
200
+ def add_error options = {}, &block
201
+ @context.add_error @error_options.merge(options), &block
202
+ end
203
+
204
+ def errors? criteria = {}, &block
205
+
206
+ if criteria[:location].kind_of? RelativeLocation
207
+ criteria = criteria.dup
208
+ criteria[:location] = @current_location.relative criteria[:location].location
209
+ end
210
+
211
+ @context.errors? criteria, &block
212
+ end
213
+
214
+ # TODO: override errors? to support matching relative error locations
215
+
216
+ def with_error_options error_options = {}, &block
217
+ previous_error_options = @error_options
218
+ @error_options = error_options
219
+ block.call
220
+ @error_options = previous_error_options
221
+ end
222
+
223
+ protected
224
+
225
+ def method_missing name, *args, &block
226
+ @context.send name, *args, &block
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,20 @@
1
+ require 'i18n'
2
+
3
+ # TODO: support interpolating source and target name (e.g. "Project name cannot be null.")
4
+ class Errapi::Plugins::I18nMessages
5
+ class << self
6
+
7
+ def serialize_error error, serialized
8
+ return if serialized.key? :message
9
+
10
+ if I18n.exists? translation_key = "errapi.#{error.reason}"
11
+ interpolation_values = INTERPOLATION_KEYS.inject({}){ |memo,key| memo[key] = error.send(key); memo }.reject{ |k,v| v.nil? }
12
+ serialized[:message] = I18n.t translation_key, interpolation_values
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ INTERPOLATION_KEYS = %i(check_value checked_value)
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ module Errapi
2
+ class Plugins::Location
3
+ class << self
4
+ attr_writer :config
5
+ attr_accessor :camelize
6
+
7
+ def serialize_error error, serialized
8
+ if error.location && error.location.respond_to?(:serialize)
9
+
10
+ serialized_location = error.location.serialize
11
+ unless serialized_location.nil?
12
+ serialized[:location] = serialized_location
13
+ serialized[location_type_key] = error.location.location_type if error.location.respond_to? :location_type
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def location_type_key
21
+ camelize? ? :locationType : :location_type
22
+ end
23
+
24
+ def camelize?
25
+ @camelize.nil? ? @config.options.camelize : @camelize
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ module Errapi
2
+ class Plugins::Reason
3
+ class << self
4
+ attr_writer :config
5
+ attr_accessor :camelize
6
+
7
+ def serialize_error error, serialized
8
+ serialized[:reason] = serialized_reason error
9
+ end
10
+
11
+ private
12
+
13
+ def serialized_reason error
14
+ camelize? ? Utils.camelize(error.reason.to_s).to_sym : error.reason
15
+ end
16
+
17
+ def camelize?
18
+ @camelize.nil? ? @config.options.camelize : @camelize
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ module Errapi::Plugins
2
+ end
3
+
4
+ Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib }