Skip to content

Commit

Permalink
IDL to avpr/avsc converter; support multiple throw types
Browse files Browse the repository at this point in the history
  • Loading branch information
seriyps committed Mar 14, 2020
1 parent 788f725 commit e8c8739
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 38 deletions.
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
{cover_enabled, true}.
{cover_export_enabled, true}.

%% {yrl_opts, [{verbose, true}]}.
{yrl_opts, [{verbose, true}]}.
173 changes: 173 additions & 0 deletions src/avro_idl.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
-module(avro_idl).

-export([new_context/1,
str_to_avpr/2,
protocol_to_avpr/2,
typedecl_to_avsc/2]).
-include("idl.hrl").

-record(st, {cwd}).

new_context(Cwd) ->
#st{cwd = Cwd}.

str_to_avpr(String, Cwd) ->
str_to_avpr(String, Cwd, [drop_comments, trim_doc]).

str_to_avpr(String, Cwd, Opts) ->
{ok, T0, _} = avro_idl_lexer:string(String),
T = avro_idl_lexer:preprocess(T0, Opts),
{ok, Tree} = avro_idl_parser:parse(T),
protocol_to_avpr(Tree, new_context(Cwd)).

protocol_to_avpr(#protocol{name = Name,
meta = Meta,
definitions = Defs0}, St) ->
Defs = process_imports(Defs0, St),
{Types, Messages} =
lists:partition(fun(#function{}) -> false;
(_) -> true
end, Defs),
Protocol0 =
#{protocol => Name,
types =>
lists:map(
fun(Type) ->
typedecl_to_avsc(Type, St)
end, Types),
messages =>
lists:map(
fun(Message) ->
message_to_avsc(Message, St)
end, Messages)
},
meta(Protocol0, Meta).

process_imports(Defs, _St) ->
%% TODO
lists:filter(fun({import, _, _}) -> false;
(_) -> true
end, Defs).

typedecl_to_avsc(#enum{name = Name, meta = Meta, variants = Vars}, _St) ->
meta(
#{type => enum,
name => Name,
variants => Vars
},
Meta);
typedecl_to_avsc(#fixed{name = Name, meta = Meta, size = Size}, _St) ->
meta(
#{type => fixed,
name => Name,
size => Size},
Meta);
typedecl_to_avsc(#error{name = Name, meta = Meta, fields = Fields}, St) ->
meta(
#{type => error,
name => Name,
fields => [field_to_avsc(Field, St) || Field <- Fields]},
Meta);
typedecl_to_avsc(#record{name = Name, meta = Meta, fields = Fields}, St) ->
meta(
#{type => record,
name => Name,
fields => [field_to_avsc(Field, St) || Field <- Fields]},
Meta).

field_to_avsc(#field{name = Name, meta = Meta,
type = Type, default = Default}, St) ->
meta(
default(
#{name => Name,
type => type_to_avsc(Type, St)},
Default), % TODO: maybe validate default matches type
Meta).

message_to_avsc(#function{name = Name, meta = Meta,
arguments = Args, return = Return,
extra = Extra}, St) ->
%% TODO: arguments can just reuse `#field{}`
ArgsSchema =
[default(
#{name => ArgName,
type => type_to_avsc(Type, St)},
Default)
|| {arg, ArgName, Type, Default} <- Args],
Schema0 =
#{name => Name,
request => ArgsSchema,
response => type_to_avsc(Return, St)},
Schema1 = case Extra of
undefined -> Schema0;
oneway ->
Schema0#{'one-way' => true};
{throws, ThrowsTypes} ->
%% Throws = [type_to_avsc(TType, St)
%% || TType <- ThrowsTypes],
Schema0#{error => ThrowsTypes}
end,
meta(Schema1, Meta).


type_to_avsc(void, _St) ->
null;
type_to_avsc(null, _St) ->
null;
type_to_avsc(T, _St) when T == int;
T == long;
T == string;
T == boolean;
T == float;
T == double;
T == bytes ->
T;
type_to_avsc({decimal, Precision, Scale}, _St) ->
#{type => bytes,
'logicalType' => "decimal",
precision => Precision,
scale => Scale};
type_to_avsc(date, _St) ->
#{type => int,
'logicalType' => "date"};
type_to_avsc(time_ms, _St) ->
#{type => int,
'logicalType' => "time-millis"};
type_to_avsc(timestamp_ms, _St) ->
#{type => long,
'logicalType' => "timestamp-millis"};
type_to_avsc({custom, Id}, _St) ->
Id;
type_to_avsc({union, Types}, St) ->
[type_to_avsc(Type, St) || Type <- Types];
type_to_avsc({array, Of}, St) ->
#{type => array,
items => type_to_avsc(Of, St)};
type_to_avsc({map, ValType}, St) ->
#{type => map,
values => type_to_avsc(ValType, St)}.

meta(Schema, Meta) ->
{Docs, Annotations} =
lists:partition(
fun({doc, _}) -> true;
(#annotation{}) -> false
end, Meta),
Schema1 = case Docs of
[] -> Schema;
_ ->
DocStrings = [S || {doc, S} <- Docs],
Schema#{"doc" => lists:flatten(lists:join(
"\n", DocStrings))}
end,
lists:foldl(
fun(#annotation{name = Name, value = Value}, Schema2) ->
maps:is_key(Name, Schema2) andalso
error({duplicate_annotation, Name, Value, Schema2}),
Schema2#{Name => Value}
end, Schema1, Annotations).

default(Obj, undefined) ->
Obj;
default(Obj, Default) ->
Obj#{default => Default}.
17 changes: 13 additions & 4 deletions src/avro_idl_parser.yrl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Nonterminals
fixed
array
map
function fun_return fun_arguments fun_argument fun_extra
function fun_return fun_arguments fun_argument fun_extra throws
data array_of_data array_of_data_tail map_of_data map_of_data_tail.

Rootsymbol protocol.
Expand Down Expand Up @@ -111,7 +111,7 @@ declaration -> function : '$1'.

import ->
import_k import_file_type string_v ';' :
{import, '$2', value_of('$3')}.
#import{type = '$2', file_path = value_of('$3')}.

import_file_type -> idl_k : idl.
import_file_type -> protocol_k : protocol.
Expand Down Expand Up @@ -223,6 +223,9 @@ map ->
function ->
fun_return id '(' fun_arguments ')' fun_extra ';' :
#function{name = value_of('$2'), arguments = '$4', return = '$1', extra = '$6'}.
function ->
doc_v function :
('$2')#function{meta = [{doc, value_of('$1')}]}.

fun_return -> type : '$1'.
fun_return -> void_k : void.
Expand All @@ -247,12 +250,18 @@ fun_argument ->
fun_extra ->
'$empty' : undefined.
fun_extra ->
throws_k id :
{throws, value_of('$2')}.
throws_k id throws :
{throws, [value_of('$2') | '$3']}.
fun_extra ->
oneway_k :
oneway.

throws ->
'$empty' :
[].
throws ->
',' id throws:
[value_of('$2') | '$3'].

%% == Data (JSON) for default values
data -> string_v : value_of('$1').
Expand Down
6 changes: 5 additions & 1 deletion src/idl.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
{name,
value}).

-record(import,
{type,
file_path}).

-record(enum,
{name,
meta = [],
Expand Down Expand Up @@ -35,7 +39,7 @@

-record(function,
{name,
%% meta = [],
meta = [],
arguments = [],
return,
extra}).
77 changes: 47 additions & 30 deletions test/avro_idl_parse_tests.erl
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
%% coding: latin-1
%%%-------------------------------------------------------------------
%%% Copyright (c) 2013-2018 Klarna AB
%%%
%%% 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.
%%%
%%%-------------------------------------------------------------------
%% @doc Tests for IDL lexer + parser
-module(avro_idl_parse_tests).

-include("../src/idl.hrl").
Expand Down Expand Up @@ -79,7 +61,12 @@ parse_annotations_test() ->
{doc, "My Rec Field"},
#annotation{name = "aliases",
value = ["my_alias"]}],
type = string}]}]
type = string}]},
#function{name = "hello",
meta = [{doc, "My Fun"}],
arguments = [],
return = string,
extra = undefined}]
},
parse_idl("annotations")).

Expand All @@ -102,23 +89,53 @@ full_protocol_test() ->
#function{name = "ping"}]},
parse_idl("full_protocol")).

protocol_with_typedeffs_test() ->
protocol_with_typedefs_test() ->
?assertMatch(
#protocol{name = "MyProto",
definitions =
[{import, idl, "foo.avdl"},
{import, protocol, "bar.avpr"},
{import, schema, "baz.avsc"},
[#import{type = idl, file_path = "foo.avdl"},
#import{type = protocol, file_path = "bar.avpr"},
#import{type = schema, file_path = "baz.avsc"},
#enum{name = "MyEnum1"},
#enum{name = "MyEnum2"},
#fixed{name = "MyFix"},
#record{name = "MyRec"},
#record{name = "MyAnnotated"},
#record{name = "MyRec",
fields =
[#field{name = "my_int", type = int},
#field{name = "my_string", type = string},
#field{name = "my_float", type = float},
#field{name = "my_bool", type = boolean,
default = false},
#field{name = "my_custom",
type = {custom, "MyFix"}},
#field{name = "my_union",
type = {union, [boolean, null]},
default = null},
#field{name = "my_date",
type = date},
#field{name = "my_decimal",
type = {decimal, 5, 2}},
#field{name = "my_int_array",
type = {array, int}},
#field{},
#field{},
#field{name = "my_map",
type = {map, float}}
]},
#record{name = "MyAnnotated",
fields =
[#field{
name = "error",
type = {custom,
"org.erlang.www.MyError"}}
]},
#error{name = "MyError"},
#function{name = "mul"},
#function{name = "append"},
#function{name = "gen_server_cast"},
#function{name = "ping"}]},
#function{name = "div",
extra = {throws, ["DivisionByZero"]}},
#function{name = "append",
extra = {throws, ["MyError", "TheirError"]}},
#function{name = "gen_server_cast", extra = oneway},
#function{name = "ping", extra = undefined}]},
parse_idl("protocol_with_typedefs")).

parse_idl(Name) ->
Expand Down
Loading

0 comments on commit e8c8739

Please sign in to comment.