Skip to content

Commit

Permalink
Merge branch 'feature/existence-mix-task'
Browse files Browse the repository at this point in the history
  • Loading branch information
alexocode committed Mar 6, 2020
2 parents 14c6286 + 027790f commit 39bbef0
Show file tree
Hide file tree
Showing 25 changed files with 776 additions and 130 deletions.
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.9.0-otp-22
erlang 22.0.7
elixir 1.10.1
erlang 22.2.7
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
[![Build Status](https://travis-ci.org/sascha-wolf/knigge.svg?branch=master)](https://travis-ci.org/sascha-wolf/knigge)
[![Coverage Status](https://coveralls.io/repos/github/sascha-wolf/knigge/badge.svg?branch=master)](https://coveralls.io/github/sascha-wolf/knigge?branch=master)
[![Inline docs](https://inch-ci.org/github/sascha-wolf/knigge.svg?branch=master)](https://inch-ci.org/github/sascha-wolf/knigge)
[![Hexdocs.pm](https://img.shields.io/badge/hexdocs-online-blue)](https://hexdocs.pm/knigge/)
[![Hex.pm](https://img.shields.io/hexpm/v/knigge.svg)](https://hex.pm/packages/knigge)
[![Hex.pm Downloads](https://img.shields.io/hexpm/dt/knigge)](https://hex.pm/packages/knigge)
[![Featured - ElixirRadar](https://img.shields.io/badge/featured-ElixirRadar-543A56)](https://app.rdstation.com.br/mail/0ddee1c8-2ce9-405b-b95f-09c883099090?utm_campaign=elixir_radar_202&utm_medium=email&utm_source=RD+Station)
[![Featured - ElixirWeekly](https://img.shields.io/badge/featured-ElixirWeekly-875DB0)](https://elixirweekly.net/issues/161)

Expand All @@ -22,11 +24,16 @@ passing the behaviour which should be "facaded" as an option.

## Overview

- [Installation](#installation)
- [Motivation](#motivation)
- [Examples](#examples)
- [Options](#options)
- [Knigge and the `:test` environment](#knigge-and-the-test-environment)
- [Knigge](#knigge)
- [Overview](#overview)
- [Installation](#installation)
- [Motivation](#motivation)
- [Examples](#examples)
- [`defdefault` - Fallback implementations for optional callbacks](#defdefault---fallback-implementations-for-optional-callbacks)
- [Options](#options)
- [Verifying your Implementations - `mix knigge.verify`](#verifying-your-implementations---mix-kniggeverify)
- [Knigge and the `:test` environment](#knigge-and-the-test-environment)
- [Compiler Warnings](#compiler-warnings)

## Installation

Expand Down Expand Up @@ -195,6 +202,16 @@ as option - by default `Knigge` delegates at runtime in your `:test`s.

For further information about options check the [`Knigge.Options` module](https://hexdocs.pm/knigge/Knigge.Options.html).

## Verifying your Implementations - `mix knigge.verify`

Before version 1.2.0 `Knigge` tried to check at compile time if the implementation of your facade existed.
Due to the way the Elixir compiler goes about compiling your modules this didn't work as expected - [checkout this page if you're interested in the details](https://hexdocs.pm/knigge/the-existence-check.html).

As an alternative `Knigge` now offers the `mix knigge.verify` task which verifies that the implementation modules of your facades actually exist.
The task returns with an error code when an implementation is missing, which allows you to plug it into your CI pipeline - for example as `MIX_ENV=prod mix knigge.verify`.

For details check the documentation of `mix knigge.verify` by running `mix help knigge.verify`.

## Knigge and the `:test` environment

To give the maximum amount of flexibility `Knigge` delegates at runtime in your
Expand All @@ -211,7 +228,7 @@ In case you change the `delegate_at_runtime?` configuration to anything which
excludes the `:test` environment you will - most likely - encounter compiler
warnings like this:

```
```text
warning: function MyMock.my_great_callback/1 is undefined (module MyMock is not available)
lib/my_facade.ex:1
Expand Down
1 change: 0 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use Mix.Config

config :knigge,
check_if_exists?: [except: :test],
delegate_at_runtime?: false
5 changes: 5 additions & 0 deletions coveralls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"skip_files": [
"lib/mix/tasks"
]
}
21 changes: 16 additions & 5 deletions lib/knigge.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ defmodule Knigge do
For further information about options check the `Knigge.Options` module.
## Verifying your Implementations - `mix knigge.verify`
Before version 1.2.0 `Knigge` tried to check at compile time if the implementation of your facade existed.
Due to the way the Elixir compiler goes about compiling your modules this didn't work as expected - [checkout this page if you're interested in the details](https://hexdocs.pm/knigge/the-existence-check.html).
As an alternative `Knigge` now offers the `mix knigge.verify` task which verifies that the implementation modules of your facades actually exist.
The task returns with an error code when an implementation is missing, which allows you to plug it into your CI pipeline - for example as `MIX_ENV=prod mix knigge.verify`.
For details check the documentation of `mix knigge.verify` by running `mix help knigge.verify`.
## Knigge and the `:test` environment
To give the maximum amount of flexibility `Knigge` delegates at runtime in your
Expand Down Expand Up @@ -198,10 +208,14 @@ defmodule Knigge do
behaviour =
options
|> Knigge.Behaviour.fetch!()
|> Knigge.Module.ensure_exists!(options, __ENV__)
|> Knigge.Module.ensure_exists!(__ENV__)

@__knigge__ {:options, options}

@doc "Acts as a \"flag\" to mark this module as a Knigge module."
@spec __knigge__() :: :ok
def __knigge__, do: :ok

@doc "Access Knigge internal values, such as the implementation being delegated to etc."
@spec __knigge__(:behaviour) :: module()
@spec __knigge__(:implementation) :: module()
Expand All @@ -215,10 +229,7 @@ defmodule Knigge do
Knigge.Implementation.fetch!(__knigge__(:options))
end
else
implementation =
options
|> Knigge.Implementation.fetch!()
|> Knigge.Module.ensure_exists!(options, __ENV__)
implementation = Knigge.Implementation.fetch!(options)

def __knigge__(:implementation) do
unquote(implementation)
Expand Down
15 changes: 15 additions & 0 deletions lib/knigge/cli/output.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Knigge.CLI.Output do
@moduledoc false

@device :stdio

def info(device \\ @device, message), do: print(device, [:blue, message])
def success(device \\ @device, message), do: print(device, [:green, message])
def error(device \\ @device, message), do: print(device, [:red, message])
def warn(device \\ @device, message), do: print(device, [:gold, message])

def print(device \\ @device, message)

def print(:stdio, message), do: Bunt.puts(message)
def print(:stderr, message), do: Bunt.warn(message)
end
13 changes: 5 additions & 8 deletions lib/knigge/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,27 +63,24 @@ defmodule Knigge.Code do
Default.callback_to_defdefault(callback,
from: module,
default: default,
delegate_at_runtime?: delegate_at_runtime?,
env: env
delegate_at_runtime?: delegate_at_runtime?
)
end

true ->
Delegate.callback_to_defdelegate(callback,
from: module,
delegate_at_runtime?: delegate_at_runtime?,
env: env
delegate_at_runtime?: delegate_at_runtime?
)
end
end
end

defp get_behaviour(module, env) do
opts = Knigge.options!(module)

opts
module
|> Knigge.options!()
|> Knigge.Behaviour.fetch!()
|> Knigge.Module.ensure_exists!(opts, env)
|> Knigge.Module.ensure_exists!(env)
end

defp get_callbacks(module) do
Expand Down
11 changes: 4 additions & 7 deletions lib/knigge/code/default.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ defmodule Knigge.Code.Default do
{name, arity},
from: module,
default: {args, block},
delegate_at_runtime?: false,
env: env
delegate_at_runtime?: false
) do
implementation = Implementation.fetch_for!(module, env)
implementation = Implementation.fetch_for!(module)

cond do
Module.open?(implementation) ->
Expand All @@ -26,8 +25,7 @@ defmodule Knigge.Code.Default do
{name, arity},
from: module,
default: {args, block},
delegate_at_runtime?: true,
env: env
delegate_at_runtime?: true
)

function_exported?(implementation, name, arity) ->
Expand All @@ -48,8 +46,7 @@ defmodule Knigge.Code.Default do
{name, arity},
from: _module,
default: {args, block},
delegate_at_runtime?: true,
env: _env
delegate_at_runtime?: true
) do
quote do
def unquote(name)(unquote_splicing(args)) do
Expand Down
6 changes: 3 additions & 3 deletions lib/knigge/code/delegate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ defmodule Knigge.Code.Delegate do

alias Knigge.Implementation

def callback_to_defdelegate({name, arity}, from: module, delegate_at_runtime?: false, env: env) do
def callback_to_defdelegate({name, arity}, from: module, delegate_at_runtime?: false) do
callback_to_defdelegate({name, arity},
from: module,
to: Implementation.fetch_for!(module, env)
to: Implementation.fetch_for!(module)
)
end

def callback_to_defdelegate({name, arity}, from: _module, delegate_at_runtime?: true, env: _env) do
def callback_to_defdelegate({name, arity}, from: _module, delegate_at_runtime?: true) do
quote bind_quoted: [name: name, arity: arity] do
args = Macro.generate_arguments(arity, __MODULE__)

Expand Down
8 changes: 3 additions & 5 deletions lib/knigge/implementation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ defmodule Knigge.Implementation do
implementation
end

def fetch_for!(module, env) do
opts = Knigge.options!(module)

opts
def fetch_for!(module) do
module
|> Knigge.options!()
|> Knigge.Implementation.fetch!()
|> Knigge.Module.ensure_exists!(opts, env)
end
end
52 changes: 45 additions & 7 deletions lib/knigge/module.ex
Original file line number Diff line number Diff line change
@@ -1,21 +1,59 @@
defmodule Knigge.Module do
@moduledoc false

alias Knigge.Options
module = inspect(__MODULE__)

def ensure_exists!(module, opts, env) do
unless Knigge.Module.exists?(module, opts) do
def ensure_exists!(module, env) do
unless Knigge.Module.exists?(module) do
Knigge.Error.module_not_loaded!(module, env)
end

module
end

def exists?(module, %Options{} = opts) do
if opts.check_if_exists? do
Code.ensure_loaded?(module) or Module.open?(module)
else
@doc """
Returns true if a module exists, false otherwise.
## Examples
iex> #{module}.exists?(This.Does.Not.Exist)
false
iex> #{module}.exists?(Knigge)
true
"""
@spec exists?(module :: module()) :: boolean()
def exists?(module) do
Module.open?(module) or Code.ensure_loaded?(module)
end

@doc """
Returns all modules which `use Knigge` for the given app. If the app does not
exist an error is returned. To determine if `Knigge` is `use`d we check if the
module exports the `__knigge__/0` function which acts as a "tag".
`fetch_for_app/1` makes use of `Code.ensure_loaded?/1` to force the module
being loaded. Since this results in a `GenServer.call/3` to the code server
__do not use this__ in a hot code path, as it __will__ result in a slowdown!
## Examples
iex> #{module}.fetch_for_app(:this_does_not_exist)
{:error, :undefined}
iex> #{module}.fetch_for_app(:knigge)
{:ok, []}
"""
@spec fetch_for_app(app :: atom()) :: {:ok, list(module())} | {:error, :undefined}
def fetch_for_app(app) do
with {:ok, modules} <- :application.get_key(app, :modules) do
{:ok, Enum.filter(modules, &uses_knigge?/1)}
else
:undefined -> {:error, :undefined}
end
end

defp uses_knigge?(module) do
Code.ensure_loaded?(module) and function_exported?(module, :__knigge__, 0)
end
end
Loading

0 comments on commit 39bbef0

Please sign in to comment.