diff --git a/CHANGELOG.md b/CHANGELOG.md --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,17 @@ # Changelog -All notable changes to atex will be documented in this file. +All notable changes to elixir-dasl 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 + +- `~CID` sigil for easily constructing known CIDs. +- New inspect string for CIDs using the new sigil. ## [0.1.0] - 2026-04-08 diff --git a/lib/dasl/cid.ex b/lib/dasl/cid.ex --- a/lib/dasl/cid.ex +++ b/lib/dasl/cid.ex @@ -153,6 +153,34 @@ end end @doc """ + Constructs a `DASL.CID` from a string-encoded CID, raising on failure. + + Equivalent to `new/1` but raises `ArgumentError` instead of returning + `{:error, reason}`. Useful in contexts where an invalid CID is a + programming error rather than expected user input. + + ## Examples + + iex> cid = DASL.CID.new!("bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e") + iex> cid.codec + :raw + + iex> DASL.CID.new!("not-a-cid") + ** (ArgumentError) invalid CID 'not-a-cid': "CID must start with 'b'" + + """ + @spec new!(String.t()) :: t() + def new!(cid_string) when is_binary(cid_string) do + case new(cid_string) do + {:ok, cid} -> + cid + + {:error, reason} -> + raise ArgumentError, message: "invalid CID '#{cid_string}': #{inspect(reason)}" + end + end + + @doc """ Encodes a `DASL.CID` back to its canonical string form. ## Examples @@ -279,6 +307,26 @@ candidate = :crypto.hash(:sha256, data) :crypto.hash_equals(digest, candidate) end + @doc """ + Sigil for constructing a `DASL.CID` from a compile-time string literal. + + Delegates to `new!/1`, so it raises `ArgumentError` on an invalid CID + string. Import `DASL.CID` (or `import DASL.CID, only: [sigil_CID: 2]`) to + bring the sigil into scope. + + ## Examples + + iex> import DASL.CID + iex> cid = ~CID"bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e" + iex> cid.codec + :raw + iex> cid.version + 1 + + """ + @spec sigil_CID(String.t(), list()) :: t() + def sigil_CID(string, _), do: new!(string) + defp decode_codec(@codec_raw), do: {:ok, :raw} defp decode_codec(@codec_drisl), do: {:ok, :drisl} @@ -296,6 +344,6 @@ defimpl Inspect, for: DASL.CID do def inspect(cid, _opts) do cid = DASL.CID.encode(cid) - ~s'DASL.CID.new("#{cid}")' + ~s'~CID"#{cid}"' end end diff --git a/test/dasl/cid_test.exs b/test/dasl/cid_test.exs --- a/test/dasl/cid_test.exs +++ b/test/dasl/cid_test.exs @@ -108,6 +108,71 @@ assert {:error, "unsupported CID version: 2"} = DASL.CID.new(bad_str) end end + describe "new!/1" do + test "returns a CID struct on a valid string" do + assert %DASL.CID{} = DASL.CID.new!(@raw_cid) + end + + test "returned struct matches new/1" do + {:ok, expected} = DASL.CID.new(@raw_cid) + assert DASL.CID.new!(@raw_cid) == expected + end + + test "raises ArgumentError on an invalid prefix" do + assert_raise ArgumentError, ~r/invalid CID 'not-a-cid'/, fn -> + DASL.CID.new!("not-a-cid") + end + end + + test "raises ArgumentError on malformed base32" do + assert_raise ArgumentError, ~r/invalid CID/, fn -> + DASL.CID.new!("b!!!!!") + end + end + + test "raises ArgumentError on an invalid CID version embedded in the bytes" do + bad_bytes = <<2, 0x55, 0x12, 0x20>> <> @digest + bad_str = "b" <> Base.encode32(bad_bytes, case: :lower, padding: false) + + assert_raise ArgumentError, ~r/unsupported CID version/, fn -> + DASL.CID.new!(bad_str) + end + end + end + + describe "~CID sigil" do + import DASL.CID, only: [sigil_CID: 2] + + test "constructs a CID struct from a literal string" do + assert %DASL.CID{} = + ~CID"bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e" + end + + test "produced struct equals new!/1" do + assert ~CID"bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e" == + DASL.CID.new!(@raw_cid) + end + + test "codec is preserved for raw" do + assert ~CID"bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e".codec == :raw + end + + test "codec is preserved for drisl" do + assert ~CID"bafyreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e".codec == :drisl + end + + test "round-trips through encode" do + assert DASL.CID.encode(~CID"bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e") == + @raw_cid + end + + test "raises ArgumentError on an invalid CID string" do + assert_raise ArgumentError, ~r/invalid CID/, fn -> + DASL.CID.sigil_CID("not-a-cid", []) + end + end + end + describe "encode/1" do test "round-trips a raw CID" do {:ok, cid} = DASL.CID.new(@raw_cid)