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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +225 -0
- data/lib/eluvia/active_model/amount_attribute.rb +108 -0
- data/lib/eluvia/active_model/array_attribute.rb +134 -0
- data/lib/eluvia/active_model/attributes.rb +417 -0
- data/lib/eluvia/active_model/enum_array_attribute.rb +60 -0
- data/lib/eluvia/active_model/enum_attribute.rb +174 -0
- data/lib/eluvia/active_model/object_attribute.rb +143 -0
- data/lib/eluvia/active_model/range_attribute.rb +74 -0
- data/lib/eluvia/active_record/file_array_attribute.rb +116 -0
- data/lib/eluvia/active_record/file_attribute.rb +158 -0
- data/lib/eluvia/active_record/filtering.rb +680 -0
- data/lib/eluvia/active_record/filters_model.rb +58 -0
- data/lib/eluvia/active_record/ordering.rb +110 -0
- data/lib/eluvia/base/config.rb +38 -0
- data/lib/eluvia/base/version.rb +12 -0
- data/lib/eluvia/errors/bad_request.rb +17 -0
- data/lib/eluvia/errors/forbidden.rb +17 -0
- data/lib/eluvia/errors/not_found.rb +17 -0
- data/lib/eluvia/errors/service_unavailable.rb +17 -0
- data/lib/eluvia/errors/standard_error.rb +33 -0
- data/lib/eluvia/errors/unauthorized.rb +17 -0
- data/lib/eluvia/errors/unprocessable_entity.rb +26 -0
- data/lib/eluvia/fieldset/rest_field.rb +83 -0
- data/lib/eluvia/fieldset/rest_fieldset.rb +158 -0
- data/lib/eluvia/fieldset.rb +12 -0
- data/lib/eluvia/handlers/error_handler.rb +60 -0
- data/lib/eluvia/handlers/pagination_handler.rb +25 -0
- data/lib/eluvia/handlers/params_handler.rb +43 -0
- data/lib/eluvia/helpers/attachment_helper.rb +37 -0
- data/lib/eluvia/integrations/eluvia_integration.rb +281 -0
- data/lib/eluvia/models/file.rb +44 -0
- data/lib/eluvia/models/file_wrapper.rb +38 -0
- data/lib/eluvia/models/image_wrapper.rb +38 -0
- data/lib/eluvia/serializers/error_serializer.rb +21 -0
- data/lib/eluvia/serializers/fieldset_serializer.rb +63 -0
- data/lib/eluvia/services/health_service.rb +56 -0
- data/lib/eluvia/uploads.rb +13 -0
- data/lib/eluvia/utils/hash_and_array.rb +98 -0
- data/lib/eluvia/utils/jwt.rb +36 -0
- data/lib/eluvia/utils/string.rb +31 -0
- data/lib/eluvia/utils/uuid.rb +21 -0
- data/lib/eluvia-base.rb +79 -0
- 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,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,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
|