Skip to content

Commit

Permalink
Merge pull request #47 from basho/feature/json-writer
Browse files Browse the repository at this point in the history
Add JSON writer #47 [JIRA: RIAK-1486]

Reviewed-by: andrewjstone
  • Loading branch information
borshop committed Jan 27, 2015
2 parents 45f19cf + a91239d commit 431e695
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 5 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Clique provides the application developer with the following capabilities:
configuration across one or all nodes: i.e. `riak-admin set anti-entropy=on --all`
* Return a standard status format that allows output of a variety of content
types: human-readable, csv, html, etc... (Note that currently only
human-readable and CSV output formats are implemented)
human-readable, CSV, and JSON output formats are implemented)

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

(Note that the JSON writer is a special case, in that it is only available if
the mochijson2 module is present at startup. We wanted to avoid having to
introduce MochiWeb as a hard dependency, so instead we allow users of Clique to
decide for themselves if/how they want to include the mochijson2 module.)

Writing custom output writers is relatively undocumented right now, and the
values passed to the `write/1` callback may be subject to future changes. But,
the `clique_*_writer` modules in the clique source tree provide good examples
the `clique_*_writer` modules in the Clique source tree provide good examples
that can be used for reference.

### run/1
Expand Down
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{eunit_opts, [verbose]}.

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

{erl_first_files, [
"src/clique_writer.erl",
Expand Down
122 changes: 122 additions & 0 deletions src/clique_json_writer.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
%% -------------------------------------------------------------------
%%
%% Copyright (c) 2015 Basho Technologies, Inc. All Rights Reserved.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
-module(clique_json_writer).

%% @doc Write status information in JSON format.
%%
%% The current clique -> JSON translation looks something like this:
%% {text, "hello world"} ->
%% {"type" : "text", "text" : "hello world"}
%% {text, [<<he>>, $l, "lo", ["world"]]} ->
%% {"type" : "text", "text" : "hello world"}
%% {list, ["a", "b", <<"c">>]} ->
%% {"type" : "list", "list" : ["a", "b", "c"]}
%% {list, "Camels", ["Dromedary", "Bactrian", "Sopwith"] ->
%% {"type" : "list", "title" : "Camels", "list" : ["Dromedary", "Bactrian", "Sopwith"]}
%% {alert, [{text, "Shields failing!"}]} ->
%% {"type" : "alert", "alert" : [{"type" : "text", "text" : "Shields failing!"}]}
%% usage ->
%% {"type" : "usage",
%% "usage" : "Usage: riak-admin cluster self-destruct [--delay <delayseconds>]"}
%% {table, [[{name, "Nick"}, {species, "human"}], [{name, "Rowlf"}, {species, "dog"}]]} ->
%% {"type" : "table",
%% "table" : [{"name" : "Nick", "species" : "human"}, {"name", "Rowlf", "species", "dog"}]}

-behavior(clique_writer).

-export([write/1]).

-include("clique_status_types.hrl").

-record(context, {alert_set=false :: boolean(),
alert_list=[] :: [elem()],
output=[] :: iolist()}).

-spec write(status()) -> iolist().
write(Status) ->
PreparedOutput = lists:reverse(prepare(Status)),
[mochijson2:encode(PreparedOutput), "\n"].

%% @doc Returns status data that's been prepared for conversion to JSON.
%% Just reverse the list and pass it to mochijson2:encode and you're set.
prepare(Status) ->
Ctx = clique_status:parse(Status, fun prepare_status/2, #context{}),
Ctx#context.output.

%% @doc Write status information in JSON format.
-spec prepare_status(elem(), #context{}) -> #context{}.
prepare_status(alert, Ctx=#context{alert_set=true}) ->
%% TODO: Should we just return an error instead?
throw({error, nested_alert, Ctx});
prepare_status(Term, Ctx=#context{alert_set=true, alert_list=AList}) ->
Ctx#context{alert_list=[Term | AList]};
prepare_status(alert, Ctx) ->
Ctx#context{alert_set=true};
prepare_status(alert_done, Ctx = #context{alert_list=AList, output=Output}) ->
%% AList is already reversed, and prepare returns reversed output, so they cancel out
AlertJsonVal = prepare(AList),
AlertJson = {struct, [{<<"type">>, <<"alert">>}, {<<"alert">>, AlertJsonVal}]},
Ctx#context{alert_set=false, alert_list=[], output=[AlertJson | Output]};
prepare_status({list, Data}, Ctx=#context{output=Output}) ->
Ctx#context{output=[prepare_list(Data) | Output]};
prepare_status({list, Title, Data}, Ctx=#context{output=Output}) ->
Ctx#context{output=[prepare_list(Title, Data) | Output]};
prepare_status({text, Text}, Ctx=#context{output=Output}) ->
Ctx#context{output=[prepare_text(Text) | Output]};
prepare_status({table, Rows}, Ctx=#context{output=Output}) ->
Ctx#context{output=[prepare_table(Rows) | Output]};
prepare_status(done, Ctx) ->
Ctx.

prepare_list(Data) ->
prepare_list(undefined, Data).

prepare_list(Title, Data) ->
FlattenedData = [erlang:iolist_to_binary(S) || S <- Data],
TitleProp = case Title of
undefined ->
[];
_ ->
[{<<"title">>, erlang:iolist_to_binary(Title)}]
end,
Props = lists:flatten([{<<"type">>, <<"list">>}, TitleProp, {<<"list">>, FlattenedData}]),
{struct, Props}.

prepare_text(Text) ->
{struct, [{<<"type">>, <<"text">>}, {<<"text">>, erlang:iolist_to_binary(Text)}]}.

prepare_table(Rows) ->
TableData = [prepare_table_row(R) || R <- Rows],
{struct, [{<<"type">>, <<"table">>}, {<<"table">>, TableData}]}.

prepare_table_row(Row) ->
[{key_to_binary(K), prepare_table_value(V)} || {K, V} <- Row].

key_to_binary(Key) when is_atom(Key) ->
list_to_binary(atom_to_list(Key));
key_to_binary(Key) when is_list(Key) ->
list_to_binary(Key).

prepare_table_value(Value) when is_list(Value) ->
%% TODO: This could definitely be done more efficiently.
%% Maybe we could write a strip func that works directly on iolists?
list_to_binary(string:strip(binary_to_list(iolist_to_binary(Value))));
prepare_table_value(Value) ->
Value.
8 changes: 8 additions & 0 deletions src/clique_writer.erl
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@
init() ->
_ = ets:new(?writer_table, [public, named_table]),
ets:insert(?writer_table, ?BUILTIN_WRITERS),
%% We don't want to make mochiweb into a hard dependency, so only load
%% the JSON writer if we have the mochijson2 module available:
case code:which(mochijson2) of
non_existing ->
ok;
_ ->
ets:insert(?writer_table, {"json", clique_json_writer})
end,
ok.

-spec register(string(), module()) -> true.
Expand Down

0 comments on commit 431e695

Please sign in to comment.