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

Support for | in keywords and symbols, support of namespaced maps, special support for :nil/:true/:false, fixes, formatting, deps bump #44

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 6 additions & 6 deletions lib/eden.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ defmodule Eden do
alias Eden.Decode
alias Eden.Exception, as: Ex

@default_handlers %{"inst" => &Eden.Tag.inst/1,
"uuid" => &Eden.Tag.uuid/1}
@default_handlers %{"inst" => &Eden.Tag.inst/1, "uuid" => &Eden.Tag.uuid/1}

@doc """
Encodes an *Elixir* term that implements the `Eden.Encode` protocol.
Expand Down Expand Up @@ -55,7 +54,7 @@ defmodule Eden do
iex> Eden.encode({:a, 1})
{:error, Protocol.UndefinedError}
"""
@spec encode(Encode.t) :: {:ok, String.t} | {:error, atom}
@spec encode(Encode.t()) :: {:ok, String.t()} | {:error, atom}
def encode(data) do
try do
{:ok, encode!(data)}
Expand All @@ -70,7 +69,7 @@ defmodule Eden do

Returns the function result otherwise.
"""
@spec encode!(Encode.t) :: String.t
@spec encode!(Encode.t()) :: String.t()
def encode!(data) do
Encode.encode(data)
end
Expand Down Expand Up @@ -102,7 +101,7 @@ defmodule Eden do
iex> Eden.decode("nil true false .")
{:error, Eden.Exception.UnexpectedInputError}
"""
@spec decode(String.t, Keyword.t) :: {:ok, any} | {:error, atom}
@spec decode(String.t(), Keyword.t()) :: {:ok, any} | {:error, atom}
def decode(input, opts \\ []) do
try do
{:ok, decode!(input, opts)}
Expand All @@ -117,11 +116,12 @@ defmodule Eden do

Returns the function result otherwise.
"""
@spec decode!(String.t, Keyword.t) :: any
@spec decode!(String.t(), Keyword.t()) :: any
def decode!(input, opts \\ []) do
tree = parse(input, location: true)
handlers = Map.merge(@default_handlers, opts[:handlers] || %{})
opts = [handlers: handlers]

case Decode.decode(tree, opts) do
[] -> raise Ex.EmptyInputError, input
[data] -> data
Expand Down
87 changes: 70 additions & 17 deletions lib/eden/decode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,81 +9,134 @@ defmodule Eden.Decode do
def decode(children, opts) when is_list(children) do
Enum.map(children, fn x -> decode(x, opts) end)
end

def decode(%Node{type: :root, children: children}, opts) do
decode(children, opts)
end
def decode(%Node{type: :nil}, _opts) do
:nil

def decode(%Node{type: nil}, _opts) do
nil
end
def decode(%Node{type: :true}, _opts) do
:true

def decode(%Node{type: true}, _opts) do
true
end
def decode(%Node{type: :false}, _opts) do
:false

def decode(%Node{type: false}, _opts) do
false
end

def decode(%Node{type: :string, value: value}, _opts) do
value
end

def decode(%Node{type: :character, value: value}, _opts) do
%Character{char: value}
end

def decode(%Node{type: :symbol, value: value}, _opts) do
%Symbol{name: value}
end

def decode(%Node{type: :keyword, value: value}, _opts) when value in ["nil", "true", "false"] do
{:keyword, String.to_atom(value)}
end

def decode(%Node{type: :keyword, value: value}, _opts) do
String.to_atom(value)
end

def decode(%Node{type: :integer, value: value}, _opts) do
value = String.trim_trailing(value, "N")
:erlang.binary_to_integer(value)
end

def decode(%Node{type: :float, value: value}, _opts) do
value = String.trim_trailing(value, "M")
# Elixir/Erlang don't convert to float if there
# is no decimal part.
final_value = if not String.contains?(value, ".") do
if String.match?(value, ~r/[eE]/) do
String.replace(value, ~r/[eE]/, ".0E")
final_value =
if not String.contains?(value, ".") do
if String.match?(value, ~r/[eE]/) do
String.replace(value, ~r/[eE]/, ".0E")
else
value <> ".0"
end
else
value <> ".0"
value
end
else
value
end

:erlang.binary_to_float(final_value)
end

def decode(%Node{type: :list, children: children}, opts) do
decode(children, opts)
end

def decode(%Node{type: :vector, children: children}, opts) do
children
|> decode(opts)
|> Array.from_list
|> Array.from_list()
end

def decode(%Node{type: :map, children: children} = node, opts) do
if Integer.is_odd(length children) do
if Integer.is_odd(length(children)) do
raise Ex.OddExpressionCountError, node
end

children
|> decode(opts)
|> Enum.chunk_every(2)
|> Enum.map(fn [a, b] -> {a, b} end)
|> Enum.into(%{})
end

def decode(%Node{type: :ns_map, value: value, children: children} = node, opts) do
if Integer.is_odd(length(children)) do
raise Ex.OddExpressionCountError, node
end

children
|> Enum.chunk_every(2)
|> Enum.map(fn [a, b] -> {decode_ns_map_key(a, value, opts), decode(b, opts)} end)
|> Enum.into(%{})
end

def decode(%Node{type: :set, children: children}, opts) do
children
|> decode(opts)
|> Enum.into(MapSet.new)
|> Enum.into(MapSet.new())
end

def decode(%Node{type: :tag, value: name, children: [child]}, opts) do
case Map.get(opts[:handlers], name) do
nil ->
%Tag{name: name, value: decode(child, opts)}

handler ->
handler.(decode(child, opts))
end
end

def decode(%Node{type: type}, _opts) do
raise "Unrecognized node type: #{inspect type}"
raise "Unrecognized node type: #{inspect(type)}"
end

defp decode_ns_map_key(%Node{type: :keyword, value: value}, ns, _opts) do
if not String.contains?(value, "/") do
(ns <> "/" <> value) |> String.to_atom()
else
case value do
"_/" <> kw ->
kw |> String.to_atom()

_ ->
value |> String.to_atom()
end
end
end

defp decode_ns_map_key(node, _ns, opts) do
decode(node, opts)
end
end
42 changes: 30 additions & 12 deletions lib/eden/encode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,60 @@ alias Eden.Tag
defprotocol Eden.Encode do
@fallback_to_any true

@spec encode(any) :: String.t
@spec encode(any) :: String.t()
def encode(value)
end

defmodule Eden.Encode.Utils do
def wrap(str, first, last )do
def wrap(str, first, last) do
first <> str <> last
end
end

defimpl Encode, for: Atom do
def encode(atom) when atom in [nil, true, false] do
Atom.to_string atom
Atom.to_string(atom)
end

def encode(atom) do
":" <> Atom.to_string atom
":" <> Atom.to_string(atom)
end
end

defimpl Encode, for: Tuple do
def encode({:keyword, atom}) when atom in [nil, true, false] do
":" <> Atom.to_string(atom)
end
end

defimpl Encode, for: Symbol do
def encode(symbol) do symbol.name end
def encode(symbol) do
symbol.name
end
end

defimpl Encode, for: BitString do
def encode(string) do "\"#{string}\"" end
def encode(string) do
"\"#{string}\""
end
end

defimpl Encode, for: Character do
def encode(char) do "\\#{char.char}" end
def encode(char) do
"\\#{char.char}"
end
end

defimpl Encode, for: Integer do
def encode(int) do "#{inspect int}" end
def encode(int) do
"#{inspect(int)}"
end
end

defimpl Encode, for: Float do
def encode(float) do "#{inspect float}" end
def encode(float) do
"#{inspect(float)}"
end
end

defimpl Encode, for: List do
Expand All @@ -59,7 +76,7 @@ end
defimpl Encode, for: Array do
def encode(array) do
array
|> Array.to_list
|> Array.to_list()
|> Enum.map(&Encode.encode/1)
|> Enum.join(", ")
|> Utils.wrap("[", "]")
Expand All @@ -69,7 +86,7 @@ end
defimpl Encode, for: Map do
def encode(map) do
map
|> Map.to_list
|> Map.to_list()
|> Enum.map(fn {k, v} -> Encode.encode(k) <> " " <> Encode.encode(v) end)
|> Enum.join(", ")
|> Utils.wrap("{", "}")
Expand Down Expand Up @@ -109,7 +126,8 @@ defimpl Encode, for: Any do
def encode(struct) when is_map(struct) do
Encode.encode(Map.from_struct(struct))
end
def encode(value) do

def encode(value) do
raise %Protocol.UndefinedError{protocol: Encode, value: value}
end
end
28 changes: 18 additions & 10 deletions lib/eden/exception.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
defmodule Eden.Exception do

defmodule Util do
def token_message(token) do
msg = "(#{inspect token.type}) #{inspect token.value}"
msg = "(#{inspect(token.type)}) #{inspect(token.value)}"

if Map.has_key?(token, :location) do
loc = token.location
msg <> " at line #{inspect loc.line} and column #{inspect loc.col}."
msg <> " at line #{inspect(loc.line)} and column #{inspect(loc.col)}."
else
msg
end
Expand Down Expand Up @@ -34,56 +34,64 @@ defmodule Eden.Exception do

defmodule UnexpectedTokenError do
defexception [:message]

def exception(token) do
%UnexpectedTokenError{message: Util.token_message(token)}
end
end

defmodule UnbalancedDelimiterError do
defexception [:message]

def exception(msg) do
%UnbalancedDelimiterError{message: "#{inspect msg}"}
%UnbalancedDelimiterError{message: "#{inspect(msg)}"}
end
end

defmodule OddExpressionCountError do
defexception [:message]

def exception(msg) do
%OddExpressionCountError{message: "#{inspect msg}"}
%OddExpressionCountError{message: "#{inspect(msg)}"}
end
end

defmodule IncompleteTagError do
defexception [:message]

def exception(msg) do
%IncompleteTagError{message: "#{inspect msg}"}
%IncompleteTagError{message: "#{inspect(msg)}"}
end
end

defmodule MissingDiscardExpressionError do
defexception [:message]

def exception(msg) do
%MissingDiscardExpressionError{message: "#{inspect msg}"}
%MissingDiscardExpressionError{message: "#{inspect(msg)}"}
end
end

## Decode Exceptions

defmodule EmptyInputError do
defexception [:message]

def exception(msg) do
%EmptyInputError{message: "#{inspect msg}"}
%EmptyInputError{message: "#{inspect(msg)}"}
end
end

defmodule NotImplementedError do
defexception [:message]

def exception(msg) when is_binary(msg) do
%NotImplementedError{message: msg}
end

def exception({function, arity}) do
function = Atom.to_string function
%NotImplementedError{message: "#{function}/#{inspect arity}"}
function = Atom.to_string(function)
%NotImplementedError{message: "#{function}/#{inspect(arity)}"}
end
end
end
Loading