Getting Started

Description

StructAccess provides a generic implementation of the Access behaviour for the module where this library is used.

Installation

The package can be installed by adding struct_access to your list of dependencies in mix.exs:

def deps do
  [
    {:struct_access, "~> 1.1.2"}
  ]
end

Why?

Why might you want to do this? So that you can take advantage of standard elixir access functions including the very useful Kernel.get_in/2 and Kernel.put_in/2 functions with nested structs.

How does it work?

When working with nested maps, the Kernel.get_in/2 function can be used to easily and safely query these maps like so:

iex> map = %{
             animals_sounds:
               %{
                 cat: "meow",
                 dog: "woof"
               }
           }

iex> get_in(map, [:animal_sounds, :dog]
"woof"

If you try the same thing with these maps defined as structs you will see an error.

iex> defmodule AnimalSounds do
...>   defstruct cat: "meow", dog: "woof"
...> end

iex> defmodule Things do
...>   defstruct animal_sounds: %AnimalSounds{}
...> end

iex> things = %Things{}
%Things{animal_sounds: %AnimalSounds{cat: "meow", dog: "woof"}}

iex> get_in(things, [:animal_sounds, :cat])
** (UndefinedFunctionError) function AnimalSounds.fetch/2 is undefined (AnimalSounds does not implement the Access behaviour)
    AnimalSounds.fetch(%AnimalSounds{cat: "meow", dog: "woof"}, :animal_sounds)
    (elixir) lib/access.ex:308: Access.get/3
    (elixir) lib/kernel.ex:2036: Kernel.get_in/2

The solution to this is fairly straightforward, you need to add @behaviour Access to your module and then implement all of the Access callbacks. It might take you a bit to figure out, but it's straightforward to implement these callbacks so that your structs behave just like maps (with a caveat or two).

But what if you have a bunch of structs in your projects that you'd like to behave in this way? You'll end up repeating this implementation in each of your structs. You might even decide that you should extract that to a macro so that you can conveniently just use that macro in each of your structs removing the repitition.

That's exactly what StructAccess does.

Here's how to use it to make the above example just work:

iex> defmodule AnimalSounds do
...>   use StructAccess
...>   defstruct cat: "meow", dog: "woof"
...> end

iex> defmodule Things do
...>   use StructAccess
...>   defstruct animal_sounds: %AnimalSounds{}
...> end

iex> things = %Things{}
%Things{animal_sounds: %AnimalSounds{cat: "meow", dog: "woof"}}

iex> get_in(things, [:animal_sounds, :cat])
"meow"

To define these callback and include the proper behavior all you have to do is add use StructAccess to the module defining your struct.

Adding

use StructAccess

to a module is equivalent to adding the following to that module:

@behaviour Access

defmacro __using__(_opts) do
  quote do
    @behaviour Access

    @impl Access
    def fetch(struct, key), do: StructAccess.fetch(struct, key)

    @impl Access
    def get_and_update(struct, key, fun) when is_function(fun, 1) do
      StructAccess.get_and_update(struct, key, fun)
    end

    @impl Access
    def pop(struct, key, default \\ nil) do
      StructAccess.pop(struct, key, default)
    end

    defoverridable Access
  end
end

This module is simply a shortcut to avoid that boilerplate.

If any of the implementations in StructAccess are not sufficient, they all can be overridden.

Caveats

Access.pop/2

One of the callbacks that needs to be implemented for the Access behavior is Access.pop/2.

The intention of this function is that it removes that specified key from the map/structure, returning both that value and the updated map/structure without that key.

This makes a lot of sense for a Map or Keyword, but not so much a struct, where it is impossible to remove a key. As a compromise, for cases where pop must be used, the generic implementation used in StructAccess simply sets the value of the key to be popped to nil.