glia-errors 0.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,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
+ }