1%%
2%% Erlang Redis client
3%%
4%% Usage:
5%%   {ok, Client} = eredis:start_link().
6%%   {ok, <<"OK">>} = eredis:q(Client, ["SET", "foo", "bar"]).
7%%   {ok, <<"bar">>} = eredis:q(Client, ["GET", "foo"]).
8
9-module(eredis).
10-include("eredis.hrl").
11
12%% Default timeout for calls to the client gen_server
13%% Specified in http://www.erlang.org/doc/man/gen_server.html#call-3
14-define(TIMEOUT, 5000).
15
16-export([start_link/0, start_link/1, start_link/2, start_link/3, start_link/4,
17         start_link/5, start_link/6, stop/1, q/2, q/3, qp/2, qp/3, q_noreply/2,
18         q_async/2, q_async/3]).
19
20%% Exported for testing
21-export([create_multibulk/1]).
22
23%% Type of gen_server process id
24-type client() :: pid() |
25                  atom() |
26                  {atom(),atom()} |
27                  {global,term()} |
28                  {via,atom(),term()}.
29
30%%
31%% PUBLIC API
32%%
33
34start_link() ->
35    start_link("127.0.0.1", 6379, 0, "").
36
37start_link(Host, Port) ->
38    start_link(Host, Port, 0, "").
39
40start_link(Host, Port, Database) ->
41    start_link(Host, Port, Database, "").
42
43start_link(Host, Port, Database, Password) ->
44    start_link(Host, Port, Database, Password, 100).
45
46start_link(Host, Port, Database, Password, ReconnectSleep) ->
47    start_link(Host, Port, Database, Password, ReconnectSleep, ?TIMEOUT).
48
49start_link(Host, Port, Database, Password, ReconnectSleep, ConnectTimeout)
50  when is_list(Host),
51       is_integer(Port),
52       is_integer(Database) orelse Database == undefined,
53       is_list(Password),
54       is_integer(ReconnectSleep) orelse ReconnectSleep =:= no_reconnect,
55       is_integer(ConnectTimeout) ->
56
57    eredis_client:start_link(Host, Port, Database, Password,
58                             ReconnectSleep, ConnectTimeout).
59
60%% @doc: Callback for starting from poolboy
61-spec start_link(server_args()) -> {ok, Pid::pid()} | {error, Reason::term()}.
62start_link(Args) ->
63    Host           = proplists:get_value(host, Args, "127.0.0.1"),
64    Port           = proplists:get_value(port, Args, 6379),
65    Database       = proplists:get_value(database, Args, 0),
66    Password       = proplists:get_value(password, Args, ""),
67    ReconnectSleep = proplists:get_value(reconnect_sleep, Args, 100),
68    ConnectTimeout = proplists:get_value(connect_timeout, Args, ?TIMEOUT),
69    start_link(Host, Port, Database, Password, ReconnectSleep, ConnectTimeout).
70
71stop(Client) ->
72    eredis_client:stop(Client).
73
74-spec q(Client::client(), Command::[any()]) ->
75               {ok, return_value()} | {error, Reason::binary() | no_connection}.
76%% @doc: Executes the given command in the specified connection. The
77%% command must be a valid Redis command and may contain arbitrary
78%% data which will be converted to binaries. The returned values will
79%% always be binaries.
80q(Client, Command) ->
81    call(Client, Command, ?TIMEOUT).
82
83q(Client, Command, Timeout) ->
84    call(Client, Command, Timeout).
85
86
87-spec qp(Client::client(), Pipeline::pipeline()) ->
88                [{ok, return_value()} | {error, Reason::binary()}] |
89                {error, no_connection}.
90%% @doc: Executes the given pipeline (list of commands) in the
91%% specified connection. The commands must be valid Redis commands and
92%% may contain arbitrary data which will be converted to binaries. The
93%% values returned by each command in the pipeline are returned in a list.
94qp(Client, Pipeline) ->
95    pipeline(Client, Pipeline, ?TIMEOUT).
96
97qp(Client, Pipeline, Timeout) ->
98    pipeline(Client, Pipeline, Timeout).
99
100-spec q_noreply(Client::client(), Command::[any()]) -> ok.
101%% @doc Executes the command but does not wait for a response and ignores any errors.
102%% @see q/2
103q_noreply(Client, Command) ->
104    cast(Client, Command).
105
106-spec q_async(Client::client(), Command::[any()]) -> ok.
107% @doc Executes the command, and sends a message to this process with the response (with either error or success). Message is of the form `{response, Reply}', where `Reply' is the reply expected from `q/2'.
108q_async(Client, Command) ->
109    q_async(Client, Command, self()).
110
111-spec q_async(Client::client(), Command::[any()], Pid::pid()|atom()) -> ok.
112%% @doc Executes the command, and sends a message to `Pid' with the response (with either or success).
113%% @see 1_async/2
114q_async(Client, Command, Pid) when is_pid(Pid) ->
115    Request = {request, create_multibulk(Command), Pid},
116    gen_server:cast(Client, Request).
117
118%%
119%% INTERNAL HELPERS
120%%
121
122call(Client, Command, Timeout) ->
123    Request = {request, create_multibulk(Command)},
124    gen_server:call(Client, Request, Timeout).
125
126pipeline(_Client, [], _Timeout) ->
127    [];
128pipeline(Client, Pipeline, Timeout) ->
129    Request = {pipeline, [create_multibulk(Command) || Command <- Pipeline]},
130    gen_server:call(Client, Request, Timeout).
131
132cast(Client, Command) ->
133    Request = {request, create_multibulk(Command)},
134    gen_server:cast(Client, Request).
135
136-spec create_multibulk(Args::[any()]) -> Command::iolist().
137%% @doc: Creates a multibulk command with all the correct size headers
138create_multibulk(Args) ->
139    ArgCount = [<<$*>>, integer_to_list(length(Args)), <<?NL>>],
140    ArgsBin = lists:map(fun to_bulk/1, lists:map(fun to_binary/1, Args)),
141
142    [ArgCount, ArgsBin].
143
144to_bulk(B) when is_binary(B) ->
145    [<<$$>>, integer_to_list(iolist_size(B)), <<?NL>>, B, <<?NL>>].
146
147%% @doc: Convert given value to binary. Fallbacks to
148%% term_to_binary/1. For floats, throws {cannot_store_floats, Float}
149%% as we do not want floats to be stored in Redis. Your future self
150%% will thank you for this.
151to_binary(X) when is_list(X)    -> list_to_binary(X);
152to_binary(X) when is_atom(X)    -> atom_to_binary(X, utf8);
153to_binary(X) when is_binary(X)  -> X;
154to_binary(X) when is_integer(X) -> integer_to_binary(X);
155to_binary(X) when is_float(X)   -> throw({cannot_store_floats, X});
156to_binary(X)                    -> term_to_binary(X).
157