defmodule AvroEx do
@moduledoc """
AvroEx is a library for encoding and decoding data with Avro schemas.
Supports parsing schemas, encoding data, and decoding data.
For encoding and decoding, the following type chart should be referenced:
| Avro Types | Elixir Types |
|------------|:------------:|
| boolean | boolean |
| integer | integer |
| long | integer |
| float | decimal |
| double | decimal |
| bytes | binary |
| string | String.t, atom |
| null | nil |
| Record | map |
| Enum | String.t, atom (corresponding to the enum's symbol list) |
"""
alias AvroEx.Schema
alias AvroEx.Schema.Context
@type encoded_avro :: binary
@doc """
Checks to see if the given data is encodable using the given schema. Helpful for unit testing.
iex> AvroEx.encodable?(%Schema{schema: %Primitive{type: :string}}, "wut")
true
iex> AvroEx.encodable?(%Schema{schema: %Primitive{type: :string}}, 12345)
false
"""
@spec encodable?(AvroEx.Schema.t(), any) :: boolean
defdelegate encodable?(schema, data), to: AvroEx.Schema
@spec parse_schema(Schema.json_schema()) :: {:ok, Schema.t()} | {:error, AvroEx.Schema.DecodeError.t()}
@deprecated "Use AvroEx.decode_schema/1 instead"
def parse_schema(json), do: decode_schema(json, [])
@spec parse_schema!(Schema.json_schema()) :: Schema.t() | no_return
@deprecated "Use AvroEx.decode_schema!/1 instead"
def parse_schema!(json), do: decode_schema!(json, [])
@doc """
Given an Elixir or JSON-encoded schema, parses the schema and returns a `t:AvroEx.Schema.t/0` struct representing the schema.
Errors for invalid JSON, invalid schemas, and bad name references.
## Options
* `:strict` - whether to strictly validate the schema, defaults to `false`. Recommended to turn this on for locally owned schemas, but not for interop with external schemas.
## Examples
iex> AvroEx.decode_schema("string")
{:ok, %AvroEx.Schema{schema: %AvroEx.Schema.Primitive{type: :string}}}
iex> json= ~S({\"fields\":[{\"name\":\"a\",\"type\":\"string\"}],\"name\":\"my_type\",\"type\":\"record\"})
iex> {:ok, %Schema{schema: record}} = AvroEx.decode_schema(json)
iex> match?(%Record{}, record)
true
"""
@spec decode_schema(term(), Keyword.t()) :: {:ok, Schema.t()} | {:error, AvroEx.Schema.DecodeError.t()}
def decode_schema(schema, opts \\ []) do
try do
{:ok, decode_schema!(schema, opts)}
rescue
error -> {:error, error}
end
end
@doc """
Same as `AvroEx.decode_schema/1`, but raises an exception on failure instead of
returning an error tuple.
## Examples
iex> AvroEx.decode_schema!("int")
%AvroEx.Schema{schema: %AvroEx.Schema.Primitive{type: :int}}
"""
@spec decode_schema!(term(), Keyword.t()) :: Schema.t()
def decode_schema!(schema, opts \\ []) do
if is_binary(schema) and not Schema.Parser.primitive?(schema) do
schema
|> Jason.decode!()
|> Schema.Parser.parse!(opts)
else
Schema.Parser.parse!(schema, opts)
end
end
@doc """
Encodes the given schema to JSON
## Options
* `canonical` - Encodes the schema into its [Parsing Canonical Form](https://avro.apache.org/docs/current/spec.html#Parsing+Canonical+Form+for+Schemas), default `false`
## Examples
iex> schema = AvroEx.decode_schema!(%{"type" => "int", "logicalType" => "date"})
iex> AvroEx.encode_schema(schema)
~S({"type":"int","logicalType":"date"})
iex> schema = AvroEx.decode_schema!(%{"type" => "int", "logicalType" => "date"})
iex> AvroEx.encode_schema(schema, canonical: true)
~S("int")
"""
@spec encode_schema(Schema.t(), Keyword.t()) :: String.t()
def encode_schema(%Schema{} = schema, opts \\ []) do
AvroEx.Schema.Encoder.encode(schema, opts)
end
@doc """
Given `t:AvroEx.Schema.t/0` and `term()`, takes the data and encodes it according to the schema.
## Examples
iex> schema = AvroEx.decode_schema!("int")
iex> AvroEx.encode(schema, 1234)
{:ok, <<164, 19>>}
"""
@spec encode(Schema.t(), term) ::
{:ok, encoded_avro} | {:error, AvroEx.EncodeError.t() | Exception.t()}
def encode(schema, data) do
AvroEx.Encode.encode(schema, data)
end
@doc """
Same as `encode/2`, but returns the encoded value directly.
Raises `t:AvroEx.EncodeError.t/0` on error.
## Examples
iex> schema = AvroEx.decode_schema!("boolean")
iex> AvroEx.encode!(schema, true)
<<1>>
"""
@spec encode!(Schema.t(), term()) :: encoded_avro()
def encode!(schema, data) do
case AvroEx.Encode.encode(schema, data) do
{:ok, data} -> data
{:error, error} -> raise error
end
end
@doc """
Given an encoded message and its accompanying schema, decodes the message.
iex> schema = AvroEx.decode_schema!("boolean")
iex> AvroEx.decode(schema, <<1>>)
{:ok, true}
"""
@spec decode(Schema.t(), encoded_avro) ::
{:ok, term}
| {:error, AvroEx.DecodeError.t()}
def decode(schema, message) do
case AvroEx.Decode.decode(schema, message) do
{:ok, value, _} -> {:ok, value}
{:error, error} -> {:error, error}
end
end
@doc """
Same as decode/2, but returns raw decoded value.
Raises `t:AvroEx.DecodeError.t/0` on error.
## Examples
iex> schema = AvroEx.decode_schema!("string")
iex> encoded = AvroEx.encode!(schema, "hello")
iex> AvroEx.decode!(schema, encoded)
"hello"
"""
@spec decode!(Schema.t(), encoded_avro()) :: term()
def decode!(schema, message) do
case AvroEx.Decode.decode(schema, message) do
{:ok, value, _} -> value
{:error, error} -> raise error
end
end
@deprecated "Use AvroEx.Schema.Context.lookup/2"
@spec named_type(Schema.full_name(), Schema.t() | Context.t()) :: nil | Schema.schema_types()
def named_type(name, %Schema{context: %Context{} = context}) when is_binary(name) do
named_type(name, context)
end
def named_type(name, %Context{} = context) do
Context.lookup(context, name)
end
end