From c16ab81f29aa6931a86f50b3f32e2462a7bfdb3e Mon Sep 17 00:00:00 2001
From: Michael Maier <zookzook@unitybox.de>
Date: Tue, 10 Dec 2019 20:23:32 +0100
Subject: [PATCH] added some tests for `get_bytes` and `get_size`, bump version
 0.1.3

---
 CHANGELOG.md                       |  11 ++
 README.md                          |   2 +-
 lib/hocon.ex                       | 161 ++++++++++++++++++-----------
 lib/hocon/parser.ex                |  21 +++-
 mix.exs                            |   4 +-
 test/hocon_test.exs                |  59 +----------
 test/parser/basic_usage_test.exs   |   3 +-
 test/parser/byte_units_test.exs    |  20 ++--
 test/parser/error_test.exs         |   2 +-
 test/parser/get_test.exs           |  22 ++++
 test/parser/substitutions_test.exs |  58 +++++++++++
 11 files changed, 225 insertions(+), 138 deletions(-)
 create mode 100644 test/parser/get_test.exs
 create mode 100644 test/parser/substitutions_test.exs

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20b6abc..40a7005 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+## 0.1.3
+
+* Enhancements
+    * added support for size unit format
+    * added more tests 
+    * extended the documentation 
+
+* Bugfixes
+    * The `Hocon.Tokenizer` handles the variable byte length of UTF-8 strings correctly.
+    * Escaping unicodes in quoted string is now supported.
+        
 ## 0.1.2
 
 * Enhancements
diff --git a/README.md b/README.md
index 3187fc6..60f7f87 100644
--- a/README.md
+++ b/README.md
@@ -107,4 +107,4 @@ https://github.com/lightbend/config/blob/master/HOCON.md
 - [ ] allow URL for included files
 - [ ] duration unit format
 - [ ] period unit format
-- [ ] size unit format
\ No newline at end of file
+- [x] size unit format
\ No newline at end of file
diff --git a/lib/hocon.ex b/lib/hocon.ex
index 2ac9aa9..02bcecc 100644
--- a/lib/hocon.ex
+++ b/lib/hocon.ex
@@ -1,32 +1,7 @@
 defmodule Hocon do
   @moduledoc"""
 
-  This module paresed and decodes a hocon configuration string.
-
-  ## [specification](https://github.com/lightbend/config/blob/master/HOCON.md) coverages:
-
-  - [x] parsing JSON
-  - [x] comments
-  - [x] omit root braces
-  - [x] key-value separator
-  - [x] commas are optional if newline is present
-  - [x] whitespace
-  - [x] duplicate keys and object merging
-  - [x] unquoted strings
-  - [x] multi-line strings
-  - [x] value concatenation
-  - [x] object concatenation
-  - [x] array concatenation
-  - [x] path expressions
-  - [x] path as keys
-  - [x] substitutions
-  - [ ] includes
-  - [x] conversion of numerically-indexed objects to arrays
-  - [ ] allow URL for included files
-  - [ ] duration unit format
-  - [ ] period unit format
-  - [x] size unit format
-
+  This module paresed and decodes a [hocon](https://github.com/lightbend/config/blob/master/HOCON.md) configuration string.
 
   ## Example
 
@@ -40,13 +15,13 @@ defmodule Hocon do
 
   The Parser returns a map, because in Elixir it is a common use case to use pattern matching on maps to
   extract specific values and keys. Therefore the `Hocon.decode/2` function returns a map. To support
-  interpreting a value with some family of units, you can call some conversion functions like `get_bytes/1`.
+  interpreting a value with some family of units, you can call some conversion functions like `as_bytes/1`.
 
   ## Example
 
        iex> conf = ~s(limit : "512KB")
        iex> {:ok, %{"limit" => limit}} = Hocon.decode(conf)
-       iex> Hocon.get_bytes(limit)
+       iex> Hocon.as_bytes(limit)
        524288
 
   """
@@ -137,59 +112,125 @@ defmodule Hocon do
   in case of errors.
   """
   def decode!(string, opts \\ []) do
-    with {:ok, result} <- Parser.decode(string, opts) do
-      result
+    Parser.decode!(string, opts)
+  end
+
+  @doc """
+  Returns a value for the `keypath` from a map or a successfull parse HOCON string.
+
+  ## Example
+      iex> conf = Hocon.decode!(~s(a { b { c : "10kb" } }))
+      %{"a" => %{"b" => %{"c" => "10kb"}}}
+      iex> Hocon.get(conf, "a.b.c")
+      "10kb"
+      iex> Hocon.get(conf, "a.b.d")
+      nil
+      iex> Hocon.get(conf, "a.b.d", "1kb")
+      "1kb"
+  """
+  def get(root, keypath, default \\ nil) do
+    keypath = keypath
+              |> String.split(".")
+              |> Enum.map(fn str -> String.trim(str) end)
+    case get_in(root, keypath) do
+       nil -> default
+        other -> other
+    end
+  end
+
+  @doc """
+  Same a `get/3` but the value is interpreted like a number by using the power of 2.
+
+  ## Example
+      iex> conf = Hocon.decode!(~s(a { b { c : "10kb" } }))
+      %{"a" => %{"b" => %{"c" => "10kb"}}}
+      iex> Hocon.get_bytes(conf, "a.b.c")
+      10240
+      iex> Hocon.get_bytes(conf, "a.b.d")
+      nil
+      iex> Hocon.get_bytes(conf, "a.b.d", 1024)
+      1024
+  """
+  def get_bytes(root, keypath, default \\ nil) do
+    keypath = keypath
+              |> String.split(".")
+              |> Enum.map(fn str -> String.trim(str) end)
+    case get_in(root, keypath) do
+      nil -> default
+      other -> as_bytes(other)
     end
   end
 
+  @doc """
+  Same a `get/3` but the value is interpreted like a number by using the power of 10.
+
+  ## Example
+      iex> conf = Hocon.decode!(~s(a { b { c : "10kb" } }))
+      %{"a" => %{"b" => %{"c" => "10kb"}}}
+      iex> Hocon.get_bytes(conf, "a.b.c")
+      10240
+      iex> Hocon.get_bytes(conf, "a.b.d")
+      nil
+      iex> Hocon.get_bytes(conf, "a.b.d", 1024)
+      1024
+  """
+  def get_size(root, keypath, default \\ nil) do
+    keypath = keypath
+              |> String.split(".")
+              |> Enum.map(fn str -> String.trim(str) end)
+    case get_in(root, keypath) do
+      nil -> default
+      other -> as_size(other)
+    end
+  end
 
   @doc """
   Returns the size of the `string` by using the power of 2.
 
   ## Example
-      iex> Hocon.get_bytes("512kb")
+      iex> Hocon.as_bytes("512kb")
       524288
-      iex> Hocon.get_bytes("125 gigabytes")
+      iex> Hocon.as_bytes("125 gigabytes")
       134217728000
   """
-  def get_bytes(value) when is_number(value), do: value
-  def get_bytes(string) when is_binary(string) do
-    get_bytes(Regex.named_captures(~r/(?<value>\d+)(\W)?(?<unit>[[:alpha:]]+)?/, String.downcase(string)))
+  def as_bytes(value) when is_number(value), do: value
+  def as_bytes(string) when is_binary(string) do
+    as_bytes(Regex.named_captures(~r/(?<value>\d+)(\W)?(?<unit>[[:alpha:]]+)?/, String.downcase(string)))
   end
-  def get_bytes(%{"unit" => "", "value" => value}), do: parse_integer(value, 1)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(b byte bytes), do: parse_integer(value, 1)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(k kb kilobyte kilobytes), do: parse_integer(value, @kb)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(m mb megabyte megabytes), do: parse_integer(value, @mb)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(g gb gigabyte gigabytes), do: parse_integer(value, @gb)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(t tb terabyte terabytes), do: parse_integer(value, @tb)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(p pb petabyte petabytes), do: parse_integer(value, @pb)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(e eb exabyte exabytes), do: parse_integer(value, @eb)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(z zb zettabyte zettabytes), do: parse_integer(value, @zb)
-  def get_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(y yb yottabyte yottabytes), do: parse_integer(value, @yb)
+  def as_bytes(%{"unit" => "", "value" => value}), do: parse_integer(value, 1)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(b byte bytes), do: parse_integer(value, 1)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(k kb kilobyte kilobytes), do: parse_integer(value, @kb)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(m mb megabyte megabytes), do: parse_integer(value, @mb)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(g gb gigabyte gigabytes), do: parse_integer(value, @gb)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(t tb terabyte terabytes), do: parse_integer(value, @tb)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(p pb petabyte petabytes), do: parse_integer(value, @pb)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(e eb exabyte exabytes), do: parse_integer(value, @eb)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(z zb zettabyte zettabytes), do: parse_integer(value, @zb)
+  def as_bytes(%{"unit" => unit, "value" => value}) when unit in ~w(y yb yottabyte yottabytes), do: parse_integer(value, @yb)
 
   @doc """
   Returns the size of the `string` by using the power of 10.
 
   ## Example
-      iex> Hocon.get_size("512kb")
+      iex> Hocon.as_size("512kb")
       512000
-      iex> Hocon.get_size("125 gigabytes")
+      iex> Hocon.as_size("125 gigabytes")
       125000000000
   """
-  def get_size(value) when is_number(value), do: value
-  def get_size(string) when is_binary(string) do
-    get_size(Regex.named_captures(~r/(?<value>\d+)(\W)?(?<unit>[[:alpha:]]+)?/, String.downcase(string)))
+  def as_size(value) when is_number(value), do: value
+  def as_size(string) when is_binary(string) do
+    as_size(Regex.named_captures(~r/(?<value>\d+)(\W)?(?<unit>[[:alpha:]]+)?/, String.downcase(string)))
   end
-  def get_size(%{"unit" => "", "value" => value}), do: parse_integer(value, 1)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(b byte bytes), do: parse_integer(value, 1)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(k kb kilobyte kilobytes), do: parse_integer(value, @kb_10)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(m mb megabyte megabytes), do: parse_integer(value, @mb_10)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(g gb gigabyte gigabytes), do: parse_integer(value, @gb_10)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(t tb terabyte terabytes), do: parse_integer(value, @tb_10)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(p pb petabyte petabytes), do: parse_integer(value, @pb_10)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(e eb exabyte exabytes), do: parse_integer(value, @eb_10)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(z zb zettabyte zettabytes), do: parse_integer(value, @zb_10)
-  def get_size(%{"unit" => unit, "value" => value}) when unit in ~w(y yb yottabyte yottabytes), do: parse_integer(value, @yb_10)
+  def as_size(%{"unit" => "", "value" => value}), do: parse_integer(value, 1)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(b byte bytes), do: parse_integer(value, 1)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(k kb kilobyte kilobytes), do: parse_integer(value, @kb_10)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(m mb megabyte megabytes), do: parse_integer(value, @mb_10)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(g gb gigabyte gigabytes), do: parse_integer(value, @gb_10)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(t tb terabyte terabytes), do: parse_integer(value, @tb_10)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(p pb petabyte petabytes), do: parse_integer(value, @pb_10)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(e eb exabyte exabytes), do: parse_integer(value, @eb_10)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(z zb zettabyte zettabytes), do: parse_integer(value, @zb_10)
+  def as_size(%{"unit" => unit, "value" => value}) when unit in ~w(y yb yottabyte yottabytes), do: parse_integer(value, @yb_10)
 
   defp parse_integer(string, factor) do
     with {result, ""} <- Integer.parse(string) do
diff --git a/lib/hocon/parser.ex b/lib/hocon/parser.ex
index f4fe99e..969ab2b 100644
--- a/lib/hocon/parser.ex
+++ b/lib/hocon/parser.ex
@@ -15,16 +15,29 @@ defmodule Hocon.Parser do
 
   """
   def decode(string, opts \\ []) do
+    try do
+      {:ok, decode!(string, opts)}
+    catch
+      error -> error
+    end
+
+  end
+
+  @doc"""
+  Similar to `decode/2` except it will unwrap the error tuple and raise
+  in case of errors.
+  """
+  def decode!(string, opts \\ []) do
     with {:ok, ast} <- Tokenizer.decode(string) do
       with {[], result } <- ast
                             |> contact_rule([])
-                            |> parse_root(),
-                result   <- Document.convert(result, opts) do
-        {:ok, result}
+                            |> parse_root() do
+        Document.convert(result, opts)
       end
     end
   end
 
+
   def contact_rule([], result) do
     Enum.reverse(result)
   end
@@ -137,7 +150,7 @@ defmodule Hocon.Parser do
     {rest, doc} = Document.put(result, to_string(key), value, rest)
     parse_object(rest, doc, root)
   end
-  defp parse_object(tokens, result, root) do
+  defp parse_object(_tokens, _result, _root) do
     throw {:error, "syntax error"}
   end
 
diff --git a/mix.exs b/mix.exs
index 49967d6..bb35015 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,7 +1,7 @@
 defmodule Hocon.MixProject do
   use Mix.Project
 
-  @version "0.1.2"
+  @version "0.1.3"
 
   def project do
     [
@@ -31,8 +31,6 @@ defmodule Hocon.MixProject do
     [
       {:excoveralls, "~> 0.12.1", only: :test},
       {:ex_doc, "~> 0.21", only: :dev, runtime: false}
-      # {:dep_from_hexpm, "~> 0.3.0"},
-      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
     ]
   end
 
diff --git a/test/hocon_test.exs b/test/hocon_test.exs
index 0714bb1..c1cb17c 100644
--- a/test/hocon_test.exs
+++ b/test/hocon_test.exs
@@ -5,12 +5,8 @@ defmodule HoconTest do
   alias Hocon.Tokenizer
 
   test "Parse and show the config.conf" do
-
     {:ok, body} = File.read("./test/data/config.conf")
-    result = Parser.decode(body)
-
-    #IO.puts inspect result
-
+    {:ok, _} = Parser.decode(body)
     assert true
   end
 
@@ -95,31 +91,6 @@ defmodule HoconTest do
     assert {:ok, %{"key" => "horse is my favorite animal"}} == Hocon.decode(~s(key : "horse " is my favorite animal))
   end
 
-  test "Parsing substitutions" do
-    assert {:ok, %{"key" => "${animal.favorite} is my favorite animal", "animal" => %{"favorite" => "dog"}}} == Hocon.decode(~s(animal { favorite : "dog" }, key : """${animal.favorite} is my favorite animal"""))
-    assert {:ok, %{"key" => "dog is my favorite animal", "animal" => %{"favorite" => "dog"}}} == Hocon.decode(~s(animal { favorite : "dog" }, key : ${animal.favorite} is my favorite animal))
-    assert {:ok, %{"key" => "dog is my favorite animal", "animal" => %{"favorite" => "dog"}}} == Hocon.decode(~s(animal { favorite : "dog" }, key : ${animal.favorite}" is my favorite animal"))
-    assert catch_throw(Hocon.decode(~s(key : ${animal.favorite}" is my favorite animal"))) == {:not_found, "animal.favorite"}
-    assert {:ok, %{"key" => "Max limit is 10", "limit" => %{"max" => 10}}} == Hocon.decode(~s(limit { max : 10 }, key : Max limit is ${limit.max}))
-    assert {:ok, %{"key" => "Max limit is ${limit.max}", "limit" => %{"max" => 10}}} == Hocon.decode(~s(limit { max : 10 }, key : """Max limit is ${limit.max}"""))
-    assert {:ok, %{"key" => "Max limit is ${limit.max}", "limit" => %{"max" => 10}}} == Hocon.decode(~s(limit { max : 10 }, key : "Max limit is ${limit.max}"))
-  end
-
-  test "Parsing complex substitutions" do
-    assert {:ok, %{"animal" => %{"favorite" => "dog"}, "a" => %{"b" => %{"c" => "dog"}}}} == Hocon.decode(~s(animal { favorite : "dog" }, a { b { c : ${animal.favorite}}}))
-    assert {:ok, %{"bar" => %{"baz" => 42, "foo" => 42}}} == Hocon.decode(~s(bar : { foo : 42, baz : ${bar.foo}}))
-    assert {:ok, %{"bar" => %{"baz" => 43, "foo" => 43}}} == Hocon.decode(~s(bar : { foo : 42, baz : ${bar.foo} }\nbar : { foo : 43 }))
-    assert {:ok, %{"bar" => %{"a" => 4, "b" => 3}, "foo" => %{"c" => 3, "d" => 4}}} == Hocon.decode(~s(bar : { a : ${foo.d}, b : 1 }\nbar.b = 3\nfoo : { c : ${bar.b}, d : 2 }\nfoo.d = 4))
-    assert {:ok, %{"a" => "2 2", "b" => 2}} == Hocon.decode(~s(a : ${b}, b : 2\n a : ${a} ${b}))
-  end
-
-  test "Parsing self-references substitutions" do
-    assert {:ok, %{"foo" => %{"a" => 2, "c" => 1}}} == Hocon.decode(~s(foo : { a : { c : 1 } }\nfoo : ${foo.a}\nfoo : { a : 2 }))
-    assert {:ok, %{"foo" => "1 2"}} == Hocon.decode(~s(foo : { bar : 1, baz : 2 }\nfoo : ${foo.bar} ${foo.baz}))
-    assert {:ok, %{"foo" => "1 2", "baz" => 2}} == Hocon.decode(~s(baz : 2\nfoo : { bar : 1, baz : 2 }\nfoo : ${foo.bar} ${baz}))
-    assert {:ok, %{"path" => "a:b:c:d"}} == Hocon.decode(~s(path : "a:b:c"\npath : ${path}":d"))
-    assert catch_throw(Hocon.decode(~s(foo : { bar : 1 }\nfoo : ${foo.bar} ${foo.baz}))) == {:not_found, "foo.foo.baz"}
-  end
 
   test "Parsing json" do
     assert {:ok, %{"a" => %{"b" => "c"}}} == Hocon.decode(~s({"a" : { "b" : "c"}}))
@@ -127,14 +98,6 @@ defmodule HoconTest do
     assert {:ok, %{"a" => "b", "c" => ["a", "b", "c"], "x" => 10.99}} == Hocon.decode(~s({"a" : "b", "c" : ["a", "b", "c"], "x" : 10.99}))
   end
 
-  test "Parsing substitutions with cycles" do
-    assert catch_throw(Hocon.decode(~s(bar : ${foo}\nfoo : ${bar}))) == {:circle_detected, "foo"}
-    assert catch_throw(Hocon.decode(~s(a : ${b}\nb : ${c}\nc : ${a}))) == {:circle_detected, "b"}
-    assert catch_throw(Hocon.decode(~s(a : 1\nb : 2\na : ${b}\nb : ${a}))) == {:circle_detected, "b"}
-    assert catch_throw(Hocon.decode(~s(a : { b : ${a} }))) == {:circle_detected, "a"}
-    assert catch_throw(Hocon.decode(~s(a : { b : ${x} }))) == {:not_found, "x"}
-  end
-
   test "Parsing unquoted strings as values" do
     assert {:ok, %{"a" => "c"}} == Hocon.decode(~s({a : b\n a : c}))
   end
@@ -143,24 +106,4 @@ defmodule HoconTest do
     assert {:ok, %{"a" => %{"b" => %{"c" => 1}}}} == Hocon.decode(~s({"a" { "b" { c : 1 }}}))
   end
 
-  test "Parsing substitutions with environment variables" do
-    System.put_env("MY_HOME", "/home/greta")
-    assert {:ok, %{"path" => "/home/greta"}} == Hocon.decode(~s(path : ${MY_HOME}))
-    System.put_env("MY_HOME", "/home")
-    assert {:ok, %{"path" => "/home/greta"}} == Hocon.decode(~s(path : ${MY_HOME}\n path : ${path}"/greta"))
-    assert {:ok, %{"path" => ["/home", "usr/bin"]}} == Hocon.decode(~s(path : [${MY_HOME}]\n path : ${path} [ /usr/bin ]))
-  end
-
-  test "Parsing  += field separator" do
-    assert {:ok, %{"a" => [1, "a"]}} == Hocon.decode(~s(a += 1\n a+= a))
-    assert {:ok, %{"a" => [1, "a", 2, 3], "b" => 3}} == Hocon.decode(~s(b : 3, a += 1\n a+= a\n a += 2\n a += ${b}))
-    assert {:ok, %{"b" => 3, "dic" => %{"a" => [1, "a", 2, 3]}}} == Hocon.decode(~s(b : 3, dic { a += 1\n a+= a\n a += 2\n a += ${b} }))
-  end
-
-  test "Parsing optional substitutions " do
-    assert {:ok, %{"path" => ""}} == Hocon.decode(~s(path : ${?THE_HOME}))
-    assert {:ok, %{"a" => [1, "a", 2, ""]}} == Hocon.decode(~s(a += 1\n a+= a\n a += 2\n a += ${?b}))
-    assert {:ok, %{"bar" => %{"baz" => "", "fooz" => 42}}} == Hocon.decode(~s(bar : { fooz : 42, baz : ${?bar.foo}}))
-  end
-
 end
diff --git a/test/parser/basic_usage_test.exs b/test/parser/basic_usage_test.exs
index 68dc361..61adc27 100644
--- a/test/parser/basic_usage_test.exs
+++ b/test/parser/basic_usage_test.exs
@@ -4,7 +4,8 @@ defmodule Parser.BasicUsageTest do
   test "parsing a simple object" do
     assert {:ok, %{"key" => "value"}} == Hocon.decode(~s(key = value))
     assert %{"key" => "value"} == Hocon.decode!(~s(key = value))
-    assert catch_throw(Hocon.decode(~s({a : b :}))) ==  {:error, "syntax error"}
+    assert {:error, "syntax error"} == Hocon.decode(~s({a : b :}))
+    assert catch_throw(Hocon.decode!(~s({a : b :}))) ==  {:error, "syntax error"}
     assert catch_throw(Hocon.decode!(~s({a : b :}))) ==  {:error, "syntax error"}
   end
 
diff --git a/test/parser/byte_units_test.exs b/test/parser/byte_units_test.exs
index 1758642..8877f2e 100644
--- a/test/parser/byte_units_test.exs
+++ b/test/parser/byte_units_test.exs
@@ -21,9 +21,9 @@ defmodule Parser.ByteUnitsTest do
   @zb_10 @eb_10 * 1000
   @yb_10 @zb_10 * 1000
 
-  test "get_bytes/1" do
-    assert Hocon.get_bytes(10) == 10
-    assert Hocon.get_bytes("10") == 10
+  test "as_bytes/1" do
+    assert Hocon.as_bytes(10) == 10
+    assert Hocon.as_bytes("10") == 10
     assert assert_bytes(~w(b byte bytes), 1) == true
     assert assert_bytes(~w(k kb kilobyte kilobytes), @kb) == true
     assert assert_bytes(~w(m mb megabyte megabytes), @mb) == true
@@ -35,9 +35,9 @@ defmodule Parser.ByteUnitsTest do
     assert assert_bytes(~w(y yb yottabyte yottabytes), @yb) == true
   end
 
-  test "get_size/1" do
-    assert Hocon.get_size(10) == 10
-    assert Hocon.get_size("10") == 10
+  test "as_size/1" do
+    assert Hocon.as_size(10) == 10
+    assert Hocon.as_size("10") == 10
     assert assert_size(~w(b byte bytes), 1) == true
     assert assert_size(~w(k kb kilobyte kilobytes), @kb_10) == true
     assert assert_size(~w(m mb megabyte megabytes), @mb_10) == true
@@ -55,8 +55,8 @@ defmodule Parser.ByteUnitsTest do
       result = value * factor
       string_1 = to_string(value) <> " " <> unit
       string_2 = to_string(value) <> unit
-      Hocon.get_bytes(string_1) == result &&
-      Hocon.get_bytes(string_2) == result
+      Hocon.as_bytes(string_1) == result &&
+      Hocon.as_bytes(string_2) == result
     end)
   end
 
@@ -66,8 +66,8 @@ defmodule Parser.ByteUnitsTest do
       result = value * factor
       string_1 = to_string(value) <> " " <> unit
       string_2 = to_string(value) <> unit
-      Hocon.get_size(string_1) == result &&
-      Hocon.get_size(string_2) == result
+      Hocon.as_size(string_1) == result &&
+      Hocon.as_size(string_2) == result
     end)
   end
 end
diff --git a/test/parser/error_test.exs b/test/parser/error_test.exs
index e17f039..f860c6c 100644
--- a/test/parser/error_test.exs
+++ b/test/parser/error_test.exs
@@ -2,7 +2,7 @@ defmodule Parser.ErrorTest do
   use ExUnit.Case, async: true
 
   test "parsing a wrong object" do
-    assert catch_throw(Hocon.decode(~s({a : b :}))) ==  {:error, "syntax error"}
+    assert catch_throw(Hocon.decode!(~s({a : b :}))) ==  {:error, "syntax error"}
   end
 
 end
diff --git a/test/parser/get_test.exs b/test/parser/get_test.exs
new file mode 100644
index 0000000..fd247c9
--- /dev/null
+++ b/test/parser/get_test.exs
@@ -0,0 +1,22 @@
+defmodule Parser.GetTest do
+  use ExUnit.Case, async: true
+
+  test "get a nested value" do
+    conf = Hocon.decode!(~s(a { b { c : "10kb" } }))
+    assert "10kb" == Hocon.get(conf, "a.b.c", nil)
+    assert "10kb" == Hocon.get(conf, "a.b.d", "10kb")
+  end
+
+  test "get a nested value as bytes" do
+    conf = Hocon.decode!(~s(a { b { c : "10kb" } }))
+    assert (10*1024) == Hocon.get_bytes(conf, "a.b.c", nil)
+    assert (10*1024) == Hocon.get_bytes(conf, "a.b.d", 10*1024)
+  end
+
+  test "get a nested value as size" do
+    conf = Hocon.decode!(~s(a { b { c : "10kb" } }))
+    assert (10*1000) == Hocon.get_size(conf, "a.b.c", nil)
+    assert (10*1000) == Hocon.get_size(conf, "a.b.d", 10*1000)
+  end
+
+end
diff --git a/test/parser/substitutions_test.exs b/test/parser/substitutions_test.exs
new file mode 100644
index 0000000..0a86c05
--- /dev/null
+++ b/test/parser/substitutions_test.exs
@@ -0,0 +1,58 @@
+defmodule Parser.SubstitutionsTest do
+  use ExUnit.Case, async: true
+
+  test "Parsing substitutions" do
+    assert {:ok, %{"key" => "${animal.favorite} is my favorite animal", "animal" => %{"favorite" => "dog"}}} == Hocon.decode(~s(animal { favorite : "dog" }, key : """${animal.favorite} is my favorite animal"""))
+    assert {:ok, %{"key" => "dog is my favorite animal", "animal" => %{"favorite" => "dog"}}} == Hocon.decode(~s(animal { favorite : "dog" }, key : ${animal.favorite} is my favorite animal))
+    assert {:ok, %{"key" => "dog is my favorite animal", "animal" => %{"favorite" => "dog"}}} == Hocon.decode(~s(animal { favorite : "dog" }, key : ${animal.favorite}" is my favorite animal"))
+    assert catch_throw(Hocon.decode!(~s(key : ${animal.favorite}" is my favorite animal"))) == {:not_found, "animal.favorite"}
+    assert {:ok, %{"key" => "Max limit is 10", "limit" => %{"max" => 10}}} == Hocon.decode(~s(limit { max : 10 }, key : Max limit is ${limit.max}))
+    assert {:ok, %{"key" => "Max limit is ${limit.max}", "limit" => %{"max" => 10}}} == Hocon.decode(~s(limit { max : 10 }, key : """Max limit is ${limit.max}"""))
+    assert {:ok, %{"key" => "Max limit is ${limit.max}", "limit" => %{"max" => 10}}} == Hocon.decode(~s(limit { max : 10 }, key : "Max limit is ${limit.max}"))
+  end
+
+  test "Parsing complex substitutions" do
+    assert {:ok, %{"animal" => %{"favorite" => "dog"}, "a" => %{"b" => %{"c" => "dog"}}}} == Hocon.decode(~s(animal { favorite : "dog" }, a { b { c : ${animal.favorite}}}))
+    assert {:ok, %{"bar" => %{"baz" => 42, "foo" => 42}}} == Hocon.decode(~s(bar : { foo : 42, baz : ${bar.foo}}))
+    assert {:ok, %{"bar" => %{"baz" => 43, "foo" => 43}}} == Hocon.decode(~s(bar : { foo : 42, baz : ${bar.foo} }\nbar : { foo : 43 }))
+    assert {:ok, %{"bar" => %{"a" => 4, "b" => 3}, "foo" => %{"c" => 3, "d" => 4}}} == Hocon.decode(~s(bar : { a : ${foo.d}, b : 1 }\nbar.b = 3\nfoo : { c : ${bar.b}, d : 2 }\nfoo.d = 4))
+    assert {:ok, %{"a" => "2 2", "b" => 2}} == Hocon.decode(~s(a : ${b}, b : 2\n a : ${a} ${b}))
+  end
+
+  test "Parsing self-references substitutions" do
+    assert {:ok, %{"foo" => %{"a" => 2, "c" => 1}}} == Hocon.decode(~s(foo : { a : { c : 1 } }\nfoo : ${foo.a}\nfoo : { a : 2 }))
+    assert {:ok, %{"foo" => "1 2"}} == Hocon.decode(~s(foo : { bar : 1, baz : 2 }\nfoo : ${foo.bar} ${foo.baz}))
+    assert {:ok, %{"foo" => "1 2", "baz" => 2}} == Hocon.decode(~s(baz : 2\nfoo : { bar : 1, baz : 2 }\nfoo : ${foo.bar} ${baz}))
+    assert {:ok, %{"path" => "a:b:c:d"}} == Hocon.decode(~s(path : "a:b:c"\npath : ${path}":d"))
+    assert catch_throw(Hocon.decode!(~s(foo : { bar : 1 }\nfoo : ${foo.bar} ${foo.baz}))) == {:not_found, "foo.foo.baz"}
+  end
+
+  test "Parsing substitutions with cycles" do
+    assert catch_throw(Hocon.decode!(~s(bar : ${foo}\nfoo : ${bar}))) == {:circle_detected, "foo"}
+    assert catch_throw(Hocon.decode!(~s(a : ${b}\nb : ${c}\nc : ${a}))) == {:circle_detected, "b"}
+    assert catch_throw(Hocon.decode!(~s(a : 1\nb : 2\na : ${b}\nb : ${a}))) == {:circle_detected, "b"}
+    assert catch_throw(Hocon.decode!(~s(a : { b : ${a} }))) == {:circle_detected, "a"}
+    assert catch_throw(Hocon.decode!(~s(a : { b : ${x} }))) == {:not_found, "x"}
+  end
+
+  test "Parsing substitutions with environment variables" do
+    System.put_env("MY_HOME", "/home/greta")
+    assert {:ok, %{"path" => "/home/greta"}} == Hocon.decode(~s(path : ${MY_HOME}))
+    System.put_env("MY_HOME", "/home")
+    assert {:ok, %{"path" => "/home/greta"}} == Hocon.decode(~s(path : ${MY_HOME}\n path : ${path}"/greta"))
+    assert {:ok, %{"path" => ["/home", "usr/bin"]}} == Hocon.decode(~s(path : [${MY_HOME}]\n path : ${path} [ /usr/bin ]))
+  end
+
+  test "Parsing  += field separator" do
+    assert {:ok, %{"a" => [1, "a"]}} == Hocon.decode(~s(a += 1\n a+= a))
+    assert {:ok, %{"a" => [1, "a", 2, 3], "b" => 3}} == Hocon.decode(~s(b : 3, a += 1\n a+= a\n a += 2\n a += ${b}))
+    assert {:ok, %{"b" => 3, "dic" => %{"a" => [1, "a", 2, 3]}}} == Hocon.decode(~s(b : 3, dic { a += 1\n a+= a\n a += 2\n a += ${b} }))
+  end
+
+  test "Parsing optional substitutions " do
+    assert {:ok, %{"path" => ""}} == Hocon.decode(~s(path : ${?THE_HOME}))
+    assert {:ok, %{"a" => [1, "a", 2, ""]}} == Hocon.decode(~s(a += 1\n a+= a\n a += 2\n a += ${?b}))
+    assert {:ok, %{"bar" => %{"baz" => "", "fooz" => 42}}} == Hocon.decode(~s(bar : { fooz : 42, baz : ${?bar.foo}}))
+  end
+
+end