AshPagify.Misc (ash_pagify v1.4.2)

View Source

Miscellaneous functions for AshPagify.

Summary

Functions

Convert map string keys to :atom keys. This is useful when you have a map that was created from JSON or other external source and you want to convert the keys to atoms.

Coerce a maybe empty map to nil if it is empty.

Deeply merges two maps or lists, preferring values from the right map or list.

Returns the option with the given key.

Returns the global option derived from a map or a function referenced in the application environment.

Deep merges two lists, preferring values from the right list.

Deeply merges two maps, preferring values from the right map.

Puts a value under key only if the value is not nil, [], "", or %{}.

Put compiled ash_pagify scopes into the options if they are not already there.

Puts the scopes params of a AshPagify struct into a keyword list only if they don't match the defaults either passed as last argument or loaded on the fly.

Remove nil values from a map or struct. Does not work with nested maps.

Returns the resource option derived from a map or a function reference in the resource ash_pagfiy_options function.

Convert map :atom keys to string keys.

Returns a list of unique keywords from a list of keywords while preserving the order of the first occurrence of each keyword.

Walks a map or list and applies a serializer to the keys.

Functions

atomize_keys(map_or_struct, opts \\ [])

@spec atomize_keys(map() | struct(), Keyword.t()) :: map() | struct()

Convert map string keys to :atom keys. This is useful when you have a map that was created from JSON or other external source and you want to convert the keys to atoms.

You can specify a list of keys to convert or a depth to which to convert keys. If you specify a depth of 1, only the top level keys will be converted. If you specify a depth of 2, the top level keys and the keys of any maps in the top level will be converted. And so on.

If you set the existing? option to true, the function will use the String.to_existing_atom/1 function to convert the keys.

List of options:

  • keys: A list of keys to convert. If a key is not in the list, it will not be converted. Default is an empty list and all keys will be converted.
  • depth: The depth to which to convert keys. Default is nil and all keys will be converted.
  • existing?: If true, the function will use String.to_existing_atom/1 to convert the keys. Default is false.

Example

iex> AshPagify.Misc.atomize_keys(%{"a" => 1, "b" => 2})
%{a: 1, b: 2}

iex> AshPagify.Misc.atomize_keys(%{"a" => 1, "b" => %{"c" => 3}})
%{a: 1, b: %{c: 3}}

iex> AshPagify.Misc.atomize_keys(%{"a" => 1, "b" => %{"c" => 3}}, keys: ["b"])
%{"a" => 1, b: %{"c" => 3}}

iex> AshPagify.Misc.atomize_keys(%{"a" => 1, "b" => %{"c" => 3}}, keys: ["b", "c"])
%{"a" => 1, b: %{c: 3}}

iex> AshPagify.Misc.atomize_keys(%{"a" => 1, "b" => %{"c" => 3}}, keys: ["b", "d"], depth: 1)
%{"a" => 1, b: %{"c" => 3}}

iex> AshPagify.Misc.atomize_keys(%{"a" => 1, "b" => %{"c" => 3}}, keys: ["b", "c"], depth: 2)
%{"a" => 1, b: %{c: 3}}

coerce_maybe_empty_map(maybe_empty_map)

@spec coerce_maybe_empty_map(any()) :: map() | nil

Coerce a maybe empty map to nil if it is empty.

Example

iex> AshPagify.Misc.coerce_maybe_empty_map(%{})
nil

iex> AshPagify.Misc.coerce_maybe_empty_map(%{a: 1})
%{a: 1}

iex> AshPagify.Misc.coerce_maybe_empty_map(nil)
nil

deep_merge(left, right)

@spec deep_merge(map() | list(), map() | list()) :: map() | list()

Deeply merges two maps or lists, preferring values from the right map or list.

If a key exists in both maps or lists, and both values are maps or lists as well, these can be merged recursively. If a key exists in both maps or lists, but at least one of the values is NOT a map or list, we fall back to standard merge behavior, preferring the value on the right.

Example:

iex> AshPagify.Misc.deep_merge(%{a: 1, b: %{c: 2}}, %{b: %{d: 3}})
%{a: 1, b: %{c: 2, d: 3}}

one level of maps without conflict

iex> AshPagify.Misc.deep_merge(%{a: 1}, %{b: 2})
%{a: 1, b: 2}

two levels of maps without conflict

iex> AshPagify.Misc.deep_merge(%{a: [%{b: 1}]}, %{a: [%{c: 3}]})
%{a: [%{b: 1}, %{c: 3}]}

three levels of maps without conflict

iex> AshPagify.Misc.deep_merge(%{a: %{b: %{c: 1}}}, %{a: %{b: %{d: 2}}})
%{a: %{b: %{c: 1, d: 2}}}

non-map value in left

iex> AshPagify.Misc.deep_merge(%{a: 1}, %{a: %{b: 2}})
%{a: %{b:  2}}

non-map value in right

iex> AshPagify.Misc.deep_merge(%{a: %{b: 1}}, %{a: 2})
%{a: 2}

non-map value in both

iex> AshPagify.Misc.deep_merge(%{a: 1}, %{a: 2})
%{a: 2}

map of list

iex> AshPagify.Misc.deep_merge(%{a: [1, 2]}, %{a: [2, 3]})
%{a: [1, 2, 3]}

map of list of map

iex> AshPagify.Misc.deep_merge(%{a: [%{b: 1}, %{c: 2}]}, %{a: [%{c: 3}, %{d: 4}]})
%{a: [%{b: 1}, %{c: 3}, %{d: 4}]}

map of different types

iex> AshPagify.Misc.deep_merge(%{a: [1, 2]}, %{a: %{b: 2}})
%{a: %{b: 2}}

map of list of different types

iex> AshPagify.Misc.deep_merge(%{a: [1, 2]}, %{a: [%{b: 2}]})
%{a: [1, 2, %{b: 2}]}

get_option(key, opts \\ [], default \\ nil)

@spec get_option(atom(), Keyword.t(), any()) :: any()

Returns the option with the given key.

The look-up order is:

  1. the keyword list passed as the second argument
  2. the Ash.Resource resource, if the passed list includes the :for option
  3. the application environment
  4. the AshPagify default value if defined
  5. the default passed as the last argument

For the :scopes option, the function will deep merge the options in reverse order (keyword overrides resource, resource overrides global, etc.)

Examples for :scopes

iex> alias AshPagify.Factory.Post
iex> opts = [
...>   scopes: %{
...>     role: [
...>       %{name: :user, filter: %{name: "changed"}},
...>       %{name: :other, filter: %{name: "other"}}
...>     ],
...>     status: [
...>       %{name: :all, filter: nil, default?: true},
...>       %{name: :active, filter: %{age: %{lt: 10}}},
...>       %{name: :inactive, filter: %{age: %{gte: 10}}}
...>     ]
...>   },
...>   for: Post
...> ]
iex> get_option(:scopes, opts, %{
...>   role: [
...>     %{name: :default, filter: %{author: "Default"}}
...>   ]
...> })
%{
  role: [
    %{name: :admin, filter: %{author: "John"}},
    %{name: :user, filter: %{name: "changed"}},
    %{name: :other, filter: %{name: "other"}},
    %{name: :default, filter: %{author: "Default"}}
  ],
  status: [
    %{name: :inactive, filter: %{age: %{gte: 10}}},
    %{name: :all, filter: nil, default?: true},
    %{name: :active, filter: %{age: %{lt: 10}}}
  ]
}

global_option(key)

@spec global_option(atom()) :: any()

Returns the global option derived from a map or a function referenced in the application environment.

list_merge(left, right)

@spec list_merge(keyword(), keyword()) :: keyword()

Deep merges two lists, preferring values from the right list.

If a key exists in both lists, and both values are lists as well, these can be merged recursively. If a key exists in both lists, but at least one of the values is NOT a list, we fall back to standard merge behavior, preferring the value on the right.

Example:

iex> list_merge(
...>   [aria: [role: "navigation"]],
...>   [aria: [label: "pagination"]]
...> )
[aria: [role: "navigation", label: "pagination"]]

iex> list_merge(
...>   [class: "a"],
...>   [class: "b"]
...> )
[class: "b"]

map_merge(left, right)

@spec map_merge(map(), map()) :: map()

Deeply merges two maps, preferring values from the right map.

If a key exists in both maps, and both values are maps as well, these can be merged recursively. If a key exists in both maps, but at least one of the values is NOT a map, we fall back to standard merge behavior, preferring the value on the right.

Example:

iex> AshPagify.Misc.map_merge(%{a: 1, b: %{c: 2}}, %{b: %{d: 3}})
%{a: 1, b: %{c: 2, d: 3}}

one level of maps without conflict

iex> AshPagify.Misc.map_merge(%{a: 1}, %{b: 2})
%{a: 1, b: 2}

two levels of maps without conflict

iex> AshPagify.Misc.map_merge(%{a: %{b: 1}}, %{a: %{c: 3}})
%{a: %{b: 1, c: 3}}

three levels of maps without conflict

iex> AshPagify.Misc.map_merge(%{a: %{b: %{c: 1}}}, %{a: %{b: %{d: 2}}})
%{a: %{b: %{c: 1, d: 2}}}

non-map value in left

iex> AshPagify.Misc.map_merge(%{a: 1}, %{a: %{b: 2}})
%{a: %{b:  2}}

non-map value in right

iex> AshPagify.Misc.map_merge(%{a: %{b: 1}}, %{a: 2})
%{a: 2}

non-map value in both

iex> AshPagify.Misc.map_merge(%{a: 1}, %{a: 2})
%{a: 2}

maybe_put(params, key, value, default \\ nil)

@spec maybe_put(Keyword.t(), atom(), any(), any()) :: keyword()

Puts a value under key only if the value is not nil, [], "", or %{}.

If a :default value is passed, it only puts the value into the list if the value does not match the default value.

iex> maybe_put([], :a, "b")
[a: "b"]

iex> maybe_put([], :a, nil)
[]

iex> maybe_put([], :a, [])
[]

iex> maybe_put([], :a, %{})
[]

iex> maybe_put([], :a, "")
[]

iex> maybe_put([], :a, "a", "a")
[]

iex> maybe_put([], :a, "a", "b")
[a: "a"]

maybe_put_compiled_scopes(query_or_resource, opts \\ [])

@spec maybe_put_compiled_scopes(Ash.Query.t() | Ash.Resource.t(), Keyword.t()) ::
  Keyword.t()

Put compiled ash_pagify scopes into the options if they are not already there.

Example

iex> alias AshPagify.Factory.Post
iex> AshPagify.Misc.maybe_put_compiled_scopes(Post)
[
  __compiled_default_scopes: %{status: :all},
  __compiled_scopes: %{
    role: [
      %{name: :admin, filter: %{author: "John"}},
      %{name: :user, filter: %{author: "Doe"}}
    ],
    status: [
      %{name: :all, filter: nil, default?: true},
      %{name: :active, filter: %{age: %{lt: 10}}},
      %{name: :inactive, filter: %{age: %{gte: 10}}}
    ]
  }
]

Or with default scopes passed as opts

iex> alias AshPagify.Factory.Post
iex> scopes = %{role: [%{name: :user, filter: %{author: "Doe"}, default?: true}]}
iex> AshPagify.Misc.maybe_put_compiled_scopes(Post, [scopes: scopes])
[
  __compiled_default_scopes: %{role: :user, status: :all},
  __compiled_scopes: %{
    role: [
      %{name: :admin, filter: %{author: "John"}},
      %{name: :user, filter: %{author: "Doe"}, default?: true}
    ],
    status: [
      %{name: :all, filter: nil, default?: true},
      %{name: :active, filter: %{age: %{lt: 10}}},
      %{name: :inactive, filter: %{age: %{gte: 10}}}
    ]
  },
  scopes: scopes
]

maybe_put_scopes(keywords, ash_pagify, opts \\ [])

@spec maybe_put_scopes(Keyword.t(), AshPagify.t(), Keyword.t()) :: Keyword.t()

Puts the scopes params of a AshPagify struct into a keyword list only if they don't match the defaults either passed as last argument or loaded on the fly.

Example:

iex> maybe_put_scopes([], %AshPagify{scopes: %{status: :inactive}}, default_scopes: %{status: :active})
[scopes: %{status: :inactive}]

iex> maybe_put_scopes([], %AshPagify{scopes: %{status: :active}}, default_scopes: %{status: :active})
[]

iex> alias AshPagify.Factory.Post
iex> maybe_put_scopes([], %AshPagify{scopes: %{status: :active}}, for: Post)
[scopes: %{status: :active}]

remove_nil_values(map_or_struct)

Remove nil values from a map or struct. Does not work with nested maps.

Example

iex> AshPagify.Misc.remove_nil_values(%{a: 1, b: nil, c: 3})
%{a: 1, c: 3}

iex> AshPagify.Misc.remove_nil_values(%{a: 1, b: %{c: nil, d: 4}})
%{a: 1, b: %{c: nil, d: 4}}

resource_option(resource, key)

@spec resource_option(atom(), atom()) :: any()

Returns the resource option derived from a map or a function reference in the resource ash_pagfiy_options function.

stringify_keys(map_or_struct, opts \\ [])

@spec stringify_keys(map() | struct(), Keyword.t()) :: map() | struct()

Convert map :atom keys to string keys.

You can specify a list of keys to convert or a depth to which to convert keys. If you specify a depth of 1, only the top level keys will be converted. If you specify a depth of 2, the top level keys and the keys of any maps in the top level will be converted. And so on.

List of options:

  • keys: A list of keys to convert. If a key is not in the list, it will not be converted. Default is an empty list and all keys will be converted.
  • depth: The depth to which to convert keys. Default is nil and all keys will be converted.

Example

iex> AshPagify.Misc.stringify_keys(%{a: 1, b: 2})
%{"a" => 1, "b" => 2}

iex> AshPagify.Misc.stringify_keys(%{a: 1, b: %{c: 3}})
%{"a" => 1, "b" => %{"c" => 3}}

iex> AshPagify.Misc.stringify_keys(%{a: 1, b: %{c: 3}}, keys: [:b])
%{:a => 1, "b" => %{c: 3}}

iex> AshPagify.Misc.stringify_keys(%{a: 1, b: %{c: 3}}, keys: [:b, :c])
%{:a => 1, "b" => %{"c" => 3}}

iex> AshPagify.Misc.stringify_keys(%{a: 1, b: %{c: 3}}, keys: [:b, :d], depth: 1)
%{:a => 1, "b" => %{c: 3}}

iex> AshPagify.Misc.stringify_keys(%{a: 1, b: %{c: 3}}, keys: [:b, :c], depth: 2)
%{:a => 1, "b" => %{"c" => 3}}

unique_keywords(keyword_list)

Returns a list of unique keywords from a list of keywords while preserving the order of the first occurrence of each keyword.

Example

iex> AshPagify.Misc.unique_keywords([:a, :b, :a, :c, :b])
[:a, :b, :c]

iex> AshPagify.Misc.unique_keywords([a: 1, b: 2, a: 3, c: 4, b: 5])
[a: 1, b: 2, c: 4]

walk(map_or_list, serializer \\ &default_serializer/2, opts \\ [], current_depth \\ 1)

@spec walk(map() | list(), (term(), Keyword.t() -> term()), Keyword.t(), integer()) ::
  term()

Walks a map or list and applies a serializer to the keys.

The serializer function receives the key and the opts. The serializer function should return the new key.

The walk function will walk the map or list and apply the serializer to the keys. If the depth is specified and it is reached, the serializer will not be applied to the keys at that depth.

The serializer function can be used to convert the keys to atoms, strings, or any other format.

Example

iex> walk(%{"a" => 1, "b" => %{"c" => 3}}, fn key, _opts -> String.to_atom(key) end)
%{a: 1, b: %{c: 3}}

iex> walk(%{"a" => 1, "b" => %{"c" => 3}}, fn key, _opts -> String.to_atom(key) end, depth: 1)
%{b: %{"c" => 3}, a: 1}