├── .formatter.exs ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── config └── config.exs ├── docs └── Getting Started.md ├── lib └── struct_access.ex ├── mix.exs ├── mix.lock └── test ├── struct_access_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | struct_access-*.tar 24 | 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.1.2 4 | * Further improves documentation 5 | 6 | ## v1.1.1 7 | * Improves documentation 8 | 9 | ## v1.1.0 10 | * Implementations moved to `StructAccess` from the `__using__` macro itself. 11 | This should improve compilation times slightly by requiring less code to be 12 | added to modules that use `StructAccess` 13 | ([#1](https://github.com/mbramson/struct_access/pull/1)) 14 | * No longer implement `c:Access.get/2` as this was removed in Elixir `1.7.0`. 15 | As such `StructAccess` no longer supports Elixir versions `< 1.7.0`. 16 | 17 | ## v1.0.0 18 | * Initial Release 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mathew Bramson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StructAccess 2 | 3 | StructAccess provides a generic implementation of the `Access` behaviour for 4 | the module where this library is used. 5 | 6 | Official documentation is at [https://hexdocs.pm/struct_access](https://hexdocs.pm/struct_access). 7 | 8 | ## Installation 9 | 10 | The package can be installed by adding `struct_access` to your list of 11 | dependencies in `mix.exs`: 12 | 13 | ```elixir 14 | def deps do 15 | [ 16 | {:struct_access, "~> 1.1.2"} 17 | ] 18 | end 19 | ``` 20 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :struct_access, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:struct_access, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /docs/Getting Started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Description 4 | 5 | StructAccess provides a generic implementation of the `Access` behaviour for 6 | the module where this library is used. 7 | 8 | ## Installation 9 | 10 | The package can be installed by adding `struct_access` to your list of 11 | dependencies in `mix.exs`: 12 | 13 | ```elixir 14 | def deps do 15 | [ 16 | {:struct_access, "~> 1.1.2"} 17 | ] 18 | end 19 | ``` 20 | 21 | ## Why? 22 | 23 | Why might you want to do this? So that you can take advantage of standard 24 | elixir access functions including the very useful `Kernel.get_in/2` and 25 | `Kernel.put_in/2` functions with nested structs. 26 | 27 | ## How does it work? 28 | 29 | When working with nested maps, the `Kernel.get_in/2` function can be used to 30 | easily and safely query these maps like so: 31 | 32 | ``` 33 | iex> map = %{ 34 | animals_sounds: 35 | %{ 36 | cat: "meow", 37 | dog: "woof" 38 | } 39 | } 40 | 41 | iex> get_in(map, [:animal_sounds, :dog] 42 | "woof" 43 | ``` 44 | 45 | If you try the same thing with these maps defined as structs you will see an 46 | error. 47 | 48 | ``` 49 | iex> defmodule AnimalSounds do 50 | ...> defstruct cat: "meow", dog: "woof" 51 | ...> end 52 | 53 | iex> defmodule Things do 54 | ...> defstruct animal_sounds: %AnimalSounds{} 55 | ...> end 56 | 57 | iex> things = %Things{} 58 | %Things{animal_sounds: %AnimalSounds{cat: "meow", dog: "woof"}} 59 | 60 | iex> get_in(things, [:animal_sounds, :cat]) 61 | ** (UndefinedFunctionError) function AnimalSounds.fetch/2 is undefined (AnimalSounds does not implement the Access behaviour) 62 | AnimalSounds.fetch(%AnimalSounds{cat: "meow", dog: "woof"}, :animal_sounds) 63 | (elixir) lib/access.ex:308: Access.get/3 64 | (elixir) lib/kernel.ex:2036: Kernel.get_in/2 65 | ``` 66 | 67 | The solution to this is fairly straightforward, you need to add `@behaviour 68 | Access` to your module and then implement all of the `Access` callbacks. It 69 | might take you a bit to figure out, but it's straightforward to implement these 70 | callbacks so that your structs behave just like maps (with a caveat or two). 71 | 72 | But what if you have a bunch of structs in your projects that you'd like to 73 | behave in this way? You'll end up repeating this implementation in each of your 74 | structs. You might even decide that you should extract that to a macro so that 75 | you can conveniently just `use` that macro in each of your structs removing the 76 | repitition. 77 | 78 | That's exactly what `StructAccess` does. 79 | 80 | Here's how to use it to make the above example just work: 81 | 82 | ``` 83 | iex> defmodule AnimalSounds do 84 | ...> use StructAccess 85 | ...> defstruct cat: "meow", dog: "woof" 86 | ...> end 87 | 88 | iex> defmodule Things do 89 | ...> use StructAccess 90 | ...> defstruct animal_sounds: %AnimalSounds{} 91 | ...> end 92 | 93 | iex> things = %Things{} 94 | %Things{animal_sounds: %AnimalSounds{cat: "meow", dog: "woof"}} 95 | 96 | iex> get_in(things, [:animal_sounds, :cat]) 97 | "meow" 98 | ``` 99 | 100 | To define these callback and include the proper behavior all you have to do 101 | is add `use StructAccess` to the module defining your struct. 102 | 103 | Adding 104 | 105 | ``` 106 | use StructAccess 107 | ``` 108 | 109 | to a module is equivalent to adding the following to that module: 110 | 111 | ``` 112 | @behaviour Access 113 | 114 | defmacro __using__(_opts) do 115 | quote do 116 | @behaviour Access 117 | 118 | @impl Access 119 | def fetch(struct, key), do: StructAccess.fetch(struct, key) 120 | 121 | @impl Access 122 | def get_and_update(struct, key, fun) when is_function(fun, 1) do 123 | StructAccess.get_and_update(struct, key, fun) 124 | end 125 | 126 | @impl Access 127 | def pop(struct, key, default \\ nil) do 128 | StructAccess.pop(struct, key, default) 129 | end 130 | 131 | defoverridable Access 132 | end 133 | end 134 | ``` 135 | 136 | This module is simply a shortcut to avoid that boilerplate. 137 | 138 | If any of the implementations in `StructAccess` are not sufficient, they all 139 | can be overridden. 140 | 141 | ## Caveats 142 | 143 | ### Access.pop/2 144 | 145 | One of the callbacks that needs to be implemented for the `Access` behavior is `c:Access.pop/2`. 146 | 147 | The intention of this function is that it removes that specified key from the 148 | map/structure, returning both that value and the updated map/structure without 149 | that key. 150 | 151 | This makes a lot of sense for a `Map` or `Keyword`, but not so much a struct, 152 | where it is impossible to remove a key. As a compromise, for cases where pop 153 | must be used, the generic implementation used in `StructAccess` simply sets the 154 | value of the key to be popped to `nil`. 155 | -------------------------------------------------------------------------------- /lib/struct_access.ex: -------------------------------------------------------------------------------- 1 | defmodule StructAccess do 2 | @moduledoc """ 3 | Provides a standard callback implementation for the `Access` behaviour. 4 | 5 | Implements the following callbacks for the struct where this module is used: 6 | - `c:Access.fetch/2` 7 | - `c:Access.get_and_update/3` 8 | - `c:Access.pop/2` 9 | 10 | To define these callback and include the proper behavior all you have to do 11 | is add `use StructAccess` to the module defining your struct. 12 | 13 | Adding 14 | 15 | ``` 16 | use StructAccess 17 | ``` 18 | 19 | to a module is equivalent to adding the following to that module: 20 | 21 | ``` 22 | @behaviour Access 23 | 24 | @impl Access 25 | def fetch(struct, key), do: StructAccess.fetch(struct, key) 26 | 27 | @impl Access 28 | def get_and_update(struct, key, fun) when is_function(fun, 1) do 29 | StructAccess.get_and_update(struct, key, fun) 30 | end 31 | 32 | @impl Access 33 | def pop(struct, key, default \\\\ nil) do 34 | StructAccess.pop(struct, key, default) 35 | end 36 | 37 | defoverridable Access 38 | ``` 39 | 40 | This module is simply a shortcut to avoid that boilerplate. 41 | 42 | If any of the implementations in `StructAccess` are not sufficient, they all 43 | can be overridden. 44 | """ 45 | 46 | @behaviour Access 47 | 48 | defmacro __using__(_opts) do 49 | quote do 50 | @behaviour Access 51 | 52 | @impl Access 53 | def fetch(struct, key), do: StructAccess.fetch(struct, key) 54 | 55 | @impl Access 56 | def get_and_update(struct, key, fun) when is_function(fun, 1) do 57 | StructAccess.get_and_update(struct, key, fun) 58 | end 59 | 60 | @impl Access 61 | def pop(struct, key, default \\ nil) do 62 | StructAccess.pop(struct, key, default) 63 | end 64 | 65 | defoverridable Access 66 | end 67 | end 68 | 69 | @doc """ 70 | Retrieves the given key from the given struct. 71 | 72 | Implements the `c:Access.fetch/2` callback. 73 | """ 74 | def fetch(struct, key), do: Map.fetch(struct, key) 75 | 76 | @doc """ 77 | Retrieves the given key from the given struct with a default. 78 | """ 79 | def get(struct, key, default \\ nil), do: Map.get(struct, key, default) 80 | 81 | @doc """ 82 | Retrives the given key from the given struct and updates it at the same time. 83 | 84 | Implements the `c:Access.get_and_update/3` callback. 85 | """ 86 | def get_and_update(struct, key, fun) when is_function(fun, 1) do 87 | current = get(struct, key) 88 | 89 | case fun.(current) do 90 | {get, update} -> 91 | {get, Map.put(struct, key, update)} 92 | 93 | :pop -> 94 | pop(struct, key) 95 | 96 | other -> 97 | raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}" 98 | end 99 | end 100 | 101 | @doc """ 102 | Pops the given key from the given struct. As struct keys can't be deleted 103 | this simply sets the value of the popped key to `nil`. 104 | 105 | Implements the `c:Access.pop/2` callback. 106 | """ 107 | def pop(struct, key, default \\ nil) do 108 | case fetch(struct, key) do 109 | {:ok, old_value} -> 110 | {old_value, Map.put(struct, key, nil)} 111 | 112 | :error -> 113 | {default, struct} 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule StructAccess.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :struct_access, 7 | version: "1.1.2", 8 | elixir: "~> 1.7", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | description: description(), 12 | package: package(), 13 | deps: deps(), 14 | docs: docs(), 15 | name: "StructAccess", 16 | source_url: "https://github.com/mbramson/struct_access" 17 | ] 18 | end 19 | 20 | # Run "mix help compile.app" to learn about applications. 21 | def application do 22 | [ 23 | extra_applications: [:logger] 24 | ] 25 | end 26 | 27 | # Run "mix help deps" to learn about dependencies. 28 | defp deps do 29 | [ 30 | {:ex_doc, "~> 0.21", only: :dev} 31 | ] 32 | end 33 | 34 | defp description() do 35 | """ 36 | StructAccess provides a generic implementation of the `Access` behaviour 37 | for the module where this library is used. 38 | """ 39 | end 40 | 41 | defp package() do 42 | [ 43 | files: ["lib", "mix.exs", "README.md", "LICENSE.md"], 44 | maintainers: ["Mathew Bramson"], 45 | licenses: ["MIT"], 46 | links: %{"GitHub" => "https://github.com/mbramson/struct_access"} 47 | ] 48 | end 49 | 50 | defp docs do 51 | [main: "getting-started", 52 | formatter_opts: [gfm: true], 53 | source_url: "https://github.com/mbramson/struct_access", 54 | extras: [ 55 | "docs/Getting Started.md" 56 | ]] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.3.5", "0db71c8290b5bc81cb0101a2a507a76dca659513984d683119ee722828b424f6", [:mix], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.21.1", "5ac36660846967cd869255f4426467a11672fec3d8db602c429425ce5b613b90", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 7 | } 8 | -------------------------------------------------------------------------------- /test/struct_access_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AnimalSounds do 2 | use StructAccess 3 | defstruct cat: "meow", dog: "woof" 4 | end 5 | 6 | defmodule Things do 7 | use StructAccess 8 | defstruct animal_sounds: %AnimalSounds{} 9 | end 10 | 11 | defmodule StructAccessTest do 12 | use ExUnit.Case 13 | doctest StructAccess 14 | 15 | setup do 16 | {:ok, [sounds: %AnimalSounds{}, things: %Things{}]} 17 | end 18 | 19 | test "can use the square bracket notation on struct", %{sounds: sounds} do 20 | assert "meow" = sounds[:cat] 21 | end 22 | 23 | test "can use get_in on the nested structs", %{things: things} do 24 | assert "meow" = get_in(things, [:animal_sounds, :cat]) 25 | end 26 | 27 | test "can use put_in on the nested structs", %{things: things} do 28 | result = put_in(things, [:animal_sounds, :cat], "moo") 29 | assert %Things{animal_sounds: %AnimalSounds{cat: "moo"}} = result 30 | end 31 | 32 | test "pop nils out a struct key", %{sounds: sounds} do 33 | assert {value, new_struct} = AnimalSounds.pop(sounds, :cat) 34 | assert "meow" == value 35 | assert %AnimalSounds{cat: nil} = new_struct 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------