Eluvia Base

This library provides a set of Ruby on Rails customizations and other helpers in order to meet common Eluvia standards for API communication (https://sparktech.myjetbrains.com/youtrack/articles/BASE-A-4/Communication-interface-FE-BE) and Eluvia requirements for backend application behavior.

It applies to both client and server sides:

  • Pagination and ordering is handled for the API requests (server side).
  • Extended params parsing is implemented for the API requests (server side).
  • Extended filtering is handled for the API requests (server side).
  • Errors are serialized in the API response in a standard way (server side).
  • Integration helpers are implemented to provide easy communication with other Eluvia components (client side).

1. Installation

1.1. Gem

To install the library to your project, just add this to your Gemfile:

gem 'eluvia-base'

New dependencies should be installed with:

bundle install

1.2. Initializer

Next, create an initializer config/initializers/eluvia_base.rb with the configuration:

Eluvia::Base.setup do |config|
  config.api_case = ENV.fetch('API_CASE') { '' }
  config.private_api_key = ENV.fetch('PRIVATE_API_KEY') { '' }
  config.public_api_key = ENV.fetch('PUBLIC_API_KEY') { '' }
end

For full list of configuration options, see lib/eluvia/base/config.rb file.

2. Usage

2.1. Application controller setup

In order to use all the implemented functionality, you should integrate all defined handlers to your main ApplicationController:


class ApplicationController < ActionController::API
  include Eluvia::ErrorHandler
  include Eluvia::PaginationHandler
  include Eluvia::ParamsHandler

  # ...
end

2.2. Pagination

In order to interpret limit, offset and order_by parameters, you can user set_pagination decorator. It will transform the input parameters into the @order_by, @page, @limit and @padding instance variables which can be easily used in Active Record and Kaminari interfaces.


class TestRecordsController < ApplicationController
  # ...

  before_action -> { set_pagination(20, 0, 'created_at:desc') }, only: [:index]

  # ...

  def index
    @test_records = TestRecord.all.order(@order_by).page(@page).per(@limit).padding(@padding)
  end

  # ...
end

2.3. Params parser

You can use parse_json_param helper to parse JSON without predefined structure. In case you know the input JSON structure, you should use standard "strong params" mechanism.


class TestRecordsController < ApplicationController
  # ...

  def create
    @test_record = TestRecord.create(test_record_params)
    # ...
  end

  # ...

  def test_record_params
    result = params.permit(
      :param_1,
      :param_2,
    )
    result[:additional_params] = parse_json_param(params[:additional_params])
    result
  end

  # ...
end

2.4. Eluvia integration

Integration class can be implemented with the help of Eluvia::EluviaIntegration concern. This concern acts as a wrapper over RestClient and brings some functionality for JSON parsing, pagination and Eluvia specific headers composition.


class TestRecordsIntegration
  include Singleton
  include Eluvia::EluviaIntegration

  def initialize
    @service_url = '...'
  end

  def get_some_data(session_id)
    parse_get_request(compose_url('some-data'), compose_headers_both(session_id))
  end

  def post_some_data(session_id, data)
    parse_post_request(compose_url('some-data'), compose_headers_both(session_id), data)
  end

end

For the full list of features, see Eluvia::EluviaIntegration interface.

2.5. Raising error

You can raise en exception Eluvia::Errors::XXX anywhere in the code and this exception will be automatically formatted as standardized error response.

For example, this exception:


raise Eluvia::Errors::NotFound.new('Test record was not found.')

will be formatted into this response:

{
  "errors": [
    {
      "message": "Test record was not found.",
      "error": "Not Found",
      "status": 404,
      "source": null,
      "timestamp": "2022-11-04T17:11:00.969Z"
    }
  ]
}

For example, this exception:


raise Eluvia::Errors::UnprocessableEntity.new('param_1' => 'Param 1 must not be blank.', 
                                              'param_2' => 'Param 2 must not be blank.')

will be formatted into this response:

{
  "errors": [
    {
      "message": "Param 1 must not be blank.",
      "error": "Unprocessable Entity",
      "status": 422,
      "source": "param_1",
      "timestamp": "2022-11-04T17:11:00.969Z"
    },
    {
      "message": "Param 2 must not be blank.",
      "error": "Unprocessable Entity",
      "status": 422,
      "source": "param_2",
      "timestamp": "2022-11-04T17:11:00.969Z"
    }
  ]
}

For the full list of available exceptions, see content of lib/eluvia/errors folder.

2.6. Chunked/direct upload finalization

Eluvia::File carries an upload_key attribute, an alternative to content (base64 encoded file data) for file_attr fields backed by an underlying ActiveStorage attachment. When upload_key is present, the setter no longer base64-decodes content — instead it delegates to Eluvia::Uploads.finalizer, a registrable extension point that finalizes a previously uploaded (chunked/direct) temp object into the ActiveStorage attachment. This lets a separate library (e.g. a chunked upload provider) implement the actual storage logic (S3 copy_object, tus, ...) without eluvia-base depending on it.

Register a finalizer, typically from an initializer:


Eluvia::Uploads.finalizer = ->(attachment_record, upload_key, filename) do
  # Resolve `upload_key` in your storage backend and attach the resulting blob to `attachment_record`,
  # using `filename` as the final (sanitized) filename.
end

If upload_key is present on the assigned Eluvia::File but no finalizer is registered, a Eluvia::Errors::StandardError is raised.