README.md in pragma-2.0.0 vs README.md in pragma-2.1.0

- old
+ new

@@ -1,13 +1,14 @@ # Pragma -[![Build Status](https://img.shields.io/travis/pragmarb/pragma.svg?maxAge=3600&style=flat-square)](https://travis-ci.org/pragmarb/pragma) -[![Dependency Status](https://img.shields.io/gemnasium/pragmarb/pragma.svg?maxAge=3600&style=flat-square)](https://gemnasium.com/github.com/pragmarb/pragma) -[![Code Climate](https://img.shields.io/codeclimate/github/pragmarb/pragma.svg?maxAge=3600&style=flat-square)](https://codeclimate.com/github/pragmarb/pragma) -[![Coveralls](https://img.shields.io/coveralls/pragmarb/pragma.svg?maxAge=3600&style=flat-square)](https://coveralls.io/github/pragmarb/pragma) +[![Build Status](https://travis-ci.org/pragmarb/pragma.svg?branch=master)](https://travis-ci.org/pragmarb/pragma) +[![Dependency Status](https://gemnasium.com/badges/github.com/pragmarb/pragma.svg)](https://gemnasium.com/github.com/pragmarb/pragma) +[![Coverage Status](https://coveralls.io/repos/github/pragmarb/pragma/badge.svg?branch=master)](https://coveralls.io/github/pragmarb/pragma?branch=master) +[![Maintainability](https://api.codeclimate.com/v1/badges/e51e8d7489eb72ab97ba/maintainability)](https://codeclimate.com/github/pragmarb/pragma/maintainability) -Welcome to Pragma, a pragmatic (duh!), opinionated architecture for building JSON APIs with Ruby! +Welcome to Pragma, an expressive, opinionated ecosystem for building beautiful RESTful APIs with +Ruby. You can think of this as a meta-gem that pulls in the following pieces: - [Pragma::Operation](https://github.com/pragmarb/pragma-operation); - [Pragma::Policy](https://github.com/pragmarb/pragma-policy); @@ -97,12 +98,14 @@ ├── operation │   ├── create.rb │   ├── destroy.rb │   ├── index.rb │   └── update.rb + └── decorator + | ├── collection.rb + | └── instance.rb └── policy.rb - └── decorator.rb ``` Your modules and classes would, of course, follow the same structure: `API::V1::Article::Policy` and so on and so forth. @@ -126,19 +129,370 @@ module V1 module Article module Operation class Create < Pragma::Operation::Create # This assumes that you have the following: - # - a policy that responds to #create? - # - a Create contract - # - a decorator - # - an Article model + # 1) an Article model + # 2) a Policy (responding to #create?) + # 3) a Create contract + # 4) an Instance decorator end end end end end ``` + +## Macros + +The FF are implemented through their own set of macros, which take care of stuff like authorizing, +paginating, filtering etc. + +If you want, you can use these macros in your own operations. + +### Classes + +**Used in:** Index, Show, Create, Update, Destroy + +The `Classes` macro is responsible of tying together all the Pragma components: put it into an +operation and it will determine the class names of the related policy, model, decorators and +contract. You can override any of these classes when defining the operation or at runtime if you +wish. + +Example usage: + +```ruby +module API + module V1 + module Article + module Operation + class Create < Pragma::Operation::Base + # Let the macro figure out class names. + step Pragma::Operation::Macro::Classes() + step :execute! + + # But override the contract. + self['contract.default.class'] = Contract::CustomCreate + + def execute!(options) + # `options` contains the following: + # + # `model.class` + # `policy.default.class` + # `policy.default.scope.class` + # `decorator.instance.class` + # `decorator.collection.class` + # `contract.default.class` + # + # These will be `nil` if the expected classes do not exist. + end + end + end + end + end +end +``` + +### Model + +**Used in:** Index, Show, Create, Update, Destroy + +The `Model` macro provides support for performing different operations with models. It can either +build a new instance of the model, if you are creating a new record, for instance, or it can find +an existing record by ID. + +Example of building a new record: + +```ruby +module API + module V1 + module Article + module Operation + class Create < Pragma::Operation::Base + # This step can be done by Classes if you want. + self['model.class'] = ::Article + + step Pragma::Operation::Macro::Model(:build) + step :save! + + def save!(options) + # Here you'd usually validate and assign parameters before saving. + + # ... + + options['model'].save! + end + end + end + end + end +end +``` + +As we mentioned, `Model` can also be used to find a record by ID: + +```ruby +module API + module V1 + module Article + module Operation + class Show < Pragma::Operation::Base + # This step can be done by Classes if you want. + self['model.class'] = ::Article + + step Pragma::Operation::Macro::Model(:find_by), fail_fast: true + step :respond! + + def respond!(options) + options['result.response'] = Response::Ok.new( + entity: options['model'] + ) + end + end + end + end + end +end +``` + +In the example above, if the record is not found, the macro will respond with `404 Not Found` and a +descriptive error message for you. If you want to override the error handling logic, you can remove +the `fail_fast` option and instead implement your own `failure` step. + +### Policy + +**Used in:** Index, Show, Create, Update, Destroy + +The `Policy` macro ensures that the current user can perform an operation on a given record. + +Here's a usage example: + +```ruby +module API + module V1 + module Article + module Operation + class Show < Pragma::Operation::Base + # This step can be done by Classes if you want. + self['policy.default.class'] = Policy + + step :model! + step Pragma::Operation::Macro::Policy(), fail_fast: true + step :respond! + + def model!(params:, **) + options['model'] = ::Article.find(params[:id]) + end + end + end + end + end +end +``` + +If the user is not authorized to perform the operation (i.e. if the policy's `#show?` method returns +`false`), the macro will respond with `403 Forbidden` and a descriptive error message. If you want +to override the error handling logic, you can remove the `fail_fast` option and instead implement +your own `failure` step. + +### Filtering + +**Used in:** Index + +The `Filtering` macro provides a simple interface to define basic filters for your API. You simply +include the macro and configure which filters you want to expose to the users. + +```ruby +module API + module V1 + module Article + module Operation + class Index < Pragma::Operation::Base + step :model! + step Pragma::Operation::Macro::Filtering() + step :respond! + + self['filtering.filters'] = [ + Pragma::Operation::Filter::Equals.new(param: :by_category, column: :category_id), + Pragma::Operation::Filter::Ilike.new(param: :by_title, column: :title) + ] + + def model!(params:, **) + options['model'] = ::Article.all + end + end + end + end + end +end +``` + +With the example above, you are exposing the `by_category` filter and the `by_title` filters. The +following filters are available for ActiveRecord currently: + +- `Equals`: performs an equality (`=`) comparison. +- `Like`: performs a `LIKE` comparison. +- `Ilike`: performs an `ILIKE` comparison. + +Support for more clauses as well as more ORMs will come soon. + +### Ordering + +**Used in:** Index + +As the name suggests, the `Ordering` macro allows you to easily implement default and user-defined +ordering. + +Here's an example: + +```ruby +module API + module V1 + module Article + module Operation + class Index < Pragma::Operation::Base + # This step can be done by Classes if you want. + self['model.class'] = ::Article + + self['ordering.default_column'] = :published_at + self['ordering.default_direction'] = :desc + self['ordering.columns'] = %i[title published_at updated_at] + + step :model! + + # This will override `model` with the ordered relation. + step Pragma::Operation::Macro::Ordering(), fail_fast: true + + step :respond! + + def model!(options) + options['model'] = options['model.class'].all + end + + def respond!(options) + options['result.response'] = Response::Ok.new( + entity: options['model'] + ) + end + end + end + end + end +end +``` + +If the user provides an invalid order column or direction, the macro will respond with `422 Unprocessable Entity` +and a descriptive error message. If you wish to implement your own error handling logic, you can +remove the `fail_fast` option and implement your own `failure` step. + +The macro accepts the following options, which can be defined on the operation or at runtime: + +- `ordering.columns`: an array of columns the user can order by. +- `ordering.default_column`: the default column to order by (default: `created_at`). +- `ordering.default_direction`: the default direction to order by (default: `desc`). +- `ordering.column_param`: the name of the parameter which will contain the order column. +- `ordering.direction_param`: the name of the parameter which will contain the order direction. + +### Pagination + +**Used in:** Index + +The `Pagination` macro is responsible for paginating collections of records through +[will_paginate](https://github.com/mislav/will_paginate). It also allows your users to set the +number of records per page. + +```ruby +module API + module V1 + module Article + module Operation + class Index < Pragma::Operation::Base + # This step can be done by Classes if you want. + self['model.class'] = ::Article + + step :model! + + # This will override `model` with the paginated relation. + step Pragma::Operation::Macro::Pagination(), fail_fast: true + + step :respond! + + def model!(options) + options['model'] = options['model.class'].all + end + + def respond!(options) + options['result.response'] = Response::Ok.new( + entity: options['model'] + ) + end + end + end + end + end +end +``` + +In the example above, if the page or per-page number fail validation, the macro will respond with +`422 Unprocessable Entity` and a descriptive error message. If you wish to implement your own error +handling logic, you can remove the `fail_fast` option and implement your own `failure` step. + +The macro accepts the following options, which can be defined on the operation or at runtime: + +- `pagination.page_param`: the parameter that will contain the page number. +- `pagination.per_page_param`: the parameter that will contain the number of items to include in each page. +- `pagination.default_per_page`: the default number of items per page. +- `pagination.max_per_page`: the max number of items per page. + +This macro is best used in conjunction with the [Collection](https://github.com/pragmarb/pragma-decorator#collection) +and [Pagination](https://github.com/pragmarb/pragma-decorator#pagination) modules of +[Pragma::Decorator](https://github.com/pragmarb/pragma-decorator), which will expose all the +pagination metadata. + +### Decorator + +**Used in:** Index, Show, Create, Update + +The `Decorator` macro uses one of your decorators to decorate the model. If you are using +[expansion](https://github.com/pragmarb/pragma-decorator#associations), it will also make sure that +the expansion parameter is valid. + +Example usage: + +```ruby +module API + module V1 + module Article + module Operation + class Show < Pragma::Operation::Base + # This step can be done by Classes if you want. + self['decorator.instance.class'] = Decorator::Instance + + step :model! + step Pragma::Operation::Macro::Decorator(), fail_fast: true + step :respond! + + def model!(params:, **) + options['model'] = ::Article.find(params[:id]) + end + + def respond!(options) + # Pragma does this for you in the default operations. + options['result.response'] = Response::Ok.new( + entity: options['result.decorator.instance'] + ) + end + end + end + end + end +end +``` + +The macro accepts the following options, which can be defined on the operation or at runtime: + +- `expand.enabled`: whether associations can be expanded. +- `expand.limit`: how many associations can be expanded at once. ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/pragmarb/pragma.