carbonyte 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
 - data/README.md +0 -3
 - data/Rakefile +6 -0
 - data/app/controllers/carbonyte/application_controller.rb +17 -0
 - data/app/controllers/carbonyte/concerns/correlatable.rb +48 -0
 - data/app/controllers/carbonyte/concerns/loggable.rb +16 -0
 - data/app/controllers/carbonyte/concerns/policiable.rb +27 -0
 - data/app/controllers/carbonyte/concerns/rescuable.rb +93 -47
 - data/app/controllers/carbonyte/concerns/serializable.rb +48 -0
 - data/app/interactors/carbonyte/application_interactor.rb +14 -0
 - data/app/interactors/carbonyte/finders/application_finder.rb +4 -6
 - data/app/policies/carbonyte/application_policy.rb +55 -0
 - data/config/routes.rb +1 -0
 - data/lib/carbonyte.rb +1 -0
 - data/lib/carbonyte/engine.rb +14 -0
 - data/lib/carbonyte/initializers.rb +9 -0
 - data/lib/carbonyte/initializers/lograge.rb +74 -0
 - data/lib/carbonyte/version.rb +1 -1
 - data/spec/api/health_spec.rb +12 -0
 - data/spec/controllers/concerns/rescuable_spec.rb +89 -0
 - data/spec/controllers/concerns/serializable_spec.rb +69 -0
 - data/spec/dummy/Rakefile +8 -0
 - data/spec/dummy/app/controllers/application_controller.rb +7 -0
 - data/spec/dummy/app/models/application_record.rb +5 -0
 - data/spec/dummy/app/models/user.rb +5 -0
 - data/spec/dummy/bin/rails +6 -0
 - data/spec/dummy/bin/rake +6 -0
 - data/spec/dummy/bin/setup +35 -0
 - data/spec/dummy/config.ru +7 -0
 - data/spec/dummy/config/application.rb +33 -0
 - data/spec/dummy/config/boot.rb +7 -0
 - data/spec/dummy/config/database.yml +25 -0
 - data/spec/dummy/config/environment.rb +7 -0
 - data/spec/dummy/config/environments/development.rb +64 -0
 - data/spec/dummy/config/environments/production.rb +114 -0
 - data/spec/dummy/config/environments/test.rb +51 -0
 - data/spec/dummy/config/initializers/content_security_policy.rb +29 -0
 - data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -0
 - data/spec/dummy/config/initializers/inflections.rb +17 -0
 - data/spec/dummy/config/initializers/wrap_parameters.rb +16 -0
 - data/spec/dummy/config/locales/en.yml +33 -0
 - data/spec/dummy/config/puma.rb +40 -0
 - data/spec/dummy/config/routes.rb +5 -0
 - data/spec/dummy/config/spring.rb +8 -0
 - data/spec/dummy/config/storage.yml +34 -0
 - data/spec/dummy/db/migrate/20201007132857_create_users.rb +11 -0
 - data/spec/dummy/db/schema.rb +21 -0
 - data/spec/interactors/application_interactor_spec.rb +23 -0
 - data/spec/interactors/finders/application_finder_spec.rb +60 -0
 - data/spec/policies/application_policy_spec.rb +20 -0
 - data/spec/rails_helper.rb +65 -0
 - data/spec/spec_helper.rb +101 -0
 - data/spec/support/carbonyte/api_helpers.rb +13 -0
 - data/spec/support/carbonyte/spec_helper.rb +7 -0
 - metadata +81 -23
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: e0182bc2a05957e2aa33ff9d29b1bef6e80e0bf46e043cab50ea83088a8b9979
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 9975a93ce713a8a3dad2a0493905a75f257f9f2d36403ece1e145e0ed03896c3
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 4546a5fb7554b04c2cc1f38657d315c157611ee4cd16af526443c3d24d8216976392950016ce144ab7dc5eefb9949ea0070c4031c5dadea3a8dcd1fcfa10c61b
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: f9808c42d3dcea7d9d293ce3481dfd79b5215218587cedba4b62e114843cbc268bc793cc2b0a2f28e2d5a96e40e90ed2c8908601a277911c6e4d27cd433b08c6
         
     | 
    
        data/README.md
    CHANGED
    
    
    
        data/Rakefile
    CHANGED
    
    
| 
         @@ -3,5 +3,22 @@ 
     | 
|
| 
       3 
3 
     | 
    
         
             
            module Carbonyte
         
     | 
| 
       4 
4 
     | 
    
         
             
              # Carbonyte base class for all controllers
         
     | 
| 
       5 
5 
     | 
    
         
             
              class ApplicationController < ActionController::API
         
     | 
| 
      
 6 
     | 
    
         
            +
                include Concerns::Correlatable
         
     | 
| 
      
 7 
     | 
    
         
            +
                include Concerns::Loggable
         
     | 
| 
      
 8 
     | 
    
         
            +
                include Concerns::Rescuable
         
     | 
| 
      
 9 
     | 
    
         
            +
                include Concerns::Policiable
         
     | 
| 
      
 10 
     | 
    
         
            +
                include Concerns::Serializable
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                # GET /health
         
     | 
| 
      
 13 
     | 
    
         
            +
                def health
         
     | 
| 
      
 14 
     | 
    
         
            +
                  payload = {
         
     | 
| 
      
 15 
     | 
    
         
            +
                    correlation_id: correlation_id,
         
     | 
| 
      
 16 
     | 
    
         
            +
                    db_status: ActiveRecord::Base.connected? ? 'OK' : 'Not Connected',
         
     | 
| 
      
 17 
     | 
    
         
            +
                    environment: Rails.env,
         
     | 
| 
      
 18 
     | 
    
         
            +
                    pid: ::Process.pid
         
     | 
| 
      
 19 
     | 
    
         
            +
                  }
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  render json: payload, status: :ok
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
       6 
23 
     | 
    
         
             
              end
         
     | 
| 
       7 
24 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,48 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'request_store'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Carbonyte
         
     | 
| 
      
 6 
     | 
    
         
            +
              # Module enclosing all concerns
         
     | 
| 
      
 7 
     | 
    
         
            +
              module Concerns
         
     | 
| 
      
 8 
     | 
    
         
            +
                # The Correlatable concern automatically manages the correlation ID
         
     | 
| 
      
 9 
     | 
    
         
            +
                module Correlatable
         
     | 
| 
      
 10 
     | 
    
         
            +
                  extend ActiveSupport::Concern
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  included do
         
     | 
| 
      
 13 
     | 
    
         
            +
                    before_action :find_or_create_correlation_id
         
     | 
| 
      
 14 
     | 
    
         
            +
                    after_action :send_correlation_id
         
     | 
| 
      
 15 
     | 
    
         
            +
                  end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  # Retrieves the request correlation ID
         
     | 
| 
      
 18 
     | 
    
         
            +
                  def correlation_id
         
     | 
| 
      
 19 
     | 
    
         
            +
                    RequestStore.store[:correlation_id]
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                  protected
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  # Finds the correlation ID in the request.
         
     | 
| 
      
 25 
     | 
    
         
            +
                  # Can be overridden to fit the application
         
     | 
| 
      
 26 
     | 
    
         
            +
                  def find_correlation_id
         
     | 
| 
      
 27 
     | 
    
         
            +
                    request.headers['X-Correlation-Id'] || request.headers['X-Request-Id']
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  # Generates a correlation ID (defaults to Rails auto-generated one).
         
     | 
| 
      
 31 
     | 
    
         
            +
                  # Can be overridden to fit the application
         
     | 
| 
      
 32 
     | 
    
         
            +
                  def create_correlation_id
         
     | 
| 
      
 33 
     | 
    
         
            +
                    request.request_id || SecureRandom.uuid
         
     | 
| 
      
 34 
     | 
    
         
            +
                  end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                  private
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  def find_or_create_correlation_id
         
     | 
| 
      
 39 
     | 
    
         
            +
                    correlation_id = find_correlation_id || create_correlation_id
         
     | 
| 
      
 40 
     | 
    
         
            +
                    RequestStore.store[:correlation_id] = correlation_id
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  def send_correlation_id
         
     | 
| 
      
 44 
     | 
    
         
            +
                    response.set_header('X-Correlation-Id', correlation_id)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,16 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Carbonyte
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Concerns
         
     | 
| 
      
 5 
     | 
    
         
            +
                # The Loggable concern integrates with lograge to log efficiently
         
     | 
| 
      
 6 
     | 
    
         
            +
                module Loggable
         
     | 
| 
      
 7 
     | 
    
         
            +
                  extend ActiveSupport::Concern
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  # Used for logging IP.
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # @see Carbonyte::Engine
         
     | 
| 
      
 11 
     | 
    
         
            +
                  def remote_ip
         
     | 
| 
      
 12 
     | 
    
         
            +
                    request.headers['HTTP_X_FORWARDED_FOR'] || request.headers['HTTP_X_REAL_IP'] || request.remote_ip
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
              end
         
     | 
| 
      
 16 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,27 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Carbonyte
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Concerns
         
     | 
| 
      
 5 
     | 
    
         
            +
                # The Policiable concern integrates with Pundit to manage policies
         
     | 
| 
      
 6 
     | 
    
         
            +
                module Policiable
         
     | 
| 
      
 7 
     | 
    
         
            +
                  extend ActiveSupport::Concern
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  included do
         
     | 
| 
      
 10 
     | 
    
         
            +
                    include Pundit
         
     | 
| 
      
 11 
     | 
    
         
            +
                    rescue_from Pundit::NotAuthorizedError, with: :policy_not_authorized
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  private
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  def policy_not_authorized(exc)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    payload = {
         
     | 
| 
      
 18 
     | 
    
         
            +
                      code: exc.class.name,
         
     | 
| 
      
 19 
     | 
    
         
            +
                      source: { policy: exc.policy.class.name },
         
     | 
| 
      
 20 
     | 
    
         
            +
                      title: 'Not Authorized By Policy',
         
     | 
| 
      
 21 
     | 
    
         
            +
                      detail: exc.message
         
     | 
| 
      
 22 
     | 
    
         
            +
                    }
         
     | 
| 
      
 23 
     | 
    
         
            +
                    render json: serialized_errors(payload), status: :forbidden
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -1,61 +1,107 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            module Carbonyte
         
     | 
| 
       4 
     | 
    
         
            -
               
     | 
| 
       5 
     | 
    
         
            -
             
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
       8 
     | 
    
         
            -
                 
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
                included do
         
     | 
| 
       11 
     | 
    
         
            -
                  rescue_from Pundit::NotAuthorizedError, with: :unauthorized
         
     | 
| 
       12 
     | 
    
         
            -
                  rescue_from Interactor::Failure, with: :interactor_failure
         
     | 
| 
       13 
     | 
    
         
            -
                  rescue_from ActiveRecord::RecordInvalid, with: :record_invalid
         
     | 
| 
       14 
     | 
    
         
            -
                end
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Concerns
         
     | 
| 
      
 5 
     | 
    
         
            +
                # The Rescuable concern rescues controllers from bubbling up the most common exceptions
         
     | 
| 
      
 6 
     | 
    
         
            +
                # and provides a uniformed response body in case of such errors.
         
     | 
| 
      
 7 
     | 
    
         
            +
                # @see https://jsonapi.org/format/#error-objects
         
     | 
| 
      
 8 
     | 
    
         
            +
                module Rescuable
         
     | 
| 
      
 9 
     | 
    
         
            +
                  extend ActiveSupport::Concern
         
     | 
| 
       15 
10 
     | 
    
         | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
      
 11 
     | 
    
         
            +
                  included do
         
     | 
| 
      
 12 
     | 
    
         
            +
                    # The first one MUST be StandardError as otherwise will catch all others
         
     | 
| 
      
 13 
     | 
    
         
            +
                    rescue_from StandardError, with: :internal_error
         
     | 
| 
       17 
14 
     | 
    
         | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
      
 15 
     | 
    
         
            +
                    rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
         
     | 
| 
      
 16 
     | 
    
         
            +
                    rescue_from Interactor::Failure, with: :interactor_failure
         
     | 
| 
      
 17 
     | 
    
         
            +
                    rescue_from ActiveRecord::RecordInvalid,
         
     | 
| 
      
 18 
     | 
    
         
            +
                                ActiveRecord::RecordNotSaved,
         
     | 
| 
      
 19 
     | 
    
         
            +
                                with: :record_invalid
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
       22 
21 
     | 
    
         | 
| 
       23 
     | 
    
         
            -
             
     | 
| 
       24 
     | 
    
         
            -
                   
     | 
| 
       25 
     | 
    
         
            -
             
     | 
| 
       26 
     | 
    
         
            -
                     
     | 
| 
       27 
     | 
    
         
            -
                     
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
                  }
         
     | 
| 
       30 
     | 
    
         
            -
                  render json: serialized_errors(payload), status: :unauthorized
         
     | 
| 
       31 
     | 
    
         
            -
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
                  # Upon rescuing an exception, stores that exception in RequestStore for later logging
         
     | 
| 
      
 23 
     | 
    
         
            +
                  # @param exception [StandardError] the rescued exception
         
     | 
| 
      
 24 
     | 
    
         
            +
                  def rescue_with_handler(exception)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    RequestStore.store[:rescued_exception] = exception
         
     | 
| 
      
 26 
     | 
    
         
            +
                    super
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
       32 
28 
     | 
    
         | 
| 
       33 
     | 
    
         
            -
             
     | 
| 
       34 
     | 
    
         
            -
                   
     | 
| 
       35 
     | 
    
         
            -
                     
     | 
| 
       36 
     | 
    
         
            -
             
     | 
| 
       37 
     | 
    
         
            -
             
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
       39 
     | 
    
         
            -
             
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
       41 
     | 
    
         
            -
             
     | 
| 
      
 29 
     | 
    
         
            +
                  # This is a special case
         
     | 
| 
      
 30 
     | 
    
         
            +
                  def route_not_found
         
     | 
| 
      
 31 
     | 
    
         
            +
                    payload = {
         
     | 
| 
      
 32 
     | 
    
         
            +
                      code: 'RoutingError',
         
     | 
| 
      
 33 
     | 
    
         
            +
                      source: request.path,
         
     | 
| 
      
 34 
     | 
    
         
            +
                      title: 'Route not found',
         
     | 
| 
      
 35 
     | 
    
         
            +
                      detail: "No route matches #{request.path}"
         
     | 
| 
      
 36 
     | 
    
         
            +
                    }
         
     | 
| 
      
 37 
     | 
    
         
            +
                    render json: serialized_errors(payload), status: :not_found
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
       42 
39 
     | 
    
         | 
| 
       43 
     | 
    
         
            -
             
     | 
| 
       44 
     | 
    
         
            -
             
     | 
| 
       45 
     | 
    
         
            -
                   
     | 
| 
       46 
     | 
    
         
            -
             
     | 
| 
      
 40 
     | 
    
         
            +
                  private
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  def serialized_errors(payload)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    payload = [payload] unless payload.is_a?(Array)
         
     | 
| 
      
 44 
     | 
    
         
            +
                    { errors: payload }.to_json
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
       47 
46 
     | 
    
         | 
| 
       48 
     | 
    
         
            -
             
     | 
| 
       49 
     | 
    
         
            -
             
     | 
| 
       50 
     | 
    
         
            -
             
     | 
| 
       51 
     | 
    
         
            -
                       
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
             
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
      
 47 
     | 
    
         
            +
                  def unauthenticated(exc)
         
     | 
| 
      
 48 
     | 
    
         
            +
                    payload = {
         
     | 
| 
      
 49 
     | 
    
         
            +
                      code: exc.class.name,
         
     | 
| 
      
 50 
     | 
    
         
            +
                      title: 'User Not Authenticated',
         
     | 
| 
      
 51 
     | 
    
         
            +
                      detail: exc.message
         
     | 
| 
      
 52 
     | 
    
         
            +
                    }
         
     | 
| 
      
 53 
     | 
    
         
            +
                    render json: serialized_errors(payload), status: :unauthorized
         
     | 
| 
      
 54 
     | 
    
         
            +
                  end
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                  def record_not_found(exc)
         
     | 
| 
      
 57 
     | 
    
         
            +
                    payload = {
         
     | 
| 
      
 58 
     | 
    
         
            +
                      code: exc.class.name,
         
     | 
| 
      
 59 
     | 
    
         
            +
                      source: {
         
     | 
| 
      
 60 
     | 
    
         
            +
                        model: exc.model,
         
     | 
| 
      
 61 
     | 
    
         
            +
                        id: exc.id
         
     | 
| 
      
 62 
     | 
    
         
            +
                      },
         
     | 
| 
      
 63 
     | 
    
         
            +
                      title: 'Resource Not Found',
         
     | 
| 
      
 64 
     | 
    
         
            +
                      detail: exc.message
         
     | 
| 
      
 65 
     | 
    
         
            +
                    }
         
     | 
| 
      
 66 
     | 
    
         
            +
                    render json: serialized_errors(payload), status: :not_found
         
     | 
| 
      
 67 
     | 
    
         
            +
                  end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                  def interactor_failure(exc)
         
     | 
| 
      
 70 
     | 
    
         
            +
                    payload = {
         
     | 
| 
      
 71 
     | 
    
         
            +
                      code: exc.class.name,
         
     | 
| 
      
 72 
     | 
    
         
            +
                      source: { interactor: exc.context.current_interactor.class.name },
         
     | 
| 
      
 73 
     | 
    
         
            +
                      title: 'Interactor Failure',
         
     | 
| 
      
 74 
     | 
    
         
            +
                      detail: exc.context.error
         
     | 
| 
      
 75 
     | 
    
         
            +
                    }
         
     | 
| 
      
 76 
     | 
    
         
            +
                    render json: serialized_errors(payload), status: :unprocessable_entity
         
     | 
| 
      
 77 
     | 
    
         
            +
                  end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                  def record_invalid(exc)
         
     | 
| 
      
 80 
     | 
    
         
            +
                    payload = errors_for_record(exc.record, exc.class.name).flatten
         
     | 
| 
      
 81 
     | 
    
         
            +
                    render json: serialized_errors(payload), status: :unprocessable_entity
         
     | 
| 
      
 82 
     | 
    
         
            +
                  end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                  def errors_for_record(record, code)
         
     | 
| 
      
 85 
     | 
    
         
            +
                    record.errors.messages.map do |field, errors|
         
     | 
| 
      
 86 
     | 
    
         
            +
                      errors.map do |error_message|
         
     | 
| 
      
 87 
     | 
    
         
            +
                        {
         
     | 
| 
      
 88 
     | 
    
         
            +
                          code: code,
         
     | 
| 
      
 89 
     | 
    
         
            +
                          source: { pointer: "attributes/#{field}" },
         
     | 
| 
      
 90 
     | 
    
         
            +
                          title: 'Invalid Field',
         
     | 
| 
      
 91 
     | 
    
         
            +
                          detail: error_message
         
     | 
| 
      
 92 
     | 
    
         
            +
                        }
         
     | 
| 
      
 93 
     | 
    
         
            +
                      end
         
     | 
| 
       57 
94 
     | 
    
         
             
                    end
         
     | 
| 
       58 
95 
     | 
    
         
             
                  end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                  def internal_error(exc)
         
     | 
| 
      
 98 
     | 
    
         
            +
                    payload = {
         
     | 
| 
      
 99 
     | 
    
         
            +
                      code: exc.class.name,
         
     | 
| 
      
 100 
     | 
    
         
            +
                      title: 'Internal Server Error',
         
     | 
| 
      
 101 
     | 
    
         
            +
                      detail: exc.message
         
     | 
| 
      
 102 
     | 
    
         
            +
                    }
         
     | 
| 
      
 103 
     | 
    
         
            +
                    render json: serialized_errors(payload), status: :internal_server_error
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
       59 
105 
     | 
    
         
             
                end
         
     | 
| 
       60 
106 
     | 
    
         
             
              end
         
     | 
| 
       61 
107 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,48 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Carbonyte
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Concerns
         
     | 
| 
      
 5 
     | 
    
         
            +
                # The Serializable concern helps with JSON:API serialization
         
     | 
| 
      
 6 
     | 
    
         
            +
                module Serializable
         
     | 
| 
      
 7 
     | 
    
         
            +
                  extend ActiveSupport::Concern
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  # Helper method to render json responses
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # @param object [Any] the object (or list) to be serialized
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # @param serializer_class [JSONAPI::Serializer] serializer class to use. Inferred if not provided.
         
     | 
| 
      
 12 
     | 
    
         
            +
                  # @param status [Symbol, Number] the status code to return, defaults to 200 (OK)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  # @example Serializes `user` using UserSerializer and returns status 200 (OK)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  #   user = User.find(123)
         
     | 
| 
      
 15 
     | 
    
         
            +
                  #   serialize(user)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # @example Serializes `created_record` using the specified UserSerializer and returning status 201 (CREATED)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  #   serialize(created_record, serializer: UserSerializer, status: :created)
         
     | 
| 
      
 18 
     | 
    
         
            +
                  def serialize(object, serializer_class: nil, status: :ok)
         
     | 
| 
      
 19 
     | 
    
         
            +
                    serializer_class ||= serializer_for(object)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    render json: serializer_class.new(object, serializer_options).serializable_hash.to_json, status: status
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  # Default options for serializers
         
     | 
| 
      
 24 
     | 
    
         
            +
                  def serializer_options
         
     | 
| 
      
 25 
     | 
    
         
            +
                    {
         
     | 
| 
      
 26 
     | 
    
         
            +
                      include: include_option,
         
     | 
| 
      
 27 
     | 
    
         
            +
                      params: {
         
     | 
| 
      
 28 
     | 
    
         
            +
                        current_user: current_user
         
     | 
| 
      
 29 
     | 
    
         
            +
                      }
         
     | 
| 
      
 30 
     | 
    
         
            +
                    }
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  # Default include options for serializers
         
     | 
| 
      
 34 
     | 
    
         
            +
                  def include_option
         
     | 
| 
      
 35 
     | 
    
         
            +
                    return [] unless params[:include]
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                    params[:include].split(',').map(&:to_sym)
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  private
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  def serializer_for(object)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    object = object.first if object.is_a?(Array)
         
     | 
| 
      
 44 
     | 
    
         
            +
                    "#{object.class.name}Serializer".constantize
         
     | 
| 
      
 45 
     | 
    
         
            +
                  end
         
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -4,5 +4,19 @@ module Carbonyte 
     | 
|
| 
       4 
4 
     | 
    
         
             
              # Carbonyte base class for all interactors
         
     | 
| 
       5 
5 
     | 
    
         
             
              class ApplicationInteractor
         
     | 
| 
       6 
6 
     | 
    
         
             
                include Interactor
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                # Interactor hooks are not inherited by subclasses, so we need a hack.
         
     | 
| 
      
 9 
     | 
    
         
            +
                # @see https://github.com/collectiveidea/interactor/issues/114
         
     | 
| 
      
 10 
     | 
    
         
            +
                def self.inherited(klass)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  klass.class_eval do
         
     | 
| 
      
 12 
     | 
    
         
            +
                    before do
         
     | 
| 
      
 13 
     | 
    
         
            +
                      context.current_interactor = self
         
     | 
| 
      
 14 
     | 
    
         
            +
                    end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                    after do
         
     | 
| 
      
 17 
     | 
    
         
            +
                      context.current_interactor = nil
         
     | 
| 
      
 18 
     | 
    
         
            +
                    end
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
                end
         
     | 
| 
       7 
21 
     | 
    
         
             
              end
         
     | 
| 
       8 
22 
     | 
    
         
             
            end
         
     | 
| 
         @@ -25,8 +25,6 @@ module Carbonyte 
     | 
|
| 
       25 
25 
     | 
    
         
             
                    paginate
         
     | 
| 
       26 
26 
     | 
    
         
             
                  end
         
     | 
| 
       27 
27 
     | 
    
         | 
| 
       28 
     | 
    
         
            -
                  protected
         
     | 
| 
       29 
     | 
    
         
            -
             
     | 
| 
       30 
28 
     | 
    
         
             
                  # Returns true if the provided relation can be included
         
     | 
| 
       31 
29 
     | 
    
         
             
                  # @param _rel [Symbol] the relation to include
         
     | 
| 
       32 
30 
     | 
    
         
             
                  def can_include?(_rel)
         
     | 
| 
         @@ -37,9 +35,9 @@ module Carbonyte 
     | 
|
| 
       37 
35 
     | 
    
         | 
| 
       38 
36 
     | 
    
         
             
                  def include_relations
         
     | 
| 
       39 
37 
     | 
    
         
             
                    context.params[:include].split(',').each do |relation|
         
     | 
| 
       40 
     | 
    
         
            -
                      next unless can_include?(relation)
         
     | 
| 
      
 38 
     | 
    
         
            +
                      next unless can_include?(relation.to_sym)
         
     | 
| 
       41 
39 
     | 
    
         | 
| 
       42 
     | 
    
         
            -
                      context.scope = context.scope.includes(relation)
         
     | 
| 
      
 40 
     | 
    
         
            +
                      context.scope = context.scope.includes(relation.to_sym)
         
     | 
| 
       43 
41 
     | 
    
         
             
                    end
         
     | 
| 
       44 
42 
     | 
    
         
             
                  end
         
     | 
| 
       45 
43 
     | 
    
         | 
| 
         @@ -48,9 +46,9 @@ module Carbonyte 
     | 
|
| 
       48 
46 
     | 
    
         
             
                  end
         
     | 
| 
       49 
47 
     | 
    
         | 
| 
       50 
48 
     | 
    
         
             
                  def sort_exp(prop)
         
     | 
| 
       51 
     | 
    
         
            -
                    return prop unless prop.start_with?('-')
         
     | 
| 
      
 49 
     | 
    
         
            +
                    return prop.to_sym unless prop.start_with?('-')
         
     | 
| 
       52 
50 
     | 
    
         | 
| 
       53 
     | 
    
         
            -
                    { prop[1..] => :desc }
         
     | 
| 
      
 51 
     | 
    
         
            +
                    { prop[1..].to_sym => :desc }
         
     | 
| 
       54 
52 
     | 
    
         
             
                  end
         
     | 
| 
       55 
53 
     | 
    
         | 
| 
       56 
54 
     | 
    
         
             
                  def sort
         
     | 
| 
         @@ -0,0 +1,55 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Carbonyte
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Carbonyte base class for all policies
         
     | 
| 
      
 5 
     | 
    
         
            +
              class ApplicationPolicy
         
     | 
| 
      
 6 
     | 
    
         
            +
                attr_reader :user, :record
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                # Initializes a new policy with the current user and the record
         
     | 
| 
      
 9 
     | 
    
         
            +
                def initialize(user, record)
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @user = user
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @record = record
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                # Can the user get a list of records?
         
     | 
| 
      
 15 
     | 
    
         
            +
                def index?
         
     | 
| 
      
 16 
     | 
    
         
            +
                  true
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                # Can the user get this specific record?
         
     | 
| 
      
 20 
     | 
    
         
            +
                def show?
         
     | 
| 
      
 21 
     | 
    
         
            +
                  true
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                # Can the user create a new record?
         
     | 
| 
      
 25 
     | 
    
         
            +
                def create?
         
     | 
| 
      
 26 
     | 
    
         
            +
                  true
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                # Can the user update this record?
         
     | 
| 
      
 30 
     | 
    
         
            +
                def update?
         
     | 
| 
      
 31 
     | 
    
         
            +
                  true
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                # Can the user destroy this record?
         
     | 
| 
      
 35 
     | 
    
         
            +
                def destroy?
         
     | 
| 
      
 36 
     | 
    
         
            +
                  true
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                # Carbonyte base class for all policy scopes
         
     | 
| 
      
 40 
     | 
    
         
            +
                class Scope
         
     | 
| 
      
 41 
     | 
    
         
            +
                  attr_reader :user, :scope
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  # Initializes a new scope from the user and a base scope
         
     | 
| 
      
 44 
     | 
    
         
            +
                  def initialize(user, scope)
         
     | 
| 
      
 45 
     | 
    
         
            +
                    @user = user
         
     | 
| 
      
 46 
     | 
    
         
            +
                    @scope = scope
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  # Resolves the scope effectively triggering the query
         
     | 
| 
      
 50 
     | 
    
         
            +
                  def resolve
         
     | 
| 
      
 51 
     | 
    
         
            +
                    scope.all
         
     | 
| 
      
 52 
     | 
    
         
            +
                  end
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
              end
         
     | 
| 
      
 55 
     | 
    
         
            +
            end
         
     |