defmodule Pathex.Lenses do @moduledoc """ Module with collection of prebuilt paths """ alias __MODULE__.{Some, Any, All, Star, Matching, Filtering} @doc """ Path function which works with **any** possible key it can find It takes any key **and than** applies inner function (or concated path) Example: iex> require Pathex iex> anyl = Pathex.Lenses.any() iex> {:ok, 1} = Pathex.view %{x: 1}, anyl iex> {:ok, [9]} = Pathex.set [8], anyl, 9 iex> {:ok, [x: 1, y: 2]} = Pathex.force_set [x: 0, y: 2], anyl, 1 Note that force setting value to empty map has undefined behaviour and therefore returns an error: iex> require Pathex iex> anyl = Pathex.Lenses.any() iex> :error = Pathex.force_set(%{}, anyl, :well) And note that this lens has keywords at head of list at a higher priority than non-keyword heads: iex> require Pathex iex> anyl = Pathex.Lenses.any() iex> {:ok, [{:x, 1}, 2]} = Pathex.set([{:x, 0}, 2], anyl, 1) iex> {:ok, [1, {:x, 2}]} = Pathex.set([0, {:x, 2}], anyl, 1) iex> {:ok, [1, 2]} = Pathex.set([{"some_tuple", "here"}, 2], anyl, 1) """ @doc export: true @spec any() :: Pathex.t() def any, do: Any.any() @doc """ Path function which works with **all** possible keys it can find It takes all keys **and than** applies inner function (or concated path) If any application fails, this lens returns `:error` Example: iex> require Pathex; import Pathex iex> alll = Pathex.Lenses.all() iex> Pathex.over!([%{x: 0}, [x: 1]], alll ~> path(:x), fn x -> x + 1 end) [%{x: 1}, [x: 2]] iex> Pathex.view!(%{x: 1, y: 2, z: 3}, alll) |> Enum.sort() [1, 2, 3] iex> Pathex.set([x: 1, y: 0], alll, 2) {:ok, [x: 2, y: 2]} """ @doc export: true @spec all() :: Pathex.t() def all, do: All.all() @doc """ Path function which applies inner function (or concated path-closure) to every value it can apply it to Example: iex> require Pathex; import Pathex iex> starl = Pathex.Lenses.star() iex> Pathex.view!(%{x: [1], y: [2], z: 3}, starl ~> path(0)) |> Enum.sort() [1, 2] iex> Pathex.set!(%{x: %{y: 0}, z: [3]}, starl ~> path(:y, :map), 1) %{x: %{y: 1}, z: [3]} iex> Pathex.view([x: 1, y: 2, z: 3], starl) {:ok, [1, 2, 3]} > Note: > It returns :error when no data was found or changed Think of this function as `filter_map`. It is particularly useful for filtering and selecting needed values with custom functions or `matching/1` macro Example: iex> require Pathex; import Pathex; require Pathex.Lenses iex> starl = Pathex.Lenses.star() iex> structure = [{1, 4}, {2, 8}, {3, 6}, {4, 10}] iex> # iex> # For example we want to select all tuples with first element greater than 2 iex> # iex> greater_than_2 = Pathex.Lenses.matching({x, _} when x > 2) iex> Pathex.view(structure, starl ~> greater_than_2) {:ok, [{3, 6}, {4, 10}]} """ @doc export: true @spec star() :: Pathex.t() def star, do: Star.star() @doc """ Path function which applies inner function (or concated path-closure) to the first value it can apply it to Example: iex> require Pathex; import Pathex iex> somel = Pathex.Lenses.some() iex> Pathex.view!([x: [11], y: [22], z: 33], somel ~> path(0)) 11 iex> Pathex.set!([x: %{y: 0}, z: %{y: 0}], somel ~> path(:y, :map), 1) [x: %{y: 1}, z: %{y: 0}] iex> Pathex.view([x: 1, y: 2, z: 3], somel) {:ok, 1} > Note: > Force update fails for empty structures Think of this function as `star() ~> any()` but optimized to work with only first element """ @doc export: true @spec some() :: Pathex.t() def some, do: Some.some() @doc """ This macro creates path-closure which successes only when input matches the `pattern`. The `pattern` can be just a pattern or a pattern with `when`. You can write this patterns just like you'd write them in `case` This function is useful when composed with `star/0` and `some/0` > Note: > In terms of functional programming, such conditional lenses are called prisms Example: iex> import Pathex.Lenses; import Pathex iex> adminl = matching(%{role: :admin}) iex> {:ok, %{name: "Name", role: :admin}} = Pathex.view(%{name: "Name", role: :admin}, adminl) iex> :error = Pathex.view(%{}, adminl) iex> import Pathex.Lenses; import Pathex iex> dots2d = [{1, 1}, {1, 5}, {3, 0}, {4, 3}] iex> higher_than_2 = matching({_x, y} when y > 2) iex> {:ok, [{1, 5}, {4, 3}]} = Pathex.view(dots2d, star() ~> higher_than_2) """ @doc export: true defmacro matching(pattern) do Matching.matching_func(pattern) end @doc """ This macro creates path-closure successes only when `predicate` returns truthy value. `predicate` is a function which takes a structures upon which the path is called and returns a boolean (or any other type which will be treated as boolean). This function is useful when composed with `star/0` and `some/0` > Note: > In terms of functional programming, such conditional lenses are called prisms Example: iex> import Pathex.Lenses; import Pathex iex> adminl = filtering(& &1.role == :admin) iex> {:ok, %{name: "Name", role: :admin}} = Pathex.view(%{name: "Name", role: :admin}, adminl) iex> :error = Pathex.view(%{role: :user}, adminl) iex> import Pathex.Lenses; import Pathex iex> dots2d = [{1, 1}, {1, 5}, {3, 0}, {4, 3}] iex> higher_than_2 = filtering(fn {_x, y} -> y > 2 end) iex> {:ok, [{1, 5}, {4, 3}]} = Pathex.view(dots2d, star() ~> higher_than_2) """ @doc export: true @spec filtering((any() -> boolean())) :: Pathex.t(any(), any()) def filtering(predicate), do: Filtering.filtering(predicate) end