Starchoice

Build Status Coverage Status Module Version Hex Docs Total Download License Last Updated

Starchoice takes his name from the satellite TV company (now called Shaw Direct) because they are selling TV decoders. Since this lib is used to declare map decoders, I thought it felt appropriate to be named that way. Maybe not. Anyway.

The goal of the library is to provide a streamline process for converting String keyed maps to well defined structures. It is highly inspired by Elm's JSON decoders where you create different JSON decoders for the same data type.

For more information about creating decoder, visit the Starchoice.Decoder module documentation.

Installation

def deps do
  [
    {:starchoice, "~> 0.3.0"}
  ]
end

Basic usage

Examples:

  • Snowhite: Snowhite uses Starchoice to decode HTTP responses from APIs.

Define decoders

You can define decoders in your struct's module by doing the following (this is the macro approach).

defmodule User do
  defstruct email: nil, password: nil, profile: nil, permissions: []
  use Starchoice.Decoder

  defdecoder do
    field(:email, with: &String.downcase/1)
    field(:password)
    field(:profile, with: Profile)
    field(:permissions, with: {Permission, :simple})
  end

  defdecoder :simple do
    field(:email)
  end
end

defmodule Profile do
  defstruct address: nil

  defdecoder do
    field(:address)
  end
end

defmodule Permission do
  defstruct name: nil, access: nil

  defdecoder :simple do
    field(:name)
    field(:access, with: &Permission.decode_access/1)
  end

  def decode_access("r"), do: :read
  def decode_access("w"), do: :write
  def decode_access("rw"), do: :full
end

We can now easily decode map payloads:

input = %{
  "email" => "NICOLAS@nboisvert.com",
  "password" => "noneofyourbusiness",
  "profile" => %{
    "address" => "Somewhere str."
  },
  "permissions" => [
    %{"name" => "Articles", "access" => "rw"}
    %{"name" => "Settings", "access" => "r"}
  ]
}
{:ok, decoded} = Starchoice.decode(input, User)
%User{
  email: "nicolas@nboisvert.com",
  password: "noneofyourbusiness",
  profile: %Profile{
    address: "Somewhere str."
  },
  permissions: [
    %Permission{name: "Articles", access: :full},
    %Permission{name: "Settings", access: :read},
  ]
}

The basic of this can easily be achieved by using Ecto. However, for building a HTTP client or packaging lib, it might be a bit overkill to import a whole library like Ecto. This lightweight package can be pretty handy and is quite extensible.

Polymorphic decoding

This is something that might become helpful. Have for an instance, an API that returns every results under a results key like {"results": [{}, {}, ...]}. It would be pretty useful to have a polymorphic decoder. It is supported out of the box by doing the following:

defmodule Results do
  defstruct results: []

  def decoder(sub_type) do
    __MODULE__
    |> Starchoice.Decoder.new()
    |> Starchoice.Decoder.put_field(:results, sub_type)
  end
end

Then you can use it like that:

input = %{"results" => [%{"email" => "email@email.com"}, %{"email" => "another_email@email.com"}]}
Starchoice.decode(input, Results.decoder({User, :simple})) # this uses the :simple decoder defined for User before.
%Results{
  results: %{
    %User{email: "email@email.com"},
    %User{email: "another_email@email.com"},
  }
}

License

This source code is licensed under the MIT license. Copyright (c) 2020-present Nicolas Boisvert.