defmodule Pathex do @moduledoc """ This module contains functions and macros to be used with `Pathex` and i To use Pathex just insert to your context. You can import Pathex in module body or even in function body. ```elixir require Pathex import Pathex, only: [path: 1, path: 2, "~>": 2, ...] ``` Or you can use `use` ```elixir defmodule MyModule do # `default_mod` option is optional # when no mod is specified, `:naive` is selected use Pathex, default_mod: :json ... end ``` This will import all operatiors and `path` macro Any macro here belongs to one of three categories: 1. Macro which creates path closure (only `path/2`) 2. Macro which uses path closure as path (`over/3`, `set/3`, `view/2`, ...) 3. Macro which creates path composition (`~>/2`, `|||/2`, ...) """ alias Pathex.Builder alias Pathex.Combination alias Pathex.Common alias Pathex.Operations alias Pathex.QuotedParser @typedoc """ Function which is passed to path-closure as second element in args tuple """ @type inner_func(output) :: (any() -> result(output)) @type inspect_args :: any() @type update_args(input, output) :: {input, inner_func(output)} @type force_update_args(input, output) :: {input, inner_func(output), any()} @typedoc "This depends on the modifier" @type pathex_compatible_structure :: map() | list() | Keyword.t() | tuple() @typedoc "Value returned by non-bang path call" @type result(inner) :: {:ok, inner} | :error | :delete_me @typedoc "Also known as [path-closure](path.md)" @type t :: t(pathex_compatible_structure(), any()) @typedoc "Also known as [path-closure](path.md)" @type t(input, output) :: (op_name(), force_update_args(input, output) | update_args(input, output) | inspect_args() -> result(output | input)) @typedoc "More about [modifiers](modifiers.md)" @type mod :: :map | :json | :naive @typep op_name :: Operations.name() @doc """ Easy and convenient way to add pathex to your module. You can specify modifier ```elixir use Pathex, default_mod: :json ``` Or just use it with default `:naive` modifier ```elixir use Pathex ``` """ @doc export: true defmacro __using__(opts) do case Keyword.get(opts, :default_mod, :naive) do :naive -> quote do require Pathex import Pathex, only: [path: 1, path: 2, ~>: 2, &&&: 2, |||: 2, alongside: 1] end mod when mod in ~w[json map]a -> quote do require Pathex import Pathex, only: [path: 1, path: 2, ~>: 2, &&&: 2, |||: 2, alongside: 1] @pathex_default_mod unquote(mod) end _wrong_mod -> raise ArgumentError, "Pathex only works with navie, json and map mods" end end @doc """ Applies `func` to the item under the `path` in `struct` and returns modified structure. Works like `Map.update!/3` but doesn't raise. Example: iex> index = 1 iex> inc = fn x -> x + 1 end iex> {:ok, [0, %{x: 9}]} = over [0, %{x: 8}], path(index / :x), inc iex> p = path "hey" / 0 iex> {:ok, %{"hey" => [2, [2]]}} = over %{"hey" => [1, [2]]}, p, inc > Note: > Exceptions from passed function left unhandled iex> over(%{1 => "x"}, path(1), fn x -> x + 1 end) ** (ArithmeticError) bad argument in arithmetic expression """ @doc export: true defmacro over(struct, path, func) do gen(path, :update, [struct, wrap_ok(func)], __CALLER__) end @doc """ Applies the `func` to the item under `path` in `struct` and returns modified structure. Works like `Map.update!/3`. Example: iex> x = 1 iex> inc = fn x -> x + 1 end iex> [0, %{x: 9}] = over! [0, %{x: 8}], path(x / :x), inc iex> p = path "hey" / 0 iex> %{"hey" => [2, [2]]} = over! %{"hey" => [1, [2]]}, p, inc """ @doc export: true defmacro over!(struct, path, func) do path |> gen(:update, [struct, wrap_ok(func)], __CALLER__) |> bang(struct, path) end @doc """ Sets `value` under `path` in `structure`. Think of it like `Map.put/3`. Example: iex> x = 1 iex> {:ok, [0, %{x: 123}]} = set [0, %{x: 8}], path(x / :x), 123 iex> p = path "hey" / 0 iex> {:ok, %{"hey" => [123, [2]]}} = set %{"hey" => [1, [2]]}, p, 123 """ @doc export: true defmacro set(struct, path, value) do gen(path, :update, [struct, quote(do: fn _ -> {:ok, unquote(value)} end)], __CALLER__) end @doc """ Sets the `value` under `path` in `struct`. Think of it like `Map.put/3`. Example: iex> x = 1 iex> [0, %{x: 123}] = set! [0, %{x: 8}], path(x / :x), 123 iex> p = path "hey" / 0 iex> %{"hey" => [123, [2]]} = set! %{"hey" => [1, [2]]}, p, 123 """ @doc export: true defmacro set!(struct, path, value) do path |> gen(:update, [struct, quote(do: fn _ -> {:ok, unquote(value)} end)], __CALLER__) |> bang(struct, path) end @doc """ Sets the `value` under `path` in `struct`. If the path does not exist it creates the path favouring maps when structure is unknown. Example: iex> x = 1 iex> {:ok, [0, %{x: 123}]} = force_set [0, %{x: 8}], path(x / :x), 123 iex> p = path "hey" / 0 iex> {:ok, %{"hey" => %{0 => 1}}} = force_set %{}, p, 1 If the item in path doesn't have the right type, it returns `:error`. Example: iex> p = path "hey" / "you" iex> :error = force_set %{"hey" => {1, 2}}, p, "value" """ @doc export: true defmacro force_set(struct, path, value) do gen( path, :force_update, [struct, quote(do: fn _ -> {:ok, unquote(value)} end), value], __CALLER__ ) end @doc """ Sets the `value` under `path` in `struct`. If the path does not exist it creates the path favouring maps when structure is unknown. Example: iex> x = 1 iex> [0, %{x: 123}] = force_set! [0, %{x: 8}], path(x / :x), 123 iex> p = path "hey" / 0 iex> %{"hey" => %{0 => 1}} = force_set! %{}, p, 1 If the item in path doesn't have the right type, it raises. Example: iex> p = path "hey" / "you" iex> force_set! %{"hey" => {1, 2}}, p, "value" ** (Pathex.Error) Type mismatch in structure """ @doc export: true defmacro force_set!(struct, path, value) do path |> gen( :force_update, [struct, quote(do: fn _ -> {:ok, unquote(value)} end), value], __CALLER__ ) |> bang(struct, path, "Type mismatch in structure") end @doc """ Applies `func` under `path` of `struct`. If the path does not exist it creates the path favouring maps when structure is unknown and inserts default value. Example: iex> x = 1 iex> {:ok, [0, %{x: {:xxx, 8}}]} = force_over([0, %{x: 8}], path(x / :x), & {:xxx, &1}, 123) iex> p = path "hey" / 0 iex> {:ok, %{"hey" => %{0 => 1}}} = force_over(%{}, p, fn x -> x + 1 end, 1) If the item in path doesn't have the right type, it returns `:error`. Example: iex> p = path "hey" / "you" iex> :error = force_over %{"hey" => {1, 2}}, p, fn x -> x end, "value" """ @doc export: true defmacro force_over(struct, path, func, value \\ nil) do gen(path, :force_update, [struct, wrap_ok(func), value], __CALLER__) end @doc """ Applies `func` under `path` of `struct`. If the path does not exist it creates the path favouring maps when structure is unknown and inserts default value. Example: iex> x = 1 iex> [0, %{x: {:xxx, 8}}] = force_over!([0, %{x: 8}], path(x / :x), & {:xxx, &1}, 123) iex> p = path "hey" / 0 iex> %{"hey" => %{0 => 1}} = force_over!(%{}, p, fn x -> x + 1 end, 1) If the item in path doesn't have the right type, it raises. Example: iex> p = path "hey" / "you" iex> force_over! %{"hey" => {1, 2}}, p, fn x -> x end, "value" ** (Pathex.Error) Type mismatch in structure """ @doc export: true defmacro force_over!(struct, path, func, value \\ nil) do path |> gen(:force_update, [struct, wrap_ok(func), value], __CALLER__) |> bang(struct, path, "Type mismatch in structure") end @doc """ Applies `func` under `path` in `struct` and returns result of this `func`. Example: iex> x = 1 iex> {:ok, 9} = at [0, %{x: 8}], path(x / :x), fn x -> x + 1 end iex> p = path "hey" / 0 iex> {:ok, {:here, 9}} = at(%{"hey" => {9, -9}}, p, & {:here, &1}) """ @doc export: true defmacro at(struct, path, func) do gen(path, :view, [struct, wrap_ok(func)], __CALLER__) end @doc """ Applies `func` under `path` in `struct` and returns result of this `func`. Raises if path is not found. Example: iex> x = 1 iex> 9 = at! [0, %{x: 8}], path(x / :x), fn x -> x + 1 end iex> p = path "hey" / 0 iex> {:here, 9} = at!(%{"hey" => {9, -9}}, p, & {:here, &1}) """ @doc export: true defmacro at!(struct, path, func) do path |> gen(:view, [struct, wrap_ok(func)], __CALLER__) |> bang(struct, path) end @doc """ Gets the value under `path` in `struct`. Example: iex> x = 1 iex> {:ok, 8} = view [0, %{x: 8}], path(x / :x) iex> p = path "hey" / 0 iex> {:ok, 9} = view %{"hey" => {9, -9}}, p """ @doc export: true defmacro view(struct, path) do gen(path, :view, [struct, quote(do: fn x -> {:ok, x} end)], __CALLER__) end @doc """ Gets the value under `path` in `struct`. Raises if `path` not found. Example: iex> x = 1 iex> 8 = view! [0, %{x: 8}], path(x / :x) iex> p = path "hey" / 0 iex> 9 = view! %{"hey" => {9, -9}}, p """ @doc export: true defmacro view!(struct, path) do path |> gen(:view, [struct, quote(do: fn x -> {:ok, x} end)], __CALLER__) |> bang(struct, path) end @doc """ Gets the value under `path` in `struct` or returns `default` when `path` is not present. Example: iex> x = 1 iex> 8 = get([0, %{x: 8}], path(x / :x)) iex> p = path "hey" / "you" iex> nil = get(%{"hey" => [x: 1]}, p) iex> :default = get(%{"hey" => [x: 1]}, p, :default) """ @doc export: true defmacro get(struct, path, default \\ nil) do res = gen(path, :view, [struct, quote(do: fn x -> {:ok, x} end)], __CALLER__) quote do case unquote(res) do {:ok, value} -> value :error -> unquote(default) end end |> Common.set_generated() end @doc """ Gets the value under `path` in `struct` or returns default value if not found. Example: iex> x = 1 iex> true = exists?([0, %{x: 8}], path(x / :x)) iex> p = path "hey" / "you" iex> false = exists?(%{"hey" => [x: 1]}, p) """ @doc export: true defmacro exists?(struct, path) do res = gen(path, :view, [struct, quote(do: fn _ -> true end)], __CALLER__) quote do with :error <- unquote(res) do false end end |> Common.set_generated() end @doc """ Deletes value under `path` in `struct`. Example: iex> x = 1 iex> {:ok, [0, %{}]} = delete([0, %{x: 8}], path(x / :x)) iex> :error = delete([0, %{x: 8}], path(1 / :y)) """ @doc export: true defmacro delete(struct, path) do path |> gen(:delete, [struct, quote(do: fn _ -> :delete_me end)], __CALLER__) |> wrap_delete_me() end @doc """ Deletes value under `path` in `struct` or raises if value is not found. Example: iex> x = 1 iex> [0, %{}] = delete!([0, %{x: 8}], path(x / :x)) """ @doc export: true defmacro delete!(struct, path) do path |> gen(:delete, [struct, quote(do: fn _ -> :delete_me end)], __CALLER__) |> wrap_delete_me() |> bang(struct, path) end defp wrap_delete_me(call) do quote do with :delete_me <- unquote(call) do :error end end end @doc """ Macro which gets value in the structure and deletes it. Note that current implementation of this function performs double lookup. Example: iex> {:ok, {1, [2, 3]}} = pop([1, 2, 3], path(0)) """ @doc export: true defmacro pop(struct, path) do view = gen(path, :view, [struct, quote(do: fn x -> {:ok, x} end)], __CALLER__) delete = gen(path, :delete, [struct, quote(do: fn _ -> :delete_me end)], __CALLER__) quote do with( {:ok, value} <- unquote(view), {:ok, structure} <- unquote(delete) ) do {:ok, {value, structure}} end end end @doc """ Gets value under `path` in `struct` and then deletes it. Note that current implementation of this function performs double lookup. Example: iex> {1, [2, 3]} = pop!([1, 2, 3], path(0)) """ @doc export: true defmacro pop!(struct, path) do view = path |> gen(:view, [struct, quote(do: fn x -> {:ok, x} end)], __CALLER__) |> bang(struct, path) delete = path |> gen(:delete, [struct, quote(do: fn _ -> :delete_me end)], __CALLER__) |> wrap_delete_me() |> bang(struct, path) quote do value = unquote(view) structure = unquote(delete) {value, structure} end end @doc """ Creates path from `quoted` ast. Paths look like unix fs path and consist of elements separated from each other with `/`. See For example: iex> x = 1 iex> mypath = path 1 / :atom / "string" / {"tuple?"} / x iex> structure = [0, [atom: %{"string" => %{{"tuple?"} => %{1 => 2}}}]] iex> {:ok, 2} = view structure, mypath Default [modifier](modifiers.md) of this `path/2` is `:naive` which means that * Every variable is treated as index or key to tuple, list, map and keyword * Every atom is treated as key to map or keyword * Every integer is treated as index to tuple, list or key to map * Every other data type is treated as key to map > Note: > `-1` allows data to be prepended to the list iex> x = -1 iex> p1 = path(-1) iex> p2 = path(x) iex> {:ok, [1, 2]} = force_set([2], p1, 1) iex> {:ok, [1, 2]} = force_set([2], p2, 1) """ @doc export: true defmacro path(quoted, mod \\ nil) do mod = get_mod(mod, __CALLER__) {binds, combination} = QuotedParser.parse(quoted, __CALLER__, mod) if Macro.Env.in_match?(__CALLER__) do else end combination |> assert_combination_length(__CALLER__) |> Builder.build(Operations.builders_for_combination(combination)) |> Common.set_generated() |> prepend_binds(binds) end @doc """ Creates composition of two paths similar to concatenating them together. This means that `a ~> b` path-closure applies `a` and only if it returns `{:ok, something}` it applies `b` to `something` Example: iex> p1 = path :x / :y iex> p2 = path :a / :b iex> composed_path = p1 ~> p2 iex> {:ok, 1} = view %{x: [y: [a: [a: 0, b: 1]]]}, composed_path """ @doc export: true defmacro a ~> b, do: do_concat(a, b, __CALLER__) @doc """ The same as `Pathex.~>/2` for those who do not like operators Example: iex> p1 = path :x / :y iex> p2 = path :a / :b iex> composed_path = concat(p1, p2) iex> {:ok, 1} = view %{x: [y: [a: [a: 0, b: 1]]]}, composed_path """ @doc export: true defmacro concat(a, b), do: do_concat(a, b, __CALLER__) defp do_concat(a, b, caller) do {:~>, [], [a, b]} |> QuotedParser.parse_composition(:~>) |> Builder.build_composition(:~>, caller) |> Common.set_generated() end @doc """ Creates composition of two paths which has some inspiration from logical `and`. This means that `a &&& b` path-closure tries to apply `a` and only if it returns `{:ok, something}`, tries apply `b` and if `b` returns **exactly the same** as `a` does, the `a &&& b` returns `{:ok, something}` Example: iex> p1 = path :x / :y iex> p2 = path :a / :b iex> ap = p1 &&& p2 iex> {:ok, 1} = view %{x: %{y: 1}, a: [b: 1]}, ap iex> :error = view %{x: %{y: 1}, a: [b: 2]}, ap iex> {:ok, %{x: %{y: 2}, a: [b: 2]}} = set %{x: %{y: 1}, a: [b: 1]}, ap, 2 iex> {:ok, %{x: %{y: 2}, a: %{b: 2}}} = force_set %{}, ap, 2 """ @doc export: true defmacro a &&& b do {:&&&, [], [a, b]} |> QuotedParser.parse_composition(:&&&) |> Builder.build_composition(:&&&, __CALLER__) |> Common.set_generated() end @doc """ Creates composition of two paths which has some inspiration from logical `or`. This means that `a ||| b` path-closure tries to apply `a` and only if it returns `:error`, tries apply `b` Example: iex> p1 = path :x / :y iex> p2 = path :a / :b iex> op = p1 ||| p2 iex> {:ok, 1} = view %{x: %{y: 1}, a: [b: 2]}, op iex> {:ok, 2} = view %{x: 1, a: [b: 2]}, op iex> {:ok, %{x: %{y: 2}, a: [b: 1]}} = set %{x: %{y: 1}, a: [b: 1]}, op, 2 iex> {:ok, %{x: %{y: 2}}} = force_set %{}, op, 2 iex> {:ok, %{x: %{}, a: [b: 1]}} = force_set %{x: %{y: 1}, a: [b: 1]}, op, 2 """ @doc export: true defmacro a ||| b do {:|||, [], [a, b]} |> QuotedParser.parse_composition(:|||) |> Builder.build_composition(:|||, __CALLER__) |> Common.set_generated() end @doc """ This macro creates compositions of paths which work along with each other Think of `alongside([path1, path2, path3])` as `path1 &&& path2 &&& path3` The only difference is that for viewing alongside returns list of variables Example: iex> pa = alongside [path(:x), path(:y)] iex> {:ok, [1, 2]} = view(%{x: 1, y: 2}, pa) iex> {:ok, %{x: 3, y: 3}} = set(%{x: 1, y: 2}, pa, 3) iex> :error = set(%{x: 1}, pa, 3) iex> {:ok, %{x: 1, y: 1}} = force_set(%{}, pa, 1) """ @doc export: true defmacro alongside(list) do list |> Builder.build_composition(:alongside, __CALLER__) |> Common.set_generated() end @doc """ Inspect the given path-closure and returns string which corresponds to given path-closure Example: iex> index = 1 iex> p = path(:x) ~> path(:y / index) &&& path(-1) iex> Pathex.inspect(p) "path(:x) ~> path(:y / 1) &&& path(-1)" """ @spec inspect(Pathex.t()) :: iodata() @doc export: true def inspect(path_closure) when is_function(path_closure, 2) do Macro.to_string path_closure.(:inspect, []) end # Helpers # Helper for generating code for path operation # Special case for inline paths defp gen({:path, _, [path | tail]}, op, args, caller) do mod = tail |> List.first() |> get_mod(caller) path_func = build_only(path, op, caller, mod) quote generated: true do unquote(path_func).(unquote_splicing(args)) end |> Common.set_generated() end # Case for not inlined paths defp gen(path, op, args, _caller) do quote generated: true do unquote(path).(unquote(op), {unquote_splicing(args)}) end |> Common.set_generated() end defp wrap_ok(func) do quote do fn x -> {:ok, unquote(func).(x)} end end end # Helper for generating raising functions @spec bang(Macro.t(), Operations.t(), Macro.t(), binary()) :: Macro.t() defp bang(quoted, structure, path, err_str \\ "Couldn't find element") do quote generated: true do case unquote(quoted) do {:ok, value} -> value :error -> raise Pathex.Error, message: unquote(err_str), path: unquote(path), structure: unquote(structure) end end end defp get_mod(nil, %Macro.Env{module: nil}), do: :naive defp get_mod(nil, %Macro.Env{module: module}) do Module.get_attribute(module, :pathex_default_mod) || :naive end defp get_mod(mod, _), do: detect_mod(mod) # Helper for detecting mod @spec detect_mod(mod() | charlist()) :: mod() | no_return() defp detect_mod(mod) when mod in ~w[naive map json]a, do: mod defp detect_mod(str) when is_binary(str), do: detect_mod('#{str}') defp detect_mod('json'), do: :json defp detect_mod('map'), do: :map defp detect_mod('naive'), do: :naive defp detect_mod(_), do: raise("Can't have this modifier set") # Builds only one clause of a path defp build_only(path, opname, caller, mod) do {binds, combination} = path |> fetch_args(caller) |> QuotedParser.parse(caller, mod) %{^opname => builder} = Operations.builders_for_combination(combination) combination |> Builder.build_only(builder) |> prepend_binds(binds) end defp fetch_args(path, caller) do case Macro.prewalk(path, &Macro.expand(&1, caller)) do {{:., _, [__MODULE__, :path]}, _, args} -> args {:path, meta, args} = full -> case Keyword.fetch(meta, :import) do {:ok, __MODULE__} -> args _ -> full end args -> args end end # This function raises warning if combination will lead to very big closure @maximum_combination_size 256 defp assert_combination_length(combination, env) do size = Combination.size(combination) if size > @maximum_combination_size do {func, arity} = env.function || {:nofunc, 0} stacktrace = [{env.module, func, arity, [file: '#{env.file}', line: env.line]}] """ This path will generate too many clauses, and therefore will slow down the compilation and increase amount of generated code. Current combination has #{size} clauses while suggested amount is #{@maximum_combination_size} It would be better to split this closure in different paths with `Pathex.~>/2` Or change the modifier to one which generates less code: `:map` or `:json` """ |> IO.warn(stacktrace) end combination end defp prepend_binds(combination, binds) do quote do unquote_splicing(binds) unquote(combination) end end end