diff --git a/CHANGELOG.md b/CHANGELOG.md index 721ca88..73b2531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 4fa1b5c..ad66b2d 100644 --- a/README.md +++ b/README.md @@ -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([ @@ -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> @@ -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 diff --git a/lib/sqids.ex b/lib/sqids.ex index 7e1c68a..570f62b 100644 --- a/lib/sqids.ex +++ b/lib/sqids.ex @@ -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 @@ -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 diff --git a/test/sqids_test.exs b/test/sqids_test.exs index f5b1810..7a26bf2 100644 --- a/test/sqids_test.exs +++ b/test/sqids_test.exs @@ -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))}") @@ -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