Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UUID conversion to and from 16 byte fixed sequences #1

Merged
merged 4 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,33 @@ on:
types: [opened, reopened, synchronize]
push:
branches:
- "master"
- "main"
jobs:
test:
name: Build and test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Elixir
uses: erlef/[email protected]
with:
elixir-version: '1.13.3' # Define the elixir version [required]
otp-version: '24.2.1' # Define the OTP version [required]
- name: Restore dependencies cache
uses: actions/cache@v2
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Run tests
run: mix test
- name: Check formatting
run: mix format --check-formatted
- name: Credo
run: mix credo
- uses: actions/checkout@v2
- name: Set up Elixir
uses: erlef/[email protected]
with:
elixir-version: "1.13.3" # Define the elixir version [required]
otp-version: "24.2.1" # Define the OTP version [required]
- name: Restore dependencies cache
uses: actions/cache@v2
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Run tests
run: mix test
- name: Check formatting
run: mix format --check-formatted
- name: Credo
run: mix credo

dialyzer:
name: Run Dialyzer for type checking
Expand All @@ -56,7 +56,7 @@ jobs:
- name: Run Dialyzer
uses: erlef/[email protected]
with:
elixir-version: '1.13.3' # Define the elixir version [required]
otp-version: '24.2.1' # Define the OTP version [required]
elixir-version: "1.13.3" # Define the elixir version [required]
otp-version: "24.2.1" # Define the OTP version [required]
- run: mix deps.get
- run: mix dialyzer
54 changes: 54 additions & 0 deletions .github/workflows/common-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# This file is synced with beam-community/common-config. Any changes will be overwritten.

name: Common Config

on:
push:
branches:
- main
paths:
- .github/workflows/common-config.yaml
repository_dispatch:
types:
- common-config
schedule:
- cron: "8 12 8 * *"
workflow_dispatch: {}

concurrency:
group: Common Config

jobs:
Sync:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
persist-credentials: true

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup Elixir
uses: stordco/actions-elixir/setup@v1
with:
github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
elixir-version: "1.15"
otp-version: "26.0"

- name: Sync
uses: stordco/actions-sync@v1
with:
commit-message: "chore: sync files with beam-community/common-config"
pr-enabled: true
pr-labels: common-config
pr-title: "chore: sync files with beam-community/common-config"
pr-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
sync-auth: doomspork:${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
sync-branch: latest
sync-repository: github.com/beam-community/common-config.git
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## v2.2.0 - July 31st, 2024

### Added

- Support for encoding and decoding Decimals

### Fixed

- Incorrect error for decimal encoding
- String.slice deprecation warning

## v2.1.0 - March 28th, 2023

### Added
Expand Down
21 changes: 21 additions & 0 deletions lib/avro_ex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ defmodule AvroEx do
of blocks with their counts. This allows consumers of the encoded data to skip
over those blocks in an efficient manner. Using the option `include_block_byte_size: true`
enables adding those additional values.

## UUID encoding

UUIDs can be decoded as strings using the canonical hex representation with 37 bytes.
Alternatively, encoding UUIDs in their 16 byte binary representation is much
more compact, saving 21 bytes per encoding.
See "UUIDs" on `decode/3` for how to convert binary representations back to
canonical strings during decoding.
"""
@spec encode(Schema.t(), term, keyword()) ::
{:ok, encoded_avro} | {:error, AvroEx.EncodeError.t() | Exception.t()}
Expand Down Expand Up @@ -185,6 +193,19 @@ defmodule AvroEx do

Otherwise, an approximate number is calculated.

## UUIDs

When decoding a 16 byte fixed quantity with logical type "uuid", specify
`uuid_format: :binary` to retain the binary representation or
`uuid_format: :canonical_string` to convert to the canonical, hex as string representation.

iex> schema = AvroEx.decode_schema!(~S({"type": "fixed", "size": 16, "name": "fixed_uuid", "logicalType":"uuid"}))
iex> binary_uuid = <<85, 14, 132, 0, 226, 155, 65, 212, 167, 22, 68, 102, 85, 68, 0, 0>>
iex> AvroEx.decode(schema, binary_uuid, uuid_format: :binary)
{:ok, binary_uuid}
iex> AvroEx.decode(schema, binary_uuid, uuid_format: :canonical_string)
{:ok, "550e8400-e29b-41d4-a716-446655440000"}

"""
@spec decode(Schema.t(), encoded_avro, keyword()) ::
{:ok, term}
Expand Down
19 changes: 19 additions & 0 deletions lib/avro_ex/decode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,25 @@ defmodule AvroEx.Decode do
{:lists.nth(index + 1, symbols), rest}
end

defp do_decode(%Fixed{size: size = 16, metadata: %{"logicalType" => "uuid"}}, %Context{}, data, opts)
when is_binary(data) do
<<fixed::binary-size(size), rest::binary>> = data

case Keyword.get(opts, :uuid_format, :binary) do
:binary ->
{fixed, rest}

:canonical_string ->
case Uniq.UUID.parse(fixed) do
{:ok, uuid} ->
{Uniq.UUID.to_string(uuid, :default), rest}

_ ->
error({:invalid_binary_uuid, fixed})
end
end
end

defp do_decode(%Fixed{size: size}, %Context{}, data, _) when is_binary(data) do
<<fixed::binary-size(size), rest::binary>> = data
{fixed, rest}
Expand Down
5 changes: 5 additions & 0 deletions lib/avro_ex/decode_error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ defmodule AvroEx.DecodeError do
message = "Invalid UTF-8 string found #{inspect(str)}."
%__MODULE__{message: message}
end

def new({:invalid_binary_uuid, binary_uuid}) do
message = "Invalid binary UUID found #{inspect(binary_uuid)}."
%__MODULE__{message: message}
end
end
5 changes: 5 additions & 0 deletions lib/avro_ex/encode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ defmodule AvroEx.Encode do
bin
end

defp do_encode(%Fixed{size: 16, metadata: %{"logicalType" => "uuid"}} = f, %Context{} = context, bin, opts)
when is_binary(bin) do
do_encode(f, context, Uniq.UUID.string_to_binary!(bin), opts)
end

defp do_encode(%Fixed{} = fixed, %Context{} = context, bin, _) when is_binary(bin) do
error({:incorrect_fixed_size, fixed, bin, context})
end
Expand Down
5 changes: 5 additions & 0 deletions lib/avro_ex/schema/fixed.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@ defmodule AvroEx.Schema.Fixed do
true
end

def match?(%__MODULE__{size: 16, metadata: %{"logicalType" => "uuid"}}, %Context{}, data)
when is_binary(data) do
Uniq.UUID.valid?(data)
end

def match?(_fixed, _context, _data), do: false
end
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule AvroEx.Mixfile do
use Mix.Project

@url "http://github.com/beam-community/avro_ex"
@version "2.1.0"
@version "2.2.0"

def project do
[
Expand Down Expand Up @@ -37,7 +37,8 @@ defmodule AvroEx.Mixfile do
{:dialyxir, "~> 1.1", only: :dev, runtime: false},
{:ex_doc, "~> 0.20", only: :dev, runtime: false},
{:stream_data, "~> 0.5", only: [:dev, :test]},
{:decimal, "~> 2.0", optional: true}
{:decimal, "~> 2.0", optional: true},
{:uniq, "~> 0.6"}
]
end

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
"uniq": {:hex, :uniq, "0.6.1", "369660ecbc19051be526df3aa85dc393af5f61f45209bce2fa6d7adb051ae03c", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "6426c34d677054b3056947125b22e0daafd10367b85f349e24ac60f44effb916"},
}
27 changes: 27 additions & 0 deletions test/decode_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,20 @@ defmodule AvroEx.Decode.Test do
"decimalField4" => 5.3e-11
}
end

test "16 byte fixed uuid" do
{:ok, fixed_uuid_schema} =
AvroEx.decode_schema(~S({"type": "fixed", "size": 16, "name": "fixed_uuid", "logicalType":"uuid"}))

# Example from https://en.wikipedia.org/wiki/Universally_unique_identifier#Textual_representation
canonical_string = "550e8400-e29b-41d4-a716-446655440000"
binary = :binary.encode_unsigned(113_059_749_145_936_325_402_354_257_176_981_405_696)

assert {:ok, ^binary} = AvroEx.decode(fixed_uuid_schema, binary, uuid_format: :binary)
assert {:ok, ^binary} = AvroEx.decode(fixed_uuid_schema, binary)

assert {:ok, ^canonical_string} = AvroEx.decode(fixed_uuid_schema, binary, uuid_format: :canonical_string)
end
end

describe "DecodingError" do
Expand All @@ -354,5 +368,18 @@ defmodule AvroEx.Decode.Test do
AvroEx.decode!(schema, <<"\nhell", 0xFFFF::16>>)
end
end

test "invalid fixed uuid" do
{:ok, fixed_uuid_schema} =
AvroEx.decode_schema(~S({"type": "fixed", "size": 16, "name": "fixed_uuid", "logicalType":"uuid"}))

non_uuid_binary = :binary.list_to_bin(List.duplicate(1, 16))

assert_raise DecodeError,
"Invalid binary UUID found <<1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1>>.",
fn ->
AvroEx.decode!(fixed_uuid_schema, non_uuid_binary, uuid_format: :canonical_string)
end
end
end
end
Loading
Loading