Agents lang-erlang-library-dev
Erlang-specific library development patterns. Use when creating OTP libraries, designing public APIs with process patterns, configuring rebar3, managing application resources, publishing to Hex, or writing EDoc. Extends meta-library-dev with Erlang/OTP tooling and idioms.
install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/lang-erlang-library-dev" ~/.claude/skills/arustydev-agents-lang-erlang-library-dev && rm -rf "$T"
manifest:
content/skills/lang-erlang-library-dev/SKILL.mdsource content
Erlang Library Development
Erlang-specific patterns for OTP library development. This skill extends
meta-library-dev with Erlang/OTP tooling, process-oriented design, and ecosystem practices.
This Skill Extends
- Foundational library patterns (API design, versioning, testing strategies)meta-library-dev
- Core Erlang patterns (processes, OTP behaviors, supervision)lang-erlang-dev
For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first. For foundational Erlang patterns, see lang-erlang-dev.
This Skill Adds
- Erlang tooling: rebar3 configuration, .app.src files, application structure
- Hex publishing: Package metadata, versioning, documentation
- Library idioms: Public API design, behavior callbacks, application supervision
- OTP conventions: Application structure, configuration, resource management
- EDoc: Documentation generation, type specifications, function docs
- Dialyzer: Type analysis, PLT files, type specifications
This Skill Does NOT Cover
- General library patterns - see
meta-library-dev - Core Erlang/OTP basics - see
lang-erlang-dev - Web frameworks - see framework-specific skills
- Distributed systems - see
lang-erlang-distributed-dev - Performance optimization - see
lang-erlang-performance-dev
Quick Reference
| Task | Command/Pattern |
|---|---|
| New library app | |
| New OTP library | |
| Compile | |
| Test | or |
| Type check | |
| Generate docs | |
| Publish (dry run) | |
| Publish to Hex | |
| Shell with library | |
| Run tests | |
rebar3 Configuration
Basic rebar.config
{erl_opts, [ debug_info, warnings_as_errors, warn_export_all, warn_unused_import, warn_untyped_record ]}. {deps, []}. {project_plugins, [rebar3_hex, rebar3_ex_doc]}. {hex, [ {doc, #{provider => ex_doc}} ]}. {profiles, [ {test, [ {deps, [ {proper, "1.4.0"}, {meck, "0.9.2"} ]}, {erl_opts, [nowarn_export_all]} ]}, {prod, [ {erl_opts, [no_debug_info, warnings_as_errors]} ]} ]}. {dialyzer, [ {warnings, [ unmatched_returns, error_handling, underspecs ]}, {plt_extra_apps, [ssl, crypto]} ]}. {xref_checks, [ undefined_function_calls, undefined_functions, locals_not_used, deprecated_function_calls, deprecated_functions ]}. {cover_enabled, true}. {cover_opts, [verbose]}.
Application Resource File (.app.src)
{application, mylib, [{description, "A library for doing X efficiently"}, {vsn, "0.1.0"}, {registered, []}, {applications, [ kernel, stdlib ]}, {env, []}, {modules, []}, {licenses, ["Apache-2.0"]}, {links, [ {"GitHub", "https://github.com/username/mylib"}, {"Hex", "https://hex.pm/packages/mylib"} ]}, {build_tools, ["rebar3"]}, {files, [ "src", "include", "rebar.config", "rebar.lock", "README.md", "LICENSE" ]} ]}.
OTP Application (.app.src with supervisor)
{application, mylib, [{description, "OTP library with supervision tree"}, {vsn, "0.1.0"}, {registered, [mylib_sup]}, {mod, {mylib_app, []}}, {applications, [ kernel, stdlib, sasl ]}, {env, [ {pool_size, 10}, {timeout, 5000} ]}, {modules, []}, {licenses, ["MIT"]}, {links, [{"GitHub", "https://github.com/username/mylib"}]}, {build_tools, ["rebar3"]} ]}.
Project Structure
Simple Library (No Supervision)
mylib/ ├── rebar.config ├── rebar.lock ├── README.md ├── LICENSE ├── src/ │ ├── mylib.app.src # Application resource file │ ├── mylib.erl # Main public API module │ ├── mylib_parser.erl # Internal module │ └── mylib_types.hrl # Type definitions ├── include/ # Public header files │ └── mylib.hrl ├── test/ │ ├── mylib_eunit.erl # EUnit tests │ └── mylib_SUITE.erl # Common Test suite ├── doc/ # Generated by EDoc │ └── overview.edoc # Documentation overview └── priv/ # Static resources └── templates/
OTP Application Library
mylib/ ├── rebar.config ├── src/ │ ├── mylib.app.src │ ├── mylib_app.erl # Application behavior │ ├── mylib_sup.erl # Root supervisor │ ├── mylib.erl # Public API │ ├── mylib_server.erl # gen_server worker │ └── mylib_worker.erl # Worker process ├── include/ │ └── mylib.hrl ├── test/ │ ├── mylib_eunit.erl │ ├── mylib_SUITE.erl │ └── mylib_prop.erl # PropEr property tests ├── doc/ │ └── overview.edoc └── priv/ └── config/ └── defaults.config
Public API Design
Module Organization
Single entry point for users:
-module(mylib). -export([ start/0, stop/0, process/1, process/2, format/1 ]). %% @doc Start the application -spec start() -> ok | {error, term()}. start() -> application:ensure_all_started(mylib). %% @doc Stop the application -spec stop() -> ok. stop() -> application:stop(mylib). %% @doc Process input with default options -spec process(Input :: binary()) -> {ok, term()} | {error, term()}. process(Input) -> process(Input, #{}). %% @doc Process input with custom options -spec process(Input :: binary(), Options :: map()) -> {ok, term()} | {error, term()}. process(Input, Options) -> mylib_parser:parse(Input, Options).
Behavior Definition
Define custom behaviors for extensibility:
-module(mylib_handler). %% Behavior callback definitions -callback init(Args :: term()) -> {ok, State :: term()} | {error, Reason :: term()}. -callback handle_event(Event :: term(), State :: term()) -> {ok, NewState :: term()} | {error, Reason :: term()}. -callback terminate(Reason :: term(), State :: term()) -> ok. -optional_callbacks([terminate/2]). %% Export behavior -export_type([handler/0]). -type handler() :: module().
Implement the behavior:
-module(my_custom_handler). -behaviour(mylib_handler). -export([init/1, handle_event/2, terminate/2]). init(Args) -> {ok, #{args => Args}}. handle_event(Event, State) -> io:format("Received: ~p~n", [Event]), {ok, State}. terminate(_Reason, _State) -> ok.
Options and Configuration
Use maps for flexible options:
-module(mylib_config). -export([parse_options/1, defaults/0]). -type options() :: #{ timeout => pos_integer(), retry => boolean(), max_retries => pos_integer(), format => json | xml | binary }. -export_type([options/0]). %% @doc Default configuration -spec defaults() -> options(). defaults() -> #{ timeout => 5000, retry => true, max_retries => 3, format => json }. %% @doc Merge user options with defaults -spec parse_options(UserOpts :: map()) -> options(). parse_options(UserOpts) -> maps:merge(defaults(), UserOpts).
Type Specifications and Dialyzer
Complete Type Specs
-module(mylib_types). %% Exported types -export_type([ user_id/0, user/0, result/0, error_reason/0 ]). %% Type definitions -type user_id() :: non_neg_integer(). -type user() :: #{ id := user_id(), name := binary(), email := binary(), created_at := calendar:datetime() }. -type error_reason() :: not_found | invalid_input | timeout | {internal_error, term()}. -type result() :: {ok, term()} | {error, error_reason()}. %% Records with types -record(config, { timeout :: pos_integer(), max_connections :: pos_integer(), handler :: module() }). -type config() :: #config{}. -export_type([config/0]).
Function Specifications
-module(mylib_api). %% @doc Create a new user -spec create_user(Name :: binary(), Email :: binary()) -> {ok, mylib_types:user()} | {error, mylib_types:error_reason()}. create_user(Name, Email) when is_binary(Name), is_binary(Email) -> UserId = generate_id(), User = #{ id => UserId, name => Name, email => Email, created_at => calendar:universal_time() }, {ok, User}. %% @doc Find user by ID -spec find_user(mylib_types:user_id()) -> {ok, mylib_types:user()} | {error, not_found}. find_user(UserId) when is_integer(UserId), UserId >= 0 -> case lookup_user(UserId) of undefined -> {error, not_found}; User -> {ok, User} end. %% Internal function - no spec needed (dialyzer infers) lookup_user(UserId) -> ets:lookup(users_table, UserId).
Dialyzer PLT Management
% rebar.config {dialyzer, [ {warnings, [ unmatched_returns, error_handling, underspecs, unknown ]}, {plt_apps, all_deps}, {plt_extra_apps, [ssl, crypto, public_key]}, {plt_location, local}, {base_plt_location, global} ]}.
EDoc Documentation
Module Documentation
%%% @doc Main API module for MyLib. %%% %%% This module provides the primary interface for working with MyLib. %%% All operations are safe to use from multiple processes concurrently. %%% %%% == Quick Start == %%% %%% ``` %%% 1> mylib:start(). %%% ok %%% 2> {ok, Result} = mylib:process(<<"input">>). %%% {ok, #{data => <<"processed">>}} %%% ''' %%% %%% == Configuration == %%% %%% The application can be configured via application environment: %%% %%% ``` %%% {mylib, [ %%% {timeout, 10000}, %%% {pool_size, 20} %%% ]} %%% ''' %%% %%% @end -module(mylib). -author("Your Name <your.email@example.com>"). -copyright("2025 Your Name"). -version("0.1.0").
Function Documentation
%%% @doc Process input data with options. %%% %%% This function processes the input binary according to the provided %%% options and returns the result. Processing is done asynchronously %%% in a worker pool. %%% %%% == Options == %%% %%% <ul> %%% <li>`timeout' - Maximum time in milliseconds (default: 5000)</li> %%% <li>`format' - Output format: `json' or `binary' (default: json)</li> %%% <li>`retry' - Whether to retry on failure (default: true)</li> %%% </ul> %%% %%% == Examples == %%% %%% ``` %%% %% Simple processing %%% {ok, Result} = mylib:process(<<"data">>). %%% %%% %% With custom timeout %%% {ok, Result} = mylib:process(<<"data">>, #{timeout => 10000}). %%% %%% %% Binary output format %%% {ok, Binary} = mylib:process(<<"data">>, #{format => binary}). %%% ''' %%% %%% @see process/1 %%% @end -spec process(Input :: binary(), Options :: map()) -> {ok, term()} | {error, term()}. process(Input, Options) -> % Implementation ok.
Type Documentation
%%% @type user_id() = non_neg_integer(). %%% Unique identifier for a user. %%% @type user() = #{ %%% id := user_id(), %%% name := binary(), %%% email := binary(), %%% created_at := calendar:datetime() %%% }. %%% User record containing all user information. %%% @type error_reason() = %%% not_found | %%% invalid_input | %%% timeout | %%% {internal_error, term()}. %%% Possible error reasons returned by library functions.
Overview Documentation
Create
doc/overview.edoc:
@author Your Name <your.email@example.com> @copyright 2025 Your Name @version 0.1.0 @title MyLib - Efficient Data Processing Library @doc == Overview == MyLib is a high-performance library for processing data in Erlang/OTP applications. It provides a simple, consistent API while leveraging OTP principles for reliability and scalability. == Features == <ul> <li>Concurrent processing with worker pools</li> <li>Automatic retries and error handling</li> <li>Multiple output formats</li> <li>Full type specifications</li> <li>Comprehensive test coverage</li> </ul> == Installation == Add to your `rebar.config':
{deps, [ {mylib, "0.1.0"} ]}. '''
== Quick Start ==
%% Start the application ok = mylib:start(). %% Process some data {ok, Result} = mylib:process(<<"input data">>). %% Stop the application ok = mylib:stop(). ''' @end
Testing Patterns
EUnit Tests
-module(mylib_eunit). -include_lib("eunit/include/eunit.hrl"). %%% Setup/Teardown setup() -> application:ensure_all_started(mylib). cleanup(_) -> application:stop(mylib). %%% Test Fixtures mylib_test_() -> {setup, fun setup/0, fun cleanup/1, [ {"Process valid input", fun test_process_valid/0}, {"Process invalid input", fun test_process_invalid/0}, {"Process with timeout", fun test_process_timeout/0} ]}. %%% Tests test_process_valid() -> Input = <<"valid input">>, {ok, Result} = mylib:process(Input), ?assertMatch(#{data := _}, Result). test_process_invalid() -> Input = <<"">>, ?assertEqual({error, invalid_input}, mylib:process(Input)). test_process_timeout() -> Input = <<"data">>, Options = #{timeout => 1}, ?assertMatch({error, timeout}, mylib:process(Input, Options)). %%% Property-Based Tests prop_never_crashes_test_() -> {timeout, 60, fun() -> ?assert(proper:quickcheck(prop_no_crash(), [{numtests, 1000}])) end}. prop_no_crash() -> ?FORALL(Input, binary(), case mylib:process(Input) of {ok, _} -> true; {error, _} -> true end ).
Common Test Suites
-module(mylib_SUITE). -include_lib("common_test/include/ct.hrl"). %% CT callbacks -export([all/0, groups/0, init_per_suite/1, end_per_suite/1]). -export([init_per_testcase/2, end_per_testcase/2]). %% Test cases -export([ test_basic_processing/1, test_concurrent_processing/1, test_error_handling/1 ]). %%% CT Callbacks all() -> [ {group, basic}, {group, concurrent}, {group, errors} ]. groups() -> [ {basic, [parallel], [test_basic_processing]}, {concurrent, [parallel], [test_concurrent_processing]}, {errors, [sequence], [test_error_handling]} ]. init_per_suite(Config) -> {ok, _} = application:ensure_all_started(mylib), Config. end_per_suite(_Config) -> application:stop(mylib), ok. init_per_testcase(_TestCase, Config) -> Config. end_per_testcase(_TestCase, _Config) -> ok. %%% Test Cases test_basic_processing(Config) -> Input = <<"test data">>, {ok, Result} = mylib:process(Input), ct:log("Result: ~p", [Result]), #{data := Data} = Result, true = is_binary(Data). test_concurrent_processing(Config) -> Inputs = [<<"input", (integer_to_binary(N))/binary>> || N <- lists:seq(1, 100)], Self = self(), [spawn(fun() -> {ok, _} = mylib:process(Input), Self ! {done, N} end) || {N, Input} <- lists:enumerate(Inputs)], receive_n(100). test_error_handling(Config) -> {error, invalid_input} = mylib:process(<<>>), {error, timeout} = mylib:process(<<"data">>, #{timeout => 1}). %%% Helpers receive_n(0) -> ok; receive_n(N) -> receive {done, _} -> receive_n(N - 1) after 5000 -> ct:fail("Timeout waiting for concurrent operations") end.
PropEr Property Tests
-module(mylib_prop). -include_lib("proper/include/proper.hrl"). -include_lib("eunit/include/eunit.hrl"). %%% Generators user_id() -> non_neg_integer(). user_name() -> ?LET(Len, range(1, 50), ?LET(Chars, vector(Len, range($a, $z)), list_to_binary(Chars))). valid_user() -> ?LET({Id, Name, Email}, {user_id(), user_name(), user_name()}, #{id => Id, name => Name, email => <<Email/binary, "@example.com">>}). %%% Properties prop_create_user_returns_user() -> ?FORALL({Name, Email}, {user_name(), user_name()}, begin {ok, User} = mylib:create_user(Name, Email), maps:is_key(id, User) andalso maps:get(name, User) =:= Name andalso maps:get(email, User) =:= Email end). prop_roundtrip_serialization() -> ?FORALL(User, valid_user(), begin Serialized = mylib:serialize(User), {ok, Deserialized} = mylib:deserialize(Serialized), User =:= Deserialized end). %%% EUnit wrapper properties_test_() -> {timeout, 120, [ ?_assert(proper:quickcheck(prop_create_user_returns_user(), [{numtests, 100}])), ?_assert(proper:quickcheck(prop_roundtrip_serialization(), [{numtests, 100}])) ]}.
Publishing to Hex
Hex Package Configuration
Add to
rebar.config:
{project_plugins, [ rebar3_hex, rebar3_ex_doc ]}. {hex, [ {doc, #{provider => ex_doc}} ]}. {ex_doc, [ {source_url, <<"https://github.com/username/mylib">>}, {extras, [<<"README.md">>, <<"LICENSE">>, <<"CHANGELOG.md">>]}, {main, <<"readme">>} ]}.
Pre-publish Checklist
- Version bumped in
.app.src - CHANGELOG.md updated with version changes
- README.md is current and comprehensive
- All tests pass:
rebar3 do eunit, ct - Dialyzer passes:
rebar3 dialyzer - Documentation builds:
rebar3 edoc - Application metadata complete in
.app.src - License file(s) included
- No uncommitted changes
- Dry run succeeds:
rebar3 hex publish --dry-run
Publishing Commands
# Generate documentation rebar3 hex docs # Dry run to check package rebar3 hex publish --dry-run # Actually publish rebar3 hex publish # Publish documentation separately rebar3 hex publish docs # Revert a package (within 24h or 1 hour for new packages) rebar3 hex revert 0.1.0 # Retire a version (soft deprecation) rebar3 hex retire mylib 0.1.0 other "Please upgrade to 0.2.0"
Version Retirement Reasons
# Available retirement reasons: rebar3 hex retire mylib VERSION other "Custom message" rebar3 hex retire mylib VERSION security "Security vulnerability found" rebar3 hex retire mylib VERSION deprecated "Use new-package instead" rebar3 hex retire mylib VERSION invalid "Invalid release" rebar3 hex retire mylib VERSION renamed "Package renamed to new-name"
Application Supervision
Application Behavior
-module(mylib_app). -behaviour(application). -export([start/2, stop/1]). start(_StartType, _StartArgs) -> case mylib_sup:start_link() of {ok, Pid} -> {ok, Pid}; Error -> Error end. stop(_State) -> ok.
Root Supervisor
-module(mylib_sup). -behaviour(supervisor). -export([start_link/0, init/1]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> SupFlags = #{ strategy => one_for_one, intensity => 5, period => 60 }, ChildSpecs = [ #{ id => mylib_server, start => {mylib_server, start_link, []}, restart => permanent, shutdown => 5000, type => worker, modules => [mylib_server] }, #{ id => mylib_pool_sup, start => {mylib_pool_sup, start_link, []}, restart => permanent, shutdown => infinity, type => supervisor, modules => [mylib_pool_sup] } ], {ok, {SupFlags, ChildSpecs}}.
Worker Pool Supervisor
-module(mylib_pool_sup). -behaviour(supervisor). -export([start_link/0, init/1]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> PoolSize = application:get_env(mylib, pool_size, 10), SupFlags = #{ strategy => one_for_one, intensity => 10, period => 60 }, ChildSpecs = [ #{ id => {mylib_worker, N}, start => {mylib_worker, start_link, [N]}, restart => permanent, shutdown => 5000, type => worker, modules => [mylib_worker] } || N <- lists:seq(1, PoolSize) ], {ok, {SupFlags, ChildSpecs}}.
Configuration Management
Application Environment
% src/mylib.app.src {application, mylib, [{env, [ {pool_size, 10}, {timeout, 5000}, {retry_count, 3}, {log_level, info} ]} ]}.
Runtime Configuration
-module(mylib_config). -export([get/1, get/2, set/2]). %% @doc Get configuration value -spec get(Key :: atom()) -> term(). get(Key) -> case application:get_env(mylib, Key) of {ok, Value} -> Value; undefined -> error({config_not_found, Key}) end. %% @doc Get configuration value with default -spec get(Key :: atom(), Default :: term()) -> term(). get(Key, Default) -> application:get_env(mylib, Key, Default). %% @doc Set configuration value at runtime -spec set(Key :: atom(), Value :: term()) -> ok. set(Key, Value) -> application:set_env(mylib, Key, Value).
Config Files (sys.config)
% config/sys.config [ {mylib, [ {pool_size, 20}, {timeout, 10000}, {log_level, debug} ]}, {sasl, [ {sasl_error_logger, {file, "log/sasl-error.log"}}, {errlog_type, error} ]}, {kernel, [ {logger_level, info} ]} ].
Common Patterns
Singleton Server
-module(mylib_server). -behaviour(gen_server). %% API -export([start_link/0, call/1, cast/1, get_state/0]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). -define(SERVER, ?MODULE). %%% API start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). call(Request) -> gen_server:call(?SERVER, Request). cast(Request) -> gen_server:cast(?SERVER, Request). get_state() -> gen_server:call(?SERVER, get_state). %%% Callbacks init([]) -> State = #{ started_at => erlang:system_time(second), requests => 0 }, {ok, State}. handle_call(get_state, _From, State) -> {reply, State, State}; handle_call(Request, _From, State = #{requests := Count}) -> Result = process_request(Request), {reply, Result, State#{requests => Count + 1}}. handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. %%% Internal process_request(Request) -> {ok, Request}.
Resource Pool Pattern
-module(mylib_pool). -export([checkout/0, checkin/1, with_resource/1]). -define(POOL, mylib_resource_pool). %% @doc Check out a resource from the pool -spec checkout() -> {ok, pid()} | {error, no_resources}. checkout() -> case ets:lookup(?POOL, available) of [{available, [Pid | Rest]}] -> ets:insert(?POOL, {available, Rest}), ets:insert(?POOL, {in_use, Pid}), {ok, Pid}; _ -> {error, no_resources} end. %% @doc Return a resource to the pool -spec checkin(pid()) -> ok. checkin(Pid) -> ets:delete_object(?POOL, {in_use, Pid}), [{available, Available}] = ets:lookup(?POOL, available), ets:insert(?POOL, {available, [Pid | Available]}), ok. %% @doc Execute function with a resource -spec with_resource(fun((pid()) -> term())) -> {ok, term()} | {error, term()}. with_resource(Fun) -> case checkout() of {ok, Resource} -> try Result = Fun(Resource), {ok, Result} after checkin(Resource) end; Error -> Error end.
Anti-Patterns
1. Exposing Internal Processes
% Bad: Exposes internal PIDs -spec get_worker() -> pid(). get_worker() -> whereis(mylib_worker). % Good: Provide functional API -spec process(Input :: term()) -> {ok, term()} | {error, term()}. process(Input) -> mylib_worker:process(Input).
2. Breaking Module Contracts
% Bad: Changing return type in new version % v0.1.0 -spec parse(binary()) -> map(). % v0.2.0 - BREAKING -spec parse(binary()) -> {ok, map()} | {error, term()}. % Good: Add new function, deprecate old % v0.2.0 -spec parse(binary()) -> map(). % Deprecated -spec parse_safe(binary()) -> {ok, map()} | {error, term()}.
3. Ignoring Application Lifecycle
% Bad: Starting processes in module -module(mylib). start_worker() -> spawn(fun() -> worker_loop() end). % Good: Use supervision tree -module(mylib_sup). init([]) -> ChildSpecs = [worker_spec()], {ok, {sup_flags(), ChildSpecs}}.
4. Missing Type Specs
% Bad: No specs, dialyzer can't verify process(Input) -> transform(Input). % Good: Full specifications -spec process(Input :: binary()) -> {ok, term()} | {error, atom()}. process(Input) when is_binary(Input) -> transform(Input).
References
- Foundational library patternsmeta-library-dev
- Core Erlang/OTP patternslang-erlang-dev- Rebar3 Documentation
- Hex Package Manager
- EDoc User's Guide
- Dialyzer User's Guide
- OTP Design Principles