Skip to content

Commit

Permalink
Merge pull request #1 from salemove/fixed_uuid
Browse files Browse the repository at this point in the history
Add UUID conversion to and from 16 byte fixed sequences
  • Loading branch information
urmastalimaa authored Feb 21, 2025
2 parents f4091e2 + c5abbde commit 1839f9d
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 47 deletions.
50 changes: 25 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,37 @@ on:
types: [opened, reopened, synchronize]
push:
branches:
- "master"
- "main"
jobs:
test:
name: Build and test
runs-on: ubuntu-latest
runs-on: ubuntu-22.04

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
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set mix file hash
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
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

jobs:
Publish:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
env:
HEX_API_KEY: ${{ secrets.HEXPM_SECRET }}
steps:
Expand Down
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

0 comments on commit 1839f9d

Please sign in to comment.