Skip to content

Commit

Permalink
Make web_pipe compatible with dry-auto_inject
Browse files Browse the repository at this point in the history
We remove the built-in resolution of plugs from a container and instead
rely on the common mechanism provided by dry-auto_inject.

As we're already allowing the injection of plugs and middlewares through
keyword arguents given on `#initialize`, web_pipe remains compatible
with the keyword arguments strategy of dry-auto_inject. We make sure
that the order of inclusion of either web_pipe or dry-auto_inject is
irrelevant for a correct behavior via dispatching any extra keyword
arguments to `super`.

```ruby
WebPipe.load_extensions(:params)

class CreateUserApp
  include WebPipe
  include Deps[:create_user]

  plug :html, WebPipe::Plugs::ContentType.('text/html')
  plug :create

  private

  def create(conn)
    create_user.(conn.params)
  end
end
```
  • Loading branch information
waiting-for-dev committed Dec 11, 2023
1 parent 8129f12 commit 79f6c53
Show file tree
Hide file tree
Showing 16 changed files with 182 additions and 189 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gemspec

group :development do
gem "dry-auto_inject", "~> 1.0"
gem "dry-schema", "~> 1.0"
gem "dry-transformer", "~> 0.1"
gem "pry-byebug"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ functions on an immutable struct.
1. [URL](docs/extensions/url.md)
1. Recipes
1. [hanami 2 & dry-rb integration](docs/recipes/hanami_2_and_dry_rb_integration.md)
1. [Injecting dependencies through dry-auto_inject](docs/recipes/injecting_dependencies_through_dry_auto_inject.md)
1. [hanami-router integration](docs/recipes/hanami_router_integration.md)
1. [Using all RESTful methods](docs/recipes/using_all_restful_methods.md)

Expand Down
20 changes: 2 additions & 18 deletions docs/dsl_free_usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,8 @@ run app

As you see, the instance of `WebPipe::Pipe` is itself the rack application.

As with the DSL, plug operations can be resolved from a container given on
initialization.

```ruby
container = {
fetch_name: ->(conn) { conn.add(:name, conn.params['name']) },
render: ->(conn) { conn.set_response_body("Hello, #{conn.fetch(:name)}") }
}

app = WebPipe::Pipe.new(container: container)
.plug(:fetch_name, :fetch_name)
.plug(:render, :render)

run app
```

Likewise, you can provide a context object to resolve methods when only a name
is given on `#plug`:
You can provide a context object to resolve methods when only a name is given
on `#plug`:

```ruby
class Context
Expand Down
13 changes: 5 additions & 8 deletions docs/extensions/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ injection container to be accessible from a `WebPipe::Conn` instance.
The container to use must be configured under the `:container` config key. It
will be accessible through the `#container` method.

You may be wondering why you should worry about configuring a container for a
connection instance when you already have access to the container configured
for an application (where you can resolve plugged operations). The idea is
decoupling operations from application DSL. If you decide to get rid of the DSL
at any time in the future, the process will be straightforward if operations
are using the container configured in a connection instance.
Although you'll usually want to configure the container in the application
class (for instance, using
[dry-system](https://dry-rb.org/gems/dry-system/main/)), having it at the
connection struct level is useful for building other extensions that may need
to access to it, like the [`hanami_view`](hanami_view.md) one.

```ruby
require 'web_pipe'
Expand All @@ -20,8 +19,6 @@ require 'my_container'
WebPipe.load_extensions(:container)

class MyApp
include WebPipe.(container: MyContainer)

plug :config, WebPipe::Plugs::Config.(
container: MyContainer
)
Expand Down
22 changes: 0 additions & 22 deletions docs/plugging_operations/resolving_operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,3 @@ class MyApp
end
end
```

## Container

Operations can be resolved from a dependency injection container.

A container is anything that responds to `#[]` (accepting `Symbol` or `String`
as argument) to resolve a dependency. It can be configured at the
moment you include the `WebPipe` module:

```ruby
MyContainer = Hash[
'plugs.html' => lambda do |conn|
conn.add_response_header('Content-Type' => 'text/html')
end
]

class MyApp
include WebPipe.(container: MyContainer)

plug :html, 'plugs.html'
end
```
11 changes: 6 additions & 5 deletions docs/recipes/hanami_2_and_dry_rb_integration.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Hanami 2 and dry-rb integration

`web_pipe` has been designed to integrate smoothly with
the [hanami](https://hanamirb.org/) & [dry-rb](https://dry-rb.org/) ecosystems. It shares the same design
principles, and it ships with some extensions that even make this
integration painless (like [`:dry-schema`](../extensions/dry_schema.md)
extension or [`:hanami_view`](../extensions/hanami_view.md)).
`web_pipe` has been designed to integrate smoothly with the
[hanami](https://hanamirb.org/) & [dry-rb](https://dry-rb.org/) ecosystems. It
shares the same design principles. It ships with some extensions that even make
this integration painless (like [`:dry-schema`](../extensions/dry_schema.md)
extension or [`:hanami_view`](../extensions/hanami_view.md)), and it seamlessly
[integrates with dry-auto_inject](injecting_dependencies_through_dry_auto_inject.md).

If you want to use `web_pipe` within a hanami 2 application, you can take
inspiration from this sample todo app:
Expand Down
21 changes: 21 additions & 0 deletions docs/recipes/injecting_dependencies_through_dry_auto_inject.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Injecting dependencies through dry-auto_inject

`web_pipe` allows injecting [plugs](`../plugging_operations/injecting_operations.md`) and [middlewares](`../using_rack_middlewares/injecting_middlewares.md`) at initialization time. As they are given as keyword arguments to the `#initialize` method, `web_pipe` is only compatible with the [keyword argument strategy from dry-auto_inject](https://dry-rb.org/gems/dry-auto_inject/main/injection-strategies/#keyword-arguments-code-kwargs-code). This is useful in the case you need to use other collaborator from your plugs' definitions.

```ruby
WebPipe.load_extensions(:params)

class CreateUserApp
include WebPipe
include Deps[:create_user]

plug :html, WebPipe::Plugs::ContentType.('text/html')
plug :create

private

def create(conn)
create_user.(conn.params)
end
end
```
13 changes: 1 addition & 12 deletions lib/web_pipe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,7 @@ def self.loader
# Includes an instance of `Builder`. That means that `Builder#included` is
# eventually called.
def self.included(klass)
klass.include(call)
end

# Chained to {Module#include} to make the DSL available and provide options.
#
# @param container [#[]] Container from where resolve operations. See
# {WebPipe::Plug}.
#
# @example
# include WebPipe.call(container: Container)
def self.call(**opts)
DSL::Builder.new(**opts)
klass.include(DSL::Builder.new)
end

register_extension :container do
Expand Down
5 changes: 2 additions & 3 deletions lib/web_pipe/dsl/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ module DSL
class Builder < Module
attr_reader :class_context, :instance_context

def initialize(container: Pipe::EMPTY_CONTAINER)
def initialize
@class_context = ClassContext.new
@instance_context = InstanceContext.new(
class_context: class_context,
container: container
class_context: class_context
)
super()
end
Expand Down
16 changes: 8 additions & 8 deletions lib/web_pipe/dsl/instance_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,31 @@ class InstanceContext < Module
call middlewares operations to_proc to_middlewares
].freeze

attr_reader :container, :class_context
attr_reader :class_context

def initialize(container:, class_context:)
@container = container
def initialize(class_context:)
@class_context = class_context
super()
end

def included(klass)
klass.include(dynamic_module(class_context.ast, container))
klass.include(dynamic_module(class_context.ast))
end

private

def dynamic_module(ast, container)
def dynamic_module(ast)
Module.new.tap do |mod|
define_initialize(mod, ast, container)
define_initialize(mod, ast)
define_pipe_methods(mod)
end
end

# rubocop:disable Metrics/MethodLength
def define_initialize(mod, ast, container)
def define_initialize(mod, ast)
mod.define_method(:initialize) do |plugs: {}, middlewares: {}, **kwargs|
acc = Pipe.new(container: container, context: self)
super(**kwargs) # Compatibility with dry-auto_inject
acc = Pipe.new(context: self)
@pipe = ast.reduce(acc) do |pipe, node|
method, args, kwargs, block = node
if block
Expand Down
18 changes: 1 addition & 17 deletions lib/web_pipe/pipe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,6 @@ module WebPipe
#
# run app
class Pipe
# Container that resolves nothing
EMPTY_CONTAINER = {}.freeze

# @!attribute [r] container
# Container from where resolve operations. See {#plug}.
attr_reader :container

# @!attribute [r] context
# Object from where resolve operations. See {#plug}.
attr_reader :context
Expand All @@ -49,28 +42,21 @@ class Pipe
# @api private
EMPTY_MIDDLEWARE_SPECIFICATIONS = [].freeze

# @api private
Container = Types.Interface(:[])

# @api private
attr_reader :plugs

# @api private
attr_reader :middleware_specifications

# @param container [#to_h] Container from where resolve plug's operations
# (see {#plug}).
# @param context [Any] Object from where resolve plug's operations (see
# {#plug})
def initialize(
container: EMPTY_CONTAINER,
context: nil,
plugs: EMPTY_PLUGS,
middleware_specifications: EMPTY_MIDDLEWARE_SPECIFICATIONS
)
@plugs = plugs
@middleware_specifications = middleware_specifications
@container = Container[container]
@context = context
end

Expand All @@ -80,7 +66,6 @@ def initialize(
#
# - Through the `spec` parameter as:
# - Anything responding to `#call` (like a {Proc}).
# - As a string or symbol key for something registered in {#container}.
# - Anything responding to `#to_proc` (like another {WebPipe::Pipe}
# instance or an instance of a class including {WebPipe}).
# - As `nil` (default), meaning that the operation is a method in
Expand Down Expand Up @@ -147,7 +132,7 @@ def compose(name, spec)
# @return [Hash{Symbol => Proc}]
def operations
@operations ||= Hash[
plugs.map { |plug| [plug.name, plug.(container, context)] }
plugs.map { |plug| [plug.name, plug.(context)] }
]
end

Expand Down Expand Up @@ -209,7 +194,6 @@ def rack_app

def with(plugs: nil, middleware_specifications: nil)
self.class.new(
container: container,
context: context,
middleware_specifications: middleware_specifications ||
self.middleware_specifications,
Expand Down
17 changes: 5 additions & 12 deletions lib/web_pipe/plug.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ def initialize(name)
super(
<<~MSG
Plug with name +#{name}+ can't be resolved. You must provide
something responding to `#call` or `#to_proc`, or a key for
something registered in the container obeying those exact
constraints. If nothing is given, it's expected to be a method
something responding to `#call` or `#to_proc`
. If nothing is given, it's expected to be a method
defined in the context object.
MSG
)
Expand All @@ -39,23 +38,17 @@ def with(new_spec)
new(spec: new_spec)
end

# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/AbcSize
def call(container, context)
if spec.respond_to?(:to_proc) && !spec.is_a?(Symbol)
def call(context)
if spec.respond_to?(:to_proc)
spec.to_proc
elsif spec.respond_to?(:call)
if spec.respond_to?(:call)
spec
elsif spec.nil?
context.method(name)
elsif container[spec]
with(container[spec]).(container, context)
else
raise InvalidPlugError, name
end
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize

def self.inject(plugs, injections)
plugs.map do |plug|
Expand Down
Loading

0 comments on commit 79f6c53

Please sign in to comment.