Skip to content

Commit

Permalink
Make it easier to store context in module attributes
Browse files Browse the repository at this point in the history
Add raising versions of `new/0` and `new/1`.

This is a good alternative to `use Sqids`, since it's much simpler and
in most cases options will already be known at compile time (unlike,
say, a crypto library which might need to retrieve a key from
application env config).

GitHub issue:
* #8
  • Loading branch information
g-andrade committed Oct 29, 2023
1 parent 6d2c7b9 commit 305d0b6
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 16 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `new!/0` and `new!/1` to ease storing context in module attributes

## [0.1.0] - 2023-10-28

### Added
Expand Down
50 changes: 38 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,47 @@ iex> ^numbers = Sqids.decode!(sqids, id)
> canonical, you have to re-encode decoded numbers and check that the
> generated ID matches.
### Convenience: placing Sqids under your app's supervision tree
### Convenience: create context at compile time

This allows you to encode and decode IDs without managing context.
Having to pass `sqids` context on every encode and decode call can be
cumbersome.

Functions are generated which retrieve the underlying Sqids context
transparently. The context is stored in a uniquely named
To work around this, you can create context with `new!/0` or `new/1` at compile
time if all options are either default or known at that moment:

```elixir
iex> defmodule MyApp.CompileTimeSqids do
iex> @context Sqids.new!()
iex> def encode!(numbers), do: Sqids.encode!(@context, numbers)
iex> def decode!(id), do: Sqids.decode!(@context, id)
iex> end
iex>
iex> numbers = [1, 2, 3]
iex> id = MyApp.CompileTimeSqids.encode!(numbers)
iex> ^id = "86Rf07"
iex> ^numbers = MyApp.CompileTimeSqids.decode!(id)
```

### Convenience: place context under your supervision tree

This also allows you to encode and decode IDs without managing context.

If not options are known at compile time but you'd still like to not pass
context on every encode and decode call, you can `use Sqids`, which will
generate functions that retrieve the underlying context transparently and call
`Sqids` for you.

The context is stored in a uniquely named
[`persistent_term`](https://www.erlang.org/doc/man/persistent_term), managed by
a uniquely named process. Both names are derived from your module's.
a uniquely named process, which is to be placed under your application's
supervision tree. Both names are derived from your module's.

```elixir
iex> defmodule MyApp.Sqids do
iex> defmodule MyApp.SupervisedSqids do
iex> use Sqids
iex> # Functions encrypt/1, encrypt!/1, decrypt/1, decrypt!/1, etc
iex> # will be generated.
iex>
iex>
iex> @impl true
iex> def child_spec() do
iex> child_spec([
Expand All @@ -109,7 +135,7 @@ iex> defmodule MyApp.Application do
iex> # ...
iex> def start(_type, _args) do
iex> children = [
iex> MyApp.Sqids,
iex> MyApp.SupervisedSqids,
iex> # ...
iex> ]
iex>
Expand All @@ -121,15 +147,15 @@ iex>
iex>
iex> {:ok, _} = MyApp.Application.start(:normal, [])
iex> numbers = [1, 2, 3]
iex> id = MyApp.Sqids.encode!(numbers)
iex> id = MyApp.SupervisedSqids.encode!(numbers)
iex> ^id = "86Rf07"
iex> ^numbers = MyApp.Sqids.decode!(id)
iex> ^numbers = MyApp.SupervisedSqids.decode!(id)
```

### Custom configuration

Examples of custom configuration follow. All options are applicable to the
generated module shown before.
Examples of custom configuration follow. All options are applicable to the two
convenient ways of creating context shown above.

Note that different options can be used together for further customization.
Check the [API reference](https://hexdocs.pm/sqids/api-reference.html) for
Expand Down
22 changes: 19 additions & 3 deletions lib/sqids.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ defmodule Sqids do
@moduledoc """
Sqids API
> ℹ️ Check out the [docs entry page](readme.html) for an example on how to `use
> Sqids` to generate functions that bypass the need to pass along `sqids` context
> on every encode or decode call.
> ℹ️ Check out the [docs entry page](readme.html) on how to make
> `Sqids` easier to use by not passing the context on every encode/decode
> call, through either:
> * creation of context at compile time under a module attribute,
> * or the `use Sqids` macro to generate functions that retrieve context transparently.
"""

alias Sqids.Alphabet
Expand Down Expand Up @@ -99,6 +101,20 @@ defmodule Sqids do
raise %ArgumentError{message: "Opts not a proper list: #{inspect(opts)}"}
end

@doc """
Like `new/0` and `new/1` but raises in case of error.
"""
@spec new!(opts()) :: t()
def new!(opts \\ []) do
case new(opts) do
{:ok, sqids} ->
sqids

{:error, reason} ->
raise %ArgumentError{message: error_reason_to_string(reason)}
end
end

@doc """
Tries to encode zero or more `numbers` into as an `id`, according to
`sqids`'s alphabet, blocklist, and minimum length. Returns an error
Expand Down
41 changes: 40 additions & 1 deletion test/sqids_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ defmodule SqidsTest do
end
end

def new_sqids(:"Direct API!", opts) do
sqids = Sqids.new!(opts)
{:ok, {:direct_api, sqids}}
end

def new_sqids(:"Using module", opts) do
module_name = String.to_atom("#{__MODULE__}.UsingModule.#{:rand.uniform(Bitwise.<<<(1, 64))}")

Expand Down Expand Up @@ -488,7 +493,41 @@ defmodule SqidsTest do

import SqidsTest.Shared

for access_type <- [:"Direct API", :"Using module"] do
test "new!/0: it works" do
sqids = Sqids.new!()

numbers = [1, 2, 3]
id = "86Rf07"

assert Sqids.encode(sqids, numbers) === {:ok, id}
assert Sqids.decode!(sqids, id) === numbers
end

test "new!/1: it works with valid opts" do
{:ok, instance} = new_sqids(:"Direct API!")

numbers = [1, 2, 3]
id = "86Rf07"

assert encode!(instance, numbers) === id
assert decode!(instance, id) === numbers
end

test "new!/1: errors raised" do
assert_raise ArgumentError, "Alphabet contains multibyte graphemes: [\"ë\"]", fn ->
new_sqids(:"Direct API!", alphabet: "ë1092")
end

assert_raise ArgumentError, "Alphabet contains repeated graphemes: [\"a\"]", fn ->
new_sqids(:"Direct API!", alphabet: "aabcdefg")
end

assert_raise ArgumentError, "Alphabet is too small: [min_length: 3, alphabet: \"ab\"]", fn ->
new_sqids(:"Direct API!", alphabet: "ab")
end
end

for access_type <- [:"Direct API", :"Direct API!", :"Using module"] do
test "#{access_type}: new/1: options is not a proper list" do
at = unquote(access_type)
assert_raise ArgumentError, "Opts not a proper list: :not_a_list", fn -> new_sqids(at, :not_a_list) end
Expand Down

0 comments on commit 305d0b6

Please sign in to comment.