Skip to content
One-way pipe rack application builder
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
bin
lib
spec
.gitignore
.rspec
.travis.yml
.yardopts
CHANGELOG.md
Gemfile
Gemfile.lock
README.md
Rakefile
web_pipe.gemspec

README.md

Gem Version Build Status

WebPipe

web_pipe is a rack application builder through a pipe of operations applied to an immutable struct.

You can also think of it as a web controllers builder (the C in MVC) totally declouped from the web routing (which you can still do with something like hanami-router, http_router or plain rack's map method).

If you are familiar with rack you know that it models a two-way pipe, where each middleware in the stack has the chance to modify the request before it arrives to the actual application, and the response once it comes back from the application:


  --------------------->  request ----------------------->

  Middleware 1          Middleware 2          Application

  <--------------------- response <-----------------------


web_pipe follows a simpler but equally powerful model of a one-way pipe and abstracts it on top of rack. A struct that contains all the data from a web request is piped trough a stack of operations which take it as argument and return a new instance of it where response data can be added at any step.


  Operation 1          Operation 2          Operation 3

  --------------------- request/response ---------------->

In addition to that, any operation in the stack has the power to stop the propagation of the pipe, leaving any downstream operation unexecuted. This is mainly useful to unauthorize a request while being sure that nothing else will be done to the response.

As you may know, this is the same model used by Elixir's plug, from which web_pipe takes inspiration.

This library has been designed to work frictionless along the dry-rb ruby ecosystem and it uses some of its libraries internally.

Usage

This is a sample config.ru for a contrived application built with web_pipe. It simply fetches a user from an id request parameter. If the user is not found, it returns a not found response. If it is found, it will unauthorize when it is a non admin user or greet it otherwise:

rackup --port 4000
# http://localhost:4000?id=1 => Hello Alice
# http://localhost:4000?id=2 => Unauthorized
# http://localhost:4000?id=3 => Not found
# config.ru
require "web_pipe"

UsersRepo = {
  1 => { name: 'Alice', admin: true },
  2 => { name: 'Joe', admin: false }
}

class GreetingAdminApp
  include WebPipe

  plug :set_content_type
  plug :fetch_user
  plug :authorize
  plug :greet

  private

  def set_content_type(conn)
    conn.add_response_header(
      'Content-Type', 'text/html'
    )
  end

  def fetch_user(conn)
    user = UsersRepo[conn.params['id'].to_i]
    if user
      conn.
        put(:user, user)
    else
      conn.
        set_status(404).
        set_response_body('<h1>Not foud</h1>').
        taint
    end
  end

  def authorize(conn)
    if conn.fetch(:user)[:admin]
      conn
    else
      conn.
        set_status(401).
        set_response_body('<h1>Unauthorized</h1>').
        taint
    end
  end
  
  def greet(conn)
    conn.
      set_response_body("<h1>Hello #{conn.fetch(:user)[:name]}</h1>")
  end
end

run GreetingAdminApp.new

As you see, steps required are:

  • Include WebPipe in a class.
  • Specify the stack of operations with plug.
  • Implement those operations.
  • Initialize the class to obtain resulting rack application.

WebPipe::Conn is a struct of request and response date, seasoned with methods that act on its data. These methods are designed to return a new instance of the struct each time, so they encourage immutability and make method chaining possible.

Each operation in the pipe must accept a single argument of a WebPipe::Conn instance and it must also return an instance of it. In fact, what the first operation in the pipe takes is a WebPipe::Conn::Clean subclass instance. When one of your operations calls #taint on it, a WebPipe::Conn::Dirty is returned and the pipe is halted. This one or the 'clean' instance that reaches the end of the pipe will be in command of the web response.

Operations have the chance to prepare data to be consumed by downstream operations. Data can be added to the struct through #put(key, value), while it can be consumed with #fetch(key).

Attributes and methods in WebPipe::Conn are fully documented.

Specifying operations

There are several ways you can plug operations to the pipe:

Instance methods

Operations can be just methods defined in the pipe class. This is what you saw in the previous example:

class App
  include WebPipe

  plug :hello

  private

  def hello(conn)
    # ...
  end

Proc (or anything responding to #call)

Operations can also be defined inline, through the with: keyword, as anything that responds to #call, like a Proc:

class App
  include WebPipe

  plug :hello, with: ->(conn) { conn }
end

Container

When with: is a String or a Symbol, it can be used as the key to resolve an operation from a container. A container is just anything responding to #[].

The container to be used is configured when you include WebPipe:

class App
  Container = Hash[
    'plugs.hello' => ->(conn) { conn }
  ]

  include WebPipe.(container: Container)

  plug :hello, with: 'plugs.hello'
end

Operations injection

Operations can be injected when the application is initialized, overriding those configured through plug:

class App
  include WebPipe

  plug :hello, with: ->(conn) { conn.set_response_body('Hello') }
end

run App.new(
  hello: ->(conn) { conn.set_response_body('Injected') }
)

In the previous example, resulting response body would be Injected.

Rack middlewares

Rack middlewares can be added to the generated application through use. They will be executed in declaration order before the pipe of plugs:

class App
  include WebPipe

  use Middleware1
  use Middleware2, option_1: value_1

  plug :hello, with: ->(conn) { conn }
end

Standalone usage

If you prefer, you can use the application builder without the DSL. For that, you just have to initialize a WebPipe::App with an array of all the operations to be performed:

require 'web_pipe/app`

op_1 = ->(conn) { conn.set_status(200) }
op_2 = ->(conn) { conn.set_response_body('Hello') }

WebPipe::App.new([op_1, op_2])

Current status

web_pipe is in active development. The very basic features to build a rack application are all available. However, very necessary conveniences to build a production application, for example a session mechanism, are still missing.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/waiting-for-dev/web_pipe.

Release Policy

web_pipe follows the principles of semantic versioning.

You can’t perform that action at this time.