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}.