Skip to content
Closed
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
6 changes: 6 additions & 0 deletions bench/snapshots/2018-09-07_22-57-47.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
duration:1.0;mem stats:false;sys mem stats:false
module;test;tags;iterations;elapsed
BasicBench macro_underscore - long 50000 2586714
BasicBench macro_underscore - short 100000 1585019
BasicBench regex_underscore - long 20000 1675608
BasicBench regex_underscore - short 100000 2475117
6 changes: 6 additions & 0 deletions bench/snapshots/2018-09-07_22-58-15.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
duration:1.0;mem stats:false;sys mem stats:false
module;test;tags;iterations;elapsed
BasicBench macro_underscore - long 50000 2669197
BasicBench macro_underscore - short 100000 1551578
BasicBench regex_underscore - long 20000 1587548
BasicBench regex_underscore - short 100000 2386194
67 changes: 1 addition & 66 deletions lib/atomic_map.ex
Original file line number Diff line number Diff line change
@@ -1,68 +1,3 @@
defmodule AtomicMap.Opts do
@moduledoc ~S"""
Set any value to `false` to disable checking for that kind of key.
"""
defstruct safe: true,
underscore: true,
ignore: false
end

defmodule AtomicMap do
def convert(v, opts \\ %{})

def convert(struct=%{__struct__: type}, opts=%AtomicMap.Opts{}) do
struct
|> Map.from_struct()
|> convert(opts)
|> Map.put(:__struct__, type)
end
def convert(map, opts=%AtomicMap.Opts{}) when is_map(map) do
map |> Enum.reduce(%{}, fn({k,v}, acc)->
k = k |> convert_key(opts)
v = v |> convert(opts)
acc |> Map.put(k, v)
end)
end
def convert(list, opts=%AtomicMap.Opts{}) when is_list(list) do
list |> Enum.map(fn(x)-> convert(x, opts) end)
end
def convert(tuple, opts=%AtomicMap.Opts{}) when is_tuple(tuple) do
tuple |> Tuple.to_list |> convert(opts) |> List.to_tuple()
end
def convert(v, _opts=%AtomicMap.Opts{}), do: v

# if you pass a plain map or keyword list as opts, those will match and convert it to struct
def convert(v, opts=%{}), do: convert(v, struct(AtomicMap.Opts, opts))
def convert(v, opts) when is_list(opts), do: convert(v, Enum.into(opts, %{}))

defp convert_key(k, opts) do
k
|> as_underscore(opts.underscore)
|> as_atom(opts.safe, opts.ignore)
end

# params: key, safe, ignore
defp as_atom(s, true, true) when is_binary(s) do
try do
as_atom(s, true, false)
rescue
ArgumentError -> s
end
end
defp as_atom(s, true, false) when is_binary(s) do
s |> String.to_existing_atom()
end
defp as_atom(s, false, _) when is_binary(s), do: s |> String.to_atom()
defp as_atom(s, _, _), do: s

defp as_underscore(s, true) when is_number(s), do: s
defp as_underscore(s, true) when is_binary(s), do: s |> do_underscore()
defp as_underscore(s, true) when is_atom(s), do: s |> Atom.to_string() |> as_underscore(true)
defp as_underscore(s, false), do: s

defp do_underscore(s) do
s
|> Macro.underscore()
|> String.replace(~r/-/, "_")
end
use AtomicMap.Base
end
51 changes: 51 additions & 0 deletions lib/atomic_map/base.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule AtomicMap.Base do
@moduledoc """
Base module for AtomicMap.
"""

import Macro, only: [escape: 1]
alias AtomicMap.{Key, Opts}

@doc false
@callback convert(data :: any(), opts :: Opts.t) :: map()

@doc false
defmacro __using__(opts \\ []) do
atomic_map_opts = struct(Opts, opts)

quote do
Module.register_attribute(__MODULE__, :regex_patterns, [accumulate: true])

@before_compile AtomicMap.Base

import AtomicMap.Base, only: [replace: 2]
alias AtomicMap.Converters


def convert(term, opts \\ unquote(escape(atomic_map_opts)))
def convert(term, %Opts{} = opts) do
conver_key_fn = Key.convert_fn(__MODULE__.__regex_patterns__(), opts)
Converters.convert(term, conver_key_fn)
end
def convert(term, %{} = opts) do
convert(term, struct(Opts, opts))
end
def convert(term, opts) when is_list(opts) do
convert(term, Enum.into(opts, %{}))
end
end
end

defmacro __before_compile__ _ do
quote do
def __regex_patterns__, do: @regex_patterns
end
end

defmacro replace(pattern, replacement) do
quote do
Module.put_attribute __MODULE__, :regex_patterns,
{unquote(pattern), unquote(replacement)}
end
end
end
55 changes: 55 additions & 0 deletions lib/atomic_map/converters.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defprotocol AtomicMap.Converters do
@moduledoc """
The `AtomicMap.Converters` protocol.
"""

@doc """
Convert
"""
@spec convert(
term :: term(),
convert_key_fn :: (any() -> atom())
) :: map()
@fallback_to_any true
def convert(term, convert_key_fn)
end


alias AtomicMap.Converters

defimpl AtomicMap.Converters, for: Map do
def convert(term, convert_key_fn) do
Enum.reduce(term, %{}, fn {k, v}, acc ->
key = convert_key_fn.(k)
val = Converters.convert(v, convert_key_fn)

Map.put(acc, key, val)
end)
end
end

defimpl AtomicMap.Converters, for: List do
def convert(term, convert_key_fn) do
Enum.map(term, & Converters.convert(&1, convert_key_fn))
end
end

defimpl AtomicMap.Converters, for: Tuple do
def convert(term, convert_key_fn) do
term
|> Tuple.to_list()
|> Converters.convert(convert_key_fn)
|> List.to_tuple()
end
end

defimpl AtomicMap.Converters, for: Any do
def convert(%{__struct__: type} = term, convert_key_fn) do
term
|> Map.from_struct()
|> Converters.convert(convert_key_fn)
|> Map.put(:__struct__, type)
end

def convert(term, _), do: term
end
62 changes: 62 additions & 0 deletions lib/atomic_map/key.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
defmodule AtomicMap.Key do
@moduledoc false

alias AtomicMap.Opts

@doc """
The `convert_fn/2` returns a function that convert key into atom
"""
@spec convert_fn(list(), Opts.t) :: (any() -> atom())
def convert_fn(regex_patterns \\ [], %Opts{} = opts) do
& &1
|> as_regex_replace(regex_patterns)
|> as_underscore(opts.underscore)
|> as_atom(opts.safe, opts.ignore)
end


## Private

# params: key, safe, ignore
defp as_atom(key, true, true) when is_binary(key) do
try do
as_atom(key, true, false)
rescue
ArgumentError -> key
end
end
defp as_atom(key, true, false) when is_binary(key) do
String.to_existing_atom(key)
end
defp as_atom(key, false, _) when is_binary(key) do
String.to_atom(key)
end
defp as_atom(key, _, _), do: key

# Underscore
defp as_underscore(key, true) when is_number(key), do: key
defp as_underscore(key, true) when is_binary(key) do
do_underscore(key)
end
defp as_underscore(key, true) when is_atom(key) do
key
|> Atom.to_string()
|> as_underscore(true)
end
defp as_underscore(key, false), do: key

defp do_underscore(key) do
key
|> Macro.underscore()
|> String.replace(~r/-/, "_")
end

# Custom replacement
defp as_regex_replace(key, regex_patterns) do
Enum.reduce(regex_patterns, key, & do_regex_replace/2)
end

defp do_regex_replace({pattern, replacement}, key) do
Regex.replace(pattern, key, replacement)
end
end
8 changes: 8 additions & 0 deletions lib/atomic_map/opts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule AtomicMap.Opts do
@moduledoc ~S"""
Set any value to `false` to disable checking for that kind of key.
"""
defstruct safe: true,
underscore: true,
ignore: false
end