No release in over a year
A context-like experience for your controllers
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Runtime

>= 1.0.1, < 2.0
 Project Readme

SnFoil::Controller

build maintainability

SnFoil Controllers help seperate your business logic from your api layer.

Installation

Add this line to your application's Gemfile:

gem 'snfoil-controller'

Usage

Ultimately SnFoil Controllers are just SnFoil Contexts, but they setup their workflow a little differently. endpoint creates setup_* and process_* intervals to handle your data, and the method or block provided renders it.

Quickstart Example

# app/controllers/people_controller.rb

class PeopleController < ActionController::API
  include SnFoil::Controller

  context PeopleContext
  serializer PeopleSerializer
  context PeopleDeserializer

  endpoint :create, do |object:, **options|
    if object.errors
      render json: object.errors, status: :unprocessable_entity
    else
      render json: serialize(object, **options), status: :created
    end
  end

  endpoint :update, do |object:, **options|
    if object.errors
      render json: object.errors, status: :unprocessable_entity
    else
      render json: serialize(object, **options), status: :ok
    end
  end

  endpoint :show, do |object:, **options|
    render json: serialize(object, **options), status: :created
  end

  endpoint :delete, do |object:, **options|
    if object.errors
      render json: object.errors, status: :unprocessable_entity
    else
      render json: {}, status: :no_content
    end
  end

  setup_create { |**options| options[:params] = deserialize(params, **options) }
  setup_update { |**options| options[:params] = deserialize(params, **options) }

  process_create { |**options| run_context(**options) }
  process_update { |**options| run_context(**options) }
  process_show { |**options| run_context(**options) }
  process_delete { |**options| run_context(**options) }
end

Controller

A controller is a combination of a Context, Serializer, Deserializer, and a some Endpoints. See the Quickstart exaple above and the description of functions below for more details.

Ussing SSR

You don't need a serializer. You can just use a standard render in the endpoint's function.

# taken from https://guides.rubyonrails.org/layouts_and_rendering.html

class BooksController < ApplicationController
  def index
    @books = Book.all
  end
end

# becomes

class BooksController < ApplicationController
  include SnFoil::Controller

  endpoint(:index) { |**options| @books = Book.all }
end

Endpoint

Endpoint creates a workflow with two intervals and a primary function for rendering.

class PeopleController < ActionController::API
  include SnFoil::Controller

  endpoint(:create) { |**options| render json: options[:object] }
end

In this exmaple the setup_create and process_create intervals are defined for you and the method finally returns the block. If you don't want to provide a block you can instead pass in a method name

class PeopleController < ActionController::API
  include SnFoil::Controller

  endpoint(:create, with: :render_create)
  
  def render_create(**options)
    render json: options[:object]
  end
end

Any options passed in as arguements to endpoint will be passed to the intervals and flow through just like a Context (becuase it is a Context under the hood).

class PeopleController < ActionController::API
  include SnFoil::Controller

  endpoint(:create, with: :render_create, interesting: 'key you have there')

  setup_create do |**options|
    puts options[:interesting] # => 'key you have there'
    ...
  end
end
Arguments
name type description required
name string|symbol The name of the method to be defined on the controller and the intervals true
options keyword arguments The options you want passed down the chain of intervals and to the context false
block proc The function you want to render your controller action conditionally based on if you don't provide a `:with` in the only

There are a few reserved keyword arguements that cause different functionlity/configuration for options:

  • with - The method name to use if a block is not provided to the endpoint
  • context - The context to use for this endpoint. Overrides the one configured using #self.context
  • context_action - The method name to call on the context. Defaults to the endpoint name.
  • serializer - The serializer to use for this endpoint. Overrides the one configured using #self.serializer
  • serialize - The block used to process the serializer. Overrides the one configured using #self.serializer
  • serialize_with - The method used to process the serializer. Overrides the one configured using #self.serializer
  • deserializer - The deserializer to use for this endpoint. Overrides the one configured using #self.deserializer
  • deserialize - The block used to process the deserializer. Overrides the one configured using #self.deserializer
  • deserialize_with - The method used to process the deserializer. Overrides the one configured using #self.deserializer

Context

The main context intended to be called by the Controller.

class PeopleController < ActionController::API
  include SnFoil::Controller

  context PeopleContext
end
Arguments
name type description required
name class The context class for the controller true

You can directly call the context using the #run_context method and pass it the options. It will automatically process either the :context_action or the :controller_action from the options. This can be overridden by passing a method name to #run_context.

...
  context PeopleContext

  def some_method(**options)
    run_context(:elevate, **options) #=> Calls PeopleContext#elevate
  end
...

Serializer

The main serializer intended to be called by the Controller. Also the default serializer and block used by the '#serialize` method.

class PeopleController < ActionController::API
  include SnFoil::Controller

  serializer PeopleSerializer
end
Arguments
name type description required
name class The serializer class for the controller false
block proc The block to be called to serialize the data false
Default Call

If no block or method is provided, #serialize will try to new up the Serializer class with arguments object and options and call #to_hash.

Serializer.new(object, **options).to_hash
Passing in a Block

If you provide a block to the #self.serializer method you can define how you want the serializer to be called.

class PeopleController < ActionController::API
  include SnFoil::Controller

  serializer(PeopleSerializer) { |object, serializer, **_options| serializer.new(object).serialize }
end

Deserializer

The main deserializer intended to be called by the Controller. Also the default deserializer and block used by the '#deserialize` method.

class PeopleController < ActionController::API
  include SnFoil::Controller

  deserializer PeopleDeserializer
end
Arguments
name type description required
name class The deserializer class for the controller false
block proc The block to be called to deserialize the data false
Default Call

If no block or method is provided, #deserialize will try to new up the Deserializer class with arguments object and options and call #to_hash.

Deserializer.new(object, **options).to_hash
Passing in a Block

If you provide a block to the #self.deserializer method you can define how you want the deserializer to be called.

class PeopleController < ActionController::API
  include SnFoil::Controller

  deserializer(PeopleDeserializer) { |object, deserializer, **_options| deserializer.new(object).deserialize }
end

Serializers and Deserializers

Since Serializers seem so abundant SnFoil Controllers does not ship with any. We recommend the awesome jsonapi-serializer.

Deserializers haven't come so far - so we've setup two:

  • SnFoil::Deserializer::JSON
  • SnFoil::Deserializer::JSONAPI

These allow you to allow-list and format any incoming data into a standard more usable by your business logic.

Usage
class PeopleDeserializer
  include SnFoil::Deserializer::JSON

  key_transform :underscore

  attributes :first_name, :middle_name, :last_name
  attributes :line1, :line2, :city, :state, :zip, prefix: :address_

  has_many :books, deserializer: BookDeserializer
end

Both these deserializers share some common functions

key_transform

How you want to format the keys in the incoming payload. SnFoil::Deserializers will always :to_sym all of the keys and will by default :underscore them. You can pass in most active_support inflections or you can run some custom logic on them.

Arguments
name type description required
tranform symbol The inflection you want called on the key value. ex: `underscore`, `camelcase` false
block proc A custom proc passed the input request and the key the return value will be stored under. false
attribute

An attribute to be taken from the input payload.

attribute :first_name 
attribute :last_name
attribute :line1, :prefix: :addr_
attribute :line2, :prefix: :addr_
attribute :city, :prefix: :addr_
attribute :state, :prefix: :addr_
attribute :zip_code, key: :addr_postal_code
Arguments
name type description required
name symbol The name of the key to be output in the final hash true
options keyword arguments The options you want passed down the chain of intervals and to the context false
block proc false

If you are using a block or the :with argument it will be passed the input, the key, and any options for the deserializer. The return of the block or method is what will be used as the value instead of looking up the key directly in the input.

example:

attribute(:test) { |request, key, **options| request[:data][key] }

There are a few reserved keyword arguements that cause different functionlity/configuration for options:

  • key the name of the key from the original input payload. If not provided this defaults to the name of the attribute.
  • prefix a prefix for the key you are looking for. ex attribute(:line1, prefix: :addr_) will look for a key labeled :addr_line1
  • with the method name you want to call to lookup/parse an attribute
attributes

The same as attribute except you can pass in multiple keys.

attributes :first_name, :last_name
attributes :line1, :line2, :city, :state :prefix: :addr_
belongs_to

A standard belongs_to relationship. Instead of grabbing a single key from the payload, expects to grab a hash.

belongs_to :team
Arguments
name type description required
tranform symbol The inflection you want called on the key value. ex: `underscore`, `camelcase` false
block proc A custom proc passed the input request and the key the return value will be stored under. false
has_one

Just an alias for #belongs_to

has_many

A standard has_many relationship. Instead of grabbing a single key from the payload, expects to grab an array.

has_many :pets
Arguments
name type description required
tranform symbol The inflection you want called on the key value. ex: `underscore`, `camelcase` false
block proc A custom proc passed the input request and the key the return value will be stored under. false

JSON Deserializer

Attrbute - Namespace

The JSON Deserializer has attribute namespacing that isn't available in JSONAPI due to its structured nature.

  • namespace an array of the nested keys needed to access a value

This works with both attribute and attributes.

attribute :rank, namespace: [:military_information]

Which would pull a nested field like in the following example.

{
  "name":"John",
  ...
  "military-info": {
    "branch":"Army",
    "rank":"Private First Class"
  }
  ...
}

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/limited-effort/snfoil-controller. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

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

Code of Conduct

Everyone interacting in the Snfoil::Controller project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the code of conduct.