THRIFT-5635 Update erlang client for Erlang 23-25
Client: erl
Patch: Sergey Yelin
This closes #2677
Summary of changes:
- Add useful compiler options
- Format sources using erlfmt
- Switch to modern callbacks in thrift_* modules
- Add static analysis (dialyzer), disabled by default
- Add/fix types for API calls
NOTE: Enabling static analysis requires additional tweaks in multiplexer module.
diff --git a/lib/erl/src/thrift_json_protocol.erl b/lib/erl/src/thrift_json_protocol.erl
index c5f3da8..b0a3918 100644
--- a/lib/erl/src/thrift_json_protocol.erl
+++ b/lib/erl/src/thrift_json_protocol.erl
@@ -27,35 +27,38 @@
-include("thrift_constants.hrl").
-include("thrift_protocol.hrl").
--export([new/1, new/2,
- read/2,
- write/2,
- flush_transport/1,
- close_transport/1,
- new_protocol_factory/2
- ]).
+-export([
+ new/1, new/2,
+ read/2,
+ write/2,
+ flush_transport/1,
+ close_transport/1,
+ new_protocol_factory/2
+]).
-record(json_context, {
% the type of json_context: array or object
- type,
+ type :: undefined | array | object,
% fields read or written
- fields_processed = 0
+ fields_processed = 0 :: non_neg_integer()
}).
+-type json_context() :: #json_context{}.
+-type jsx_type() :: atom() | {atom(), atom() | string()}.
+-type jsx() :: {event, jsx_type(), [jsx_type()]}.
+
-record(json_protocol, {
- transport,
- context_stack = [],
- jsx
+ transport :: term(),
+ context_stack = [] :: [json_context()],
+ jsx :: undefined | jsx()
}).
--type state() :: #json_protocol{}.
--include("thrift_protocol_behaviour.hrl").
-define(VERSION_1, 1).
-define(JSON_DOUBLE_PRECISION, 16).
typeid_to_json(?tType_BOOL) -> "tf";
-typeid_to_json(?tType_BYTE) -> "i8";
typeid_to_json(?tType_DOUBLE) -> "dbl";
+% NOTE: ?tType_BYTE also match here
typeid_to_json(?tType_I8) -> "i8";
typeid_to_json(?tType_I16) -> "i16";
typeid_to_json(?tType_I32) -> "i32";
@@ -84,28 +87,33 @@
end_context(object) -> "}";
end_context(array) -> "]".
-
new(Transport) ->
new(Transport, _Options = []).
new(Transport, _Options) ->
- State = #json_protocol{transport = Transport},
+ State = #json_protocol{transport = Transport},
thrift_protocol:new(?MODULE, State).
flush_transport(This = #json_protocol{transport = Transport}) ->
{NewTransport, Result} = thrift_transport:flush(Transport),
- {This#json_protocol{
+ {
+ This#json_protocol{
transport = NewTransport,
context_stack = []
- }, Result}.
+ },
+ Result
+ }.
close_transport(This = #json_protocol{transport = Transport}) ->
{NewTransport, Result} = thrift_transport:close(Transport),
- {This#json_protocol{
+ {
+ This#json_protocol{
transport = NewTransport,
context_stack = [],
jsx = undefined
- }, Result}.
+ },
+ Result
+ }.
%%%
%%% instance methods
@@ -113,63 +121,82 @@
% places a new context on the stack:
write(#json_protocol{context_stack = Stack} = State0, {enter_context, Type}) ->
{State1, ok} = write_values(State0, [{context_pre_item, false}]),
- State2 = State1#json_protocol{context_stack = [
- #json_context{type=Type}|Stack]},
- write_values(State2, [list_to_binary(start_context(Type))]);
-
-% removes the topmost context from stack
-write(#json_protocol{context_stack = [CurrCtxt|Stack]} = State0, {exit_context}) ->
+ State2 = State1#json_protocol{
+ context_stack = [
+ #json_context{type = Type} | Stack
+ ]
+ },
+ write_values(State2, [list_to_binary(start_context(Type))]);
+% removes the topmost context from stack
+write(#json_protocol{context_stack = [CurrCtxt | Stack]} = State0, {exit_context}) ->
Type = CurrCtxt#json_context.type,
State1 = State0#json_protocol{context_stack = Stack},
write_values(State1, [
- list_to_binary(end_context(Type)),
- {context_post_item, false}
- ]);
-
-% writes necessary prelude to field or container depending on current context
-write(#json_protocol{context_stack = []} = This0,
- {context_pre_item, _}) -> {This0, ok};
-write(#json_protocol{context_stack = [Context|_CtxtTail]} = This0,
- {context_pre_item, MayNeedQuotes}) ->
+ list_to_binary(end_context(Type)),
+ {context_post_item, false}
+ ]);
+% writes necessary prelude to field or container depending on current context
+write(
+ #json_protocol{context_stack = []} = This0,
+ {context_pre_item, _}
+) ->
+ {This0, ok};
+write(
+ #json_protocol{context_stack = [Context | _CtxtTail]} = This0,
+ {context_pre_item, MayNeedQuotes}
+) ->
FieldNo = Context#json_context.fields_processed,
CtxtType = Context#json_context.type,
Rem = FieldNo rem 2,
case {CtxtType, FieldNo, Rem, MayNeedQuotes} of
- {array, N, _, _} when N > 0 -> % array element (not first)
+ % array element (not first)
+ {array, N, _, _} when N > 0 ->
write(This0, <<",">>);
- {object, 0, _, true} -> % non-string object key (first)
+ % non-string object key (first)
+ {object, 0, _, true} ->
write(This0, <<"\"">>);
- {object, N, 0, true} when N > 0 -> % non-string object key (not first)
+ % non-string object key (not first)
+ {object, N, 0, true} when N > 0 ->
write(This0, <<",\"">>);
- {object, N, 0, false} when N > 0-> % string object key (not first)
+ % string object key (not first)
+ {object, N, 0, false} when N > 0 ->
write(This0, <<",">>);
- _ -> % no pre-field necessary
+ % no pre-field necessary
+ _ ->
{This0, ok}
end;
-
-% writes necessary postlude to field or container depending on current context
-write(#json_protocol{context_stack = []} = This0,
- {context_post_item, _}) -> {This0, ok};
-write(#json_protocol{context_stack = [Context|CtxtTail]} = This0,
- {context_post_item, MayNeedQuotes}) ->
+% writes necessary postlude to field or container depending on current context
+write(
+ #json_protocol{context_stack = []} = This0,
+ {context_post_item, _}
+) ->
+ {This0, ok};
+write(
+ #json_protocol{context_stack = [Context | CtxtTail]} = This0,
+ {context_post_item, MayNeedQuotes}
+) ->
FieldNo = Context#json_context.fields_processed,
CtxtType = Context#json_context.type,
Rem = FieldNo rem 2,
- {This1, ok} = case {CtxtType, Rem, MayNeedQuotes} of
- {object, 0, true} -> % non-string object key
- write(This0, <<"\":">>);
- {object, 0, false} -> % string object key
- write(This0, <<":">>);
- _ -> % no pre-field necessary
- {This0, ok}
- end,
+ {This1, ok} =
+ case {CtxtType, Rem, MayNeedQuotes} of
+ % non-string object key
+ {object, 0, true} ->
+ write(This0, <<"\":">>);
+ % string object key
+ {object, 0, false} ->
+ write(This0, <<":">>);
+ % no pre-field necessary
+ _ ->
+ {This0, ok}
+ end,
NewContext = Context#json_context{fields_processed = FieldNo + 1},
- {This1#json_protocol{context_stack=[NewContext|CtxtTail]}, ok};
-
+ {This1#json_protocol{context_stack = [NewContext | CtxtTail]}, ok};
write(This0, #protocol_message_begin{
name = Name,
type = Type,
- seqid = Seqid}) ->
+ seqid = Seqid
+}) ->
write_values(This0, [
{enter_context, array},
{i32, ?VERSION_1},
@@ -177,15 +204,14 @@
{i32, Type},
{i32, Seqid}
]);
-
-write(This, message_end) ->
+write(This, message_end) ->
write_values(This, [{exit_context}]);
-
% Example field expression: "1":{"dbl":3.14}
write(This0, #protocol_field_begin{
- name = _Name,
- type = Type,
- id = Id}) ->
+ name = _Name,
+ type = Type,
+ id = Id
+}) ->
write_values(This0, [
% entering 'outer' object
{i16, Id},
@@ -193,18 +219,16 @@
{enter_context, object},
{string, typeid_to_json(Type)}
]);
-
-write(This, field_stop) ->
+write(This, field_stop) ->
{This, ok};
-
-write(This, field_end) ->
- write_values(This,[{exit_context}]);
-
+write(This, field_end) ->
+ write_values(This, [{exit_context}]);
% Example message with map: [1,"testMap",1,0,{"1":{"map":["i32","i32",3,{"7":77,"8":88,"9":99}]}}]
write(This0, #protocol_map_begin{
- ktype = Ktype,
- vtype = Vtype,
- size = Size}) ->
+ ktype = Ktype,
+ vtype = Vtype,
+ size = Size
+}) ->
write_values(This0, [
{enter_context, array},
{string, typeid_to_json(Ktype)},
@@ -212,86 +236,79 @@
{i32, Size},
{enter_context, object}
]);
-
-write(This, map_end) ->
- write_values(This,[
+write(This, map_end) ->
+ write_values(This, [
{exit_context},
{exit_context}
]);
-
write(This0, #protocol_list_begin{
- etype = Etype,
- size = Size}) ->
+ etype = Etype,
+ size = Size
+}) ->
write_values(This0, [
{enter_context, array},
{string, typeid_to_json(Etype)},
{i32, Size}
]);
-
-write(This, list_end) ->
- write_values(This,[
+write(This, list_end) ->
+ write_values(This, [
{exit_context}
]);
-
% example message with set: [1,"testSet",1,0,{"1":{"set":["i32",3,1,2,3]}}]
write(This0, #protocol_set_begin{
- etype = Etype,
- size = Size}) ->
+ etype = Etype,
+ size = Size
+}) ->
write_values(This0, [
{enter_context, array},
{string, typeid_to_json(Etype)},
{i32, Size}
]);
-
-write(This, set_end) ->
- write_values(This,[
+write(This, set_end) ->
+ write_values(This, [
{exit_context}
]);
% example message with struct: [1,"testStruct",1,0,{"1":{"rec":{"1":{"str":"worked"},"4":{"i8":1},"9":{"i32":1073741824},"11":{"i64":1152921504606847000}}}}]
-write(This, #protocol_struct_begin{}) ->
+write(This, #protocol_struct_begin{}) ->
write_values(This, [
{enter_context, object}
]);
-
-write(This, struct_end) ->
- write_values(This,[
+write(This, struct_end) ->
+ write_values(This, [
{exit_context}
]);
-
-write(This, {bool, true}) -> write_values(This, [
+write(This, {bool, true}) ->
+ write_values(This, [
{context_pre_item, true},
<<"true">>,
{context_post_item, true}
]);
-
-write(This, {bool, false}) -> write_values(This, [
+write(This, {bool, false}) ->
+ write_values(This, [
{context_pre_item, true},
<<"false">>,
{context_post_item, true}
]);
-
-write(This, {byte, Byte}) -> write_values(This, [
+write(This, {byte, Byte}) ->
+ write_values(This, [
{context_pre_item, true},
list_to_binary(integer_to_list(Byte)),
{context_post_item, true}
]);
-
write(This, {i16, I16}) ->
write(This, {byte, I16});
-
write(This, {i32, I32}) ->
write(This, {byte, I32});
-
write(This, {i64, I64}) ->
write(This, {byte, I64});
-
-write(This, {double, Double}) -> write_values(This, [
+write(This, {double, Double}) ->
+ write_values(This, [
{context_pre_item, true},
- list_to_binary(io_lib:format("~.*f", [?JSON_DOUBLE_PRECISION,Double])),
+ list_to_binary(io_lib:format("~.*f", [?JSON_DOUBLE_PRECISION, Double])),
{context_post_item, true}
]);
-
-write(This0, {string, Str}) -> write_values(This0, [
+write(This0, {string, Str}) ->
+ write_values(This0, [
{context_pre_item, false},
case is_binary(Str) of
true -> Str;
@@ -299,7 +316,6 @@
end,
{context_post_item, false}
]);
-
%% TODO: binary fields should be base64 encoded?
%% Data :: iolist()
@@ -315,10 +331,11 @@
ThisOut
end,
This0,
- ValueList),
+ ValueList
+ ),
{FinalState, ok}.
-%% I wish the erlang version of the transport interface included a
+%% I wish the erlang version of the transport interface included a
%% read_all function (like eg. the java implementation). Since it doesn't,
%% here's my version (even though it probably shouldn't be in this file).
%%
@@ -327,7 +344,7 @@
read_all(#json_protocol{transport = Transport0} = State) ->
{Transport1, Bin} = read_all_1(Transport0, []),
P = thrift_json_parser:parser(),
- [First|Rest] = P(Bin),
+ [First | Rest] = P(Bin),
State#json_protocol{
transport = Transport1,
jsx = {event, First, Rest}
@@ -336,11 +353,14 @@
read_all_1(Transport0, IoList) ->
{Transport1, Result} = thrift_transport:read(Transport0, 1),
case Result of
- {ok, <<>>} -> % nothing read: assume we're done
+ % nothing read: assume we're done
+ {ok, <<>>} ->
{Transport1, iolist_to_binary(lists:reverse(IoList))};
- {ok, Data} -> % character successfully read; read more
- read_all_1(Transport1, [Data|IoList]);
- {error, 'EOF'} -> % we're done
+ % character successfully read; read more
+ {ok, Data} ->
+ read_all_1(Transport1, [Data | IoList]);
+ % we're done
+ {error, 'EOF'} ->
{Transport1, iolist_to_binary(lists:reverse(IoList))}
end.
@@ -348,17 +368,16 @@
% type as input. Comparing the read event from the one is was passed, it
% returns an error if something other than the expected value is encountered.
% Expect also maintains the context stack in #json_protocol.
-expect(#json_protocol{jsx={event, {Type, Data}=Ev, [Next|Rest]}}=State, ExpectedType) ->
- NextState = State#json_protocol{jsx={event, Next, Rest}},
+expect(#json_protocol{jsx = {event, {Type, Data} = Ev, [Next | Rest]}} = State, ExpectedType) ->
+ NextState = State#json_protocol{jsx = {event, Next, Rest}},
case Type == ExpectedType of
- true ->
+ true ->
{NextState, {ok, convert_data(Type, Data)}};
false ->
{NextState, {error, {unexpected_json_event, Ev}}}
end;
-
-expect(#json_protocol{jsx={event, Event, Next}}=State, ExpectedEvent) ->
- expect(State#json_protocol{jsx={event, {Event, none}, Next}}, ExpectedEvent).
+expect(#json_protocol{jsx = {event, Event, Next}} = State, ExpectedEvent) ->
+ expect(State#json_protocol{jsx = {event, {Event, none}, Next}}, ExpectedEvent).
convert_data(integer, I) -> list_to_integer(I);
convert_data(float, F) -> list_to_float(F);
@@ -369,9 +388,9 @@
expect_many_1(State, [], ResultList, Status) ->
{State, {Status, lists:reverse(ResultList)}};
-expect_many_1(State, [Expected|ExpTail], ResultList, _PrevStatus) ->
+expect_many_1(State, [Expected | ExpTail], ResultList, _PrevStatus) ->
{State1, {Status, Data}} = expect(State, Expected),
- NewResultList = [Data|ResultList],
+ NewResultList = [Data | ResultList],
case Status of
% in case of error, end prematurely
error -> expect_many_1(State1, [], NewResultList, Status);
@@ -381,178 +400,202 @@
% wrapper around expect to make life easier for container opening/closing functions
expect_nodata(This, ExpectedList) ->
case expect_many(This, ExpectedList) of
- {State, {ok, _}} ->
+ {State, {ok, _}} ->
{State, ok};
- Error ->
+ Error ->
Error
end.
-read_field(#json_protocol{jsx={event, Field, [Next|Rest]}} = State) ->
- NewState = State#json_protocol{jsx={event, Next, Rest}},
+read_field(#json_protocol{jsx = {event, Field, [Next | Rest]}} = State) ->
+ NewState = State#json_protocol{jsx = {event, Next, Rest}},
{NewState, Field}.
read(This0, message_begin) ->
% call read_all to get the contents of the transport buffer into JSX.
This1 = read_all(This0),
- case expect_many(This1,
- [start_array, integer, string, integer, integer]) of
+ case
+ expect_many(
+ This1,
+ [start_array, integer, string, integer, integer]
+ )
+ of
{This2, {ok, [_, Version, Name, Type, SeqId]}} ->
case Version =:= ?VERSION_1 of
true ->
- {This2, #protocol_message_begin{name = Name,
- type = Type,
- seqid = SeqId}};
+ {This2, #protocol_message_begin{
+ name = Name,
+ type = Type,
+ seqid = SeqId
+ }};
false ->
{This2, {error, no_json_protocol_version}}
end;
- Other -> Other
+ Other ->
+ Other
end;
-
-read(This, message_end) ->
+read(This, message_end) ->
expect_nodata(This, [end_array]);
-
-read(This, struct_begin) ->
+read(This, struct_begin) ->
expect_nodata(This, [start_object]);
-
-read(This, struct_end) ->
+read(This, struct_end) ->
expect_nodata(This, [end_object]);
-
read(This0, field_begin) ->
- {This1, Read} = expect_many(This0,
- [%field id
- key,
- % {} surrounding field
- start_object,
- % type of field
- key]),
+ {This1, Read} = expect_many(
+ This0,
+ %field id
+ [
+ key,
+ % {} surrounding field
+ start_object,
+ % type of field
+ key
+ ]
+ ),
case Read of
{ok, [FieldIdStr, _, FieldType]} ->
{This1, #protocol_field_begin{
- type = json_to_typeid(FieldType),
- id = list_to_integer(FieldIdStr)}}; % TODO: do we need to wrap this in a try/catch?
- {error,[{unexpected_json_event, {end_object,none}}]} ->
+ type = json_to_typeid(FieldType),
+ % TODO: do we need to wrap this in a try/catch?
+ id = list_to_integer(FieldIdStr)
+ }};
+ {error, [{unexpected_json_event, {end_object, none}}]} ->
{This1, #protocol_field_begin{type = ?tType_STOP}};
- Other ->
+ Other ->
io:format("**** OTHER branch selected ****"),
{This1, Other}
end;
-
-read(This, field_end) ->
+read(This, field_end) ->
expect_nodata(This, [end_object]);
-
% Example message with map: [1,"testMap",1,0,{"1":{"map":["i32","i32",3,{"7":77,"8":88,"9":99}]}}]
read(This0, map_begin) ->
- case expect_many(This0,
- [start_array,
- % key type
- string,
- % value type
- string,
- % size
- integer,
- % the following object contains the map
- start_object]) of
+ case
+ expect_many(
+ This0,
+ [
+ start_array,
+ % key type
+ string,
+ % value type
+ string,
+ % size
+ integer,
+ % the following object contains the map
+ start_object
+ ]
+ )
+ of
{This1, {ok, [_, Ktype, Vtype, Size, _]}} ->
- {This1, #protocol_map_begin{ktype = Ktype,
- vtype = Vtype,
- size = Size}};
- Other -> Other
+ {This1, #protocol_map_begin{
+ ktype = Ktype,
+ vtype = Vtype,
+ size = Size
+ }};
+ Other ->
+ Other
end;
-
-read(This, map_end) ->
+read(This, map_end) ->
expect_nodata(This, [end_object, end_array]);
-
read(This0, list_begin) ->
- case expect_many(This0,
- [start_array,
- % element type
- string,
- % size
- integer]) of
+ case
+ expect_many(
+ This0,
+ [
+ start_array,
+ % element type
+ string,
+ % size
+ integer
+ ]
+ )
+ of
{This1, {ok, [_, Etype, Size]}} ->
{This1, #protocol_list_begin{
etype = Etype,
- size = Size}};
- Other -> Other
+ size = Size
+ }};
+ Other ->
+ Other
end;
-
-read(This, list_end) ->
+read(This, list_end) ->
expect_nodata(This, [end_array]);
-
% example message with set: [1,"testSet",1,0,{"1":{"set":["i32",3,1,2,3]}}]
read(This0, set_begin) ->
- case expect_many(This0,
- [start_array,
- % element type
- string,
- % size
- integer]) of
+ case
+ expect_many(
+ This0,
+ [
+ start_array,
+ % element type
+ string,
+ % size
+ integer
+ ]
+ )
+ of
{This1, {ok, [_, Etype, Size]}} ->
{This1, #protocol_set_begin{
etype = Etype,
- size = Size}};
- Other -> Other
+ size = Size
+ }};
+ Other ->
+ Other
end;
-
-read(This, set_end) ->
+read(This, set_end) ->
expect_nodata(This, [end_array]);
-
read(This0, field_stop) ->
{This0, ok};
%%
read(This0, bool) ->
{This1, Field} = read_field(This0),
- Value = case Field of
- {literal, I} ->
- {ok, I};
- _Other ->
- {error, unexpected_event_for_boolean}
- end,
+ Value =
+ case Field of
+ {literal, I} ->
+ {ok, I};
+ _Other ->
+ {error, unexpected_event_for_boolean}
+ end,
{This1, Value};
-
read(This0, byte) ->
{This1, Field} = read_field(This0),
- Value = case Field of
- {key, K} ->
- {ok, list_to_integer(K)};
- {integer, I} ->
- {ok, list_to_integer(I)};
- _Other ->
- {error, unexpected_event_for_integer}
- end,
+ Value =
+ case Field of
+ {key, K} ->
+ {ok, list_to_integer(K)};
+ {integer, I} ->
+ {ok, list_to_integer(I)};
+ _Other ->
+ {error, unexpected_event_for_integer}
+ end,
{This1, Value};
-
read(This0, i16) ->
read(This0, byte);
-
read(This0, i32) ->
read(This0, byte);
-
read(This0, i64) ->
read(This0, byte);
-
read(This0, double) ->
{This1, Field} = read_field(This0),
- Value = case Field of
- {float, I} ->
- {ok, list_to_float(I)};
- _Other ->
- {error, unexpected_event_for_double}
- end,
+ Value =
+ case Field of
+ {float, I} ->
+ {ok, list_to_float(I)};
+ _Other ->
+ {error, unexpected_event_for_double}
+ end,
{This1, Value};
-
% returns a binary directly, call binary_to_list if necessary
read(This0, string) ->
{This1, Field} = read_field(This0),
- Value = case Field of
- {string, I} ->
- {ok, I};
- {key, J} ->
- {ok, J};
- _Other ->
- {error, unexpected_event_for_string}
- end,
+ Value =
+ case Field of
+ {string, I} ->
+ {ok, I};
+ {key, J} ->
+ {ok, J};
+ _Other ->
+ {error, unexpected_event_for_string}
+ end,
{This1, Value}.
%%%% FACTORY GENERATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -561,7 +604,7 @@
new_protocol_factory(TransportFactory, _Options) ->
% Only strice read/write are implemented
F = fun() ->
- {ok, Transport} = TransportFactory(),
- thrift_json_protocol:new(Transport, [])
- end,
+ {ok, Transport} = TransportFactory(),
+ thrift_json_protocol:new(Transport, [])
+ end,
{ok, F}.