lib/json_ext.ex

defmodule Bson.JsonExt do
  @moduledoc """
  This module provides two helper methods `dump` and `load`
  methods and provide explicit BSON conversion to and from json compatible Elixir terms.
  This allows for specialized encoding and decoding of BSON documents into
  `Mongo Extended JSON <http://www.mongodb.org/display/DOCS/Mongo+Extended+JSON>`_'s *Strict* mode.
  This lets you encode / decode BSON documents to json even when
  they use special BSON types.

  Usage:
  ```

    iex> term = %{
    ...> a:  -4.230845,
    ...> b:  "hello",
    ...> c:  %{x: -1, y: 2.2001},
    ...> d:  [23, 45, 200],
    ...> eeeeeeeee:  %Bson.Bin{ subtype: Bson.Bin.subtyx(:binary),
    ...>               bin:  <<200, 12, 240, 129, 100, 90, 56, 198, 34, 0, 0>>},
    ...> f:  %Bson.Bin{ subtype: Bson.Bin.subtyx(:function),
    ...>               bin:  <<200, 12, 240, 129, 100, 90, 56, 198, 34, 0, 0>>},
    ...> g:  %Bson.Bin{ subtype: Bson.Bin.subtyx(:uuid),
    ...>               bin:  <<49, 0, 0, 0, 4, 66, 83, 79, 78, 0, 38, 0, 0, 0,
    ...>                       2, 48, 0, 8, 0, 0, 0, 97, 119, 101, 115, 111, 109,
    ...>                       101, 0, 1, 49, 0, 51, 51, 51, 51, 51, 51, 20, 64,
    ...>                       16, 50, 0, 194, 7, 0, 0, 0, 0>>},
    ...> h:  %Bson.Bin{ subtype: Bson.Bin.subtyx(:md5),
    ...>               bin:  <<200, 12, 240, 129, 100, 90, 56, 198, 34, 0, 0>>},
    ...> i:  %Bson.Bin{ subtype: Bson.Bin.subtyx(:user),
    ...>               bin:  <<49, 0, 0, 0, 4, 66, 83, 79, 78, 0, 38, 0, 0, 0, 2,
    ...>                       48, 0, 8, 0, 0, 0, 97, 119, 101, 115, 111, 109, 101,
    ...>                       0, 1, 49, 0, 51, 51, 51, 51, 51, 51, 20, 64, 16, 50,
    ...>                       0, 194, 7, 0, 0, 0, 0>>},
    ...> j:  %Bson.ObjectId{oid: <<82, 224, 229, 161, 0, 0, 2, 0, 3, 0, 0, 4>>},
    ...> k1: false,
    ...> k2: true,
    ...> l:  Bson.UTC.from_now({1390, 470561, 277000}),
    ...> m:  nil,
    ...> q1: -2000444000,
    ...> q2: -8000111000222001,
    ...> s1: :min_key,
    ...> s2: :max_key,
    ...> t: Bson.ObjectId.from_string("52e0e5a10000020003000004")
    ...> }
    ...> json = Bson.JsonExt.dump(term)
    %{
      "a"  => -4.230845,
      "b"  => "hello",
      "c"  => %{"x" => -1, "y" => 2.2001},
      "d"  => [23, 45, 200],
      "e"  => %{"$type" => "00", "$bin" => "zwgWRaOMYiAAA="},
      "f"  => %{"$type" => "01", "$bin" => "zwgWRaOMYiAAA="},
      "g"  => %{"$type" => "03",
                "$bin" => "MQAAAARCU09OACYAAAACMAAIAAAAYXdlc29tZQABMQAzMzMzMzMUQBAyAMIHAAAAAA=="},
      "h"  => %{"$type" => "05", "$bin" => "zwgWRaOMYiAAA="},
      "i"  => %{"$type" => "80",
                "$bin" => "MQAAAARCU09OACYAAAACMAAIAAAAYXdlc29tZQABMQAzMzMzMzMUQBAyAMIHAAAAAA=="},
      "j"  => %{"$oid" => "52e0e5a10000020003000004"},
      "k1" => false,
      "k2" => true,
      "l"  => %{"$date" => %{"$numberLong" => "1390470561277"}},
      "m"  => nil,
      "q1" => -2000444000,
      "q2" => -8000111000222001,
      "s1" => %{"$min_key" => 1},
      "s2" => %{"$max_key" => 1},
      "t"  => %{"$oid" => "52e0e5a10000020003000004"}
    }
    ...> bson = Bson.JsonExt.load(json)
    ...> # assert that one by one all decoded element are identical to the original
    ...> Enum.all? term, fn({k, v}) -> assert Map.get(bson, k) == v end
    true

  ```

  see `dump/1` and `load/1`
  """

  def dump(bson)
  def dump(n) when is_number(n), do: n
  def dump(str) when is_binary(str), do: str
  def dump(atom) when is_atom(atom), do: atom

  def dump(%Bson.ObjectId{oid: oid}) do
    %{"$oid" => Bson.hex(oid)}
  end

  def dump(%Bson.UTC{ms: ms}) do
    %{"$date" => %{"$numberLong" => Integer.to_string(ms)}}
  end

  def dump(%Bson.Bin{bin: bin, subtype: subtype}) do
    %{"$binary" => CBson.nif_b64encode(bin), "$type" => xsubty(subtype)}
  end

  def dump(%Bson.Timestamp{ts: s}) do
    %{"$date" => %{"$numberLong" => Integer.to_string(s)}}
  end

  def dump(bson) when is_map(bson) do
    :maps.map(fn _k, v -> dump(v) end, bson)
  end

  def dump([]), do: []

  def dump([{_, _} | _] = bson) do
    :lists.map(fn {k, v} -> {k, dump(v)} end, bson)
    |> :maps.from_list()
  end

  def dump(bson) when is_list(bson) do
    :lists.map(&dump/1, bson)
  end

  def dump(:min_key), do: %{"$minKey" => 1}
  def dump(:max_key), do: %{"$maxKey" => 1}

  def load(json)
  def load(n) when is_number(n), do: n
  def load(str) when is_binary(str), do: str
  def load(atom) when is_atom(atom), do: atom

  def load(json) when is_list(json) do
    :lists.map(&load/1, json)
  end

  def load(%{"$oid" => oid}) do
    Bson.ObjectId.from_string(oid)
  end

  def load(%{"$date" => %{"$numberLong" => ms}}) do
    %Bson.UTC{ms: String.to_integer(ms)}
  end

  def load(%{"$binary" => bin, "$type" => type}) do
    %Bson.Bin{bin: Base.decode64!(bin), subtype: String.to_integer(type, 16)}
  end

  def load(%{"$numberLong" => n}) do
    String.to_integer(n)
  end

  def load(json) when is_map(json) do
    :maps.map(fn _k, v -> load(v) end, json)
  end

  # module static binary
  defp xsubty(0x00), do: "00"
  defp xsubty(0x01), do: "01"
  defp xsubty(0x02), do: "02"
  defp xsubty(0x03), do: "03"
  defp xsubty(0x04), do: "04"
  defp xsubty(0x05), do: "05"
  defp xsubty(0x80), do: "80"

  def plain_dump(bson)
  def plain_dump(n) when is_number(n), do: n
  def plain_dump(str) when is_binary(str), do: str
  def plain_dump(atom) when is_atom(atom), do: atom
  def plain_dump(%Bson.ObjectId{oid: oid}), do: Bson.hex(oid)
  def plain_dump(%Bson.UTC{ms: ms}), do: ms
  def plain_dump(%Bson.Bin{bin: bin}), do: CBson.nif_b64encode(bin)
  def plain_dump(%Bson.Timestamp{ts: s}), do: s

  def plain_dump(bson) when is_map(bson) do
    :maps.map(fn _k, v -> plain_dump(v) end, bson)
  end

  def plain_dump([]), do: []

  def plain_dump([{_, _} | _] = bson) do
    :lists.map(fn {k, v} -> {k, plain_dump(v)} end, bson)
    |> :maps.from_list()
  end

  def plain_dump(bson) when is_list(bson) do
    :lists.map(&plain_dump/1, bson)
  end

  def plain_dump(:min_key), do: %{"$minKey" => 1}
  def plain_dump(:max_key), do: %{"$maxKey" => 1}
end