Skip to content
This repository has been archived by the owner on Feb 7, 2018. It is now read-only.

Commit

Permalink
Merge pull request #40 from Factlink/pavlov-0.1.3
Browse files Browse the repository at this point in the history
Pavlov 0.1.3
  • Loading branch information
markijbema committed Aug 5, 2013
2 parents fb0da47 + fd8daef commit 5134416
Show file tree
Hide file tree
Showing 23 changed files with 501 additions and 164 deletions.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Pavlov Changelog

## HEAD

## 0.1.3

This release brings forth lots and lots of incompatibilities. Where possible, we've tried to keep a backwards-compatible API available. You can activate this by requiring `pavlov/alpha_compatibility'.

#### New Stuff:

* Pavlov now uses Virtus for what used to be called `arguments`. Instead of specifying a list of arguments, you can now specify attributes individually, with optional defaults. Check the README on all the cool stuff you can do with these Virtus-based attributes.

#### Deprecations:

If you want to retain deprecated functionality, you can `require 'pavlov/alpha_compatibility'`.

* Deprecated `arguments` in operations.
* Deprecated `pavlov_options` that were used by the helpers.

#### Completely removed:

* Removed support for `finish_initialize`. Override the `initialize` method and call `super` instead.

## 0.1.0

Initial alpha-release. Here be dragons.
187 changes: 166 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,197 @@ The Pavlov gem provides a Command/Query/Interactor framework.
* You can have queries that return objects that don't map directly to a specific database table.
* You can replace your database from SQL-based to MongoDB, Redis or even a webservice without having to touch your business logic.

## Warning

### Use at your own risk, this is _EXTREMELY_ alpha and subject to changes without notice.

All versions < 0.2 are to be considered alpha. We're working towards a stable version 0.2, following the readme as defined here. For now, unfortunately we don't support all features described here yet.

Currently unsupported functionality, which is already described below:

* **Validating with an error object:** For now validate should throw an error when the operation isn't valid
* **Context:** For now use alpha_compatibility, and pass in `pavlov_options` as arguments.
* **Checking valid?** This can work, but only if you don't implement validate, and let it return a boolean. This API will probably change though.

## Installation

Add this line to your application's Gemfile:

gem 'pavlov'

And then execute:
Then generate some initial files with:

rails generate pavlov:install

## Usage

```ruby
class Commands::CreateBlogPost
include Pavlov::Command

attribute :id, Integer
attribute :title, String
attribute :body, String
attribute :published, Boolean

private

def validate
errors.add(:id, "can't contain spaces") if id.include?(" ")
end

def execute
$redis.hmset("blog_post:#{id}", title: title, body: body, published: published)
$redis.sadd("blog_post_list", id)
end
end

class Queries::AvailableId
include Pavlov::Query

private

def execute
generate_uuid
end

def generate_uuid
SecureRandom.hex(64) # TODO Look up actual implementation
end
end

class Interactors::CreateBlogPost
include Pavlov::Interactor

attribute :title, String
attribute :body, String
attribute :published, Boolean, default: true

private

def authorized?
context.current_user.is_admin?
end

$ bundle
def validate
errors.add(:body, "NO SHOUTING!!!!") if body.matches?(/\W[A-Z]{2,}\W/)
end

Or install it yourself as:
def execute
command :create_blog_post, id: available_id,
title: title,
body: body,
published: published
Struct.new(:title, :body).new(title, body)
end

$ gem install pavlov
def available_id
query :available_id
end
end

class PostsController < ApplicationController
include Pavlov::Helpers

## Commands, Queries and Interactors
respond_to :json

Inspiration:
http://www.confreaks.com/videos/759-rubymidwest2011-keynote-architecture-the-lost-years
http://martinfowler.com/bliki/CQRS.html
def create
interaction = interactor :create_blog_post, params[:post]

Frontend only calls interactors. Interactors call queries and commands.
Queries never call commands, they can call queries.
Commands can call commands and queries.
But keep your design _simple_ (KISS).
if interaction.valid?
respond_with interaction.call
else
respond_with {errors: interaction.errors}
end
rescue AuthorizationError
flash[:error] = "Hacker, begone!"
redirect_to root_path
end
end
```

### Usage
### Attributes

TODO
Attributes work mostly like Virtus does. Attributes are always required unless they have a default value.

### Validations

### Authorization

There are multiple facets to whether a user is authorized:
Interactors must define a method `authorized?` that determines if the interaction is allowed. If this method returns a truthy value, Pavlov will allow the interaction to be executed. This check is performed when `interaction.call` is executed.

To help you determine whether operations are allowed, you can set up a global [interaction context](#context), which you can then access from your interactors:

```ruby
class Interactors::CreateBlogPost
include Pavlov::Interactor

def authorized?
context.current_user.is_admin?
end
end
```

If the interaction is not authorized, a `Pavlov::AuthorizationError` exception will be thrown. In normal execution you wouldn't expect this to ever occur, so might be reasonable to set up a global catch for this exception that redirects users to your homepage:

```ruby
class ApplicationController
rescue_from Pavlov::AuthorizationError, with: :possible_hack_attempt

private

def possible_hack_attempt
logger.warn 'This might have been a hacker'
redirect_to root_path
end
end
```

### Context

You probably have certain aspects of your application that you always, or at least very often, want to pass into the interactors, so that they can check authorization, either in terms of blocking unauthorized executions, or automatically scoping queries so that e.g. users will only see data belonging to their account.

```ruby
class ApplicationController < ActionController::Base
include Pavlov::Helpers

before_filter :set_pavlov_context

private

def set_pavlov_context
context.add(:current_user, current_user)
end
end
```

In your tests, you could write:

```ruby
describe CreateBlogPost do
include Pavlov::Helpers

let(:user) { mock("User", is_admin?: true) }
before { context.add(:current_user, user) }

it 'should create posts' do
interactor(:create_blog_post, title: 'Foo', body: 'Bar').call
# test for the creation
end
end
```

1. Can this user execute this operation
2. On which set of objects can this user execute this operation
## Is it any good?

We decided that the best way to handle this is:
Yes.

The interactors check whether an operation is authorized before running the execution code, but not in the initialization. This is not implemented yet, but will mean an interactor has something like run which does authorize; execute.
## Related

When a operation is executed on one object and this is not authorized, this is clearly an exceptional situation (in the sense that it shouldn't happen), and an exception is thrown.
If Pavlov happens not to be to your taste, you might look at these other libraries:

When a operation is executed on a set of objects, the operation will only execute on the subset the user is authorized for.
* [Mutations](https://github.com/cypriss/mutations) provides service objects
* [Imperator](https://github.com/karmajunkie/imperator) provides command objects
* [Wisper](https://github.com/krisleech/wisper) provides callbacks

## Contributing

Expand Down
5 changes: 5 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.1.3

* change all calls to the constructor of operations to construct with named parameters instead of positional parameters
* change all tests for authorization and validation, since those now get called when invoking `#call` instead of on initialization

## before 0.1.3

* change tests which expect invocations of validations to tests which check whether an error has been thrown when you give it invalid input.
Expand Down
17 changes: 17 additions & 0 deletions lib/generators/pavlov/install_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require 'rails/generators'

module Pavlov
class InstallGenerator < Rails::Generators::Base
def self.source_root
@source_root ||= File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
end

def copy_backend_directory
directory 'backend', 'app/backend', recursive: true
end

def add_backend_to_autoload_paths
application "config.autoload_paths += %W(\#{config.root}/app/backend)"
end
end
end
3 changes: 3 additions & 0 deletions lib/generators/pavlov/templates/backend/backend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Backend
# This class is a placeholder that will be expanded by a later feature branch
end
3 changes: 3 additions & 0 deletions lib/generators/pavlov/templates/backend/command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Command
include Pavlov::Operation
end
16 changes: 16 additions & 0 deletions lib/generators/pavlov/templates/backend/interactor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Interactor
include Pavlov::Operation

# If you want your interactors to be compatible with a backgrounding
# daemon, you can use this base class to add support.
#
# Example for Resque:
#
# def self.perform(*args)
# new(*args).call
# end

def authorized?
raise NotImplementedError
end
end
3 changes: 3 additions & 0 deletions lib/generators/pavlov/templates/backend/query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Query
include Pavlov::Operation
end
1 change: 1 addition & 0 deletions lib/pavlov.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def self.query command_name, *args
end
end

require_relative 'pavlov/engine' if defined?(Rails)
require_relative 'pavlov/helpers'
require_relative 'pavlov/access_denied'
require_relative 'pavlov/validation_error'
Expand Down
Loading

0 comments on commit 5134416

Please sign in to comment.