-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WebPipe representation as a proc is the composition of all its plug operations. Thus, it is itself an operation taking a `Conn` and returning a `Conn`. It can be leveraged to be plugged as any other operation and compose WebPipe's: ```ruby class HtmlApp include WebPipe plug :html private def html(conn) conn.set_response_header('Content-Type', 'text/html') end end class App include WebPipe plug :html, &HtmlApp.new plug :body private def body(conn) conn.set_response_body('Hello, world!') end end ```
- Loading branch information
1 parent
e3f1a18
commit dfd9df4
Showing
7 changed files
with
257 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
require 'dry/initializer' | ||
require 'dry/monads/result' | ||
require 'web_pipe/types' | ||
require 'web_pipe/conn' | ||
require 'dry/monads/result/extensions/either' | ||
|
||
Dry::Monads::Result.load_extensions(:either) | ||
|
||
module WebPipe | ||
module ConnSupport | ||
# Composition of a pipe of {Operation} on a {Conn}. | ||
# | ||
# It represents the composition of a series of functions which | ||
# take a {Conn} as argument and return a {Conn}. | ||
# | ||
# However, {Conn} can itself be of two different types (subclasses | ||
# of it): a {Conn::Clean} or a {Conn::Dirty}. On execution time, | ||
# the composition is stopped whenever the stack is emptied or a | ||
# {Conn::Dirty} is returned in any of the steps. | ||
class Composition | ||
# Type for an operation. | ||
# | ||
# It should be anything callable expecting a {Conn} and | ||
# returning a {Conn}. | ||
Operation = Types.Interface(:call) | ||
|
||
# Error raised when an {Operation} returns something that is not | ||
# a {Conn}. | ||
class InvalidOperationResult < RuntimeError | ||
# @param returned [Any] What was returned from the {Operation} | ||
def initialize(returned) | ||
super( | ||
<<~eos | ||
An operation returned +#{returned.inspect}+. To be valid, | ||
an operation must return whether a | ||
WebPipe::Conn::Clean or a WebPipe::Conn::Dirty. | ||
eos | ||
) | ||
end | ||
end | ||
|
||
include Dry::Monads::Result::Mixin | ||
|
||
include Dry::Initializer.define -> do | ||
# @!attribute [r] operations | ||
# @return [Array<Operation[]>] | ||
param :operations, type: Types.Array(Operation) | ||
end | ||
|
||
# @param conn [Conn] | ||
# @return [Conn] | ||
# @raise InvalidOperationResult when an operation does not | ||
# return a {Conn} | ||
def call(conn) | ||
extract_result( | ||
apply_operations( | ||
conn | ||
) | ||
) | ||
end | ||
|
||
private | ||
|
||
def apply_operations(conn) | ||
operations.reduce(Success(conn)) do |new_conn, operation| | ||
new_conn.bind { |c| apply_operation(c, operation) } | ||
end | ||
end | ||
|
||
def apply_operation(conn, operation) | ||
result = operation.(conn) | ||
case result | ||
when Conn::Clean | ||
Success(result) | ||
when Conn::Dirty | ||
Failure(result) | ||
else | ||
raise InvalidOperationResult.new(result) | ||
end | ||
end | ||
|
||
def extract_result(result) | ||
extract_proc = :itself.to_proc | ||
|
||
result.either(extract_proc, extract_proc) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
require 'spec_helper' | ||
require 'support/env' | ||
|
||
RSpec.describe "Composition of WebPipe's" do | ||
let(:pipe) do | ||
Class.new do | ||
include WebPipe | ||
|
||
class One | ||
include WebPipe | ||
|
||
plug :one | ||
|
||
private | ||
|
||
def one(conn) | ||
conn.set_response_body('One') | ||
end | ||
end | ||
|
||
plug :one, &One.new | ||
plug :two | ||
|
||
private | ||
|
||
def two(conn) | ||
conn.set_response_body( | ||
conn.response_body[0] + 'Two' | ||
) | ||
end | ||
end.new | ||
end | ||
|
||
it 'plugging a WebPipe composes its plugs' do | ||
expect(pipe.call(DEFAULT_ENV).last).to eq(['OneTwo']) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
require 'spec_helper' | ||
require 'support/env' | ||
require 'web_pipe/conn_support/composition' | ||
require 'web_pipe/conn_support/builder' | ||
|
||
RSpec.describe WebPipe::ConnSupport::Composition do | ||
let(:conn) { WebPipe::ConnSupport::Builder.call(DEFAULT_ENV) } | ||
|
||
describe '#call' do | ||
it 'chains operations on Conn' do | ||
op_1 = ->(conn) { conn.set_status(200) } | ||
op_2 = ->(conn) { conn.set_response_body('foo') } | ||
|
||
app = described_class.new([op_1, op_2]) | ||
|
||
expect(app.call(conn)).to eq( | ||
op_2.(op_1.(conn)) | ||
) | ||
end | ||
|
||
it 'stops chain propagation once a conn is tainted' do | ||
op_1 = ->(conn) { conn.set_status(200) } | ||
op_2 = ->(conn) { conn.set_response_body('foo') } | ||
op_3 = ->(conn) { conn.taint } | ||
op_4 = ->(conn) { conn.set_response_body('bar') } | ||
|
||
app = described_class.new([op_1, op_2, op_3, op_4]) | ||
|
||
expect(app.call(conn)).to eq( | ||
op_3.(op_2.(op_1.(conn))) | ||
) | ||
end | ||
|
||
it 'raises InvalidOperationReturn when one operation does not return a Conn' do | ||
op = ->(_conn) { :foo } | ||
|
||
app = described_class.new([op]) | ||
|
||
expect { | ||
app.call(conn) | ||
}.to raise_error(WebPipe::ConnSupport::Composition::InvalidOperationResult) | ||
end | ||
end | ||
end |