Skip to content

Commit

Permalink
Polish and stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
whatyouhide committed Dec 26, 2023
1 parent f02f244 commit 442dddb
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 22 deletions.
65 changes: 65 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-20.04
env:
MIX_ENV: test
strategy:
fail-fast: false
matrix:
include:
- pair:
elixir: "1.11"
otp: "21.3"
- pair:
elixir: "1.16"
otp: "26.1"
lint: lint
steps:
- name: Check out this repository
uses: actions/checkout@v4

- name: Install Erlang and Elixir
uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.pair.otp }}
elixir-version: ${{ matrix.pair.elixir }}

- name: Cache Mix dependencies
uses: actions/cache@v3
with:
path: |
deps
_build
key: |
${{ runner.os }}-mix-${{ matrix.pair.elixir }}-${{ matrix.pair.otp }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-${{ matrix.pair.elixir }}-${{ matrix.pair.otp }}-
- name: Fetch dependencies
run: mix deps.get --check-lock

- name: Check formatting
run: mix format --check-formatted
if: ${{ matrix.lint }}

- name: Check unused dependencies
run: mix deps.unlock --check-unused
if: ${{ matrix.lint }}

- name: Compile dependencies
run: mix deps.compile

- name: Compile and check for warnings
run: mix compile --warnings-as-errors
if: ${{ matrix.lint }}

- name: Run tests
run: mix test
42 changes: 36 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# NimbleOwnership

**TODO: Add description**
[![hex.pm badge](https://img.shields.io/badge/Package%20on%20hex.pm-informational)](https://hex.pm/packages/nimble_ownership)
[![Documentation badge](https://img.shields.io/badge/Documentation-ff69b4)][docs]

> Library that allows you to manage ownership of resources across processes.
A typical use case for this library is tracking resource ownership across processes in order to isolate access to resources in **test suites**. For example, the [Mox][mox] library uses this module to track ownership of mocks across processes (in shared mode).

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `nimble_ownership` to your list of dependencies in `mix.exs`:
Add this to your `mix.exs`:

```elixir
def deps do
Expand All @@ -15,7 +19,33 @@ def deps do
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/nimble_ownership>.
## Nimble*

All nimble libraries by Dashbit:

* [NimbleCSV](https://github.com/dashbitco/nimble_csv) - simple and fast CSV parsing
* [NimbleOptions](https://github.com/dashbitco/nimble_options) - tiny library for validating and documenting high-level options
* [NimbleOwnership](https://github.com/dashbitco/nimble_ownership) - resource ownership tracking
* [NimbleParsec](https://github.com/dashbitco/nimble_parsec) - simple and fast parser combinators
* [NimblePool](https://github.com/dashbitco/nimble_pool) - tiny resource-pool implementation
* [NimblePublisher](https://github.com/dashbitco/nimble_publisher) - a minimal filesystem-based publishing engine with Markdown support and code highlighting
* [NimbleTOTP](https://github.com/dashbitco/nimble_totp) - tiny library for generating time-based one time passwords (TOTP)

## License

Copyright 2023 Dashbit

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

[docs]: https://hexdocs.pm/nimble_ownership
[mox]: https://github.com/dashbitco/mox
86 changes: 75 additions & 11 deletions lib/nimble_ownership.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,56 @@
defmodule NimbleOwnership do
@moduledoc """
TODO
Module that allows you to manage ownership of resources across processes.
The idea is that you can track ownership of terms (keys) across processes,
and allow processes to use a key through processes that are already allowed.
```mermaid
flowchart LR
pidA["Process A"]
pidB["Process B"]
pidC["Process C"]
res(["Resource"])
pidA -->|Owns| res
pidA -->|Allows| pidB
pidB -->|Can access| res
pidB -->|Allows| pidC
pidC -->|Can access| res
```
A typical use case for such a module is tracking resource ownership across processes
in order to isolate access to resources in **test suites**. For example, the
[Mox](https://hexdocs.pm/mox/Mox.html) library uses this module to track ownership
of mocks across processes (in shared mode).
## Usage
To track ownership of resources, you need to start a `NimbleOwnership` server (a process),
through `start_link/1` or `child_spec/1`.
Then, you can allow a process access to a key through `allow/5`. You can then check
if a PID can access the given key through `get_owner/3`.
### Metadata
You can store arbitrary metadata (`t:metadata/0`) alongside each "allowance", that is,
alongside the relationship between a process and a key. This metadata is returned
together with the owner PID when you call `get_owner/3`.
"""

use GenServer

@typedoc "Ownership server."
@type server() :: GenServer.server()

@typedoc "Arbitrary key."
@type key() :: term()

@typedoc "Arbitrary metadata associated with an *allowance*."
@type metadata() :: term()

@typedoc "Information about the owner of a key returned by `get_owner/3`."
@type owner_info() :: %{metadata: metadata(), owner_pid: pid()}

@typedoc false
Expand All @@ -17,23 +60,32 @@ defmodule NimbleOwnership do
}
}

@genserver_opts [
:name,
:timeout,
:debug,
:spawn_opt,
:hibernate_after
]

@doc """
Starts an ownership server.
## Options
This function supports all the options supported by `GenServer.start_link/3`, namely:
* `:name`
* `:timeout`
* `:debug`
* `:spawn_opt`
* `:hibernate_after`
#{Enum.map_join(@genserver_opts, "\n", &" * `#{inspect(&1)}`")}
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link([] = _opts) do
GenServer.start_link(__MODULE__, :ok)
def start_link(options \\ []) when is_list(options) do
{genserver_opts, other_opts} = Keyword.split(options, @genserver_opts)

if other_opts != [] do
raise ArgumentError, "unknown options: #{inspect(other_opts)}"
end

GenServer.start_link(__MODULE__, [], genserver_opts)
end

@doc """
Expand All @@ -47,9 +99,19 @@ defmodule NimbleOwnership do
This function returns an error when `pid_to_allow` is already allowed to use
`key` via **another owner PID** that is not `owner_pid`.
## Examples
iex> pid = spawn(fn -> Process.sleep(:infinity) end)
iex> {:ok, server} = NimbleOwnership.start_link()
iex> NimbleOwnership.allow(server, self(), pid, :my_key, %{counter: 1})
:ok
iex> NimbleOwnership.get_owner(server, [pid], :my_key)
%{owner_pid: self(), metadata: %{counter: 1}}
"""
@spec allow(server(), pid(), pid() | (-> pid()), key(), metadata()) ::
:ok | {:error, reason :: term()}
:ok | {:error, NimbleOwnership.Error.t()}
def allow(ownership_server, owner_pid, pid_to_allow, key, metadata)
when is_pid(owner_pid) and (is_pid(pid_to_allow) or is_function(pid_to_allow, 0)) do
GenServer.call(ownership_server, {:allow, owner_pid, pid_to_allow, key, metadata})
Expand All @@ -58,6 +120,8 @@ defmodule NimbleOwnership do
@doc """
Retrieves the first PID in `callers` that is allowed to use `key` (on
the given `ownership_server`).
See `allow/5` for examples.
"""
@spec get_owner(server(), [pid(), ...], key()) :: owner_info() | nil
def get_owner(ownership_server, [_ | _] = callers, key) when is_list(callers) do
Expand All @@ -67,7 +131,7 @@ defmodule NimbleOwnership do
## Callbacks

@impl true
def init(:ok) do
def init([]) do
{:ok, %{allowances: %{}, deps: %{}, lazy_calls: false}}
end

Expand Down
20 changes: 20 additions & 0 deletions lib/nimble_ownership/error.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule NimbleOwnership.Error do
@moduledoc """
Exception struct returned by `NimbleOwnership` functions.
"""

@type t() :: %__MODULE__{
reason: {:already_allowed, pid()}
}

defexception [:reason]

@impl true
def message(%__MODULE__{reason: reason}) do
format_reason(reason)
end

defp format_reason({:already_allowed, other_owner_pid}) do
"this PID is already allowed to access key via other owner PID #{inspect(other_owner_pid)}"
end
end
63 changes: 60 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
defmodule NimbleOwnership.MixProject do
use Mix.Project

@version "0.1.0"
@repo "https://github.com/dashbitco/nimble_ownership"

def project do
[
app: :nimble_ownership,
version: "0.1.0",
elixir: "~> 1.15",
version: @version,
elixir: "~> 1.11",
start_permanent: Mix.env() == :prod,
deps: deps()
deps: deps(),

# Hex package
package: package(),

# Docs
name: "NimbleOwnership",
source_url: @repo,
docs: docs()
]
end

Expand All @@ -17,6 +28,52 @@ defmodule NimbleOwnership.MixProject do
]
end

defp package do
[
licenses: ["Apache-2.0"],
maintainers: ["José Valim", "Andrea Leopardi"],
links: %{"GitHub" => @repo}
]
end

defp docs do
[
main: "NimbleOwnership",
source_ref: "v#{@version}",
authors: ["Andrea Leopardi"],
before_closing_body_tag: fn
:html ->
"""
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.2.3/dist/mermaid.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
mermaid.initialize({
startOnLoad: false,
theme: document.body.className.includes("dark") ? "dark" : "default"
});
let id = 0;
for (const codeEl of document.querySelectorAll("pre code.mermaid")) {
const preEl = codeEl.parentElement;
const graphDefinition = codeEl.textContent;
const graphEl = document.createElement("div");
const graphId = "mermaid-graph-" + id++;
mermaid.render(graphId, graphDefinition).then(({svg, bindFunctions}) => {
graphEl.innerHTML = svg;
bindFunctions?.(graphEl);
preEl.insertAdjacentElement("afterend", graphEl);
preEl.remove();
});
}
});
</script>
"""

:epub ->
""
end
]
end

defp deps do
[
{:ex_doc, "~> 0.31", only: :dev}
Expand Down
Loading

0 comments on commit 442dddb

Please sign in to comment.