glia-errors 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './errors/error'
4
+ require_relative './errors/error_types'
5
+ require_relative './errors/validation_errors'
6
+ require_relative './errors/mapper'
7
+
8
+ module Glia
9
+ # Glia REST API errors
10
+ module Errors
11
+ def self.from_dry_validation_result(result, custom_error_map = {})
12
+ dry_validation_version = Gem.loaded_specs['dry-validation'].version
13
+ if dry_validation_version < Gem::Version.new('1.0')
14
+ Mapper.from_dry_validation_result(result.output, result.messages, custom_error_map)
15
+ elsif dry_validation_version <= Gem::Version.new('1.6')
16
+ Mapper.from_dry_validation_result(result.to_h, result.errors.to_h, custom_error_map)
17
+ else
18
+ raise 'Unsupported dry-validation version'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glia
4
+ module Errors
5
+ # Base error
6
+ class Error
7
+ attr_reader :type, :ref, :message, :error_details
8
+
9
+ def initialize(type:, ref:, message: nil, error_details: nil)
10
+ @type = type
11
+ @ref = ref
12
+ @message = message
13
+ @error_details = error_details
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ type: type, ref: ref, message: message, error_details: error_details_to_h(@error_details)
19
+ }.compact
20
+ end
21
+
22
+ private
23
+
24
+ def error_details_to_h(details)
25
+ return details if primitive?(details)
26
+
27
+ if details.is_a?(Hash)
28
+ details.keys.each_with_object({}) do |field, hash|
29
+ hash[field] = error_details_to_h(details[field])
30
+ end
31
+ elsif details.is_a?(Array)
32
+ details.map { |error| error_details_to_h(error) }
33
+ elsif details.respond_to?(:to_h)
34
+ details.to_h
35
+ else
36
+ raise ArgumentError, "Unsupported details type: #{details.class}"
37
+ end
38
+ end
39
+
40
+ def primitive?(details)
41
+ details.nil? || [TrueClass, FalseClass, String, Integer, Float].include?(details.class)
42
+ end
43
+
44
+ # Converts from camel_case to capitalized more human readable value
45
+ # first_name => "First name"
46
+ def humanize(value)
47
+ value.to_s.capitalize.gsub('_', ' ')
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glia
4
+ module Errors
5
+ INPUT_VALIDATION_ERROR = 'input_validation_error'
6
+ INVALID_TYPE_ERROR = 'invalid_type_error'
7
+ INVALID_NUMBER_ERROR = 'invalid_number_error'
8
+ INVALID_VALUE_ERROR = 'invalid_value_error'
9
+ INVALID_FORMAT_ERROR = 'invalid_format_error'
10
+ INVALID_LENGTH_ERROR = 'invalid_length_error'
11
+ MISSING_VALUE_ERROR = 'missing_value_error'
12
+ UNKNOWN_ERROR = 'unknown_error'
13
+ end
14
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glia
4
+ module Errors
5
+ # Maps library specific errors to Glia public errors
6
+ module Mapper
7
+ INVALID_TYPE_ERROR =
8
+ lambda do |field, _value, message|
9
+ type =
10
+ case message
11
+ when 'must be an array'
12
+ InvalidTypeError::Types::ARRAY
13
+ when 'must be an integer'
14
+ InvalidTypeError::Types::INTEGER
15
+ when 'must be a number', 'must be a float', 'must be a decimal'
16
+ InvalidTypeError::Types::NUMBER
17
+ when 'must be a hash'
18
+ InvalidTypeError::Types::OBJECT
19
+ when 'must be boolean'
20
+ InvalidTypeError::Types::BOOLEAN
21
+ when 'must be a string'
22
+ InvalidTypeError::Types::STRING
23
+ end
24
+ InvalidTypeError.new(field: field, type: type)
25
+ end
26
+
27
+ INVALID_FORMAT_ERROR = ->(field, _value, _message) { InvalidFormatError.new(field: field) }
28
+ INVALID_UUID_ERROR =
29
+ lambda do |field, _value, _message|
30
+ InvalidFormatError.new(field: field, format: InvalidFormatError::Formats::UUID)
31
+ end
32
+ INVALID_NUMBER_ERROR = ->(field, _value, _message) { InvalidNumberError.new(field: field) }
33
+ INVALID_VALUE_ERROR = ->(field, _value, _message) { InvalidValueError.new(field: field) }
34
+ INVALID_LENGTH_ERROR = ->(field, _value, _message) { InvalidLengthError.new(field: field) }
35
+ MISSING_VALUE_ERROR = ->(field, _value, _message) { MissingValueError.new(field: field) }
36
+
37
+ # dry-validation shares the same message for `eql?` validation for string and number,
38
+ # so we are separating them ourselves
39
+ INVALID_NUMBER_OR_VALUE_ERROR =
40
+ lambda do |field, value, _message|
41
+ if value.is_a?(String)
42
+ InvalidValueError.new(field: field)
43
+ else
44
+ InvalidNumberError.new(field: field)
45
+ end
46
+ end
47
+
48
+ # dry-validation will return `must be a date/time` even if we provide a number or boolean,
49
+ # in which case `invalid_format_error` does not make sense, so instead we will return `invalid_type_error`
50
+ INVALID_DATE_FORMAT_OR_TYPE_ERROR =
51
+ lambda do |field, value, message|
52
+ if value.is_a?(String)
53
+ format =
54
+ case message
55
+ when 'must be a date'
56
+ InvalidFormatError::Formats::DATE
57
+ when 'must be a date time'
58
+ InvalidFormatError::Formats::DATE_TIME
59
+ when 'must be a time'
60
+ InvalidFormatError::Formats::TIME
61
+ end
62
+ InvalidFormatError.new(field: field, format: format)
63
+ else
64
+ InvalidTypeError.new(field: field, type: InvalidTypeError::Types::STRING)
65
+ end
66
+ end
67
+
68
+ ERROR_MAP = {
69
+ # InvalidTypeError
70
+ 'must be an array' => INVALID_TYPE_ERROR,
71
+ 'must be an integer' => INVALID_TYPE_ERROR,
72
+ 'must be a number' => INVALID_TYPE_ERROR,
73
+ 'must be a float' => INVALID_TYPE_ERROR,
74
+ 'must be a decimal' => INVALID_TYPE_ERROR,
75
+ 'must be a hash' => INVALID_TYPE_ERROR,
76
+ 'must be boolean' => INVALID_TYPE_ERROR,
77
+ 'must be a string' => INVALID_TYPE_ERROR,
78
+ 'must be a date time' => INVALID_DATE_FORMAT_OR_TYPE_ERROR,
79
+ 'must be a date' => INVALID_DATE_FORMAT_OR_TYPE_ERROR,
80
+ 'must be a time' => INVALID_DATE_FORMAT_OR_TYPE_ERROR,
81
+ # InvalidFormatError
82
+ 'is in invalid format' => INVALID_FORMAT_ERROR,
83
+ 'is not a valid UUID' => INVALID_UUID_ERROR,
84
+ # InvalidNumberError
85
+ 'must be less than' => INVALID_NUMBER_ERROR,
86
+ 'must be less than or equal to' => INVALID_NUMBER_ERROR,
87
+ 'must be greater than' => INVALID_NUMBER_ERROR,
88
+ 'must be greater than or equal to' => INVALID_NUMBER_ERROR,
89
+ 'must be even' => INVALID_NUMBER_ERROR,
90
+ 'must be odd' => INVALID_NUMBER_ERROR,
91
+ # InvalidValueError
92
+ 'must be true' => INVALID_VALUE_ERROR,
93
+ 'must be false' => INVALID_VALUE_ERROR,
94
+ 'must include' => INVALID_VALUE_ERROR,
95
+ 'must not include' => INVALID_VALUE_ERROR,
96
+ 'must be empty' => INVALID_VALUE_ERROR,
97
+ 'must be filled' => INVALID_VALUE_ERROR,
98
+ 'cannot be empty' => INVALID_VALUE_ERROR,
99
+ 'cannot be defined' => INVALID_VALUE_ERROR,
100
+ # InvalidNumberError or InvalidValueError
101
+ 'must be equal to' => INVALID_NUMBER_OR_VALUE_ERROR,
102
+ 'must not be equal to' => INVALID_NUMBER_OR_VALUE_ERROR,
103
+ 'must be one of' => INVALID_NUMBER_OR_VALUE_ERROR,
104
+ 'must not be one of' => INVALID_NUMBER_OR_VALUE_ERROR,
105
+ # InvalidLengthError
106
+ 'size cannot be greater than' => INVALID_LENGTH_ERROR,
107
+ 'size cannot be less than' => INVALID_LENGTH_ERROR,
108
+ 'size must be' => INVALID_LENGTH_ERROR,
109
+ 'size must be within' => INVALID_LENGTH_ERROR,
110
+ 'bytesize cannot be less than' => INVALID_LENGTH_ERROR,
111
+ 'bytesize cannot be greater than' => INVALID_LENGTH_ERROR,
112
+ 'bytes long' => INVALID_LENGTH_ERROR,
113
+ 'length must be' => INVALID_LENGTH_ERROR,
114
+ 'length must be within' => INVALID_LENGTH_ERROR,
115
+ # MissingValueError
116
+ 'is missing' => MISSING_VALUE_ERROR,
117
+ # Custom format errors
118
+ 'must be in E.164 format' => INVALID_FORMAT_ERROR,
119
+ 'must be up to 7-digit string' => INVALID_FORMAT_ERROR,
120
+ 'must be a valid email' => INVALID_FORMAT_ERROR
121
+ }.freeze
122
+
123
+ def self.from_dry_validation_result(output, messages, custom_error_map = {})
124
+ error_map = ERROR_MAP.merge(custom_error_map)
125
+ from_dry_validation_result_rec(output, messages, error_map)
126
+ end
127
+
128
+ def self.from_dry_validation_result_rec(output, messages, error_map)
129
+ error_details =
130
+ messages.keys.each_with_object({}) do |field, acc|
131
+ field_messages = messages[field]
132
+ field_value = output[field]
133
+ acc[field] =
134
+ if field_messages.is_a?(Hash)
135
+ from_dry_validation_result_rec(field_value, field_messages, error_map)
136
+ else
137
+ field_messages.map do |message|
138
+ from_dry_validation_error(field, field_value, message, error_map)
139
+ end
140
+ end
141
+ end
142
+ InputValidationError.new(error_details: error_details)
143
+ end
144
+
145
+ def self.from_dry_validation_error(field, value, message, error_map)
146
+ key = error_map.keys.find { |k| message.include?(k) }
147
+ error_or_func = error_map[key]
148
+ if error_or_func.nil?
149
+ UnknownError.new(field: field)
150
+ elsif error_or_func.respond_to?(:call)
151
+ error_or_func.call(field, value, message)
152
+ else
153
+ error_or_func
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glia
4
+ module Errors
5
+ # InputValidationError
6
+ class InputValidationError < Error
7
+ def initialize(error_details:, message: nil)
8
+ super(
9
+ type: INPUT_VALIDATION_ERROR,
10
+ ref: "https://example.com/errors/#{INPUT_VALIDATION_ERROR}.html",
11
+ message: message,
12
+ error_details: error_details
13
+ )
14
+ end
15
+ end
16
+
17
+ # InvalidNumberError
18
+ class InvalidNumberError < Error
19
+ def initialize(field:)
20
+ super(
21
+ type: INVALID_NUMBER_ERROR,
22
+ ref: "https://example.com/errors/#{INVALID_NUMBER_ERROR}.html",
23
+ message: "#{humanize(field)} is invalid"
24
+ )
25
+ end
26
+ end
27
+
28
+ # InvalidValueError
29
+ class InvalidValueError < Error
30
+ def initialize(field:)
31
+ super(
32
+ type: INVALID_VALUE_ERROR,
33
+ ref: "https://example.com/errors/#{INVALID_VALUE_ERROR}.html",
34
+ message: "#{humanize(field)} is invalid"
35
+ )
36
+ end
37
+ end
38
+
39
+ # InvalidLengthError
40
+ class InvalidLengthError < Error
41
+ def initialize(field:)
42
+ super(
43
+ type: INVALID_LENGTH_ERROR,
44
+ ref: "https://example.com/errors/#{INVALID_LENGTH_ERROR}.html",
45
+ message: "#{humanize(field)} length is invalid"
46
+ )
47
+ end
48
+ end
49
+
50
+ # InvalidFormatError
51
+ class InvalidFormatError < Error
52
+ class Formats
53
+ DATE_TIME = 'date_time'
54
+ DATE = 'date'
55
+ TIME = 'time'
56
+ UUID = 'uuid'
57
+ end
58
+
59
+ def initialize(field:, format: nil)
60
+ super(
61
+ type: INVALID_FORMAT_ERROR,
62
+ ref: "https://example.com/errors/#{INVALID_FORMAT_ERROR}.html",
63
+ message:
64
+ if format
65
+ "#{humanize(field)} has invalid format, required format is #{format}"
66
+ else
67
+ "#{humanize(field)} has invalid format"
68
+ end
69
+ )
70
+ end
71
+ end
72
+
73
+ # InvalidTypeError
74
+ class InvalidTypeError < Error
75
+ class Types
76
+ STRING = 'string'
77
+ INTEGER = 'integer'
78
+ NUMBER = 'number'
79
+ BOOLEAN = 'boolean'
80
+ ARRAY = 'array'
81
+ OBJECT = 'object'
82
+ end
83
+
84
+ def initialize(field:, type:)
85
+ super(
86
+ type: INVALID_TYPE_ERROR,
87
+ ref: "https://example.com/errors/#{INVALID_TYPE_ERROR}.html",
88
+ message: "#{humanize(field)} must be of type #{type}"
89
+ )
90
+ end
91
+ end
92
+
93
+ # MissingValueError
94
+ class MissingValueError < Error
95
+ def initialize(field:)
96
+ super(
97
+ type: MISSING_VALUE_ERROR,
98
+ ref: "https://example.com/errors/#{MISSING_VALUE_ERROR}.html",
99
+ message: "#{humanize(field)} is missing"
100
+ )
101
+ end
102
+ end
103
+
104
+ # UnknownError
105
+ class UnknownError < Error
106
+ def initialize(field:)
107
+ super(
108
+ type: UNKNOWN_ERROR,
109
+ ref: "https://example.com/errors/#{UNKNOWN_ERROR}.html",
110
+ message: "#{humanize(field)} validation failed with unknown error"
111
+ )
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,74 @@
1
+ {
2
+ "input": {
3
+ "invalid_format_abc_regex": "defg",
4
+ "invalid_format_date": "not date",
5
+ "invalid_format_date_time": "not date time",
6
+ "invalid_format_time": "not time",
7
+ "invalid_format_uuid": "not uuid",
8
+ "invalid_format_e164": "not phone number",
9
+ "invalid_format_phone_extension": "not phone extension",
10
+ "invalid_format_email": "not email"
11
+ },
12
+ "expectation": {
13
+ "type": "input_validation_error",
14
+ "ref": "https://example.com/errors/input_validation_error.html",
15
+ "error_details": {
16
+ "invalid_format_abc_regex": [
17
+ {
18
+ "type": "invalid_format_error",
19
+ "ref": "https://example.com/errors/invalid_format_error.html",
20
+ "message": "Invalid format abc regex has invalid format"
21
+ }
22
+ ],
23
+ "invalid_format_date": [
24
+ {
25
+ "type": "invalid_format_error",
26
+ "ref": "https://example.com/errors/invalid_format_error.html",
27
+ "message": "Invalid format date has invalid format, required format is date"
28
+ }
29
+ ],
30
+ "invalid_format_date_time": [
31
+ {
32
+ "type": "invalid_format_error",
33
+ "ref": "https://example.com/errors/invalid_format_error.html",
34
+ "message": "Invalid format date time has invalid format, required format is date_time"
35
+ }
36
+ ],
37
+ "invalid_format_time": [
38
+ {
39
+ "type": "invalid_format_error",
40
+ "ref": "https://example.com/errors/invalid_format_error.html",
41
+ "message": "Invalid format time has invalid format, required format is time"
42
+ }
43
+ ],
44
+ "invalid_format_uuid": [
45
+ {
46
+ "type": "invalid_format_error",
47
+ "ref": "https://example.com/errors/invalid_format_error.html",
48
+ "message": "Invalid format uuid has invalid format, required format is uuid"
49
+ }
50
+ ],
51
+ "invalid_format_e164": [
52
+ {
53
+ "type": "invalid_format_error",
54
+ "ref": "https://example.com/errors/invalid_format_error.html",
55
+ "message": "Invalid format e164 has invalid format"
56
+ }
57
+ ],
58
+ "invalid_format_phone_extension": [
59
+ {
60
+ "type": "invalid_format_error",
61
+ "ref": "https://example.com/errors/invalid_format_error.html",
62
+ "message": "Invalid format phone extension has invalid format"
63
+ }
64
+ ],
65
+ "invalid_format_email": [
66
+ {
67
+ "type": "invalid_format_error",
68
+ "ref": "https://example.com/errors/invalid_format_error.html",
69
+ "message": "Invalid format email has invalid format"
70
+ }
71
+ ]
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,106 @@
1
+ {
2
+ "input": {
3
+ "invalid_length_array_max_length_3": [1, 2, 3, 4],
4
+ "invalid_length_array_min_length_3": [1, 2],
5
+ "invalid_length_array_exact_length_3": [1, 2],
6
+ "invalid_length_array_range_length_1_2": [1, 2, 3],
7
+ "invalid_length_string_max_length_3": "abcd",
8
+ "invalid_length_string_min_length_3": "ab",
9
+ "invalid_length_string_exact_length_3": "ab",
10
+ "invalid_length_string_range_length_1_2": "abc",
11
+ "invalid_length_string_max_bytesize_3": "abcd",
12
+ "invalid_length_string_min_bytesize_3": "a",
13
+ "invalid_length_string_exact_bytesize_3": "ab",
14
+ "invalid_length_string_range_bytesize_1_2": "abc"
15
+ },
16
+ "expectation": {
17
+ "type": "input_validation_error",
18
+ "ref": "https://example.com/errors/input_validation_error.html",
19
+ "error_details": {
20
+ "invalid_length_array_max_length_3": [
21
+ {
22
+ "type": "invalid_length_error",
23
+ "ref": "https://example.com/errors/invalid_length_error.html",
24
+ "message": "Invalid length array max length 3 length is invalid"
25
+ }
26
+ ],
27
+ "invalid_length_array_min_length_3": [
28
+ {
29
+ "type": "invalid_length_error",
30
+ "ref": "https://example.com/errors/invalid_length_error.html",
31
+ "message": "Invalid length array min length 3 length is invalid"
32
+ }
33
+ ],
34
+ "invalid_length_array_exact_length_3": [
35
+ {
36
+ "type": "invalid_length_error",
37
+ "ref": "https://example.com/errors/invalid_length_error.html",
38
+ "message": "Invalid length array exact length 3 length is invalid"
39
+ }
40
+ ],
41
+ "invalid_length_array_range_length_1_2": [
42
+ {
43
+ "type": "invalid_length_error",
44
+ "ref": "https://example.com/errors/invalid_length_error.html",
45
+ "message": "Invalid length array range length 1 2 length is invalid"
46
+ }
47
+ ],
48
+ "invalid_length_string_max_length_3": [
49
+ {
50
+ "type": "invalid_length_error",
51
+ "ref": "https://example.com/errors/invalid_length_error.html",
52
+ "message": "Invalid length string max length 3 length is invalid"
53
+ }
54
+ ],
55
+ "invalid_length_string_min_length_3": [
56
+ {
57
+ "type": "invalid_length_error",
58
+ "ref": "https://example.com/errors/invalid_length_error.html",
59
+ "message": "Invalid length string min length 3 length is invalid"
60
+ }
61
+ ],
62
+ "invalid_length_string_exact_length_3": [
63
+ {
64
+ "type": "invalid_length_error",
65
+ "ref": "https://example.com/errors/invalid_length_error.html",
66
+ "message": "Invalid length string exact length 3 length is invalid"
67
+ }
68
+ ],
69
+ "invalid_length_string_range_length_1_2": [
70
+ {
71
+ "type": "invalid_length_error",
72
+ "ref": "https://example.com/errors/invalid_length_error.html",
73
+ "message": "Invalid length string range length 1 2 length is invalid"
74
+ }
75
+ ],
76
+ "invalid_length_string_max_bytesize_3": [
77
+ {
78
+ "type": "invalid_length_error",
79
+ "ref": "https://example.com/errors/invalid_length_error.html",
80
+ "message": "Invalid length string max bytesize 3 length is invalid"
81
+ }
82
+ ],
83
+ "invalid_length_string_min_bytesize_3": [
84
+ {
85
+ "type": "invalid_length_error",
86
+ "ref": "https://example.com/errors/invalid_length_error.html",
87
+ "message": "Invalid length string min bytesize 3 length is invalid"
88
+ }
89
+ ],
90
+ "invalid_length_string_exact_bytesize_3": [
91
+ {
92
+ "type": "invalid_length_error",
93
+ "ref": "https://example.com/errors/invalid_length_error.html",
94
+ "message": "Invalid length string exact bytesize 3 length is invalid"
95
+ }
96
+ ],
97
+ "invalid_length_string_range_bytesize_1_2": [
98
+ {
99
+ "type": "invalid_length_error",
100
+ "ref": "https://example.com/errors/invalid_length_error.html",
101
+ "message": "Invalid length string range bytesize 1 2 length is invalid"
102
+ }
103
+ ]
104
+ }
105
+ }
106
+ }