Workspace.Check behaviour (Workspace v0.2.1)
View SourceA behaviour for implementing workspace check.
Introduction
A check is responsible for validating that a workspace follows the configured rules.
When your mono-repo grows it is becoming more tedious to keep track with
all projects and ensure that the same standards apply to all projects. For
example you may wish to have common dependencies defined across all your
projects, or common deps
paths.
Configuration
In order to define a Workspace.Check
you must add an entry under the :checks
key of your Workspace.Config
. The supported options are for each check are:
:id
(atom/0
) - A unique identifier for the check.:module
(atom/0
) - Required. TheWorkspace.Check
module to be used.:group
- The group of the check. Can be set optionally in order to group checks together in the output ofworkspace.check
. All checks that don't have a custom group set will be listed under the default checks group with the order they were defined in your config.:opts
(keyword/0
) - The check's custom options. The default value is[]
.:description
(String.t/0
) - An optional description of the check:only
(list ofatom/0
) - A list of projects. The check will be executed only in the specified projects The default value is[]
.:ignore
(list ofatom/0
) - A list of projects to be ignored from the check The default value is[]
.:allow_failure
- A list of projects (ortrue
for all) that are allowed to fail. In case of a failure it will be logged as a warning but the exit code of check will not be set to 1. The default value isfalse
.
Configuration Examples
[
checks: [
[
module: Workspace.Checks.ValidateConfigPath,
description: "check deps_path",
opts: [
config_attribute: :deps_path,
expected_path: "deps"
]
]
]
]
The Workspace.Check
behaviour
In order to implement and use a custom check you need to:
- Define a module implementing the
Workspace.Check
behaviour. - Include your check in the workspace configuration.
The schema
Let's implement a simple check that verifies that all workspace projects have a description set.
defmodule MyCheck do
@behaviour Workspace.Check
# ...callbacks implementation...
end
We will start by defining the check's schema. This is expected to be a
NimbleOptions
schema with the custom options of your check. For the needs
of this guide we will assume that we support a single option:
@impl Workspace.Check
def schema do
schema = [
must_end_with_period: [
type: :boolean,
doc: "If set the description must end with a period",
default: false
]
]
NimbleOptions.new!(schema)
end
Schema as module attribute
Instead of defining the schema directly in the schema/0
callback it
is advised to define it as a module attribute. This way you can auto-generate
documentation in your check's moduledoc
:
defmodule MyCheck do
@behaviour Workspace.Check
@schema NimbleOptions.new!(
must_end_with_period: [
type: :boolean,
doc: "If set the description must end with a period",
default: false
]
)
@moduledoc """
My check's documentation
## Options
#{NimbleOptions.docs(@schema)}
"""
@impl Workspace.Check
def schema, do: @schema
end
The check
We can now implement the check/2
callback which is responsbible for the
actual check logic. In our simple example we only need to verify that the
:description
is set in each project's config.
The check/2
callback is expected to return a list of check results. You
can use the check_projects/3
helper method.
@impl Workspace.Check
def check(workspace, check) do
must_end_with_period = Keyword.fetch!(check[:opts], :must_end_with_period)
Workspace.Check.check_projects(workspace, check, fn project ->
description = project.config[:description]
cond do
not is_binary(description) ->
{:error, description: description, message: "description must be a string"}
must_end_with_period and not String.ends_with?(description, ".") ->
{:error, description: description, message: "description must end with a period"}
true ->
{:ok, description: description}
end
end)
end
Notice that the check_projects/3
helper expects the inner function to return
:ok
, :error
tuples where the second element is check metadata. These metadata are
used by the format_result/1
callback for pretty printing the check status
message per project.
@impl Workspace.Check
def format_result(%Workspace.Check.Result{status: :ok, meta: metadata}) do
"description set to #{metadata[:description]}"
end
def format_result(%Workspace.Check.Result{status: :error, meta: metadata}) do
message = metadata[:message]
description = metadata[:description]
[message, ", got: ", :red, inspect(description), :reset]
end
Notice how we use IO.ANSI
escape sequences for pretty printing the invalid
project description.
For more examples you can check the implementation of the checks provided by the workspace.
Summary
Callbacks
Applies a workspace check on the given workspace
Formats a check result for display purposes.
An optional definition of the custom check's options.
Functions
Helper function for running a check on all projects of a workspace.
Validates that the given config
is a valid Check
config.
Callbacks
@callback check(workspace :: Workspace.State.t(), check :: keyword()) :: [ Workspace.Check.Result.t() ]
Applies a workspace check on the given workspace
@callback format_result(result :: Workspace.Check.Result.t()) :: IO.ANSI.ansidata()
Formats a check result for display purposes.
@callback schema() :: NimbleOptions.t() | nil
An optional definition of the custom check's options.
If not set the options will not be validated and all keyword lists will be considered valid. It is advised to define it for better handling of errors.
Functions
@spec check_projects( workspace :: Workspace.State.t(), check :: keyword(), check_fun :: (Workspace.Project.t() -> {atom(), keyword()}) ) :: [Workspace.Check.Result.t()]
Helper function for running a check on all projects of a workspace.
The function must return {:ok, metadata}
or {:error, metadata}
. It returns
a Check.Result
for each checked project.
It takes care of transforming the function output to a Check.Result
struct
and handling ignored projects.
Validates that the given config
is a valid Check
config.