# SnFoil::Context ![build](https://github.com/limited-effort/snfoil-context/actions/workflows/main.yml/badge.svg) [![maintainability](https://api.codeclimate.com/v1/badges/6a7a2f643707c17cb879/maintainability)](https://codeclimate.com/github/limited-effort/snfoil-context/maintainability) SnFoil Contexts are a simple way to insure a workflow pipeline can be easily established end extended. It helps by creating workflow, allowing additional in steps at specific intervals, and reacting to a success or failure, you should find your code being more maintainable and testable. ## Installation Add this line to your application's Gemfile: ```ruby gem 'snfoil-context' ``` ## Usage While contexts are powerful, they aren't a magic bullet. Each function should strive to only contain a single purpose. This also has the added benefit of outlining some basic tests - if it is in a function it should have a related test. ### Action To start you will need to define an action. Arguments: * `name` - The name of this action will also set the name of all the hooks and methods later generated. * `with` - Keyword Param - The method name of the primary action. Either this or a block is required * `block` - Block - The block of the primary action. Either this or with is required ```ruby # lib/contexts/token_context require 'snfoil/context' class TokenContext include SnFoil::Context action :expire { |options| options[:object].update(expired_at: Time.current) } end ``` This will generate the methods and hooks of the pipeline. In this example the following get made: * setup_expire * before_expire * after_expire_success * after_expire_failure * after_expire If you want to reuse the primary action or just prefer methods, you can pass in the method name you would like to call, rather than providing a block. If a method name and a block is provided, the block is ignored. ```ruby # lib/contexts/token_context require 'snfoil/context' class TokenContext include SnFoil::Context action :expire, with: :expire_token def expire_token(options) options[:object].update(expired_at: Time.current) end end ``` #### Primary Actions The primary action is the function that determine whether or not the action is successful. To do this, the primary action must always return a truthy value if the action was successful, or a falsey one if it failed. The primary action is passed one argument which is the return value of the closest preceeding interval function. ```ruby # lib/contexts/token_context require 'snfoil/context' class TokenContext include SnFoil::Context action :expire, with: :expire_token before_expire do |options| options[:foo] = bar options end def expire_token(options) puts options[:foo] # => logs 'bar' to the console ... end end ``` #### Intervals The following are the intervals SnFoil Contexts sets up in the order they occur. The suggested uses are just very simply examples. You can chain contexts to setup very complex interactions in a very easy to manage workflow.
Name Suggested Use
setup_<action>
* find or create a model
* setup params needed later in the action
* set scoping
before_<action>
* alter model or set attributes
primary action
* persist database changes
* make primary network call
after_<action>_success
* setup additional relationships
* success specific logging
after_<action>_failure
* cleanup failed remenants
* call bug tracker
* failure specific logging
after_<action>
* perform necessary required cleanup
* log outcome
#### Hook and Method Design SnFoil Contexts try hard to not store variables longer than necessary. To facilitate this we have choosen to pass an object (we normally use a hash called options) to each hook and method, and the return from the hook or method is passed down the chain to the next hook or method. The only method or block that does not get its value passwed down the chain is the primary action - which must always return a truthy value of whether or not the action was successful. #### Hooks Hooks make it very easy to compose multiple actions that need to occur in a specific order. You can have as many repeated hooks as you would like. This makes defining single responsibility hooks very simple, and they will get called in the order they are defined. Important Note Hooks always need to return the options hash at the end. ##### Example ```ruby # Call the webhooks for third party integrations after_expire_success do |options| call_webhook_for_model(options[:object]) options end # Commit business logic to internal process after_expire_success do |options| finalize_business_logic(options[:object]) options end # notify error tracker after_expire_error do |options| notify_errors(options[:object].errors) options end ``` #### Methods Methods allow users to create inheritable actions that occur in a specific order. Methods will always run after their hook counterpart. Since these are inheritable, you can chain needed actions all the way through the parent heirarchy by using the `super` keyword. Important Note Methods always need to return the options hash at the end. Author's opinion: While simplier than hooks, they do not allow for as clean of a composition as hooks. ##### Example ```ruby # Call the webhooks for third party integrations # Commit business logic to internal process def after_expire_success(**options) options = super call_webhook_for_model(options[:object]) finalize_business_logic(options[:object]) options end # notify error tracker def after_expire_error(**options) options = super notify_errors(options[:object].errors) options end ``` ### Authorize The original purpose of all of SnFoil was to ensure there was a good consistent way to authenticate and authorize entities. As such authorize hooks were built directly into the workflow. These authorization hooks are always called twice. Once after `setup_` and once after `before_` The `authorize` method functions much like primary action except the first argument is usually the name of action you are authorizing. Arguments: * `name` - The name of this action to be authorized. If ommited, all actions without a specific associated authorize will use this one.. * `with` - Keyword Param - The method name of the primary action. Either this or a block is required * `block` - Block - The block of the primary action. Either this or with is required ```ruby # lib/contexts/token_context require 'snfoil/context' class TokenContext include SnFoil::Context action :expire, with: :expire_token authorize :expire { |options| options[:entity].is_admin? } ... end ``` You can also call authorize without an action name. This will have all action authorize with the provided method or block unless there is a more specific authorize action configured. Its probably easier explained with an example ```ruby # lib/contexts/token_context require 'snfoil/context' class TokenContext include SnFoil::Context action :expire, with: :expire_token #=> will authorize by checking the entity is an admin action :search, with: :query_tokens #=> will authorize by checking the entity is a user action :show, with: :find_token #=> will authorize by checking the entity is a user authorize :expire { |options| options[:entity].is_admin? } authorize { |options| options[:entity].is_user? } ... end ``` #### Why before and after? Simply to make sure the entity it actually allowed access the primary target and is allowed to make the requested alterations/interactions. ## 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-context. 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-context/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::Context project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/limited-effort/snfoil-context/blob/main/CODE_OF_CONDUCT.md).