|
| 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. |
0 commit comments