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.