From f4f799c9450764f2251ef71e43b6082b7ce15272 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Thu, 10 Oct 2024 22:18:01 -0400 Subject: [PATCH] feat!: allow default values for optional map keys (#32) BREAKING-CHANGE: Raises minimum Elixir version to 1.12 --- .github/workflows/ci.yml | 34 +++++++++++++++++----------------- lib/schematic.ex | 36 +++++++++++++++++++++++++++++------- mix.exs | 4 ++-- mix.lock | 2 +- test/schematic_test.exs | 14 ++++++++++++++ 5 files changed, 63 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b2c978..9893fdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,27 +11,27 @@ jobs: strategy: matrix: - otp: [23.x, 24.x, 25.x, 26.x] - elixir: [1.10.x, 1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x] + otp: [25.x, 26.x, 27.x] + elixir: [1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x] exclude: - - otp: 26.x - elixir: 1.10.x - - otp: 26.x - elixir: 1.11.x + - otp: 27.x + elixir: 1.12.x + - otp: 27.x + elixir: 1.13.x + - otp: 27.x + elixir: 1.14.x + - otp: 27.x + elixir: 1.15.x + - otp: 27.x + elixir: 1.16.x + - otp: 27.x + elixir: 1.16.x - otp: 26.x elixir: 1.12.x - otp: 26.x elixir: 1.13.x - - otp: 25.x - elixir: 1.11.x - otp: 25.x elixir: 1.12.x - - otp: 25.x - elixir: 1.10.x - - otp: 24.x - elixir: 1.10.x - - otp: 23.x - elixir: 1.15.x steps: - uses: actions/checkout@v2 @@ -56,14 +56,14 @@ jobs: formatter: runs-on: ubuntu-latest - name: Formatter (1.15.x/26.x) + name: Formatter (1.17.x/27.x) steps: - uses: actions/checkout@v2 - uses: erlef/setup-beam@v1 with: - otp-version: 26.x - elixir-version: 1.15.x + otp-version: 27.x + elixir-version: 1.17.x - uses: actions/cache@v3 with: path: | diff --git a/lib/schematic.ex b/lib/schematic.ex index c6f6223..4f91d42 100644 --- a/lib/schematic.ex +++ b/lib/schematic.ex @@ -38,10 +38,11 @@ defmodule Schematic do defmodule OptionalKey do @enforce_keys [:key] - defstruct [:key] + defstruct [:key, default: :__SCHEMATIC_EMPTY_DEFAULT__] @opaque t :: %__MODULE__{ - key: {any(), any()} | any() + key: {any(), any()} | any(), + default: any() } end @@ -460,16 +461,20 @@ defmodule Schematic do If the key _is_ provided, it must unify according to the given schematic. - Likewise, using `dump/2` will also omit that key. + You can also provide a default value for an optional key with `optional/2`. + + Likewise, using `dump/2` will also omit that key, unless it has a default value. ```elixir iex> schematic = map(%{ ...> "title" => str(), - ...> optional("description") => str() + ...> optional("description") => str(), + ...> optional("kind", "technology") => str() ...> }) - iex> {:ok, %{"title" => "Elixir 101", "description" => "An amazing programming course."}} = unify(schematic, %{"title" => "Elixir 101", "description" => "An amazing programming course."}) - iex> {:ok, %{"title" => "Elixir 101"}} = unify(schematic, %{"title" => "Elixir 101"}) - iex> {:ok, %{"title" => "Elixir 101"}} = dump(schematic, %{"title" => "Elixir 101"}) + iex> {:ok, %{"title" => "Elixir 101", "description" => "An amazing programming course.", "kind" => "technology"}} = unify(schematic, %{"title" => "Elixir 101", "description" => "An amazing programming course."}) + iex> {:ok, %{"title" => "Elixir 101", "kind" => "computer science"}} = unify(schematic, %{"title" => "Elixir 101", "kind" => "computer science"}) + iex> {:ok, %{"title" => "Elixir 101", "kind" => "computer science"}} = dump(schematic, %{"title" => "Elixir 101", "kind" => "computer science"}) + iex> {:ok, %{"title" => "Elixir 101", "kind" => "technology"}} = dump(schematic, %{"title" => "Elixir 101"}) ``` ## With `:keys` and `:values` @@ -603,6 +608,13 @@ defmodule Schematic do end if not Map.has_key?(input, from_key) and match?(%OptionalKey{}, bpk) do + acc = + if bpk.default != :__SCHEMATIC_EMPTY_DEFAULT__ do + Map.put(acc, to_key, bpk.default) + else + acc + end + [{:ok, acc}, {:errors, errors}] else case Schematic.Unification.unify(schematic, input[from_key], dir) do @@ -1017,4 +1029,14 @@ defmodule Schematic do def optional(key) do %OptionalKey{key: key} end + + @doc """ + Specifies an optional key and also a default value in the case the key is not present. + + See `map/1` for more examples and explanation. + """ + @spec optional(any(), any()) :: OptionalKey.t() + def optional(key, default) do + %OptionalKey{key: key, default: default} + end end diff --git a/mix.exs b/mix.exs index 296a951..d7f05dd 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule Schematic.MixProject do description: "Data validation and transformation", package: package(), version: "0.3.1", - elixir: "~> 1.10", + elixir: "~> 1.12", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, source_url: "https://github.com/mhanberg/schematic", @@ -41,7 +41,7 @@ defmodule Schematic.MixProject do [ {:telemetry, "~> 0.4 or ~> 1.0"}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, - {:stream_data, "~> 0.5.0", only: [:dev, :test], runtime: false} + {:stream_data, "~> 1.1", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 4d52f0b..2a8d548 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,6 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "stream_data": {:hex, :stream_data, "1.1.2", "05499eaec0443349ff877aaabc6e194e82bda6799b9ce6aaa1aadac15a9fdb4d", [:mix], [], "hexpm", "129558d2c77cbc1eb2f4747acbbea79e181a5da51108457000020a906813a1a9"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, } diff --git a/test/schematic_test.exs b/test/schematic_test.exs index b35c3c4..6b1b5cb 100644 --- a/test/schematic_test.exs +++ b/test/schematic_test.exs @@ -508,6 +508,20 @@ defmodule SchematicTest do unify(schematic, %{type: 10, name: 10}) end + test "optional keys with default value" do + schematic = + map(%{ + optional(:name, "mitch") => str(), + type: int() + }) + + assert {:ok, %{type: 10, name: "mitch"}} == unify(schematic, %{type: 10}) + assert {:ok, %{type: 10, name: "bob"}} == unify(schematic, %{type: 10, name: "bob"}) + + assert {:error, %{name: "expected a string"}} == + unify(schematic, %{type: 10, name: 10}) + end + test "empty map" do schematic = map()