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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6f3de7ff3e455a14c86e7c241ac9456ef39ca93ba40c32fd0a8dfb480986b644
4
+ data.tar.gz: 3ee766edbedf1243cf4036ef90b6e84da1b81585dd9115ed387c802280269850
5
+ SHA512:
6
+ metadata.gz: 96a8f3748400de99c05d3bc237f5d598de09c07b7366a60336a38051af342bf61a14bd35a96572a704f36f8395213b1288ebd5dd5b78099979d082a0d8016b6d
7
+ data.tar.gz: 175ea58ec0bc03a3f7a3271ab5447a2676d892df10eee44825c0effe924fa2b3029ec7c8bfc3ef253247f984847485f42e9f4cf49ef8bee64fac2cb2683532aa
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Eluvia.land s.r.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # Eluvia Base
2
+
3
+ This library provides a set of Ruby on Rails customizations and other helpers in order to meet common Eluvia standards
4
+ for API communication (https://sparktech.myjetbrains.com/youtrack/articles/BASE-A-4/Communication-interface-FE-BE) and
5
+ Eluvia requirements for backend application behavior.
6
+
7
+ It applies to both client and server sides:
8
+ - Pagination and ordering is handled for the API requests (server side).
9
+ - Extended params parsing is implemented for the API requests (server side).
10
+ - Extended filtering is handled for the API requests (server side).
11
+ - Errors are serialized in the API response in a standard way (server side).
12
+ - Integration helpers are implemented to provide easy communication with other Eluvia components (client side).
13
+
14
+ ## 1. Installation
15
+
16
+ ### 1.1. Gem
17
+
18
+ To install the library to your project, just add this to your `Gemfile`:
19
+
20
+ ```ruby
21
+ gem 'eluvia-base'
22
+ ```
23
+
24
+ New dependencies should be installed with:
25
+
26
+ ```bash
27
+ bundle install
28
+ ```
29
+
30
+ ### 1.2. Initializer
31
+
32
+ Next, create an initializer `config/initializers/eluvia_base.rb` with the configuration:
33
+
34
+ ```ruby
35
+ Eluvia::Base.setup do |config|
36
+ config.api_case = ENV.fetch('API_CASE') { '' }
37
+ config.private_api_key = ENV.fetch('PRIVATE_API_KEY') { '' }
38
+ config.public_api_key = ENV.fetch('PUBLIC_API_KEY') { '' }
39
+ end
40
+ ```
41
+
42
+ For full list of configuration options, see `lib/eluvia/base/config.rb` file.
43
+
44
+ ## 2. Usage
45
+
46
+ ### 2.1. Application controller setup
47
+
48
+ In order to use all the implemented functionality, you should integrate all defined handlers to your
49
+ main `ApplicationController`:
50
+
51
+ ```ruby
52
+
53
+ class ApplicationController < ActionController::API
54
+ include Eluvia::ErrorHandler
55
+ include Eluvia::PaginationHandler
56
+ include Eluvia::ParamsHandler
57
+
58
+ # ...
59
+ end
60
+ ```
61
+
62
+ ### 2.2. Pagination
63
+
64
+ In order to interpret `limit`, `offset` and `order_by` parameters, you can user `set_pagination` decorator. It will
65
+ transform the input parameters into the `@order_by`, `@page`, `@limit` and `@padding` instance variables which can be
66
+ easily used in Active Record and Kaminari interfaces.
67
+
68
+ ```ruby
69
+
70
+ class TestRecordsController < ApplicationController
71
+ # ...
72
+
73
+ before_action -> { set_pagination(20, 0, 'created_at:desc') }, only: [:index]
74
+
75
+ # ...
76
+
77
+ def index
78
+ @test_records = TestRecord.all.order(@order_by).page(@page).per(@limit).padding(@padding)
79
+ end
80
+
81
+ # ...
82
+ end
83
+ ```
84
+
85
+ ### 2.3. Params parser
86
+
87
+ You can use `parse_json_param` helper to parse JSON without predefined structure. In case you know the input JSON
88
+ structure, you should use standard "strong params" mechanism.
89
+
90
+ ```ruby
91
+
92
+ class TestRecordsController < ApplicationController
93
+ # ...
94
+
95
+ def create
96
+ @test_record = TestRecord.create(test_record_params)
97
+ # ...
98
+ end
99
+
100
+ # ...
101
+
102
+ def test_record_params
103
+ result = params.permit(
104
+ :param_1,
105
+ :param_2,
106
+ )
107
+ result[:additional_params] = parse_json_param(params[:additional_params])
108
+ result
109
+ end
110
+
111
+ # ...
112
+ end
113
+ ```
114
+
115
+ ### 2.4. Eluvia integration
116
+
117
+ Integration class can be implemented with the help of `Eluvia::EluviaIntegration` concern. This concern acts as
118
+ a wrapper over `RestClient` and brings some functionality for JSON parsing, pagination and Eluvia specific headers
119
+ composition.
120
+
121
+ ```ruby
122
+
123
+ class TestRecordsIntegration
124
+ include Singleton
125
+ include Eluvia::EluviaIntegration
126
+
127
+ def initialize
128
+ @service_url = '...'
129
+ end
130
+
131
+ def get_some_data(session_id)
132
+ parse_get_request(compose_url('some-data'), compose_headers_both(session_id))
133
+ end
134
+
135
+ def post_some_data(session_id, data)
136
+ parse_post_request(compose_url('some-data'), compose_headers_both(session_id), data)
137
+ end
138
+
139
+ end
140
+ ```
141
+
142
+ For the full list of features, see `Eluvia::EluviaIntegration` interface.
143
+
144
+ ### 2.5. Raising error
145
+
146
+ You can raise en exception `Eluvia::Errors::XXX` anywhere in the code and this exception will be automatically
147
+ formatted as standardized error response.
148
+
149
+ For example, this exception:
150
+
151
+ ```ruby
152
+
153
+ raise Eluvia::Errors::NotFound.new('Test record was not found.')
154
+ ```
155
+
156
+ will be formatted into this response:
157
+
158
+ ```json
159
+ {
160
+ "errors": [
161
+ {
162
+ "message": "Test record was not found.",
163
+ "error": "Not Found",
164
+ "status": 404,
165
+ "source": null,
166
+ "timestamp": "2022-11-04T17:11:00.969Z"
167
+ }
168
+ ]
169
+ }
170
+ ```
171
+
172
+ For example, this exception:
173
+
174
+ ```ruby
175
+
176
+ raise Eluvia::Errors::UnprocessableEntity.new('param_1' => 'Param 1 must not be blank.',
177
+ 'param_2' => 'Param 2 must not be blank.')
178
+ ```
179
+
180
+ will be formatted into this response:
181
+
182
+ ```json
183
+ {
184
+ "errors": [
185
+ {
186
+ "message": "Param 1 must not be blank.",
187
+ "error": "Unprocessable Entity",
188
+ "status": 422,
189
+ "source": "param_1",
190
+ "timestamp": "2022-11-04T17:11:00.969Z"
191
+ },
192
+ {
193
+ "message": "Param 2 must not be blank.",
194
+ "error": "Unprocessable Entity",
195
+ "status": 422,
196
+ "source": "param_2",
197
+ "timestamp": "2022-11-04T17:11:00.969Z"
198
+ }
199
+ ]
200
+ }
201
+ ```
202
+
203
+ For the full list of available exceptions, see content of `lib/eluvia/errors` folder.
204
+
205
+ ### 2.6. Chunked/direct upload finalization
206
+
207
+ `Eluvia::File` carries an `upload_key` attribute, an alternative to `content` (base64 encoded file data)
208
+ for `file_attr` fields backed by an underlying ActiveStorage attachment. When `upload_key` is present,
209
+ the setter no longer base64-decodes `content` — instead it delegates to `Eluvia::Uploads.finalizer`, a
210
+ registrable extension point that finalizes a previously uploaded (chunked/direct) temp object into the
211
+ ActiveStorage attachment. This lets a separate library (e.g. a chunked upload provider) implement the
212
+ actual storage logic (S3 `copy_object`, tus, ...) without eluvia-base depending on it.
213
+
214
+ Register a finalizer, typically from an initializer:
215
+
216
+ ```ruby
217
+
218
+ Eluvia::Uploads.finalizer = ->(attachment_record, upload_key, filename) do
219
+ # Resolve `upload_key` in your storage backend and attach the resulting blob to `attachment_record`,
220
+ # using `filename` as the final (sanitized) filename.
221
+ end
222
+ ```
223
+
224
+ If `upload_key` is present on the assigned `Eluvia::File` but no finalizer is registered, a
225
+ `Eluvia::Errors::StandardError` is raised.
@@ -0,0 +1,108 @@
1
+ module Eluvia
2
+ module ActiveModel
3
+ module AmountAttribute
4
+ extend ::ActiveSupport::Concern
5
+ include Eluvia::ActiveModel::Attributes
6
+
7
+ class_methods do
8
+
9
+ # *********************************************************************
10
+ # Attribute store methods
11
+ # *********************************************************************
12
+
13
+ def _amount_attrs
14
+ @amount_attrs ||= {}
15
+ end
16
+
17
+ def amount_attrs
18
+ _amount_attrs.values
19
+ end
20
+
21
+ def amount_attr_names
22
+ amount_attrs.map { |a| a[:attr_name] }
23
+ end
24
+
25
+ def has_amount_attr?(attr_name)
26
+ _amount_attrs.key?(attr_name.to_sym)
27
+ end
28
+
29
+ def find_amount_attr(attr_name)
30
+ _amount_attrs.fetch(attr_name.to_sym, nil)
31
+ end
32
+
33
+ # *********************************************************************
34
+ # Transform value methods
35
+ # *********************************************************************
36
+
37
+ def convert_value_from_major_to_minor(value_major, precision: 2)
38
+ if value_major.nil?
39
+ nil
40
+ else
41
+ (value_major.to_f * (10**precision)).round(0).to_i
42
+ end
43
+ end
44
+
45
+ def convert_value_from_minor_to_major(value_minor, precision: 2)
46
+ if value_minor.nil?
47
+ nil
48
+ else
49
+ (value_minor.to_f / (10**precision)).round(precision)
50
+ end
51
+ end
52
+
53
+ # *********************************************************************
54
+ # Attribute definition methods
55
+ # *********************************************************************
56
+
57
+ # Adds a new amount attribute to this model
58
+ #
59
+ # @param attr_name [Symbol] Attribute name
60
+ # @param precision [Integer] Number of decimal places
61
+ # @param declare [Boolean] Whether to declare the underlying attribute as integer
62
+ def amount_attr(attr_name, precision: 2, declare: false)
63
+ if declare
64
+ integer_attr attr_name
65
+ end
66
+
67
+ # Prepare internal structure
68
+ _amount_attrs[attr_name] = {
69
+ precision: precision,
70
+ }
71
+
72
+ # Define attribute for minor units
73
+ minor_attr_name = :"#{attr_name}_minor"
74
+ float_attr minor_attr_name
75
+
76
+ # Get a value in minor units
77
+ define_method(minor_attr_name) do
78
+ self.send(attr_name)
79
+ end
80
+
81
+ # Set a value in minor units
82
+ define_method(:"#{minor_attr_name}=") do |value_minor|
83
+ self.send(:"#{attr_name}=", value_minor)
84
+ value_minor
85
+ end
86
+
87
+ # Define attribute for major units
88
+ major_attr_name = :"#{attr_name}_major"
89
+ float_attr major_attr_name
90
+
91
+ # Get a value in major units
92
+ define_method(major_attr_name) do
93
+ self.class.convert_value_from_minor_to_major(self.send(attr_name), precision: precision)
94
+ end
95
+
96
+ # Set a value in major units
97
+ define_method(:"#{major_attr_name}=") do |value_major|
98
+ self.send(:"#{attr_name}=", self.class.convert_value_from_major_to_minor(value_major, precision: precision))
99
+ value_major
100
+ end
101
+
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,134 @@
1
+ module Eluvia
2
+ module ActiveModel
3
+ module ArrayAttribute
4
+ extend ::ActiveSupport::Concern
5
+ include Eluvia::ActiveModel::Attributes
6
+
7
+ class_methods do
8
+
9
+ # *********************************************************************
10
+ # Attribute store methods
11
+ # *********************************************************************
12
+
13
+ def _array_attrs
14
+ @array_attrs ||= {}
15
+ end
16
+
17
+ def array_attrs
18
+ _array_attrs.values
19
+ end
20
+
21
+ def array_attr_names
22
+ array_attrs.map { |a| a[:attr_name] }
23
+ end
24
+
25
+ def has_array_attr?(attr_name)
26
+ _array_attrs.key?(attr_name.to_sym)
27
+ end
28
+
29
+ def find_array_attr(attr_name)
30
+ _array_attrs.fetch(attr_name.to_sym, nil)
31
+ end
32
+
33
+ # *********************************************************************
34
+ # Attribute definition methods for array
35
+ # *********************************************************************
36
+
37
+ def array_column(attr_name, options = {})
38
+ array_attr(attr_name, **options)
39
+ end
40
+
41
+ # Add a new "array" attribute to this model
42
+ #
43
+ # @param attr_name [Symbol] Attribute name
44
+ # @param type [Symbol] Type of the array elements (:any, :string, :uuid, :date, :datetime, :integer, :float, :boolean)
45
+ # @param filter [Proc, nil] Optional filter proc to filter array elements
46
+ # @param transform [Proc, nil] Optional transform proc to transform array elements
47
+ # @param serialize [Boolean] Whether to serialize the array as JSON string in the underlying attribute
48
+ # @param declare [Boolean] Whether to declare the underlying attribute as any
49
+ def array_attr(attr_name, type: :string, filter: nil, transform: nil, serialize: false, declare: false)
50
+ attr_name = attr_name.to_sym
51
+ type = type.to_sym
52
+
53
+ # Declare underlying attribute
54
+ if declare
55
+ any_attr attr_name
56
+ end
57
+
58
+ # Type check
59
+ raise Eluvia::Errors::StandardError.new("Unknown type `#{type}`.") unless Eluvia::ActiveModel::Attributes::SIMPLE_TYPES.include?(type)
60
+
61
+ # Register metadata
62
+ self._array_attrs[attr_name] = {
63
+ attr_name: attr_name,
64
+ type: type,
65
+ filter: filter,
66
+ transform: transform,
67
+ serialize: serialize
68
+ }
69
+
70
+ # *******************************************************************
71
+ # Dynamically defined instance methods for array
72
+ # *******************************************************************
73
+
74
+ # Set method
75
+ define_method(:"#{attr_name}=") do |value|
76
+
77
+ # Convert string to array, we expect JSON string here
78
+ if value.is_a?(::String)
79
+ if !value.blank?
80
+ value = JSON.parse(value) rescue nil
81
+ else
82
+ value = nil
83
+ end
84
+ end
85
+
86
+ # Check input
87
+ raise Eluvia::Errors::BadRequest.new("Invalid value for array attribute `#{attr_name}`, expecting Array or nil.") unless value.nil? || value.is_a?(::Array)
88
+
89
+ # Handle blank value
90
+ if value.blank?
91
+ write_attribute(attr_name, nil)
92
+ return
93
+ end
94
+
95
+ # Clean values according to the specified type
96
+ value = value.map { |item| self.class.send(:"clean_#{type}_value", item) }
97
+
98
+ # Optional value filter
99
+ if filter && filter.respond_to?(:call)
100
+ value = value.select { |item| filter.call(item) }
101
+ end
102
+
103
+ # Optional value transform
104
+ if transform && transform.respond_to?(:call)
105
+ value = value.map { |item| transform.call(item) }.compact
106
+ end
107
+
108
+ # Store to the underlying attribute
109
+ write_attribute(attr_name, serialize ? value.to_json : value)
110
+ end
111
+
112
+ # Get method
113
+ define_method(attr_name) do
114
+ # Read value from the underlying attribute and parse it if needed
115
+ value = read_attribute(attr_name)
116
+ value = JSON.parse(value) rescue nil if serialize
117
+
118
+ # Handle blank value
119
+ return nil if value.blank?
120
+
121
+ # Check output
122
+ raise Eluvia::Errors::StandardError.new("Invalid value for array attribute `#{attr_name}`, expecting Array.") unless value.is_a?(::Array)
123
+
124
+ # Clean values according to the specified type
125
+ value = value.map { |item| self.class.send(:"clean_#{type}_value", item) }
126
+
127
+ value
128
+ end
129
+ end
130
+
131
+ end
132
+ end
133
+ end
134
+ end