Workspace.Check behaviour (Workspace v0.2.1)

View Source

A 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. The Workspace.Check module to be used.

  • :group - The group of the check. Can be set optionally in order to group checks together in the output of workspace.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 of atom/0) - A list of projects. The check will be executed only in the specified projects The default value is [].

  • :ignore (list of atom/0) - A list of projects to be ignored from the check The default value is [].

  • :allow_failure - A list of projects (or true 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 is false.

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:

  1. Define a module implementing the Workspace.Check behaviour.
  2. 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

check(workspace, check)

@callback check(workspace :: Workspace.State.t(), check :: keyword()) :: [
  Workspace.Check.Result.t()
]

Applies a workspace check on the given workspace

format_result(result)

@callback format_result(result :: Workspace.Check.Result.t()) :: IO.ANSI.ansidata()

Formats a check result for display purposes.

schema()

(optional)
@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

check_projects(workspace, check, check_fun)

@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.

validate(config)

@spec validate(config :: keyword()) :: {:ok, keyword()} | {:error, binary()}

Validates that the given config is a valid Check config.