eluvia-base 3.37.1

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +225 -0
  4. data/lib/eluvia/active_model/amount_attribute.rb +108 -0
  5. data/lib/eluvia/active_model/array_attribute.rb +134 -0
  6. data/lib/eluvia/active_model/attributes.rb +417 -0
  7. data/lib/eluvia/active_model/enum_array_attribute.rb +60 -0
  8. data/lib/eluvia/active_model/enum_attribute.rb +174 -0
  9. data/lib/eluvia/active_model/object_attribute.rb +143 -0
  10. data/lib/eluvia/active_model/range_attribute.rb +74 -0
  11. data/lib/eluvia/active_record/file_array_attribute.rb +116 -0
  12. data/lib/eluvia/active_record/file_attribute.rb +158 -0
  13. data/lib/eluvia/active_record/filtering.rb +680 -0
  14. data/lib/eluvia/active_record/filters_model.rb +58 -0
  15. data/lib/eluvia/active_record/ordering.rb +110 -0
  16. data/lib/eluvia/base/config.rb +38 -0
  17. data/lib/eluvia/base/version.rb +12 -0
  18. data/lib/eluvia/errors/bad_request.rb +17 -0
  19. data/lib/eluvia/errors/forbidden.rb +17 -0
  20. data/lib/eluvia/errors/not_found.rb +17 -0
  21. data/lib/eluvia/errors/service_unavailable.rb +17 -0
  22. data/lib/eluvia/errors/standard_error.rb +33 -0
  23. data/lib/eluvia/errors/unauthorized.rb +17 -0
  24. data/lib/eluvia/errors/unprocessable_entity.rb +26 -0
  25. data/lib/eluvia/fieldset/rest_field.rb +83 -0
  26. data/lib/eluvia/fieldset/rest_fieldset.rb +158 -0
  27. data/lib/eluvia/fieldset.rb +12 -0
  28. data/lib/eluvia/handlers/error_handler.rb +60 -0
  29. data/lib/eluvia/handlers/pagination_handler.rb +25 -0
  30. data/lib/eluvia/handlers/params_handler.rb +43 -0
  31. data/lib/eluvia/helpers/attachment_helper.rb +37 -0
  32. data/lib/eluvia/integrations/eluvia_integration.rb +281 -0
  33. data/lib/eluvia/models/file.rb +44 -0
  34. data/lib/eluvia/models/file_wrapper.rb +38 -0
  35. data/lib/eluvia/models/image_wrapper.rb +38 -0
  36. data/lib/eluvia/serializers/error_serializer.rb +21 -0
  37. data/lib/eluvia/serializers/fieldset_serializer.rb +63 -0
  38. data/lib/eluvia/services/health_service.rb +56 -0
  39. data/lib/eluvia/uploads.rb +13 -0
  40. data/lib/eluvia/utils/hash_and_array.rb +98 -0
  41. data/lib/eluvia/utils/jwt.rb +36 -0
  42. data/lib/eluvia/utils/string.rb +31 -0
  43. data/lib/eluvia/utils/uuid.rb +21 -0
  44. data/lib/eluvia-base.rb +79 -0
  45. metadata +186 -0
@@ -0,0 +1,58 @@
1
+ module Eluvia
2
+ module ActiveRecord
3
+ module FiltersModel
4
+ extend ::ActiveSupport::Concern
5
+ include Eluvia::ActiveModel::Attributes # to support xxx_attr methods
6
+ include Eluvia::ActiveModel::RangeAttribute # to support range_attr methods
7
+ include Eluvia::ActiveModel::EnumAttribute # to support enum_attr methods
8
+
9
+ included do
10
+ extend ::ActiveModel::Translation # to support human_attribute_name method
11
+ end
12
+
13
+ class_methods do
14
+
15
+ def search_fields(*fields)
16
+ if fields.empty?
17
+ @_search_fields ||= []
18
+ else
19
+ @_search_fields = fields
20
+ end
21
+ end
22
+
23
+ def _filter_attrs
24
+ @filter_attrs ||= {}
25
+ end
26
+
27
+ def filter_attrs
28
+ _filter_attrs.values
29
+ end
30
+
31
+ def filter_attr_names
32
+ filter_attrs.map { |fa| fa[:attr_name] }
33
+ end
34
+
35
+ def has_filter_attr?(attr_name)
36
+ _filter_attrs.key?(attr_name.to_sym)
37
+ end
38
+
39
+ def find_filter_attr(attr_name)
40
+ _filter_attrs.fetch(attr_name.to_sym, nil)
41
+ end
42
+
43
+ def filter_attr(attr_name, type, options = {}, &proc)
44
+ attr_name = attr_name.to_sym
45
+
46
+ simple_attr attr_name, type
47
+
48
+ _filter_attrs[attr_name] = {
49
+ attr_name: attr_name,
50
+ type: type,
51
+ options: options,
52
+ proc: proc
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,110 @@
1
+ module Eluvia
2
+ module ActiveRecord
3
+ module Ordering
4
+ extend ::ActiveSupport::Concern
5
+
6
+ class_methods do
7
+
8
+ def _order_attrs
9
+ @order_attrs ||= {}
10
+ end
11
+
12
+ def order_attrs
13
+ _order_attrs.values
14
+ end
15
+
16
+ def has_order_attr?(attr_name)
17
+ _order_attrs.key?(attr_name.to_sym)
18
+ end
19
+
20
+ def find_order_attr(attr_name)
21
+ _order_attrs.fetch(attr_name.to_sym, nil)
22
+ end
23
+
24
+ # Registers a virtual sortable column backed by a custom Arel/SQL expression (e.g. a
25
+ # correlated subquery), for values that don't map to a real column or association. Escape
26
+ # hatch analogous to `filter_attr` in the Filtering concern.
27
+ #
28
+ # @param attr_name [Symbol] name usable as a key in the `order_by` hash passed to `order_2`
29
+ # @yield returns the SQL expression (String or Arel node) to order by
30
+ def order_attr(attr_name, &expr_proc)
31
+ unless expr_proc
32
+ raise Eluvia::Errors::StandardError.new('order_attr requires a block returning the order expression.')
33
+ end
34
+ _order_attrs[attr_name.to_sym] = { attr_name: attr_name.to_sym, expr_proc: expr_proc }
35
+ end
36
+
37
+ # Adds support for ordering over related models
38
+ #
39
+ # @param order_by [Hash] Hash of column names and order directions (e.g., { 'name' => :asc, 'related_model__column' => :desc })
40
+ def order_2(order_by)
41
+ scope = all
42
+
43
+ # Collect order clauses as an array so we can mix simple hash entries with
44
+ # Arel.sql fragments (needed for `NULLS LAST` on LEFT-joined association columns).
45
+ order_clauses = []
46
+ order_by.each do |order_column, order_direction|
47
+ order_direction = order_direction.to_sym
48
+ msg = I18n.t('eluvia.errors.ordering.invalid_direction',
49
+ default: 'Invalid order direction. Allowed values are `asc` and `desc`.')
50
+ raise Eluvia::Errors::UnprocessableEntity.new({ "order_by__#{order_column}" => msg }) \
51
+ unless %i[asc desc].include?(order_direction)
52
+
53
+ if has_order_attr?(order_column)
54
+
55
+ # Custom order attr, handled with the highest priority (same as `filter_attr`)
56
+ expr = find_order_attr(order_column)[:expr_proc].call
57
+ expr_sql = expr.respond_to?(:to_sql) ? expr.to_sql : expr.to_s
58
+ order_clauses << Arel.sql("(#{expr_sql}) #{order_direction.to_s.upcase} NULLS LAST")
59
+ elsif order_column.to_s.include?('__')
60
+
61
+ # Split the column name into association and actual column name
62
+ split_column = order_column.to_s.split('__')
63
+ msg = I18n.t('eluvia.errors.ordering.invalid_definition',
64
+ default: 'Invalid definition for an `order by` attribute. Expecting only one `__` delimiter.')
65
+ raise Eluvia::Errors::UnprocessableEntity.new({ "order_by__#{order_column}" => msg }) \
66
+ unless split_column.length == 2
67
+
68
+ # Check if the association exists on the model
69
+ association_name = split_column[0].to_sym
70
+ reflection = self.reflect_on_association(association_name)
71
+ msg = I18n.t('eluvia.errors.ordering.association_not_found', default: 'Association not found.')
72
+ raise Eluvia::Errors::UnprocessableEntity.new({ "order_by__#{order_column}" => msg }) unless reflection
73
+
74
+ # Retrieve the table name of the associated model
75
+ associated_table_name = reflection.klass.table_name
76
+
77
+ # Check that the column exists on the associated model
78
+ column_name = split_column[1]
79
+ msg = I18n.t('eluvia.errors.ordering.not_found_on_association', default: 'Does not exist on association.')
80
+ raise Eluvia::Errors::UnprocessableEntity.new({ "order_by__#{order_column}" => msg }) \
81
+ unless column_name.present? && reflection.klass.column_names.include?(column_name)
82
+
83
+ # LEFT JOIN so records with a NULL FK (or no matching associated row) are kept;
84
+ # NULLS LAST so those rows sort to the end regardless of asc/desc direction.
85
+ scope = scope.left_joins(association_name)
86
+ order_clauses << Arel.sql(
87
+ "#{associated_table_name}.#{column_name} #{order_direction.to_s.upcase} NULLS LAST"
88
+ )
89
+ else
90
+
91
+ # Check that the column exists on this model
92
+ msg = I18n.t('eluvia.errors.ordering.not_found', default: 'Does not exist.')
93
+ raise Eluvia::Errors::UnprocessableEntity.new({ "order_by__#{order_column}" => msg }) \
94
+ unless self.column_names.include?(order_column.to_s)
95
+
96
+ # Standard column, just add it to the order clauses
97
+ order_clauses << { order_column => order_direction }
98
+ end
99
+ end
100
+
101
+ # Apply ordering to the collection (each clause appends to the ORDER BY chain)
102
+ order_clauses.each { |clause| scope = scope.order(clause) }
103
+
104
+ scope
105
+ end
106
+
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,38 @@
1
+ module Eluvia
2
+ module Base
3
+ module Config
4
+
5
+ # Which case paradigm the API should follow.
6
+ #
7
+ # Available values:
8
+ # - snake_case Uses snake case for all JSON keys in API (default).
9
+ # - camel_case Uses camel case for all JSON keys in API.
10
+ mattr_accessor :api_case
11
+ @@api_case = 'snake_case'
12
+
13
+ # API key recognized in x-private-api-key header. Used for server-to-server authorization in case of session_id authorization method. Also acts as secret key for JWT encoding/decoding.
14
+ mattr_accessor :private_api_key
15
+
16
+ # API key recognized in x-public-api-key header. Used for server-to-server authorization in case of session_id authorization method.
17
+ mattr_accessor :public_api_key
18
+
19
+ # Authorization method.
20
+ #
21
+ # Available values:
22
+ # - jwt Uses JSON Web Tokens (JWT) in the 'authorization' or 'x-authorization' header (default).
23
+ # - session_id Uses session IDs in the 'session-id' header in combination with 'x-private-api-key' and 'x-public-api-key' headers.
24
+ mattr_accessor :authorization_method
25
+ @@authorization_method = 'jwt'
26
+
27
+ # Whether to use secure JWT tokens with issue time and unique identifiers. If false, JWT tokens will not have
28
+ # issue time and will not have unique identifiers, which can be useful for testing purposes. (default: true)
29
+ mattr_accessor :secure_jwt
30
+ @@secure_jwt = true
31
+
32
+ # UUID seed used for generating UUIDs.
33
+ mattr_accessor :uuid_seed
34
+ @@uuid_seed = '12345678-1234-1234-1234-1234567890ab'
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ require 'json'
2
+
3
+ module Eluvia
4
+ module Base
5
+ def self.version
6
+ path = File.expand_path('../../../version.json', __dir__)
7
+ file = File.read(path)
8
+ data = JSON.parse(file)
9
+ data['version']
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ module Eluvia
2
+ module Errors
3
+ class BadRequest < Eluvia::Errors::StandardError
4
+
5
+ def initialize(message = nil)
6
+ super(
7
+ message || "The server cannot or will not process the request due to an apparent client error.",
8
+ "Bad Request",
9
+ 400,
10
+ nil,
11
+ false
12
+ )
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Eluvia
2
+ module Errors
3
+ class Forbidden < Eluvia::Errors::StandardError
4
+
5
+ def initialize(message = nil)
6
+ super(
7
+ message || "The request contained valid data and was understood by the server, but the server is refusing action.",
8
+ "Forbidden",
9
+ 403,
10
+ nil,
11
+ false
12
+ )
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Eluvia
2
+ module Errors
3
+ class NotFound < Eluvia::Errors::StandardError
4
+
5
+ def initialize(message = nil)
6
+ super(
7
+ message || "The requested resource could not be found but may be available in the future.",
8
+ "Not Found",
9
+ 404,
10
+ nil,
11
+ false
12
+ )
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Eluvia
2
+ module Errors
3
+ class ServiceUnavailable < Eluvia::Errors::StandardError
4
+
5
+ def initialize(message = nil)
6
+ super(
7
+ message || "The server cannot handle the request (because it is overloaded or down for maintenance).",
8
+ "Service Unavailable",
9
+ 503,
10
+ nil,
11
+ false
12
+ )
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ module Eluvia
2
+ module Errors
3
+ class StandardError < ::StandardError
4
+
5
+ attr_reader :message, :error, :status, :source, :timestamp, :no_sentry
6
+
7
+ def initialize(message = nil, error = nil, status = nil, source = nil, no_sentry = false)
8
+ super(message)
9
+ @message = message || "We encountered unexpected error, but our developers had been already notified about it"
10
+ @error = error || "Internal Server Error"
11
+ @status = status || 500
12
+ @source = source
13
+ @timestamp = Time.current
14
+ @no_sentry = no_sentry
15
+ end
16
+
17
+ def to_h
18
+ {
19
+ message: message,
20
+ error: error,
21
+ status: status,
22
+ source: source,
23
+ timestamp: timestamp
24
+ }
25
+ end
26
+
27
+ def to_s
28
+ to_h.to_s
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ module Eluvia
2
+ module Errors
3
+ class Unauthorized < Eluvia::Errors::StandardError
4
+
5
+ def initialize(message = nil, no_sentry = false)
6
+ super(
7
+ message || "Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided.",
8
+ "Unauthorized",
9
+ 401,
10
+ nil,
11
+ no_sentry
12
+ )
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ module Eluvia
2
+ module Errors
3
+ class UnprocessableEntity < Eluvia::Errors::StandardError
4
+
5
+ attr_reader :messages
6
+
7
+ def initialize(messages = {})
8
+ super(nil, "Unprocessable Entity", 422, nil, false)
9
+ @messages = messages
10
+ end
11
+
12
+ def to_h
13
+ messages.reduce([]) do |r, (attribute, message)|
14
+ r << {
15
+ message: (message.is_a?(Array) ? message.join(', ') : message).humanize,
16
+ error: error,
17
+ status: status,
18
+ source: attribute,
19
+ timestamp: timestamp
20
+ }
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,83 @@
1
+ module Eluvia
2
+ module Fieldset
3
+ class RestField
4
+
5
+ FIELDSET_PATTERN = /\A[^()]+\(.+\)\z/
6
+ private_constant :FIELDSET_PATTERN
7
+
8
+ attr_reader :name
9
+ attr_accessor :fieldset
10
+
11
+ def initialize(name, fieldset = nil)
12
+ @name = name
13
+ @fieldset = fieldset
14
+ end
15
+
16
+ def self.create_from_string(field_string, default_fieldset_string: nil)
17
+ if FIELDSET_PATTERN.match?(field_string)
18
+ name, subfields_string = field_string[0..-2].split('(', 2)
19
+ fieldset = RestFieldset.create_from_string(subfields_string, default_field_fieldset_string: default_fieldset_string)
20
+ else
21
+ name = field_string
22
+ fieldset = default_fieldset_string.present? ? RestFieldset.create_from_string(default_fieldset_string) : nil
23
+ end
24
+ new(name, fieldset)
25
+ end
26
+
27
+ def deep_dup
28
+ self.class.new(name, fieldset&.deep_dup)
29
+ end
30
+
31
+ def join(rest_field)
32
+ raise ArgumentError, "name mismatch ('#{name}' != '#{rest_field.name}')" unless name == rest_field.name
33
+
34
+ field = deep_dup
35
+ if field.fieldset.present? && rest_field.fieldset.present?
36
+ field.fieldset = field.fieldset.join(rest_field.fieldset)
37
+ elsif rest_field.fieldset.present?
38
+ field.fieldset = field.fieldset&.deep_dup
39
+ end
40
+ field
41
+ end
42
+
43
+ def intersection(rest_field)
44
+ raise ArgumentError, "name mismatch ('#{name}' != '#{rest_field.name}')" unless name == rest_field.name
45
+
46
+ field = deep_dup
47
+ if field.fieldset.present? && rest_field.fieldset.present?
48
+ field.fieldset = field.fieldset.intersection(rest_field.fieldset)
49
+ elsif rest_field.fieldset.present?
50
+ field.fieldset = rest_field.fieldset.deep_dup
51
+ end
52
+ field
53
+ end
54
+
55
+ def left_intersection(rest_field)
56
+ raise ArgumentError, "name mismatch ('#{name}' != '#{rest_field.name}')" unless name == rest_field.name
57
+
58
+ field = deep_dup
59
+ if field.fieldset.present? && rest_field.fieldset.present?
60
+ field.fieldset = field.fieldset.left_intersection(rest_field.fieldset)
61
+ end
62
+ field
63
+ end
64
+
65
+ def |(other)
66
+ join(other)
67
+ end
68
+
69
+ def &(other)
70
+ intersection(other)
71
+ end
72
+
73
+ def to_s
74
+ fieldset.present? ? "#{name}(#{fieldset})" : name
75
+ end
76
+
77
+ def ==(other)
78
+ other.is_a?(RestField) && name == other.name && fieldset == other.fieldset
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,158 @@
1
+ module Eluvia
2
+ module Fieldset
3
+ class RestFieldset
4
+ include Enumerable
5
+
6
+ def initialize(*fields)
7
+ @fields_map = {}
8
+ fields.each { |field| append(field) }
9
+ end
10
+
11
+ class << self
12
+
13
+ def create_from_list(fields, default_field_fieldset_string: nil)
14
+ new(*fields.map { |field_string| RestField.create_from_string(field_string, default_fieldset_string: default_field_fieldset_string) })
15
+ end
16
+
17
+ def create_from_string(fields_string, default_field_fieldset_string: nil)
18
+ new(*split_fields(fields_string).map { |field_string| RestField.create_from_string(field_string, default_fieldset_string: default_field_fieldset_string) })
19
+ end
20
+
21
+ private
22
+
23
+ def split_fields(fields_string)
24
+ fields = []
25
+ brackets = 0
26
+ field = +''
27
+
28
+ fields_string.each_char do |char|
29
+ if char == ',' && brackets.zero?
30
+ field = field.strip
31
+ fields << field unless field.empty?
32
+ field = +''
33
+ next
34
+ end
35
+
36
+ brackets += 1 if char == '('
37
+ brackets -= 1 if char == ')'
38
+ field << char
39
+ end
40
+
41
+ field = field.strip
42
+ fields << field unless field.empty?
43
+ fields
44
+ end
45
+
46
+ end
47
+
48
+ def fields
49
+ @fields_map.values
50
+ end
51
+
52
+ def join(rest_fieldset)
53
+ fieldset = deep_dup
54
+ rest_fieldset.fields.each do |rf|
55
+ fieldset.fields_map[rf.name] = if fieldset.fields_map.key?(rf.name)
56
+ fieldset.fields_map[rf.name].join(rf)
57
+ else
58
+ rf.deep_dup
59
+ end
60
+ end
61
+ fieldset
62
+ end
63
+
64
+ def intersection(rest_fieldset)
65
+ fieldset = deep_dup
66
+ fields_map = fieldset.fields_map
67
+ fieldset.fields_map = {}
68
+
69
+ fields_map.each do |name, rf|
70
+ fieldset.append(rf.intersection(rest_fieldset.fields_map[name])) if rest_fieldset.fields_map.key?(name)
71
+ end
72
+ fieldset
73
+ end
74
+
75
+ def left_intersection(rest_fieldset)
76
+ fieldset = deep_dup
77
+ fields_map = fieldset.fields_map
78
+ fieldset.fields_map = {}
79
+
80
+ fields_map.each do |name, rf|
81
+ fieldset.append(rf.left_intersection(rest_fieldset.fields_map[name])) if rest_fieldset.fields_map.key?(name)
82
+ end
83
+ fieldset
84
+ end
85
+
86
+ def delete(key)
87
+ @fields_map.delete(key.to_s)
88
+ end
89
+
90
+ def |(other)
91
+ join(other)
92
+ end
93
+
94
+ def &(other)
95
+ intersection(other)
96
+ end
97
+
98
+ def deep_dup
99
+ self.class.new(*fields.map(&:deep_dup))
100
+ end
101
+
102
+ def to_s
103
+ fields.join(',')
104
+ end
105
+
106
+ def empty?
107
+ @fields_map.empty?
108
+ end
109
+
110
+ def get(key)
111
+ @fields_map[key.to_s]
112
+ end
113
+ alias [] get
114
+
115
+ def append(field)
116
+ rest_field =
117
+ case field
118
+ when RestField
119
+ field
120
+ when String
121
+ RestField.create_from_string(field)
122
+ else
123
+ raise ArgumentError, "field can be only RestField or string ('#{field}' [#{field.class}])"
124
+ end
125
+
126
+ rest_field = @fields_map[rest_field.name].join(rest_field) if @fields_map.key?(rest_field.name)
127
+
128
+ @fields_map[rest_field.name] = rest_field
129
+ self
130
+ end
131
+
132
+ def flat
133
+ @fields_map.keys.to_set
134
+ end
135
+
136
+ def each(&block)
137
+ fields.each(&block)
138
+ end
139
+
140
+ def include?(key)
141
+ @fields_map.key?(key.to_s)
142
+ end
143
+
144
+ def has_any_key(*keys)
145
+ keys.any? { |key| include?(key) }
146
+ end
147
+
148
+ def ==(other)
149
+ other.is_a?(RestFieldset) && fields_map == other.fields_map
150
+ end
151
+
152
+ protected
153
+
154
+ attr_accessor :fields_map
155
+
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,12 @@
1
+ require 'eluvia/fieldset/rest_field'
2
+ require 'eluvia/fieldset/rest_fieldset'
3
+
4
+ module Eluvia
5
+ module Fieldset
6
+ ALL_FIELDS = '__all__'.freeze
7
+ DEFAULT_FIELDS = '__default__'.freeze
8
+
9
+ RF = RestField
10
+ RFS = RestFieldset
11
+ end
12
+ end
@@ -0,0 +1,60 @@
1
+ module Eluvia
2
+ module ErrorHandler
3
+ extend ::ActiveSupport::Concern
4
+
5
+ ERRORS = {
6
+ 'ActiveRecord::RecordNotFound' => 'Eluvia::Errors::NotFound',
7
+ 'JSON::ParserError' => 'Eluvia::Errors::BadRequest',
8
+ }.freeze
9
+
10
+ included do
11
+ rescue_from(::StandardError, with: ->(e) { handle_error(e) })
12
+ end
13
+
14
+ private
15
+
16
+ def handle_error(error)
17
+ if defined?(Sentry)
18
+ no_sentry = defined?(error.no_sentry) ? !!error.no_sentry : false
19
+ unless no_sentry
20
+ Sentry.capture_exception(error)
21
+ end
22
+ end
23
+ log_error(error)
24
+ render_error(map_error(error))
25
+ end
26
+
27
+ def map_error(error)
28
+ return error if error.class <= Errors::StandardError
29
+ if ERRORS.key?(error.class.name)
30
+ ERRORS[error.class.name].constantize.new(error.to_s)
31
+ else
32
+ Eluvia::Errors::StandardError.new(error.to_s)
33
+ end
34
+ end
35
+
36
+ def render_error(error)
37
+ render json: Eluvia::ErrorSerializer.new(error), status: error.status
38
+ end
39
+
40
+ def log_error(error)
41
+ if error.respond_to?(:messages)
42
+ message = error.messages.values.join(', ')
43
+ elsif error.respond_to?(:message)
44
+ message = error.message
45
+ else
46
+ message = 'Unknown error occurred.'
47
+ end
48
+ message = "#{error.class.name}: #{message}"
49
+ if error.respond_to?(:status)
50
+ if error.status.to_i >= 500
51
+ Rails.logger.error(message)
52
+ else
53
+ Rails.logger.info(message)
54
+ end
55
+ else
56
+ Rails.logger.error(message)
57
+ end
58
+ end
59
+ end
60
+ end