Skip to content

Commit

Permalink
Add union_typep/1 (#38)
Browse files Browse the repository at this point in the history
* Add union_typep/1 macro

* Tests, changelog, docs

* Add Run tests step to Build CI workflow
  • Loading branch information
altjohndev authored Nov 4, 2024
1 parent 16856b7 commit b02caae
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 17 deletions.
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
locals_without_parens = [union_type: 1]
locals_without_parens = [union_type: 1, union_typep: 1]

[
line_length: 120,
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/elixir-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- main
pull_request:
branches:
- '*'
- "*"

jobs:
build:
Expand All @@ -30,3 +30,6 @@ jobs:
otp-version: ${{ matrix.otp }}
build-flags: --all-warnings --warnings-as-errors

- name: Run tests
run: mix test --warnings-as-errors
shell: sh
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ erl_crash.dump
/config/*.secret.exs
.elixir_ls/
priv/plts
/tmp
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)

## [0.0.3] - 2024-XX-XX

### Added

- `UnionTypespec.union_typep/1` macro.
52 changes: 39 additions & 13 deletions lib/union_typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule UnionTypespec do
A simple, tiny, compile time-only library for defining an Elixir `@type` whose values are one of a fixed set of options.
"""

@moduledoc since: "0.0.1"

@doc """
Transforms an enumerable (like a list of atoms) into an AST for use in a typespec.
Expand All @@ -12,16 +14,17 @@ defmodule UnionTypespec do
Example:
defmodule MyModule do
import UnionTypespec, only: [union_type: 1]
defmodule MyModule do
import UnionTypespec, only: [union_type: 1]
@permissions [:view, :edit, :admin]
union_type permission :: @permissions
@permissions [:view, :edit, :admin]
union_type permission :: @permissions
@spec random_permission() :: permission()
def random_permission, do: Enum.random(@permissions)
end
@spec random_permission() :: permission()
def random_permission, do: Enum.random(@permissions)
end
"""
@doc since: "0.0.1"
defmacro union_type({:"::", _, [{name, _, _}, data]}) do
quote bind_quoted: [data: data, name: name] do
@type unquote({name, [], Elixir}) :: unquote(UnionTypespec.union_type_ast(data))
Expand All @@ -33,14 +36,37 @@ defmodule UnionTypespec do
Example:
defmodule MyModule do
@permissions [:view, :edit, :admin]
@type permission :: unquote(UnionTypespec.union_type_ast(@permissions))
defmodule MyModule do
@permissions [:view, :edit, :admin]
@type permission :: unquote(UnionTypespec.union_type_ast(@permissions))
@spec random_permission() :: permission()
def random_permission, do: Enum.random(@permissions)
end
@spec random_permission() :: permission()
def random_permission, do: Enum.random(@permissions)
end
"""
@doc since: "0.0.1"
def union_type_ast([item]), do: item
def union_type_ast([head | tail]), do: {:|, [], [head, union_type_ast(tail)]}

@doc """
Same as `#{inspect(__MODULE__)}.union_type/1` but for a private type (`@typep`).
Example:
defmodule MyModule do
import UnionTypespec, only: [union_typep: 1]
@permissions [:view, :edit, :admin]
union_typep permission :: @permissions
@spec random_permission() :: permission()
defp random_permission, do: Enum.random(@permissions)
end
"""
@doc since: "0.0.3"
defmacro union_typep({:"::", _, [{name, _, _}, data]}) do
quote bind_quoted: [data: data, name: name] do
@typep unquote({name, [], Elixir}) :: unquote(UnionTypespec.union_type_ast(data))
end
end
end
12 changes: 12 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ defmodule UnionTypespec.MixProject do
deps: deps(),
aliases: aliases(),
test_coverage: [tool: ExCoveralls],
elixirc_paths: elixirc_paths(Mix.env()),
docs: docs(),
preferred_cli_env: [
check: :test,
coveralls: :test,
Expand Down Expand Up @@ -50,6 +52,9 @@ defmodule UnionTypespec.MixProject do
]
end

defp elixirc_paths(:test), do: ~w(lib test/support)
defp elixirc_paths(_env), do: ~w(lib)

# Aliases are shortcuts or tasks specific to the current project.
# For example, to install project dependencies and perform other setup tasks, run:
#
Expand All @@ -69,4 +74,11 @@ defmodule UnionTypespec.MixProject do
]
]
end

defp docs do
[
extras: ~w(CHANGELOG.md README.md),
main: "readme"
]
end
end
21 changes: 19 additions & 2 deletions lib/test/module_test.ex → test/support/some_module.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
defmodule Test.ModuleTest do
defmodule UnionTypespec.Test.SampleModule do
@moduledoc false
import UnionTypespec, only: [union_type: 1]

import UnionTypespec, only: [union_type: 1, union_typep: 1]

@statuses [:read, :unread, :deleted]
union_type status :: @statuses

@permissions [:view, :edit, :admin]
@type permission :: unquote(UnionTypespec.union_type_ast(@permissions))

@roles [:user, :admin]
union_typep role :: @roles

@spec get_permission() :: permission()
def get_permission do
# Test the error case by returning something else (like :ok)
hd(@permissions)
end

@spec authorized?(map()) :: boolean()
def authorized?(_user) do
# Calling the private function
get_role()
true
end

@spec get_role() :: role()
defp get_role do
# Test the error case by returning something else (like :ok)
hd(@roles)
end

@spec get_status() :: status()
def get_status do
# Test the error case by returning something else (like :ok)
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()
32 changes: 32 additions & 0 deletions test/union_typespec_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule UnionTypespecTest do
use ExUnit.Case, async: true

alias UnionTypespec.Test.SampleModule

describe "union_type/1" do
test "creates a valid @type from list" do
assert fetch!(:type, :status) == "status() :: :read | :unread | :deleted"
end
end

describe "union_type_ast/1" do
test "when unquoted in a @type, creates valid value of type" do
assert fetch!(:type, :permission) == "permission() :: :view | :edit | :admin"
end
end

describe "union_typep/1" do
test "creates a valid @typep from list" do
assert fetch!(:typep, :role) == "role() :: :user | :admin"
end
end

defp fetch!(type_of_type, type_name) do
assert {:ok, types} = Code.Typespec.fetch_types(SampleModule)
assert {^type_of_type, type} = Enum.find(types, fn {_, {name, _, _}} -> name == type_name end)

type
|> Code.Typespec.type_to_quoted()
|> Macro.to_string()
end
end

0 comments on commit b02caae

Please sign in to comment.