PathMap
Deterministic helpers for traversing and mutating nested maps using explicit paths (lists of keys). Every call validates inputs, refuses to guess, and returns tagged results instead of raising.
- pure Elixir maps only (no structs/lists/tuples)
-
paths are lists; empty path (
[]) targets the root - strict and auto-vivifying variants for writes
-
explicit
{:ok, value}/{:error, reason}semantics
Purpose
PathMap focuses on predictability and safety for nested map operations without introducing a DSL or macros. If you want small, composable functions that tell you exactly why traversal failed, this library is for you.
Quick example
map = %{"config" => %{"port" => 4000}}
# Strict read
{:ok, 4000} = PathMap.fetch(map, ["config", "port"])
# Strict write (fails because the path is missing)
{:error, {:missing, ["config", "db"]}} = PathMap.put(map, ["config", "db", "port"], 5432)
# Auto-vivifying write
{:ok, map} = PathMap.put_auto(map, ["config", "db", "port"], 5432)
5432 = map["config"]["db"]["port"]
# Update with default and auto-vivify
{:ok, map} = PathMap.update_auto(%{}, [:a, :b], 0, &(&1 + 1))
1 = get_in(map, [:a, :b])Core behaviors
fetch/2returns{:ok, value}or{:error, reason}; root type is checked before path validity.get/3collapses any error to a default (nil by default).-
Strict writes (
put/3,put_new/3,update/3,update/4) require the path to exist. -
Auto-vivifying writes (
put_auto/3,put_new_auto/3,update_auto/4) create missing maps on the way. -
Empty path (
[]) targets or replaces the entire map.
Error shapes
{:not_a_map, value, prefix}— traversal hit a non-map (root or intermediate){:missing, prefix}— strict operation expected a key that was missing{:invalid_initializer, initializer}—ensure/3initializer is not arity 0{:invalid_function, fun, arity}— updater functions are wrong arity:invalid_path— path is not a list:already_exists—put_new*/3refused to overwrite an existing value:leaf_missing—update/3expected a leaf that was not present
When to use PathMap
- You need deterministic, explicit error reporting for nested map updates.
- You want strict vs auto-vivifying control without learning a DSL.
- You are working with plain maps and want small, composable functions.
- You want to keep error handling at the call site instead of rescuing exceptions.
When not to use PathMap
- You need optics over structs, lists, tuples, or multiple foci.
- You want declarative traversal/transformation DSLs or compile-time lenses.
- You need performance over complex data structures where a richer lens library shines.
Comparison
vs Pathex
- Pathex offers a macro DSL for lenses/paths over many data types (maps, lists, structs) with composable optics and transformations.
- PathMap is smaller, works on maps only, and favors explicit return values over macros and compile-time generation.
- Choose PathMap when you want simple, defensive map traversal without DSL ceremony; choose Pathex when you need broad container support and lens composition.
vs lens
lensprovides composable optics and functional patterns for many data types.- PathMap does not offer optics; it supplies straightforward functions for maps with clear error tuples.
-
Choose PathMap when you prefer concrete functions and explicit failure
reasons; choose
lenswhen you need rich lens composition and container flexibility.
API highlights
-
Read:
fetch/2— returns{:ok, value}or{:error, reason}get/3— returns value or default on any errorexists?/2,validate_path/2,valid_path?/2
-
Write (strict):
put/3— replace at path (fails if missing)put_new/3— insert only when missingupdate/3— update existing leaf (fails with:leaf_missing)update/4— update or set default at the leaf
-
Write (auto-vivifying):
put_auto/3,put_new_auto/3,update_auto/4
-
Utilities:
ensure/3— initialize missing leaf with a 0-arity function
See doctests in lib/path_map.ex and test/path_map_test.exs for detailed
examples and edge cases.
Documentation
Full API docs live at hexdocs.pm/path_map.
Requirements
-
Elixir
~> 1.19
Changelog
See CHANGELOG.md.
License
Released under the MIT License. See LICENSE for details.
Installation
Add to your mix.exs dependencies:
def deps do
[
{:path_map, "~> 0.1.0"}
]
end