Skip to content

Commit ac00915

Browse files
committed
Merge pull request #47 from basho/feature/json-writer
Add JSON writer #47 [JIRA: RIAK-1486] Reviewed-by: andrewjstone
2 parents 4320f81 + a646e7f commit ac00915

File tree

4 files changed

+140
-5
lines changed

4 files changed

+140
-5
lines changed

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Clique provides the application developer with the following capabilities:
3030
configuration across one or all nodes: i.e. `riak-admin set anti-entropy=on --all`
3131
* Return a standard status format that allows output of a variety of content
3232
types: human-readable, csv, html, etc... (Note that currently only
33-
human-readable and CSV output formats are implemented)
33+
human-readable, CSV, and JSON output formats are implemented)
3434

3535
### Why Not Clique ?
3636
* You aren't writing a CLI
@@ -312,14 +312,19 @@ clique:register_usage(["riak-admin", "handoff", "limit"], fun handoff_limit_usag
312312
### register_writer/2
313313
This is not something most applications will likely need to use, but the
314314
capability exists to create custom output writer modules. Currently you can
315-
specify the `--format=[human|csv]` flag on many commands to determine how the
316-
output will be written; registering a new writer "foo" allows you to use
315+
specify the `--format=[human|csv|json]` flag on many commands to determine how
316+
the output will be written; registering a new writer "foo" allows you to use
317317
`--format=foo` to write the output using whatever corresponding writer module
318318
you've registered.
319319

320+
(Note that the JSON writer is a special case, in that it is only available if
321+
the mochijson2 module is present at startup. We wanted to avoid having to
322+
introduce MochiWeb as a hard dependency, so instead we allow users of Clique to
323+
decide for themselves if/how they want to include the mochijson2 module.)
324+
320325
Writing custom output writers is relatively undocumented right now, and the
321326
values passed to the `write/1` callback may be subject to future changes. But,
322-
the `clique_*_writer` modules in the clique source tree provide good examples
327+
the `clique_*_writer` modules in the Clique source tree provide good examples
323328
that can be used for reference.
324329

325330
### run/1

rebar.config

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{eunit_opts, [verbose]}.
55

66
{xref_checks, []}.
7-
{xref_queries, [{"(XC - UC) || (XU - X - B - \"(cluster_info|dtrace)\" : Mod)", []}]}.
7+
{xref_queries, [{"(XC - UC) || (XU - X - B - \"(cluster_info|dtrace|mochijson2)\" : Mod)", []}]}.
88

99
{erl_first_files, [
1010
"src/clique_writer.erl",

src/clique_json_writer.erl

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
%% -------------------------------------------------------------------
2+
%%
3+
%% Copyright (c) 2015 Basho Technologies, Inc. All Rights Reserved.
4+
%%
5+
%% This file is provided to you under the Apache License,
6+
%% Version 2.0 (the "License"); you may not use this file
7+
%% except in compliance with the License. You may obtain
8+
%% a copy of the License at
9+
%%
10+
%% http://www.apache.org/licenses/LICENSE-2.0
11+
%%
12+
%% Unless required by applicable law or agreed to in writing,
13+
%% software distributed under the License is distributed on an
14+
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
%% KIND, either express or implied. See the License for the
16+
%% specific language governing permissions and limitations
17+
%% under the License.
18+
%%
19+
%% -------------------------------------------------------------------
20+
-module(clique_json_writer).
21+
22+
%% @doc Write status information in JSON format.
23+
%%
24+
%% The current clique -> JSON translation looks something like this:
25+
%% {text, "hello world"} ->
26+
%% {"type" : "text", "text" : "hello world"}
27+
%% {text, [<<he>>, $l, "lo", ["world"]]} ->
28+
%% {"type" : "text", "text" : "hello world"}
29+
%% {list, ["a", "b", <<"c">>]} ->
30+
%% {"type" : "list", "list" : ["a", "b", "c"]}
31+
%% {list, "Camels", ["Dromedary", "Bactrian", "Sopwith"] ->
32+
%% {"type" : "list", "title" : "Camels", "list" : ["Dromedary", "Bactrian", "Sopwith"]}
33+
%% {alert, [{text, "Shields failing!"}]} ->
34+
%% {"type" : "alert", "alert" : [{"type" : "text", "text" : "Shields failing!"}]}
35+
%% usage ->
36+
%% {"type" : "usage",
37+
%% "usage" : "Usage: riak-admin cluster self-destruct [--delay <delayseconds>]"}
38+
%% {table, [[{name, "Nick"}, {species, "human"}], [{name, "Rowlf"}, {species, "dog"}]]} ->
39+
%% {"type" : "table",
40+
%% "table" : [{"name" : "Nick", "species" : "human"}, {"name", "Rowlf", "species", "dog"}]}
41+
42+
-behavior(clique_writer).
43+
44+
-export([write/1]).
45+
46+
-include("clique_status_types.hrl").
47+
48+
-record(context, {alert_set=false :: boolean(),
49+
alert_list=[] :: [elem()],
50+
output=[] :: iolist()}).
51+
52+
-spec write(status()) -> {iolist(), iolist()}.
53+
write(Status) ->
54+
PreparedOutput = lists:reverse(prepare(Status)),
55+
{[mochijson2:encode(PreparedOutput), "\n"], []}.
56+
57+
%% @doc Returns status data that's been prepared for conversion to JSON.
58+
%% Just reverse the list and pass it to mochijson2:encode and you're set.
59+
prepare(Status) ->
60+
Ctx = clique_status:parse(Status, fun prepare_status/2, #context{}),
61+
Ctx#context.output.
62+
63+
%% @doc Write status information in JSON format.
64+
-spec prepare_status(elem(), #context{}) -> #context{}.
65+
prepare_status(alert, Ctx=#context{alert_set=true}) ->
66+
%% TODO: Should we just return an error instead?
67+
throw({error, nested_alert, Ctx});
68+
prepare_status(alert, Ctx) ->
69+
Ctx#context{alert_set=true};
70+
prepare_status(alert_done, Ctx = #context{alert_list=AList, output=Output}) ->
71+
%% AList is already reversed, and prepare returns reversed output, so they cancel out
72+
AlertJsonVal = prepare(AList),
73+
AlertJson = {struct, [{<<"type">>, <<"alert">>}, {<<"alert">>, AlertJsonVal}]},
74+
Ctx#context{alert_set=false, alert_list=[], output=[AlertJson | Output]};
75+
prepare_status(Term, Ctx=#context{alert_set=true, alert_list=AList}) ->
76+
Ctx#context{alert_list=[Term | AList]};
77+
prepare_status({list, Data}, Ctx=#context{output=Output}) ->
78+
Ctx#context{output=[prepare_list(Data) | Output]};
79+
prepare_status({list, Title, Data}, Ctx=#context{output=Output}) ->
80+
Ctx#context{output=[prepare_list(Title, Data) | Output]};
81+
prepare_status({text, Text}, Ctx=#context{output=Output}) ->
82+
Ctx#context{output=[prepare_text(Text) | Output]};
83+
prepare_status({table, Rows}, Ctx=#context{output=Output}) ->
84+
Ctx#context{output=[prepare_table(Rows) | Output]};
85+
prepare_status(done, Ctx) ->
86+
Ctx.
87+
88+
prepare_list(Data) ->
89+
prepare_list(undefined, Data).
90+
91+
prepare_list(Title, Data) ->
92+
FlattenedData = [erlang:iolist_to_binary(S) || S <- Data],
93+
TitleProp = case Title of
94+
undefined ->
95+
[];
96+
_ ->
97+
[{<<"title">>, erlang:iolist_to_binary(Title)}]
98+
end,
99+
Props = lists:flatten([{<<"type">>, <<"list">>}, TitleProp, {<<"list">>, FlattenedData}]),
100+
{struct, Props}.
101+
102+
prepare_text(Text) ->
103+
{struct, [{<<"type">>, <<"text">>}, {<<"text">>, erlang:iolist_to_binary(Text)}]}.
104+
105+
prepare_table(Rows) ->
106+
TableData = [prepare_table_row(R) || R <- Rows],
107+
{struct, [{<<"type">>, <<"table">>}, {<<"table">>, TableData}]}.
108+
109+
prepare_table_row(Row) ->
110+
[{key_to_binary(K), prepare_table_value(V)} || {K, V} <- Row].
111+
112+
key_to_binary(Key) when is_atom(Key) ->
113+
list_to_binary(atom_to_list(Key));
114+
key_to_binary(Key) when is_list(Key) ->
115+
list_to_binary(Key).
116+
117+
prepare_table_value(Value) when is_list(Value) ->
118+
%% TODO: This could definitely be done more efficiently.
119+
%% Maybe we could write a strip func that works directly on iolists?
120+
list_to_binary(string:strip(binary_to_list(iolist_to_binary(Value))));
121+
prepare_table_value(Value) ->
122+
Value.

src/clique_writer.erl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@
5050
init() ->
5151
_ = ets:new(?writer_table, [public, named_table]),
5252
ets:insert(?writer_table, ?BUILTIN_WRITERS),
53+
%% We don't want to make mochiweb into a hard dependency, so only load
54+
%% the JSON writer if we have the mochijson2 module available:
55+
case code:which(mochijson2) of
56+
non_existing ->
57+
ok;
58+
_ ->
59+
ets:insert(?writer_table, {"json", clique_json_writer})
60+
end,
5361
ok.
5462

5563
-spec register(string(), module()) -> true.

0 commit comments

Comments
 (0)