Current section
Files
Jump to
Current section
Files
src/clique_csv_writer.erl
%% -------------------------------------------------------------------
%%
%% Copyright (c) 2014 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_csv_writer).
%% @doc Implements a writer module for clique which outputs tabular data in CSV format.
-behavior(clique_writer).
-export([
write/1,
write_status/2
]).
-include("clique_status_types.hrl").
-spec write(status()) -> {iolist(), iolist()}.
write(Status) ->
%% First, pull out any alerts so that we can feed them
%% to the human writer and print them out on stderr:
{Alerts, NonAlerts} = lists:partition(fun({alert, _}) -> true;
(_) -> false
end, Status),
Output = clique_status:parse(NonAlerts, fun write_status/2, []),
AlertOutput = [element(1, clique_human_writer:write(A)) || {alert, A} <- Alerts],
{lists:reverse(Output), AlertOutput}.
%% @doc Write status information in csv format.
%%
%% Anything other than a table is discarded, since there's no good way to represent non-tabular
%% data in CSV. We want to be able to use the CSV output directly without needing to strip
%% off any extranious stuff.
-spec write_status(elem(), iolist()) -> iolist().
write_status({table, Rows}, Output) ->
[write_table(Rows) | Output];
write_status(_, Output) ->
Output.
write_table([]) ->
"";
write_table(Rows0) ->
Schema = [Name || {Name, _Val} <- hd(Rows0)],
Header = write_header(Schema),
Rows = write_rows([[Val || {_Name, Val} <- Row] || Row <- Rows0]),
[Header, Rows].
write_header(Schema) ->
HeaderStrs = [format_val(Name) || Name <- Schema],
[string:join(HeaderStrs, ","), "\r\n"].
write_rows(Rows) ->
[write_row(R) || R <- Rows].
write_row(Row) ->
ValStrs = [format_val(V) || V <- Row],
[string:join(ValStrs, ","), "\r\n"].
format_val(V) when is_atom(V) ->
format_val(atom_to_list(V));
format_val(V) when is_integer(V) ->
format_val(integer_to_list(V));
format_val(V) when is_binary(V) ->
format_val(unicode:characters_to_list(V, utf8));
format_val(Str0) when is_list(Str0) ->
%% TODO: This could probably be done more efficiently.
%% Maybe we could write a strip func that works directly on iolists?
Str = string:strip(binary_to_list(iolist_to_binary(Str0))),
%% If we have any line breaks, double quotes, or commas, we must surround the value with
%% double quotes.
IsEvilChar = fun(C) -> lists:member(C, [$\r, $\n, $", $,]) end,
case lists:any(IsEvilChar, Str) of
true ->
[$", escape(Str), $"];
false ->
Str
end.
escape(Str) ->
%% According to RFC 4180, any double quote chars in quoted fields
%% should be escaped with extra double quotes. e.g. "aaa","b""bb","ccc"
SplitStr = string:tokens(Str, "\""),
string:join(SplitStr, "\"\"").