Skip to content

psfblair/exdn

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Exdn - an edn parser for the Elixir platform

Exdn is a two-way translator between Elixir data structures and data following the edn specification; it wraps the erldn edn parser for Erlang, with some changes in the data formats (see below).

Installation

Once available in Hex, the package can be installed by adding exdn to your list of dependencies in mix.exs:

def deps do
  [{:exdn, "~> 2.2.1"}]
end

Usage

iex> Exdn.to_elixir! "41.2"
  41.2

iex> Exdn.to_elixir! ":foo"
  :foo

iex> Exdn.to_elixir! "true"
  true

iex> Exdn.to_elixir! "nil"
  nil

iex> Exdn.to_elixir! "\"asd\""
  "asd"

# Char
iex> Exdn.to_elixir! "\\a"
  "a"

# Symbol
iex> Exdn.to_elixir! "foo"
  {:symbol, :foo}

# edn vectors become Elixir lists:
iex> Exdn.to_elixir! "[1 :foo]"
  [1, :foo]

# edn lists are always tagged. Since Datomic is a principal use of edn, and since lists are 
# used in Datomic primarily for executable expressions rather than as data structures, we 
# use Elixir lists to represent vectors and keep edn lists specially tagged:    
iex> Exdn.to_elixir! "(1, :foo)"
  {:list, [1, :foo]}

# edn sets become Elixir sets:
iex> Exdn.to_elixir! "\#{1 \\a 1}"
  #MapSet<[1, "a"]>

# Maps become Elixir maps:
iex> Exdn.to_elixir! "{1 :foo, 2 :bar}"
  %{1 => :foo, 2 => :bar}
  
# You can also transform maps to Elixir structs by providing your own converter in the second argument:
iex> defmodule FooStruct do
...>    defstruct foo: "default"
...> end
iex> converter = fn map ->
...>    case map do
...>       %{:foo => _} -> struct(FooStruct, map)
...>       anything_else -> anything_else
...>     end
...>   end
iex>  Exdn.to_elixir! "{:foo 1, :bar 2}", converter
   %FooStruct{foo: 1}      

# Tagged expressions are converted. Standard converters for #inst and #uuid are included:
iex> Exdn.to_elixir! "#inst \"1985-04-12T23:20:50.52Z\"" 
  �lendar.DateTime{abbr: "UTC", day: 12, hour: 23, min: 20, month: 4, sec: 50,
    std_off: 0, timezone: "Etc/UTC", usec: 520000, utc_off: 0, year: 1985}

iex> Exdn.to_elixir! "#uuid \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\"" 
  "f81d4fae-7dec-11d0-a765-00a0c91e6bf6"

# You can provide your own handlers for tagged expressions:
iex> handler = fn(_tag, val, _converter, _handlers) -> val <> "-converted" end
iex> identity = &(&1)
iex> Exdn.to_elixir! "#foo \"blarg\"", identity, [{:foo, handler}] 
  "blarg-converted"

# There is a safe version that doesn't raise exceptions:
iex> Exdn.to_elixir "{1 :foo, 2 :bar}"
  {:ok, %{1 => :foo, 2 => :bar}}

iex> Exdn.to_elixir "{:foo, \\a, \\b #foo \"blarg\" }"
  {:error, %RuntimeError{:message => "Handler not found for tag foo with tagged expression blarg"}}

# There is also an "intermediate" representation that can be converted back to edn. The 
# difference is that chars and tagged expressions are converted to tagged tuples:    
iex> Exdn.to_reversible( "\\a" )
  {:char, ?a}

iex> Exdn.to_reversible "#inst \"1985-04-12T23:20:50.52Z\""
  {:tag, :inst, "1985-04-12T23:20:50.52Z"}

# An unknown tag raises no error when using the reversible conversion:
iex> Exdn.to_reversible "#foo \"blarg\""
  {:tag, :foo, "blarg"}

# The intermediate representation can be converted back to edn:
iex> Exdn.from_elixir! 41.2
  "41.2"

iex> Exdn.from_elixir! :foo
  ":foo"

iex> Exdn.from_elixir! true
  "true"

iex> Exdn.from_elixir! nil
  "nil"

iex> Exdn.from_elixir! "asd"
  "\"asd\""

iex> Exdn.from_elixir! {:char, ?a}
  "\\a"

iex> Exdn.from_elixir! {:symbol, :foo}
  "foo"

iex> Exdn.from_elixir! [1, :foo]
  "[1 :foo]"

iex> Exdn.from_elixir! {:list, [1, :foo]}
  "(1 :foo)"

iex> Exdn.from_elixir! MapSet.new([1, :foo])
  "\#{1 :foo}"

iex> Exdn.from_elixir! %{1 => :foo, 2 => :bar}
  "{1 :foo 2 :bar}"

iex> Exdn.from_elixir! %SomeStruct{foo: 1, bar: 2}
  "{:foo 1 :bar 2}"

iex> Exdn.from_elixir! {:tag, :inst, "1985-04-12T23:20:50.52Z"}
  "#inst \"1985-04-12T23:20:50.52Z\""

# There is a safe version for converting back to edn that doesn't raise exceptions:
iex> Exdn.from_elixir %{:foo => {:char, ?a}, {:char, ?b} => {:tag, :inst, "1985-04-12T23:20:50.52Z"} }
  {:ok, "{:foo \\a \\b #inst \"1985-04-12T23:20:50.52Z\"}" }

# There are also converters you can use if you want to handle chars, lists, or tags on an ad-hoc basis:
iex> Exdn.tagged_list_to_list {:list, [:foo]}
  [:foo]

iex> Exdn.tagged_char_to_string {:char, ?a}
  "a"

iex> handler = fn(_tag, val, _handlers) -> val <> "-converted" end
iex> Exdn.evaluate_tagged_expr {:tag, :foo, "blarg"}, [{:foo, handler}])
  "blarg-converted"

API

to_elixir!/1

parses an edn string into an Elixir data structure; this is not a reversible conversion as chars are converted to strings, and tagged expressions are interpreted. This function can throw exceptions; for example, if a tagged expression cannot be interpreted.

to_elixir!/2

the second argument allows you to supply your own converter function for any of the incoming data; your function will be applied recursively to every value in the edn parse tree. Generally you will want to use it to convert maps to structs, but you can use it on any incoming data value, including tagged values. (The tagged value is first passed to the converter and then, if it is still tagged, to the tagged value handlers (see the three-argument version of to_elixir! below for more on handlers). The conversion function should be a function of one parameter; generally you will want to pattern-match on incoming values with a default clause that returns the untransformed value.

to_elixir!/3

the third argument allows you to supply your own handlers for the interpretation of tagged expressions. These should be in the form of a keyword list. The first element of each pair should be a keyword corresponding to the tag, and the second element a function of four parameters (tag, value, converter, handlers) that handles the tagged values. Generally you will want to operate only on the value, but you can also use the tag, the converter passed to to_elixir! as its second parameter, or the handlers passed to to_elixir! as the third parameter.

to_elixir/1

also parses an edn string into an Elixir data structure, but does not throw exceptions. The parse result is returned as the second element of a pair whose first element is :ok -- if there is an error the first element will be :error and the second the error that was raised.

to_elixir/2

safe version of to_elixir!/2.

to_elixir/3

safe version of to_elixir!/3.

from_elixir!/1

converts an Elixir data structure in the "reversible" format (see below) into an edn string. Will raise exceptions if the data structure cannot be converted. Structs will be converted to edn maps.

from_elixir/1

safe version of from_elixir!/1 -- the edn string is returned as the second element of a pair whose first element is :ok -- if there is an error the first element will be :error and the second the error that was raised.

to_reversible/1

parses an edn string into an Elixir data structure, but in a reversible way -- chars and tagged expressions are represented using tuples whose first element is :char or :tag, respectively.

Type Mappings

edn Elixir generated by to_elixir functions when no custom converter is provided
integer integer
float float
boolean boolean
nil nil (atom)
char string
string string
list tagged list {:list, [...]}
vector list
map map
set mapset
symbol tagged atom {:symbol, atom}
tagged elements call registered handler for that tag, fail if not found

Reversible Mappings

edn Elixir generated by to_reversible or accepted by from_elixir functions
integer integer
float float
boolean boolean
nil nil (atom)
char tagged integer {:char, <integer>}
string string
list tagged list {:list, [...]}
vector list
map map
struct map
set mapset
symbol tagged atom {:symbol, atom}
tagged elements tagged tuple with tag and value {:tag, Symbol, Value}

Author

psfblair

License

MIT license