apiable_model_errors 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ac03665a8b0e776827ef06713dc1bbaf02395d24
4
+ data.tar.gz: bd876bf793d113b27749f3fd6ed266dc5613a1f4
5
+ SHA512:
6
+ metadata.gz: e7117ef3e97f0cf9acf70b5fcc057bbfe45f4a8d564cbb34fdd878e0e2a96f271550ea7de1d3697fbc9a4e5e873cbf7b99d4efe9298aa0409c05c3a89019f30e
7
+ data.tar.gz: 7274e7ce09b19d90cf66d56fb0b706da54e25fb9258686fa69e64fe65f634f3eafbe64b65f3e64fb793a8869428cd3565e615e075b8db7e591e602899a506630
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # A sample Gemfile
2
+ source "https://rubygems.org"
3
+ group :development do
4
+ gem "activemodel"
5
+ gem "rspec"
6
+ end
@@ -0,0 +1,43 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ activemodel (4.2.5)
5
+ activesupport (= 4.2.5)
6
+ builder (~> 3.1)
7
+ activesupport (4.2.5)
8
+ i18n (~> 0.7)
9
+ json (~> 1.7, >= 1.7.7)
10
+ minitest (~> 5.1)
11
+ thread_safe (~> 0.3, >= 0.3.4)
12
+ tzinfo (~> 1.1)
13
+ builder (3.2.2)
14
+ diff-lcs (1.2.5)
15
+ i18n (0.7.0)
16
+ json (1.8.3)
17
+ minitest (5.8.3)
18
+ rspec (3.4.0)
19
+ rspec-core (~> 3.4.0)
20
+ rspec-expectations (~> 3.4.0)
21
+ rspec-mocks (~> 3.4.0)
22
+ rspec-core (3.4.2)
23
+ rspec-support (~> 3.4.0)
24
+ rspec-expectations (3.4.0)
25
+ diff-lcs (>= 1.2.0, < 2.0)
26
+ rspec-support (~> 3.4.0)
27
+ rspec-mocks (3.4.1)
28
+ diff-lcs (>= 1.2.0, < 2.0)
29
+ rspec-support (~> 3.4.0)
30
+ rspec-support (3.4.1)
31
+ thread_safe (0.3.5)
32
+ tzinfo (1.2.2)
33
+ thread_safe (~> 0.1)
34
+
35
+ PLATFORMS
36
+ ruby
37
+
38
+ DEPENDENCIES
39
+ activemodel
40
+ rspec
41
+
42
+ BUNDLED WITH
43
+ 1.10.6
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Adam Cooke.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,70 @@
1
+ # APIable Model Errors
2
+
3
+ ActiveModel provides a great framework for handling validations
4
+ for models. It easily allows you to add any validation message to
5
+ any attribute however these arbitary strings aren't very helpful
6
+ if you want to deliver validation errors to APIs. API developers
7
+ don't want to see `Number is too long (maximum is 5 characters)`,
8
+ they would much rather see something they can handle programatically
9
+ like `too_long` or `blank`.
10
+
11
+ This gem extends the `ActiveModel::Errors` class to allow for a
12
+ more API-friendly hash of errors to be returned.
13
+
14
+ ## Installation
15
+
16
+ ```ruby
17
+ gem "apiable_model_errors"
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ Once you've installed the gems, you can call the `to_api_hash`
23
+ method on any `ActiveModel::Errors` object.
24
+
25
+ ```ruby
26
+ user = User.new(params)
27
+ unless user.valid?
28
+ user.errors.to_api_hash
29
+ end
30
+ ```
31
+
32
+ ## Example Output
33
+
34
+ When using this you'll receive a ruby hash however you can easily
35
+ convert this into JSON by calling `to_json` on the hash.
36
+
37
+ ```javascript
38
+ {
39
+ "name":[
40
+ {
41
+ "code":"too_long",
42
+ "message":"is too long (maximum is 4 characters)",
43
+ "options":{
44
+ "count":4
45
+ }
46
+ }
47
+ ]
48
+ }
49
+ ```
50
+
51
+ As you can see you'll receive the code, the actual translated
52
+ message from ActiveModel and any options.
53
+
54
+ ## `has_message?`
55
+
56
+ In addition to provide the `to_api_hash` method, we also provide
57
+ a method called `has_message?` which allows you to determine if a
58
+ message exists for a given attribute. This is similar to the
59
+ `added?` however does not require that you provide all options to
60
+ see if a message has been added.
61
+
62
+ ```ruby
63
+ # See if there are any :too_long messages on the name attribute
64
+ user.errors.has_message?(:name, :too_long)
65
+ # See if there are any :too_long messages on the name attribute
66
+ # with the options provided.
67
+ user.errors.has_message?(:name, :too_long, :count => 4)
68
+ # See if there are any :blank messages on the name attribute
69
+ user.errors.has_message?(:name, :blank)
70
+ ```
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "apiable_model_errors"
3
+ s.version = "1.0.0"
4
+ s.authors = ["Adam Cooke"]
5
+ s.email = ["adam@atechmedia.com"]
6
+ s.homepage = "http://adamcooke.io"
7
+ s.licenses = ['MIT']
8
+ s.summary = "Provide ActiveModel errors in an format suitable for API consumers"
9
+ s.description = "Avoid sending plain text messages to API consumers by sending them more detailed information about validation errors."
10
+ s.files = Dir["**/*"]
11
+ s.add_dependency "activemodel", ">= 4.0", "< 5.0"
12
+ end
@@ -0,0 +1,72 @@
1
+ require 'active_model'
2
+
3
+ module ApiableModelErrors
4
+
5
+ def self.included(base)
6
+ base.alias_method_chain :add, :api_errors
7
+ base.alias_method_chain :clear, :api_errors
8
+ base.alias_method_chain :delete, :api_errors
9
+ end
10
+
11
+ def to_api_hash
12
+ errors_for_api.each_with_object({}) do |(attribute, errors), hash|
13
+ hash[attribute] = []
14
+ errors.each_with_index do |(message, options), index|
15
+ if message.is_a?(String)
16
+ error = {:message => message}
17
+ else
18
+ error = {
19
+ :code => message,
20
+ :message => messages[attribute][index],
21
+ :options => options
22
+ }
23
+ end
24
+ hash[attribute] << error
25
+ end
26
+ end
27
+ end
28
+
29
+ def errors_for_api
30
+ @errors_for_api ||= {}
31
+ end
32
+
33
+ def clear_with_api_errors
34
+ clear_without_api_errors.tap do
35
+ @errors_for_api = {}
36
+ end
37
+ end
38
+
39
+ def delete_with_api_errors(key)
40
+ delete_without_api_errors(key).tap do
41
+ errors_for_api.delete(key)
42
+ end
43
+ end
44
+
45
+ def add_with_api_errors(attribute, message = :invalid, options = {})
46
+ add_without_api_errors(attribute, message, options).tap do
47
+ errors_for_api[attribute] ||= []
48
+ errors_for_api[attribute] << [message, options || {}]
49
+ end
50
+ end
51
+
52
+ def has_message?(attribute, message, options = nil)
53
+ if errors = errors_for_api[attribute]
54
+ if error = errors.select { |e| e[0] == message }.first
55
+ if options.nil?
56
+ true # No options specified, if it exists, it's here
57
+ elsif options == error[1]
58
+ true # The options match those on the subejct
59
+ else
60
+ false # The options selected don't match those on the error
61
+ end
62
+ else
63
+ false # No errors of this type
64
+ end
65
+ else
66
+ false # No errors for this attribute
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ ActiveModel::Errors.send :include, ApiableModelErrors
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ describe ApiableModelErrors do
4
+
5
+ context "#add" do
6
+ it "should should add errors to the API hash too" do
7
+ errors = ActiveModel::Errors.new(ExampleClass.new)
8
+ errors.add :example, :too_long, :count => 4
9
+ expect(errors.errors_for_api[:example]).to be_a Array
10
+ expect(errors.errors_for_api[:example].size).to be 1
11
+ expect(errors.errors_for_api[:example][0][0]).to eq(:too_long)
12
+ expect(errors.errors_for_api[:example][0][1]).to eq({:count => 4})
13
+ end
14
+
15
+ it "should still return the full message to keep same behaviour" do
16
+ errors = ActiveModel::Errors.new(ExampleClass.new)
17
+ expect(errors.add(:example, :blank)).to eq(["can't be blank"])
18
+ end
19
+ end
20
+
21
+ context "#clear" do
22
+ it "should clear API errors" do
23
+ errors = ActiveModel::Errors.new(ExampleClass.new)
24
+ errors.add :example, :blank
25
+ expect(errors.messages).to_not be_empty
26
+ expect(errors.errors_for_api).to_not be_empty
27
+ errors.clear
28
+ expect(errors.messages).to be_empty
29
+ expect(errors.errors_for_api).to be_empty
30
+ end
31
+ end
32
+
33
+ context "#delete" do
34
+ it "should delete a given API error" do
35
+ errors = ActiveModel::Errors.new(ExampleClass.new)
36
+ errors.add :example, :blank
37
+ expect {errors.delete(:example)}.to change {errors.errors_for_api.has_key?(:example)}.from(true).to(false)
38
+ end
39
+ end
40
+
41
+ context "#to_api_hash" do
42
+ subject(:errors) do
43
+ errors = ActiveModel::Errors.new(ExampleClass.new)
44
+ errors.add :example, :too_long, :count => 4
45
+ errors.add :example, :invalid
46
+ errors
47
+ end
48
+
49
+ it "shuold return a hash for returning to API consumers" do
50
+ expect(hash = errors.to_api_hash).to be_a(Hash)
51
+ end
52
+
53
+ it "should have an array of errors for each attribute" do
54
+ expect(errors.to_api_hash[:example]).to be_a(Array)
55
+ end
56
+
57
+ it "should have the code, message and options" do
58
+ error = errors.to_api_hash[:example][0]
59
+ expect(error[:code]).to eq(:too_long)
60
+ expect(error[:message]).to match /is too long/
61
+ expect(error[:options]).to be_a Hash
62
+ end
63
+ end
64
+
65
+ context "#to_api_hash (with string messages)" do
66
+ subject(:errors) do
67
+ errors = ActiveModel::Errors.new(ExampleClass.new)
68
+ errors.add :example, "is not unique"
69
+ errors
70
+ end
71
+
72
+ it "should not have a code or options" do
73
+ error = errors.to_api_hash[:example][0]
74
+ expect(error[:message]).to eq('is not unique')
75
+ expect(error.has_key?(:code)).to be false
76
+ expect(error.has_key?(:options)).to be false
77
+ end
78
+ end
79
+
80
+ context "#has_message?" do
81
+ it "should return true if the given error exists on the attribute" do
82
+ errors = ActiveModel::Errors.new(ExampleClass.new)
83
+ errors.add :example, :too_long, :count => 4
84
+ expect(errors.has_message?(:example, :too_long)).to be true
85
+ expect(errors.has_message?(:example, :too_long, :count => 4)).to be true
86
+ expect(errors.has_message?(:example, :too_long, :count => 3)).to be false
87
+ end
88
+ end
89
+
90
+ context "with presence validator" do
91
+ class PresenceExampleClass < ExampleClass
92
+ validates :example, :presence => true
93
+ end
94
+
95
+ it "should have a blank error" do
96
+ p = PresenceExampleClass.new(nil)
97
+ expect(p.valid?).to be false
98
+ expect(p.errors.has_message?(:example, :blank)).to be true
99
+ end
100
+ end
101
+
102
+ context "with numericality validator" do
103
+ class NumericalityExampleClass < ExampleClass
104
+ validates :example, :numericality => {:only_integer => true}
105
+ end
106
+
107
+ it "should have a not_a_number error" do
108
+ p = NumericalityExampleClass.new("potato")
109
+ expect(p.valid?).to be false
110
+ expect(p.errors.has_message?(:example, :not_a_number)).to be true
111
+ end
112
+ end
113
+
114
+ context "with length validator" do
115
+ class LengthExampleClass < ExampleClass
116
+ validates :example, :length => {:minimum => 2, :maximum => 5}
117
+ end
118
+
119
+ it "should have a too long error when too long" do
120
+ p = LengthExampleClass.new("morethanfive")
121
+ expect(p.valid?).to be false
122
+ expect(p.errors.has_message?(:example, :too_long)).to be true
123
+ expect(p.errors.has_message?(:example, :too_long, :count => 5)).to be true
124
+ end
125
+
126
+ it "should have a too short error when too long" do
127
+ p = LengthExampleClass.new("a")
128
+ expect(p.valid?).to be false
129
+ expect(p.errors.has_message?(:example, :too_short)).to be true
130
+ expect(p.errors.has_message?(:example, :too_short, :count => 2)).to be true
131
+ end
132
+ end
133
+
134
+ end
@@ -0,0 +1,10 @@
1
+ $:.unshift(File.expand_path(File.join('..', '..', 'lib'), __FILE__))
2
+ require 'apiable_model_errors'
3
+
4
+ class ExampleClass
5
+ include ActiveModel::Validations
6
+ attr_reader :example
7
+ def initialize(example = nil)
8
+ @example = example
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apiable_model_errors
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Cooke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-02-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ description: Avoid sending plain text messages to API consumers by sending them more
34
+ detailed information about validation errors.
35
+ email:
36
+ - adam@atechmedia.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - Gemfile
42
+ - Gemfile.lock
43
+ - MIT-LICENCE
44
+ - README.md
45
+ - apiable_model_errors.gemspec
46
+ - lib/apiable_model_errors.rb
47
+ - spec/apiable_model_errors_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: http://adamcooke.io
50
+ licenses:
51
+ - MIT
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 2.4.5
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Provide ActiveModel errors in an format suitable for API consumers
73
+ test_files: []
74
+ has_rdoc: