# SnFoil::Controller
![build](https://github.com/limited-effort/snfoil-controller/actions/workflows/main.yml/badge.svg) [![maintainability](https://api.codeclimate.com/v1/badges/10885d7b7231f3e9b0b7/maintainability)](https://codeclimate.com/github/limited-effort/snfoil-controller/maintainability)
SnFoil Controllers help seperate your business logic from your api layer.
## Installation
Add this line to your application's Gemfile:
```ruby
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
```ruby
# 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.
```ruby
# 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.
```ruby
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
```ruby
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).
```ruby
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.
```ruby
class PeopleController < ActionController::API
include SnFoil::Controller
context PeopleContext
end
```
##### Arguments
name |
type |
description |
required |
name |
class |
The context class for the controller |
true |
#### Serializer
The main serializer intended to be called by the Controller. Also the default serializer and block used by the '#serialize` method.
```ruby
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`.
```ruby
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.
```ruby
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.
```ruby
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`.
```ruby
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.
```ruby
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](https://github.com/jsonapi-serializer/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
```ruby
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.
```ruby
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:
```ruby
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.
```ruby
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.
```ruby
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.
```ruby
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 |
## 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](https://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](https://github.com/limited-effort/snfoil-controller/blob/main/CODE_OF_CONDUCT.md).
## License
The gem is available as open source under the terms of the [Apache 2 License](https://opensource.org/licenses/Apache-2.0).
## 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](https://github.com/limited-effort/snfoil-controller/blob/main/CODE_OF_CONDUCT.md).