Skip to content

Commit

Permalink
enum macro
Browse files Browse the repository at this point in the history
  • Loading branch information
piyushthapa committed Sep 2, 2024
1 parent bcc4974 commit b447bc5
Show file tree
Hide file tree
Showing 16 changed files with 521 additions and 43 deletions.
31 changes: 17 additions & 14 deletions lib/sutra/cardano/address.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@ defmodule Sutra.Cardano.Address do
alias Sutra.Cardano.Address.Credential
alias Sutra.Cardano.Address.Parser
alias Sutra.Cardano.Address.Pointer
alias Sutra.Cardano.Data
alias Sutra.Cardano.Data.Constr
alias Sutra.Data
alias Sutra.Data.Plutus.Constr

@type credential_type :: :vkey | :script
@type address_type :: :shelley | :reward | :byron
@type stake_credential :: Credential.t() | Pointer.t() | nil
@type network :: :mainnet | :testnet

@behaviour Sutra.Cardano.Data.DataBehavior

typedstruct module: Credential do
@moduledoc """
Address Credential
Expand Down Expand Up @@ -97,19 +95,24 @@ defmodule Sutra.Cardano.Address do
Bech32.encode_from_5bit(hrp, data)
end

@spec from_plutus(network(), binary()) :: Address.t() | {:error, String.t()}
def from_plutus(network, cbor) do
case Data.decode(cbor) do
{:ok, %Constr{index: 0, fields: [payment_cred, stake_cred_data]}} ->
%Address{
network: network,
address_type: :shelley,
payment_credential: fetch_payment_credential(payment_cred),
stake_credential: fetch_stake_credential(stake_cred_data)
}
@spec from_plutus(binary() | Sutra.Data.Plutus.t()) ::
Address.t() | {:error, String.t()} | {:error, any()}
def from_plutus(data) when is_binary(data) do
case Data.decode(data) do
{:ok, decoded} -> from_plutus(decoded)
{:error, reason} -> {:error, reason}
end
end

def from_plutus(%Constr{index: 0, fields: [pay_cred, stake_cred]}) do
%Address{
network: nil,
address_type: :shelley,
payment_credential: fetch_payment_credential(pay_cred),
stake_credential: fetch_stake_credential(stake_cred)
}
end

defp fetch_payment_credential(%Constr{index: indx, fields: [%CBOR.Tag{value: v}]}) do
credential_type = if indx == 0, do: :vkey, else: :script
%Credential{credential_type: credential_type, hash: v}
Expand Down
Empty file removed lib/sutra/cardano/data/parser.ex
Empty file.
7 changes: 7 additions & 0 deletions lib/sutra/cardano/types.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Sutra.Cardano.Types do
@moduledoc """
Common Cardano Data Types
"""

use Sutra.Data
end
9 changes: 9 additions & 0 deletions lib/sutra/cardano/types/datum.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Sutra.Cardano.Types.Datum do
@moduledoc """
Cardano Datum
"""

use Sutra.Data

defenum(no_datum: :null, datum_hash: :string, inline_datum: :string)
end
20 changes: 20 additions & 0 deletions lib/sutra/cardano/types/output.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Sutra.Cardano.Types.Output do
@moduledoc """
Cardano Output
"""
alias Sutra.Cardano.Types.Datum
alias Sutra.Cardano.Address

use Sutra.Data

defdata do
data(:address, Address,
encode_with: &Address.to_plutus/1,
decode_with: &Address.from_plutus/1
)

data(:value, :string)
data(:datum, Datum)
data(:reference_script, ~OPTION(:string))
end
end
15 changes: 15 additions & 0 deletions lib/sutra/cardano/utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Sutra.Utils do
@moduledoc """
Utils
"""

def safe_head([]), do: nil
def safe_head([head | _]), do: head

def safe_tail([]), do: []
def safe_tail([_ | tail]), do: tail

def identity(x), do: x

def flip(a, b, f), do: f.(b, a)
end
184 changes: 184 additions & 0 deletions lib/sutra/data.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
defmodule Sutra.Data do
@moduledoc """
Data Manager for Plutus
"""

@common_types_default_encodings %{
integer: :default,
string: :default,
enum: :default,
pairs: :default
}

alias Sutra.Data.Plutus.Constr
alias Sutra.Utils

Check warning on line 14 in lib/sutra/data.ex

View workflow job for this annotation

GitHub Actions / Test on OTP 25.0.4 / Elixir 1.15.0

The alias `Sutra.Utils` is not alphabetically ordered among its group.
alias Sutra.Data.Option
alias Sutra.Data
alias Sutra.Data.MacroHelper.EnumMacro

import Sutra.Data.Cbor, only: [extract_value: 1]

defmacro __using__(_) do
quote do
import Sutra.Data, only: [defdata: 2, defdata: 1, defenum: 1, data: 3, data: 2]
import Sutra.Data.Option
end
end

defmacro defdata(opts \\ [], do: block) do
ast = __setup__(block, opts)

case opts[:name] do
nil ->
quote do
unquote(ast)
end

module ->
quote do
defmodule unquote(module) do
unquote(ast)
end
end
end
end

def __setup__(block, _opts) do
quote do
Module.register_attribute(__MODULE__, :__fields, accumulate: true)
Module.register_attribute(__MODULE__, :__required, accumulate: true)

alias Sutra.Data

unquote(block)
defstruct @__fields

def from_plutus(%Constr{index: indx, fields: constr_fields} = tag) do
parsed =
__MODULE__.__fields__()
|> Enum.reverse()
|> Data.__handle_data_decoder__(tag)

struct(__MODULE__, parsed)
end

def to_plutus(%__MODULE__{} = _mod) do
IO.inspect("to_plutus")

Check warning on line 66 in lib/sutra/data.ex

View workflow job for this annotation

GitHub Actions / Test on OTP 25.0.4 / Elixir 1.15.0

There should be no calls to IO.inspect/1.
end

def __fields__ do
@__fields
end

def __plutus_data_info__ do
end
end
end

def __handle_data_decoder__(fields, %Constr{fields: constr_fields} = tag) do
{_, result} =
Enum.reduce(fields, {constr_fields, %{}}, fn {name, {data_type, opts}}, {c_fields, acc} ->
{value, c_fields} = __do_decode(data_type, opts, tag, c_fields)
{c_fields, Map.put(acc, name, value)}
end)

result
end

def __do_decode(%Option{option: inner}, opts, tag, fields) do
case tag do
%Constr{index: 0, fields: [v | _]} -> __do_decode(inner, opts, v, fields)
_ -> {nil, fields}
end
end

def __do_decode(:enum, opts, tag, fields) do
value =
if is_function(opts[:decode_with]) do
opts[:decode_with].(tag)
else
Enum.at(opts[:fields], tag.index)
end

{value, fields}
end

def __do_decode(_value, opts, _tag, fields) do
current_value = Utils.safe_head(fields)

if is_function(opts[:decode_with]) do
{opts[:decode_with].(current_value), Utils.safe_tail(fields)}
else
{extract_value(current_value), Utils.safe_tail(fields)}
end
end

defmacro data(name, type, opts \\ []) do
quote bind_quoted: [name: name, type: type, opts: opts] do
Sutra.Data.__handle_data__(__ENV__, name, type, opts)
end
end

defmacro defenum(opts), do: EnumMacro.__define__(opts)

def __handle_data__(%Macro.Env{module: mod}, name, type, opts) do
fields = Module.get_attribute(mod, :__fields) || []

if Enum.find(fields, fn {n, _} -> n == name end) do
raise ArgumentError, "the field #{name} is already defined"
end

if Enum.find(fields, fn {_, {t, _}} -> t == :enum end) do
raise ArgumentError,
"There must be only one data with enum type" <> "\n Found multiple data declarations"
end

{encode_with, decode_with} = set_encoder_decoder(type, opts)

Module.put_attribute(
mod,
:__fields,
{name, {type, Keyword.merge(opts, encode_with: encode_with, decode_with: decode_with)}}
)
end

defp set_encoder_decoder(%Option{option: inner_type}, opts) do
set_encoder_decoder(inner_type, opts)
end

defp set_encoder_decoder(type, opts) do
cond do
is_function(Keyword.get(opts, :encode_with)) and
is_function(Keyword.get(opts, :decode_with)) ->
{opts[:encode_with], opts[:decode_with]}

Map.has_key?(@common_types_default_encodings, type) ->
{opts[:encode_with], opts[:decode_with]}

function_exported?(type, :from_plutus, 1) &&
function_exported?(type, :to_plutus, 1) ->
{&type.from_plutus/1, &type.to_plutus/1}

runtime_module?(type) ->
{&type.from_plutus/1, &type.to_plutus/1}

true ->
raise ArgumentError, """
Invalid Type #{type}
Pass `encode_with: &some_function/1, decode_with: &some_function/1` to create custom type
"""
end
end

defp runtime_module?(type) do
case Atom.to_string(type) do
":" <> _ -> false
_ -> true
end
end

defdelegate encode(data), to: Sutra.Data.Plutus
defdelegate decode(hex), to: Sutra.Data.Plutus
defdelegate decode!(hex), to: Sutra.Data.Plutus
end
10 changes: 10 additions & 0 deletions lib/sutra/data/cbor.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Sutra.Data.Cbor do
@moduledoc """
CBOR handling
"""

def extract_value(%CBOR.Tag{value: value}), do: value
def extract_value(%Sutra.Data.Plutus.PList{value: value}), do: value
def extract_value(value), do: value
end
File renamed without changes.
File renamed without changes.
57 changes: 57 additions & 0 deletions lib/sutra/data/macro_helper.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule Sutra.Data.MacroHelper do
@moduledoc """
Helper function to define Data Macros
"""
import Sutra.Data.Cbor, only: [extract_value: 1]

# Add more common support types
@common_types [
:string,
:integer,
:null,
:address
]

@encode_decode_mapping %{
string: [
decode_with: &extract_value/1,
encode_with: &Sutra.Utils.identity/1
],
integer: [
decode_with: &extract_value/1,
encode_with: &Sutra.Utils.identity/1
],
null: [
decode_with: &extract_value/1,
encode_with: &Sutra.Utils.identity/1
],
address: [
decode_with: &Sutra.Cardano.Address.from_plutus/1,
encode_with: &Sutra.Cardano.Address.to_plutus/1
]
}

def with_encoder_decoder(type, opts) do
cond do
is_function(opts[:encode_with], 1) and is_function(opts[:decode_with], 1) ->
opts

Enum.member?(@common_types, type) ->
Keyword.merge(opts, @encode_decode_mapping[type])

module?(type) ->
opts
|> Keyword.merge(encode_with: &type.to_plutus/1, decode_with: &type.from_plutus/1)

true ->
raise ArgumentError, "Unsupported type: #{inspect(type)}"
end
end

defp module?(type) do
case Atom.to_string(type) do
"Elixir." <> _ -> true
_ -> false
end
end
end
Loading

0 comments on commit b447bc5

Please sign in to comment.