Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Middlewares #2

Merged
merged 5 commits into from Feb 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .rubocop.yml
Expand Up @@ -4,3 +4,6 @@ inherit_gem:
AllCops:
DisplayCopNames: true
TargetRubyVersion: 2.5

Naming/MemoizedInstanceVariableName:
Enabled: false
36 changes: 35 additions & 1 deletion README.md
Expand Up @@ -54,7 +54,7 @@ rescue Polist::Service::Failure => error
end
```

Note that `.run` and `.call` are just shortcuts for `MyService.new(...).run` and `MyService.new(...).call` with the only difference that they always return the service instance instead of the result of `#run` or `#call`. Unlike `#call` though, `#run` is not intended to be owerwritten in subclasses.
Note that `.run` and `.call` are just shortcuts for `MyService.new(...).run` and `MyService.new(...).call` with the only difference that they always return the service instance instead of the result of `#run` or `#call`. Unlike `#call` though, `#run` is not intended to be overwritten in subclasses.

### Using Form objects

Expand Down Expand Up @@ -169,6 +169,40 @@ c.x # => 15
c.y # => nil
```

### Using Middlewares

If you have some common things to be done in more than one service, you can define a middleware and register it inside the said services.
Every middleware takes the service into it's constructor and executes `#call`. Thus every middleware has to implement `#call` method and has a `#service` attribute reader.
Middlewares delegate `#success!`, `#fail!`, `#error!`, `#form`, `#form_attributes` to the service class they are registered in.
Every middleware should be a subclass of `Polist::Service::Middleware`. Middlewares are run before the service itself is run.

To register a middleware one should use `.register_middleware` class method on a service. More than one middleware can be registered for one service.

For example:
```ruby
class MyMiddleware < Polist::Service::Middleware
def call
fail!(code: :not_cool) if service.fail_on_middleware?
end
end

class MyService < Polist::Service
register_middleware MyMiddleware

def call
success!(code: :cool)
end

def fail_on_middleware?
true
end
end

service = MyService.run
service.success? #=> false
service.response #=> { code: :not_cool }
```

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/polist.
Expand Down
1 change: 1 addition & 0 deletions lib/polist.rb
Expand Up @@ -2,5 +2,6 @@

require "polist/builder"
require "polist/service"
require "polist/service/middleware"
require "polist/struct"
require "polist/version"
33 changes: 33 additions & 0 deletions lib/polist/service.rb
Expand Up @@ -20,10 +20,20 @@ class Form
include ActiveModel::Validations
end

module MiddlewareCaller
def call
call_middlewares
super
end
end

MiddlewareError = Class.new(StandardError)

attr_accessor :params

def self.inherited(klass)
klass.const_set(:Failure, Class.new(klass::Failure))
klass.prepend MiddlewareCaller
end

def self.build(*args)
Expand All @@ -44,6 +54,23 @@ def self.param(*names)
end
end

def self.__polist_middlewares__
@__polist_middlewares__ ||= []
end

def self.register_middleware(klass)
unless klass < Polist::Service::Middleware
raise MiddlewareError,
"Middleware #{klass} should be a subclass of Polist::Service::Middleware"
end

__polist_middlewares__ << klass
end

def self.__clear_middlewares__
@__polist_middlewares__ = []
end

def initialize(params = {})
self.params = params
end
Expand Down Expand Up @@ -76,6 +103,12 @@ def validate!

private

def call_middlewares
self.class.__polist_middlewares__.each do |middleware|
middleware.new(self).call
end
end

def form
@form ||= self.class::Form.new(form_attributes.to_snake_keys)
end
Expand Down
20 changes: 20 additions & 0 deletions lib/polist/service/middleware.rb
@@ -0,0 +1,20 @@
# frozen_string_literal: true

class Polist::Service::Middleware
def initialize(service)
@service = service
end

# Should be implemented in subclasses
def call; end

private

attr_reader :service

%i[fail! error! success! form form_attributes].each do |service_method|
define_method(service_method) do |*args|
service.send(service_method, *args)
end
end
end
98 changes: 98 additions & 0 deletions spec/polist/service/middleware_spec.rb
@@ -0,0 +1,98 @@
# frozen_string_literal: true

class FirstMiddleware < Polist::Service::Middleware
def call; end
end

class SecondMiddleware < Polist::Service::Middleware
def call; end
end

class ServiceWithMiddlewares < Polist::Service
def call
success!(success: true)
end
end

RSpec.describe Polist::Service::Middleware do
before { ServiceWithMiddlewares.__clear_middlewares__ }

context "middlewares do nothing" do
before do
ServiceWithMiddlewares.register_middleware FirstMiddleware
ServiceWithMiddlewares.register_middleware SecondMiddleware
end

shared_examples "middlewares are called" do |service_call|
specify do
expect_any_instance_of(FirstMiddleware).to receive(:call)
expect_any_instance_of(SecondMiddleware).to receive(:call)

service = service_call.()
expect(service.success?).to eq(true)
expect(service.failure?).to eq(false)
expect(service.response).to eq(success: true)
end
end

it_behaves_like "middlewares are called", -> { ServiceWithMiddlewares.run }
it_behaves_like "middlewares are called", -> { ServiceWithMiddlewares.call }
end

describe "middlewares affect the response of the service" do
describe "#run" do
specify "#response is mutated and error is rescued" do
middleware = Class.new(Polist::Service::Middleware) do
def call
fail!(failed: true)
end
end

ServiceWithMiddlewares.register_middleware middleware

expect { ServiceWithMiddlewares.run }.not_to raise_error

service = ServiceWithMiddlewares.run
expect(service.success?).to eq(false)
expect(service.failure?).to eq(true)
expect(service.response).to eq(failed: true)
end
end

describe "#call" do
specify "error is raised" do
middleware = Class.new(Polist::Service::Middleware) do
def call
fail!(failed: true)
end
end

ServiceWithMiddlewares.register_middleware middleware

expect { ServiceWithMiddlewares.call }
.to raise_error(ServiceWithMiddlewares::Failure, "{:failed=>true}")
end
end
end

describe "middlewares delegate methods to service" do
shared_examples "no errors are raised" do |service_call|
specify do
middleware = Class.new(Polist::Service::Middleware) do
def call
form
form_attributes
success!
end
end

ServiceWithMiddlewares.register_middleware middleware

expect { service_call.() }.not_to raise_error
end
end

it_behaves_like "no errors are raised", -> { ServiceWithMiddlewares.run }
it_behaves_like "no errors are raised", -> { ServiceWithMiddlewares.call }
end
end
21 changes: 21 additions & 0 deletions spec/polist/service_spec.rb
Expand Up @@ -133,4 +133,25 @@ def call
expect(service.response).to eq(error: "failure")
end
end

describe ".register_middleware" do
let(:first_middleware) { Class.new(Polist::Service::Middleware) }
let(:second_middleware) { Class.new(Polist::Service::Middleware) }

before do
BasicService.register_middleware(first_middleware)
BasicService.register_middleware(second_middleware)
end

it "stores middlewares in the service class" do
expect(BasicService.__polist_middlewares__)
.to contain_exactly(first_middleware, second_middleware)
end

it "raises error if middleware is not a subclass of Polist::Service::Middleware" do
expect { BasicService.register_middleware(String) }
.to raise_error(Polist::Service::MiddlewareError,
"Middleware String should be a subclass of Polist::Service::Middleware")
end
end
end