errapi 0.1.0 → 0.1.2

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 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 }