Nucleus Core

Gem Version Circle Code Climate

Overview

Nucleus Core defines a boundary between your business logic, and a framework.

Components

Responder - The boundary which passes request parameters to your business logic, then renders a response (requires framework request, and response adapaters).\ Operations - Service implementation that executes one side effect.\ Workflows - Service orchestration which composes complex, multi stage processes.\ Views - Presentation objects which render multiple formats.

Supported Frameworks

These packages implement request, and response adapters for their respective framework.

Getting started

  1. Install the gem
$ gem install nuclueus-core
  1. Initialize, and configure NucleusCore
require "nucleus-core"

NucleusCore.configure do |config|
  config.logger = Logger.new($stdout)
  config.default_response_format = :json
  config.exceptions = {
    not_found: ActiveRecord::RecordNotFound,
    unprocessible: [ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved],
    bad_request: Apipie::ParamError,
    unauthorized: Pundit::NotAuthorizedError
  }
end
  1. Create a class that implements the rendering methods below. Class, or instance methods can be used, make sure to initialize the responder accordingly.
class ResponderAdapter
  # entity: Nucleus::ResponseAdapter

  def render_json(entity)
  end

  def render_xml(entity)
  end

  def render_pdf(entity)
  end

  def render_csv(entity)
  end

  def render_text(entity)
  end

  def render_nothing(entity)
  end
end
  1. Create a class that implements call which returns a hash of request details. format and parameters are required, but there others can be returned.
class RequestAdapter
  def call(args={})
    { format: args[:format], parameters: args[:params], ...}
  end
end
  1. Implement your business logic using Operations, and orchestrate more complex proceedures with Workflows.

operations/fetch_order.rb

class Operations::FetchOrder < NucleusCore::Operation
  def call
    context.order = find_order(context.id)
  rescue NucleusCore::NotFound => e
    context.fail!(e.message, exception: e)
  end
end

operations/apply_order_discount.rb

class Operations::ApplyOrderDiscount < NucleusCore::Operation
  def call
    discount = context.discount || 0.25
    order = update_order(context.order, discount: discount)

    context.order = order
  rescue NucleusCore::NotFound, NucleusCore::Unprocessable => e
    context.fail!(e.message, exception: e)
  end
end

workflows/fulfill_order.rb

class Workflows::FulfillOrder < NucleusCore::Workflow
  def define
    start_node(continue: :apply_discount?)
    register_node(
      state: :apply_discount?,
      operation: Operations::FetchOrder,
      determine_signal: ->(context) { context.order.total > 10 ? :discount : :pay },
      signals: { discount: :discount_order, pay: :take_payment }
    )
    register_node(
      state: :discount_order,
      operation: Operations::ApplyOrderDiscount,
      signals: { continue: :take_payment }
    )
    register_node(
      state: :take_payment,
      operation: ->(context) { context.paid = context.order.pay! },
      signals: { continue: :completed }
    )
    register_node(
      state: :completed,
      determine_signal: ->(_) { :wait }
    )
  end
end
  1. Define your view, and it's responses.

views/order.rb

class Views::Order < NucleusCore::View
  def initialize(order)
    super(id: order.id, price: "$#{order.total}", paid: order.paid, created_at: order.created_at)
  end

  def json_response
    content = {
      payment: {
        id: id,
        price: price,
        paid: paid,
        created_at: created_at,
        signature: SecureRandom.hex
      }
    }

    NucleusCore::JsonResponse.new(content: content)
  end

  def pdf_response
    pdf = generate_pdf(id, price, paid)

    NucleusCore::PdfResponse.new(content: pdf)
  end
end
  1. Initialize the responder with your adapters, then call your business logic, and return a view.

controllers/orders_controller.rb

class OrdersController
  before_action do
    @responder = Nucleus::Responder.new(
      response_adapter: ResponseAdapter.new,
      request_adapter: RequestAdapter.new
    )

    @request = {
      format: request.format,
      parameters: request.params
    }
  end

  def create
    @responder.execute(@request) do |req|
      context, _process = Workflows::FulfillOrder.call(context: req.parameters)

      return Views::Order.new(order: context.order) if context.success?

      return context
    end
  end
end
  1. Then tell us about it!

Support

If you want to report a bug, or have ideas, feedback or questions about the gem, let me know via GitHub issues and we will do our best to provide a helpful answer.

License

The gem is available as open source under the terms of the MIT License.

Code of conduct

Everyone interacting in this project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Contribution guide

Pull requests are welcome!