From 7f2e7d3ec1fddd0b9654c67dcf17d4c9d6da3e3b Mon Sep 17 00:00:00 2001 From: Alessio Montagnani Date: Mon, 24 Jun 2024 00:16:20 +0200 Subject: [PATCH] improvement: bring uuid version 7 into the core (#1253) --- .formatter.exs | 2 + benchmarks/uuids.exs | 12 ++ documentation/dsls/DSL:-Ash.Resource.md | 61 ++++++++ lib/ash/resource/attribute.ex | 13 ++ lib/ash/resource/dsl.ex | 27 +++- lib/ash/type/type.ex | 1 + lib/ash/type/uuid_v7.ex | 96 ++++++++++++ lib/ash/uuid_v7.ex | 194 ++++++++++++++++++++++++ mix.exs | 1 + test/resource/attributes_test.exs | 33 ++++ test/type/uuid_v7_test.exs | 38 +++++ test/uuid_v7_test.exs | 60 ++++++++ 12 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 benchmarks/uuids.exs create mode 100644 lib/ash/type/uuid_v7.ex create mode 100644 lib/ash/uuid_v7.ex create mode 100644 test/type/uuid_v7_test.exs create mode 100644 test/uuid_v7_test.exs diff --git a/.formatter.exs b/.formatter.exs index f3024bd5..6df3cc80 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -211,6 +211,8 @@ spark_locals_without_parens = [ upsert_identity: 1, uuid_primary_key: 1, uuid_primary_key: 2, + uuid_v7_primary_key: 1, + uuid_v7_primary_key: 2, validate: 1, validate: 2, validate_destination_attribute?: 1, diff --git a/benchmarks/uuids.exs b/benchmarks/uuids.exs new file mode 100644 index 00000000..0f6ecb0d --- /dev/null +++ b/benchmarks/uuids.exs @@ -0,0 +1,12 @@ +Benchee.run( + %{ + "uuid_v7 raw" => fn -> + Ash.UUIDv7.bingenerate() + end, + "uuid_v7 string" => fn -> + Ash.UUIDv7.generate() + end, + "uuid_v4 string" => fn -> + Ash.UUID.generate() + end +}) diff --git a/documentation/dsls/DSL:-Ash.Resource.md b/documentation/dsls/DSL:-Ash.Resource.md index af0a21d6..eee1343c 100644 --- a/documentation/dsls/DSL:-Ash.Resource.md +++ b/documentation/dsls/DSL:-Ash.Resource.md @@ -15,6 +15,7 @@ A section for declaring attributes on the resource. * [update_timestamp](#attributes-update_timestamp) * [integer_primary_key](#attributes-integer_primary_key) * [uuid_primary_key](#attributes-uuid_primary_key) + * [uuid_v7_primary_key](#attributes-uuid_v7_primary_key) ### Examples @@ -349,6 +350,66 @@ uuid_primary_key :id +### Introspection + +Target: `Ash.Resource.Attribute` + +## attributes.uuid_v7_primary_key +```elixir +uuid_v7_primary_key name +``` + + +Declares a non writable, non-nil, primary key column of type `uuid_v7`, which defaults to `Ash.UUIDv7.generate/0`. + +Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets +the following different defaults: + +writable? false +public? true +default &Ash.UUIDv7.generate/0 +primary_key? true +type :uuid_v7 + + + + +### Examples +``` +uuid_v7_primary_key :id +``` + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`name`](#attributes-uuid_v7_primary_key-name){: #attributes-uuid_v7_primary_key-name .spark-required} | `atom` | | The name of the attribute. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`type`](#attributes-uuid_v7_primary_key-type){: #attributes-uuid_v7_primary_key-type } | `module` | `:uuid_v7` | The type of the attribute. See `Ash.Type` for more. | +| [`constraints`](#attributes-uuid_v7_primary_key-constraints){: #attributes-uuid_v7_primary_key-constraints } | `keyword` | | Constraints to provide to the type when casting the value. For more, see `Ash.Type`. | +| [`description`](#attributes-uuid_v7_primary_key-description){: #attributes-uuid_v7_primary_key-description } | `String.t` | | An optional description for the attribute. | +| [`sensitive?`](#attributes-uuid_v7_primary_key-sensitive?){: #attributes-uuid_v7_primary_key-sensitive? } | `boolean` | `false` | Whether or not the attribute value contains sensitive information, like PII. See the [Sensitive Data guide](/documentation/topics/security/sensitive-data.md) for more. | +| [`source`](#attributes-uuid_v7_primary_key-source){: #attributes-uuid_v7_primary_key-source } | `atom` | | If the field should be mapped to a different name in the data layer. Support varies by data layer. | +| [`always_select?`](#attributes-uuid_v7_primary_key-always_select?){: #attributes-uuid_v7_primary_key-always_select? } | `boolean` | `false` | Whether or not to ensure this attribute is always selected when reading from the database, regardless of applied select statements. | +| [`primary_key?`](#attributes-uuid_v7_primary_key-primary_key?){: #attributes-uuid_v7_primary_key-primary_key? } | `boolean` | `true` | Whether the attribute is the primary key. Composite primary key is also possible by using `primary_key? true` in more than one attribute. If primary_key? is true, allow_nil? must be false. | +| [`generated?`](#attributes-uuid_v7_primary_key-generated?){: #attributes-uuid_v7_primary_key-generated? } | `boolean` | `false` | Whether or not the value may be generated by the data layer. | +| [`writable?`](#attributes-uuid_v7_primary_key-writable?){: #attributes-uuid_v7_primary_key-writable? } | `boolean` | `false` | Whether or not the value can be written to. Non-writable attributes can still be written with `Ash.Changeset.force_change_attribute/3`. | +| [`public?`](#attributes-uuid_v7_primary_key-public?){: #attributes-uuid_v7_primary_key-public? } | `boolean` | `true` | Whether or not the attribute should be shown over public interfaces. See the [sensitive data guide](/documentation/topics/security/sensitive-data.md) for more. | +| [`default`](#attributes-uuid_v7_primary_key-default){: #attributes-uuid_v7_primary_key-default } | `(-> any) \| mfa \| any` | `&Ash.UUIDv7.generate/0` | A value to be set on all creates, unless a value is being provided already. Note: The default value is casted according to the type's Ash.Type.* module, before it is saved. For `:string`, for example, if `constraints: [allow_empty?: _]` is false, the value `""` will be cast to `nil`. See the `:constraints` option, the `:allow_nil?` option, and the relevant `Ash.Type.*` documentation. | +| [`update_default`](#attributes-uuid_v7_primary_key-update_default){: #attributes-uuid_v7_primary_key-update_default } | `(-> any) \| mfa \| any` | | A value to be set on all updates, unless a value is being provided already. | +| [`filterable?`](#attributes-uuid_v7_primary_key-filterable?){: #attributes-uuid_v7_primary_key-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the attribute can be referenced in filters. | +| [`sortable?`](#attributes-uuid_v7_primary_key-sortable?){: #attributes-uuid_v7_primary_key-sortable? } | `boolean` | `true` | Whether or not the attribute can be referenced in sorts. | +| [`match_other_defaults?`](#attributes-uuid_v7_primary_key-match_other_defaults?){: #attributes-uuid_v7_primary_key-match_other_defaults? } | `boolean` | `false` | Ensures that other attributes that use the same "lazy" default (a function or an mfa), use the same default value. Has no effect unless `default` is a zero argument function. | + + + + + ### Introspection Target: `Ash.Resource.Attribute` diff --git a/lib/ash/resource/attribute.ex b/lib/ash/resource/attribute.ex index cc679505..1e3ad92d 100644 --- a/lib/ash/resource/attribute.ex +++ b/lib/ash/resource/attribute.ex @@ -183,6 +183,18 @@ defmodule Ash.Resource.Attribute do |> Keyword.delete(:allow_nil?) |> Ash.OptionsHelpers.hide_all_except([:name]) + @uuid_v7_primary_key_schema @schema + |> Spark.Options.Helpers.set_default!(:public?, true) + |> Spark.Options.Helpers.set_default!(:writable?, false) + |> Spark.Options.Helpers.set_default!( + :default, + &Ash.UUIDv7.generate/0 + ) + |> Spark.Options.Helpers.set_default!(:primary_key?, true) + |> Spark.Options.Helpers.set_default!(:type, :uuid_v7) + |> Keyword.delete(:allow_nil?) + |> Ash.OptionsHelpers.hide_all_except([:name]) + @integer_primary_key_schema @schema |> Spark.Options.Helpers.set_default!(:public?, true) |> Spark.Options.Helpers.set_default!(:writable?, false) @@ -201,5 +213,6 @@ defmodule Ash.Resource.Attribute do def create_timestamp_schema, do: @create_timestamp_schema def update_timestamp_schema, do: @update_timestamp_schema def uuid_primary_key_schema, do: @uuid_primary_key_schema + def uuid_v7_primary_key_schema, do: @uuid_v7_primary_key_schema def integer_primary_key_schema, do: @integer_primary_key_schema end diff --git a/lib/ash/resource/dsl.ex b/lib/ash/resource/dsl.ex index 5793b947..c3859ac3 100644 --- a/lib/ash/resource/dsl.ex +++ b/lib/ash/resource/dsl.ex @@ -144,6 +144,30 @@ defmodule Ash.Resource.Dsl do transform: {Ash.Resource.Attribute, :transform, []} } + @uuid_v7_primary_key %Spark.Dsl.Entity{ + name: :uuid_v7_primary_key, + describe: """ + Declares a non writable, non-nil, primary key column of type `uuid_v7`, which defaults to `Ash.UUIDv7.generate/0`. + + Accepts all the same options as `d:Ash.Resource.Dsl.attributes.attribute`, except for `allow_nil?`, but it sets + the following different defaults: + + writable? false + public? true + default &Ash.UUIDv7.generate/0 + primary_key? true + type :uuid_v7 + """, + examples: [ + "uuid_v7_primary_key :id" + ], + args: [:name], + target: Ash.Resource.Attribute, + schema: Ash.Resource.Attribute.uuid_v7_primary_key_schema(), + auto_set_fields: [allow_nil?: false], + transform: {Ash.Resource.Attribute, :transform, []} + } + @attributes %Spark.Dsl.Section{ name: :attributes, describe: """ @@ -187,7 +211,8 @@ defmodule Ash.Resource.Dsl do @create_timestamp, @update_timestamp, @integer_primary_key, - @uuid_primary_key + @uuid_primary_key, + @uuid_v7_primary_key ], patchable?: true } diff --git a/lib/ash/type/type.ex b/lib/ash/type/type.ex index 17688b6e..44421da1 100644 --- a/lib/ash/type/type.ex +++ b/lib/ash/type/type.ex @@ -42,6 +42,7 @@ defmodule Ash.Type do boolean: Ash.Type.Boolean, struct: Ash.Type.Struct, uuid: Ash.Type.UUID, + uuid_v7: Ash.Type.UUIDv7, binary: Ash.Type.Binary, date: Ash.Type.Date, time: Ash.Type.Time, diff --git a/lib/ash/type/uuid_v7.ex b/lib/ash/type/uuid_v7.ex new file mode 100644 index 00000000..865e43df --- /dev/null +++ b/lib/ash/type/uuid_v7.ex @@ -0,0 +1,96 @@ +defmodule Ash.Type.UUIDv7 do + @moduledoc """ + Represents a UUID. + + A builtin type that can be referenced via `:uuid_v7` + """ + + use Ash.Type + + @impl true + def storage_type(_), do: :uuid + + @impl true + def generator(_constraints) do + StreamData.repeatedly(&Ash.UUIDv7.generate/0) + end + + @impl true + def matches_type?(value, constraints) do + case cast_input(value, constraints) do + {:ok, _} -> true + _ -> false + end + end + + @impl true + def cast_input(nil, _), do: {:ok, nil} + + def cast_input( + <>, + _ + ) do + <> + catch + :error -> :error + else + hex_uuid -> {:ok, hex_uuid} + end + + def cast_input(<<_::128>> = value, _), do: {:ok, Ash.UUIDv7.encode(value)} + def cast_input(_, _), do: :error + + @impl true + def cast_atomic(new_value, _constraints), do: {:atomic, new_value} + + @impl true + def cast_stored(nil, _), do: {:ok, nil} + def cast_stored(value, constraints), do: cast_input(value, constraints) + + @impl true + def dump_to_native(nil, _), do: {:ok, nil} + + def dump_to_native(value, _) do + case Ash.UUIDv7.decode(value) do + :error -> :error + value -> {:ok, value} + end + end + + @impl true + def dump_to_embedded(value, constraints) do + cast_input(value, constraints) + end + + @impl true + def equal?(term1, term2), do: Ash.UUIDv7.decode(term1) == Ash.UUIDv7.decode(term2) + + @compile {:inline, c: 1} + + defp c(?0), do: ?0 + defp c(?1), do: ?1 + defp c(?2), do: ?2 + defp c(?3), do: ?3 + defp c(?4), do: ?4 + defp c(?5), do: ?5 + defp c(?6), do: ?6 + defp c(?7), do: ?7 + defp c(?8), do: ?8 + defp c(?9), do: ?9 + defp c(?A), do: ?a + defp c(?B), do: ?b + defp c(?C), do: ?c + defp c(?D), do: ?d + defp c(?E), do: ?e + defp c(?F), do: ?f + defp c(?a), do: ?a + defp c(?b), do: ?b + defp c(?c), do: ?c + defp c(?d), do: ?d + defp c(?e), do: ?e + defp c(?f), do: ?f + defp c(_), do: throw(:error) +end diff --git a/lib/ash/uuid_v7.ex b/lib/ash/uuid_v7.ex new file mode 100644 index 00000000..b75b474e --- /dev/null +++ b/lib/ash/uuid_v7.ex @@ -0,0 +1,194 @@ +defmodule Ash.UUIDv7 do + @moduledoc """ + Helpers for working with UUIDs version 7. + + [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-7) + + Used for generating UUIDs version 7 with increased clock precision for better monotonicity, + as described by method 3 of the [Section 6.2](https://www.rfc-editor.org/rfc/rfc9562#name-monotonicity-and-counters + + Inspired by the work of [Ryan Winchester](https://github.com/ryanwinchester/) on [uuidv7](https://github.com/ryanwinchester/uuidv7) + + ## Examples + + iex> UUIDv7.generate() + "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" + + iex> UUIDv7.bingenerate() + <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> + + """ + + @typedoc """ + A hex-encoded UUID string. + """ + @type t :: <<_::288>> + + @typedoc """ + A raw binary representation of a UUID. + """ + @type raw :: <<_::128>> + + @version 7 + @variant 2 + + @doc """ + Generates a version 7 UUID using submilliseconds for increased clock precision. + + ## Example + + iex> UUIDv7.generate() + "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" + + """ + @spec generate() :: t + def generate, do: bingenerate() |> encode() + + @doc """ + Generates a version 7 UUID in the binary format. + + ## Example + + iex> UUIDv7.bingenerate() + <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> + + """ + @spec bingenerate() :: raw + def bingenerate do + timestamp_nanoseconds = System.system_time(:nanosecond) + timestamp_milliseconds = trunc(timestamp_nanoseconds / 1_000_000) + + nanoseconds = timestamp_nanoseconds - timestamp_milliseconds * 1_000_000 + rand_a = trunc(4096 * (nanoseconds / 1_000_000)) + + <> = :crypto.strong_rand_bytes(8) + + << + timestamp_milliseconds::big-unsigned-48, + @version::4, + rand_a::12, + @variant::2, + rand_b::62 + >> + end + + @doc """ + Extract the millisecond timestamp from the UUID. + + ## Example + + iex> UUIDv7.extract_timestamp("018ecb40-c457-73e6-a400-000398daddd9") + 1712807003223 + + """ + @spec extract_timestamp(t | raw) :: integer + def extract_timestamp(<>), do: timestamp_ms + + def extract_timestamp(<<_::288>> = uuid) do + uuid |> decode() |> extract_timestamp() + end + + @doc """ + Encode a raw UUID to the string representation. + + ## Example + + iex> UUIDv7.encode(<<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>>) + "018e90d8-06e8-7f9f-bfd7-6730ba98a51b" + + """ + @spec encode(t | raw) :: t | :error + def encode( + <<_, _, _, _, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _, ?-, _, _, _, _, + _, _, _, _, _, _, _, _>> = hex_uuid + ), + do: hex_uuid + + def encode( + <> + ) do + <> + end + + def encode(_), do: :error + + @compile {:inline, e: 1} + + defp e(0), do: ?0 + defp e(1), do: ?1 + defp e(2), do: ?2 + defp e(3), do: ?3 + defp e(4), do: ?4 + defp e(5), do: ?5 + defp e(6), do: ?6 + defp e(7), do: ?7 + defp e(8), do: ?8 + defp e(9), do: ?9 + defp e(10), do: ?a + defp e(11), do: ?b + defp e(12), do: ?c + defp e(13), do: ?d + defp e(14), do: ?e + defp e(15), do: ?f + + @doc """ + Decode a string representation of a UUID to the raw binary version. + + ## Example + + iex> UUIDv7.decode("018e90d8-06e8-7f9f-bfd7-6730ba98a51b") + <<1, 142, 144, 216, 6, 232, 127, 159, 191, 215, 103, 48, 186, 152, 165, 27>> + + """ + @spec decode(t | raw) :: raw | :error + def decode( + <<_::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, + _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, _::4, + _::4, _::4, _::4, _::4>> = raw_uuid + ), + do: raw_uuid + + def decode( + <> + ) do + <> + catch + :error -> :error + end + + def decode(_), do: :error + + @compile {:inline, d: 1} + + defp d(?0), do: 0 + defp d(?1), do: 1 + defp d(?2), do: 2 + defp d(?3), do: 3 + defp d(?4), do: 4 + defp d(?5), do: 5 + defp d(?6), do: 6 + defp d(?7), do: 7 + defp d(?8), do: 8 + defp d(?9), do: 9 + defp d(?A), do: 10 + defp d(?B), do: 11 + defp d(?C), do: 12 + defp d(?D), do: 13 + defp d(?E), do: 14 + defp d(?F), do: 15 + defp d(?a), do: 10 + defp d(?b), do: 11 + defp d(?c), do: 12 + defp d(?d), do: 13 + defp d(?e), do: 14 + defp d(?f), do: 15 + defp d(_), do: throw(:error) +end diff --git a/mix.exs b/mix.exs index 8d036c98..18b2adec 100644 --- a/mix.exs +++ b/mix.exs @@ -230,6 +230,7 @@ defmodule Ash.MixProject do Ash.Vector, Ash.Union, Ash.UUID, + Ash.UUIDv7, Ash.NotLoaded, Ash.ForbiddenField, Ash.Changeset.ManagedRelationshipHelpers, diff --git a/test/resource/attributes_test.exs b/test/resource/attributes_test.exs index 91aadf01..3c0fc910 100644 --- a/test/resource/attributes_test.exs +++ b/test/resource/attributes_test.exs @@ -51,6 +51,39 @@ defmodule Ash.Test.Resource.AttributesTest do assert nil == Ash.Resource.Info.public_attribute(Post, :bar) end + + test "uuid_v7 attributes are persisted on the resource properly" do + defposts do + attributes do + uuid_v7_primary_key :other_id + attribute :another_id, :uuid_v7 + end + end + + assert [ + _, + %Attribute{ + name: :other_id, + primary_key?: true, + public?: true, + sortable?: true, + type: Ash.Type.UUIDv7, + writable?: false + }, + %Attribute{ + name: :another_id, + primary_key?: false, + public?: false, + sortable?: true, + type: Ash.Type.UUIDv7, + writable?: true + } + ] = Ash.Resource.Info.attributes(Post) + + assert [_, %Attribute{name: :other_id}] = Ash.Resource.Info.public_attributes(Post) + + assert %Attribute{name: :other_id} = Ash.Resource.Info.attribute(Post, :other_id) + end end describe "validation" do diff --git a/test/type/uuid_v7_test.exs b/test/type/uuid_v7_test.exs new file mode 100644 index 00000000..84d43980 --- /dev/null +++ b/test/type/uuid_v7_test.exs @@ -0,0 +1,38 @@ +defmodule Ash.Test.Type.UUIDv7Test do + @moduledoc false + use ExUnit.Case, async: true + + test "it define the type correctly" do + assert :uuid = Ash.Type.storage_type(Ash.Type.UUIDv7) + assert true == Ash.Type.ash_type?(Ash.Type.UUIDv7) + assert true == Ash.Type.builtin?(Ash.Type.UUIDv7) + assert Ash.Type.UUIDv7.EctoType = Ash.Type.ecto_type(Ash.Type.UUIDv7) + end + + test "it works" do + hex_uuid = "0188aadc-f449-7818-8862-5eff12733f64" + raw_uuid = Ash.UUIDv7.decode(hex_uuid) + + assert {:ok, ^hex_uuid} = Ash.Type.cast_input(Ash.Type.UUIDv7, hex_uuid) + assert {:ok, ^hex_uuid} = Ash.Type.cast_input(Ash.Type.UUIDv7, raw_uuid) + + assert {:ok, ^hex_uuid} = Ash.Type.cast_stored(Ash.Type.UUIDv7, hex_uuid) + assert {:ok, ^hex_uuid} = Ash.Type.cast_stored(Ash.Type.UUIDv7, raw_uuid) + + assert {:ok, ^raw_uuid} = Ash.Type.dump_to_native(Ash.Type.UUIDv7, hex_uuid) + assert {:ok, ^raw_uuid} = Ash.Type.dump_to_native(Ash.Type.UUIDv7, raw_uuid) + + assert true == Ash.Type.equal?(Ash.Type.UUIDv7, raw_uuid, hex_uuid) + + assert {:ok, ^raw_uuid} = Ash.Type.apply_constraints(Ash.Type.UUIDv7, raw_uuid, []) + end + + test "it casts binary UUIDs version 7 to string" do + uuid_v7 = "01903fa1-2523-7580-a9d6-84620dcbf2ba" + + assert %StreamData{} = Ash.Type.UUIDv7.generator([]) + + assert {:ok, binary_uuid_v7} = Ash.Type.dump_to_native(Ash.Type.UUIDv7, uuid_v7) + assert {:ok, ^uuid_v7} = Ash.Type.cast_input(Ash.Type.UUIDv7, binary_uuid_v7) + end +end diff --git a/test/uuid_v7_test.exs b/test/uuid_v7_test.exs new file mode 100644 index 00000000..3d525000 --- /dev/null +++ b/test/uuid_v7_test.exs @@ -0,0 +1,60 @@ +defmodule Ash.Test.UUIDv7Test do + @moduledoc false + use ExUnit.Case, async: true + + test "generate/1 is working" do + uuid = Ash.UUIDv7.generate() + + assert <<_::64, ?-, _::32, ?-, "7", _::24, ?-, _::32, ?-, _::96>> = uuid + end + + test "generate/1 is ordered" do + uuids = + for _ <- 1..10_000 do + Ash.UUIDv7.generate() + end + + assert uuids == Enum.sort(uuids) + end + + test "bingenerate/1 is ordered" do + uuids = + for _ <- 1..10_000 do + Ash.UUIDv7.bingenerate() + end + + assert uuids == Enum.sort(uuids) + end + + test "encode/1 is working" do + hex_uuid = Ash.UUIDv7.generate() + raw_uuid = Ash.UUIDv7.bingenerate() + + encoded_hex_uuid = Ash.UUIDv7.encode(hex_uuid) + encoded_raw_uuid = Ash.UUIDv7.encode(raw_uuid) + + assert is_binary(encoded_hex_uuid) + assert Ash.UUIDv7.decode(hex_uuid) == Ash.UUIDv7.decode(encoded_hex_uuid) + + assert is_binary(encoded_raw_uuid) + assert raw_uuid == Ash.UUIDv7.decode(encoded_raw_uuid) + + assert :error = Ash.UUIDv7.encode("error") + end + + test "decode/1 is working" do + hex_uuid = Ash.UUIDv7.generate() + raw_uuid = Ash.UUIDv7.bingenerate() + + decoded_hex_uuid = Ash.UUIDv7.decode(hex_uuid) + decoded_raw_uuid = Ash.UUIDv7.decode(raw_uuid) + + assert is_binary(decoded_hex_uuid) + assert hex_uuid == Ash.UUIDv7.encode(decoded_hex_uuid) + + assert is_binary(decoded_raw_uuid) + assert Ash.UUIDv7.encode(raw_uuid) == Ash.UUIDv7.encode(decoded_raw_uuid) + + assert :error == Ash.UUIDv7.decode("error") + end +end