Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patterns in Ruby #71

Open
joshmfrankel opened this issue Oct 11, 2023 · 1 comment
Open

Patterns in Ruby #71

joshmfrankel opened this issue Oct 11, 2023 · 1 comment

Comments

@joshmfrankel
Copy link
Owner

joshmfrankel commented Oct 11, 2023

Sections for each

  • Overview
  • Responsibilities
  • How to use
  • How not to use
  • How to configure (for specific patterns)
  • Example

Model

Responsibilities
An interface between the database and ORM

How to use

  • Associations
  • Validations
  • Scopes (see anti-patterns)
  • Data specific behavior

How not to use

  • Callbacks
  • User context
  • God objects (too much information on other objects)
  • Meta-programming

Controller

Anti-patterns

  • ActiveRecord Query chains
  • Not handling error branch
  • Using non-restful routes

View

Anti-patterns

  • Setting variables
  • Performing queries
  • Not handling empty state cases
  • Forgetting pagination / limiting results

Presenter

Anti-patterns

  • Adding logic that belongs in model
  • Not abstracting common display logic

Query

  • Follow ActiveRecord finder style (e.g. .all, .find_by
  • Pres

Anti-patterns

  • More than 1 public method
  • Not preserving chainability
  • Performing commands or side-effects

Serializer

Service

  • Railway oriented programming

Job

Transformers

Translate one data format to another

Strategies

Provide concrete swappable business logic for different use-cases

Adapter

Take incoming interface and translate to another

View Component

Reusable View layer functionality and design

Policy

Enforces authorization and answers the questions:

  • Am I permitted to access this resource?
  • Am I permitted to take this action?

Api

Schemas

Value

Form

@joshmfrankel
Copy link
Owner Author

module ResultHandler
  private

  PROJECT_ROOT = '/Bend-API/'.freeze
  DEFAULT_SUCCESS_MESSAGE = 'Success'.freeze
  DEFAULT_FAILURE_MESSAGE = 'There was an error'.freeze

  def self.included(included_class)
    included_class.extend(ClassMethods)
  end

  module ClassMethods
    def handle_result(*method_names)
      handler = Module.new do
        method_names.each do |method_name|
          define_method(method_name) do |*args, **kwargs|
            handle_result { super(*args, **kwargs) }
          end
        end
      end

      prepend(handler)
    end
  end

  def handle_result
    result_object = yield

    return result_object if valid_result_object?(result_object:)

    Result.success(payload: result_object, message: DEFAULT_SUCCESS_MESSAGE)
  rescue ResultFailureError => exception
    exception.result
  rescue StandardError => exception
    relevant_backtrace = exception.backtrace.select { |trace| trace.include?(PROJECT_ROOT) }
    message = Rails.env.development? ? exception.message : DEFAULT_FAILURE_MESSAGE

    Result.failure(message:, payload: { exception:, relevant_backtrace: })
  end

  def valid_result_object?(result_object:)
    result_object.respond_to?(:success) &&
      result_object.respond_to?(:failure) &&
      result_object.respond_to?(:failure!)
  end

  def Success(payload: {}, message: DEFAULT_SUCCESS_MESSAGE)
    Result.success(payload:, message:)
  end

  def Failure(payload: {}, message: DEFAULT_FAILURE_MESSAGE)
    Result.failure(payload:, message:)
  end

  def Failure!(payload: {}, message: DEFAULT_FAILURE_MESSAGE)
    Result.failure!(payload:, message:)
  end
end

# result.rb
# Provides common interface for Railway-oriented programming
class Result
  def self.success(payload: {}, message: '')
    result = new(status: :success, payload:, message:)
    result.success
  end

  def self.failure(payload: {}, message: '')
    result = new(payload:, message:)
    result.failure
  end

  def self.failure!(payload: {}, message: '')
    result = new(payload:, message:)
    result.failure!
  end

  attr_accessor :status, :payload, :message

  def initialize(status: :success, payload: {}, message: '')
    @status = status
    @payload = payload
    @message = message
  end

  def success?
    status == :success
  end

  def failure?
    !success?
  end

  def failure(message: self.message)
    tap do |result|
      result.status = :failure
      result.message = message if message.present?
    end
  end

  def failure!(message: self.message)
    failure(message:)

    raise ResultFailureError.new(self)
  end

  def success(message: '')
    tap do |result|
      result.status = :success
      result.message = message || result.message
    end
  end
end

class ResultFailureError < StandardError
  attr_reader :result

  def initialize(result)
    @result = result
    super(result.message)
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant