Skip to content

Commit a101ace

Browse files
authored
Merge pull request #140 from norpan/binary-datetime-format
time_format and binary_format options
2 parents 9fe98ec + fbfa3f5 commit a101ace

File tree

9 files changed

+185
-45
lines changed

9 files changed

+185
-45
lines changed

lib/rethinkdb/connection.ex

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ defmodule RethinkDB.Connection do
9696
* `durability` - possible values are 'hard' and 'soft'. In soft durability mode RethinkDB will acknowledge the write immediately after receiving it, but before the write has been committed to disk.
9797
* `noreply` - set to true to not receive the result object or cursor and return immediately.
9898
* `profile` - whether or not to return a profile of the query’s execution (default: false).
99+
* `time_format` - what format to return times in (default: :native). Set this to :raw if you want times returned as JSON objects for exporting.
100+
* `binary_format` - what format to return binary data in (default: :native). Set this to :raw if you want the raw pseudotype.
99101
"""
100102
def run(query, conn, opts \\ []) do
101103
timeout = Dict.get(opts, :timeout, 5000)
@@ -110,7 +112,7 @@ defmodule RethinkDB.Connection do
110112
false -> {:query, query}
111113
end
112114
case Connection.call(conn, msg, timeout) do
113-
{response, token} -> RethinkDB.Response.parse(response, token, conn)
115+
{response, token} -> RethinkDB.Response.parse(response, token, conn, opts)
114116
:noreply -> :ok
115117
result -> result
116118
end
@@ -122,9 +124,9 @@ defmodule RethinkDB.Connection do
122124
Since a feed is tied to a particular connection, no connection is needed when calling
123125
`next`.
124126
"""
125-
def next(%{token: token, pid: pid}) do
127+
def next(%{token: token, pid: pid, opts: opts}) do
126128
case Connection.call(pid, {:continue, token}, :infinity) do
127-
{response, token} -> RethinkDB.Response.parse(response, token, pid)
129+
{response, token} -> RethinkDB.Response.parse(response, token, pid, opts)
128130
x -> x
129131
end
130132
end
@@ -137,15 +139,15 @@ defmodule RethinkDB.Connection do
137139
"""
138140
def close(%{token: token, pid: pid}) do
139141
{response, token} = Connection.call(pid, {:stop, token}, :infinity)
140-
RethinkDB.Response.parse(response, token, pid)
142+
RethinkDB.Response.parse(response, token, pid, [])
141143
end
142144

143145
@doc """
144146
`noreply_wait` ensures that previous queries with the noreply flag have been processed by the server. Note that this guarantee only applies to queries run on the given connection.
145147
"""
146148
def noreply_wait(conn, timeout \\ 5000) do
147149
{response, token} = Connection.call(conn, :noreply_wait, timeout)
148-
case RethinkDB.Response.parse(response, token, conn) do
150+
case RethinkDB.Response.parse(response, token, conn, []) do
149151
%RethinkDB.Response{data: %{"t" => 4}} -> :ok
150152
r -> r
151153
end

lib/rethinkdb/prepare.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ defmodule RethinkDB.Prepare do
3939
{[k,v], state}
4040
end
4141
defp prepare(el, state) do
42-
{el, state}
42+
if is_binary(el) and not String.valid?(el) do
43+
{RethinkDB.Query.binary(el), state}
44+
else
45+
{el, state}
46+
end
4347
end
4448
end

lib/rethinkdb/pseudotypes.ex

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ defmodule RethinkDB.Pseudotypes do
44
@moduledoc false
55
defstruct data: nil
66

7-
def parse(%{"$reql_type$" => "BINARY", "data" => data}) do
8-
%__MODULE__{data: :base64.decode(data)}
7+
def parse(%{"$reql_type$" => "BINARY", "data" => data}, opts) do
8+
case Dict.get(opts, :binary_format) do
9+
:raw ->
10+
%__MODULE__{data: data}
11+
_ ->
12+
:base64.decode(data)
13+
end
914
end
1015
end
1116

@@ -41,33 +46,56 @@ defmodule RethinkDB.Pseudotypes do
4146
@moduledoc false
4247
defstruct epoch_time: nil, timezone: nil
4348

44-
def parse(%{"$reql_type$" => "TIME", "epoch_time" => epoch_time, "timezone" => timezone}) do
45-
%__MODULE__{epoch_time: epoch_time, timezone: timezone}
49+
def parse(%{"$reql_type$" => "TIME", "epoch_time" => epoch_time, "timezone" => timezone}, opts) do
50+
case Dict.get(opts, :time_format) do
51+
:raw ->
52+
%__MODULE__{epoch_time: epoch_time, timezone: timezone}
53+
_ ->
54+
{seconds, ""} = Calendar.ISO.parse_offset(timezone)
55+
zone_abbr = case seconds do
56+
0 -> "UTC"
57+
_ -> timezone
58+
end
59+
negative = seconds < 0
60+
seconds = abs(seconds)
61+
time_zone = case {div(seconds,3600),rem(seconds,3600)} do
62+
{0,0} -> "Etc/UTC"
63+
{hours,0} ->
64+
"Etc/GMT" <> if negative do "+" else "-" end <> Integer.to_string(hours)
65+
{hours,seconds} ->
66+
"Etc/GMT" <> if negative do "+" else "-" end <> Integer.to_string(hours) <> ":" <>
67+
String.pad_leading(Integer.to_string(seconds), 2, "0")
68+
end
69+
epoch_time * 1000
70+
|> trunc()
71+
|> DateTime.from_unix!(:milliseconds)
72+
|> struct(utc_offset: seconds, zone_abbr: zone_abbr, time_zone: time_zone)
73+
end
4674
end
4775
end
4876

49-
def convert_reql_pseudotypes(nil), do: nil
50-
def convert_reql_pseudotypes(%{"$reql_type$" => "BINARY"} = data) do
51-
Binary.parse(data)
77+
def convert_reql_pseudotypes(nil, opts), do: nil
78+
def convert_reql_pseudotypes(%{"$reql_type$" => "BINARY"} = data, opts) do
79+
Binary.parse(data, opts)
5280
end
53-
def convert_reql_pseudotypes(%{"$reql_type$" => "GEOMETRY"} = data) do
81+
def convert_reql_pseudotypes(%{"$reql_type$" => "GEOMETRY"} = data, opts) do
5482
Geometry.parse(data)
5583
end
56-
def convert_reql_pseudotypes(%{"$reql_type$" => "GROUPED_DATA"} = data) do
84+
def convert_reql_pseudotypes(%{"$reql_type$" => "GROUPED_DATA"} = data, opts) do
5785
parse_grouped_data(data)
5886
end
59-
def convert_reql_pseudotypes(%{"$reql_type$" => "TIME"} = data) do
60-
Time.parse(data)
87+
def convert_reql_pseudotypes(%{"$reql_type$" => "TIME"} = data, opts) do
88+
Time.parse(data, opts)
6189
end
62-
def convert_reql_pseudotypes(list) when is_list(list) do
63-
Enum.map(list, &convert_reql_pseudotypes/1)
90+
def convert_reql_pseudotypes(list, opts) when is_list(list) do
91+
Enum.map(list, fn data -> convert_reql_pseudotypes(data, opts) end)
6492
end
65-
def convert_reql_pseudotypes(map) when is_map(map) do
93+
def convert_reql_pseudotypes(map, opts) when is_map(map) do
6694
Enum.map(map, fn {k, v} ->
67-
{k, convert_reql_pseudotypes(v)}
95+
{k, convert_reql_pseudotypes(v, opts)}
6896
end) |> Enum.into(%{})
6997
end
70-
def convert_reql_pseudotypes(string), do: string
98+
def convert_reql_pseudotypes(string, opts), do: string
7199

72100
def parse_grouped_data(%{"$reql_type$" => "GROUPED_DATA", "data" => data}) do
73101
Enum.map(data, fn ([k, data]) ->

lib/rethinkdb/query.ex

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ defmodule RethinkDB.Query do
188188
@doc """
189189
Encapsulate binary data within a query.
190190
191+
The type of data binary accepts depends on the client language. In
192+
Elixir, it expects a Binary. Using a Binary object within a query implies
193+
the use of binary and the ReQL driver will automatically perform the coercion.
194+
195+
Binary objects returned to the client in Elixir will also be
196+
Binary objects. This can be changed with the binary_format option :raw
197+
to run to return “raw” objects.
198+
191199
Only a limited subset of ReQL commands may be chained after binary:
192200
193201
* coerce_to can coerce binary objects to string types
@@ -198,9 +206,9 @@ defmodule RethinkDB.Query do
198206
* info will return information on a binary object.
199207
"""
200208
@spec binary(Q.reql_binary) :: Q.t
201-
def binary(%RethinkDB.Pseudotypes.Binary{data: data}), do: binary(data)
202-
def binary(data), do: do_binary(%{"$reql_type$" => "BINARY", "data" => :base64.encode(data)})
203-
def do_binary(data), do: %Q{query: [155, [data]]}
209+
def binary(%RethinkDB.Pseudotypes.Binary{data: data}), do: do_binary(data)
210+
def binary(data), do: do_binary(:base64.encode(data))
211+
def do_binary(data), do: %Q{query: [155, [%{"$reql_type$" => "BINARY", "data" => data}]]}
204212

205213
@doc """
206214
Call an anonymous function using return values from other ReQL commands or

lib/rethinkdb/query/macros.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,22 @@ defmodule RethinkDB.Query.Macros do
106106
m = Map.from_struct(t) |> Map.put_new("$reql_type$", "TIME")
107107
wrap(m)
108108
end
109+
def wrap(t = %DateTime{utc_offset: utc_offset, std_offset: std_offset}) do
110+
offset = utc_offset + std_offset
111+
offset_negative = offset < 0
112+
offset_hour = div(abs(offset), 3600)
113+
offset_minute = rem(abs(offset), 3600)
114+
time_zone =
115+
if offset_negative do "-" else "+" end <>
116+
String.pad_leading(Integer.to_string(offset_hour), 2, "0") <>
117+
":" <>
118+
String.pad_leading(Integer.to_string(offset_minute), 2, "0")
119+
wrap(%{
120+
"$reql_type$" => "TIME",
121+
"epoch_time" => DateTime.to_unix(t, :milliseconds) / 1000,
122+
"timezone" => time_zone
123+
})
124+
end
109125
def wrap(map) when is_map(map) do
110126
Enum.map(map, fn {k,v} ->
111127
{k, wrap(v)}

lib/rethinkdb/response.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ end
1919

2020
defmodule RethinkDB.Feed do
2121
@moduledoc false
22-
defstruct token: nil, data: nil, pid: nil, note: nil, profile: nil
22+
defstruct token: nil, data: nil, pid: nil, note: nil, profile: nil, opts: nil
2323

2424
defimpl Enumerable, for: __MODULE__ do
2525
def reduce(changes, acc, fun) do
@@ -45,13 +45,13 @@ defmodule RethinkDB.Response do
4545
@moduledoc false
4646
defstruct token: nil, data: "", profile: nil
4747

48-
def parse(raw_data, token, pid) do
48+
def parse(raw_data, token, pid, opts) do
4949
d = Poison.decode!(raw_data)
50-
data = RethinkDB.Pseudotypes.convert_reql_pseudotypes(d["r"])
50+
data = RethinkDB.Pseudotypes.convert_reql_pseudotypes(d["r"], opts)
5151
{code, resp} = case d["t"] do
5252
1 -> {:ok, %RethinkDB.Record{data: hd(data)}}
5353
2 -> {:ok, %RethinkDB.Collection{data: data}}
54-
3 -> {:ok, %RethinkDB.Feed{token: token, data: data, pid: pid, note: d["n"]}}
54+
3 -> {:ok, %RethinkDB.Feed{token: token, data: data, pid: pid, note: d["n"], opts: opts}}
5555
4 -> {:ok, %RethinkDB.Response{token: token, data: d}}
5656
16 -> {:error, %RethinkDB.Response{token: token, data: d}}
5757
17 -> {:error, %RethinkDB.Response{token: token, data: d}}

test/changes_test.exs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,32 @@ defmodule ChangesTest do
7171
[h|[]] = Task.await(t)
7272
assert %{"new_val" => %{"id" => "0"}} = h
7373
end
74+
75+
test "changes opts binary native" do
76+
q = table(@table_name) |> get("0") |> changes
77+
{:ok, changes} = {:ok, %Feed{}} = run(q)
78+
t = Task.async fn ->
79+
changes |> Enum.take(1)
80+
end
81+
data = %{"id" => "0", "binary" => binary(<<1>>)}
82+
q = table(@table_name) |> insert(data)
83+
{:ok, res} = run(q)
84+
expected = res.data["id"]
85+
[h|[]] = Task.await(t)
86+
assert %{"new_val" => %{"id" => "0", "binary" => <<1>>}} = h
87+
end
88+
89+
test "changes opts binary raw" do
90+
q = table(@table_name) |> get("0") |> changes
91+
{:ok, changes} = {:ok, %Feed{}} = run(q, [binary_format: :raw])
92+
t = Task.async fn ->
93+
changes |> Enum.take(1)
94+
end
95+
data = %{"id" => "0", "binary" => binary(<<1>>)}
96+
q = table(@table_name) |> insert(data)
97+
{:ok, res} = run(q)
98+
expected = res.data["id"]
99+
[h|[]] = Task.await(t)
100+
assert %{"new_val" => %{"id" => "0", "binary" => %RethinkDB.Pseudotypes.Binary{data: "AQ=="}}} = h
101+
end
74102
end

test/query/control_structures_test.exs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,33 @@ defmodule ControlStructuresTest do
1717
assert data == [%{"a" => 5}, %{"a" => 4, "c" => 7}]
1818
end
1919

20-
test "binary" do
20+
test "binary raw" do
21+
d = << 220, 2, 3, 4, 5, 192 >>
22+
q = binary d
23+
{:ok, %Record{data: data}} = run q, [binary_format: :raw]
24+
assert data == %RethinkDB.Pseudotypes.Binary{data: :base64.encode(d)}
25+
q = binary data
26+
{:ok, %Record{data: result}} = run q, [binary_format: :raw]
27+
assert data == result
28+
end
29+
30+
test "binary native" do
2131
d = << 220, 2, 3, 4, 5, 192 >>
2232
q = binary d
2333
{:ok, %Record{data: data}} = run q
24-
assert data == %RethinkDB.Pseudotypes.Binary{data: d}
34+
assert data == d
2535
q = binary data
26-
{:ok, %Record{data: result}} = run q
36+
{:ok, %Record{data: result}} = run q, [binary_format: :native]
37+
assert data == result
38+
end
39+
40+
test "binary native no wrapper" do
41+
d = << 220, 2, 3, 4, 5, 192 >>
42+
q = d
43+
{:ok, %Record{data: data}} = run q
44+
assert data == d
45+
q = data
46+
{:ok, %Record{data: result}} = run q, [binary_format: :native]
2747
assert data == result
2848
end
2949

@@ -39,10 +59,10 @@ defmodule ControlStructuresTest do
3959
test "branch" do
4060
q = branch(true, 1, 2)
4161
{:ok, %Record{data: data}} = run q
42-
assert data == 1
62+
assert data == 1
4363
q = branch(false, 1, 2)
4464
{:ok, %Record{data: data}} = run q
45-
assert data == 2
65+
assert data == 2
4666
end
4767

4868
test "error" do
@@ -105,7 +125,7 @@ defmodule ControlStructuresTest do
105125
end
106126

107127
test "uuid" do
108-
q = uuid
128+
q = uuid
109129
{:ok, %Record{data: data}} = run q
110130
assert String.length(String.replace(data, "-", "")) == 32
111131
end

0 commit comments

Comments
 (0)