Skip to content

Commit

Permalink
Implement additional consent (AC) string parsing
Browse files Browse the repository at this point in the history
Use single error type for ac string parsing failure

Adds a new record under the name `#addtl_consent{}` to contain the
parsed additional consent string.
  • Loading branch information
berbiche committed Nov 15, 2023
1 parent af45e7b commit 7b85358
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 1 deletion.
6 changes: 6 additions & 0 deletions include/consent_string.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
publisher_tc :: undefined | consent_segment()
}).

-record(addtl_consent, {
version :: pos_integer(),
atp_ids :: [pos_integer()]
}).

-record(consent_segment_entry_disclosed_vendors, {
max_vendor_id :: pos_integer(),
entries :: range_or_bitfield()
Expand Down Expand Up @@ -98,6 +103,7 @@
}).

-type consent() :: #consent {}.
-type addtl_consent() :: #addtl_consent {}.

-type consent_segment() :: #consent_segment {}.

Expand Down
7 changes: 6 additions & 1 deletion rebar.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{cover_excl_mods, [
consent_debug,
consent_string_cli
]}.

{edoc_opts, [
{app_default, "http://www.erlang.org/doc/man"},
{doclet, edown_doclet},
Expand Down Expand Up @@ -29,7 +34,7 @@
{edoc, [
{deps, [
{edown,
{git, "https://github.com/uwiger/edown.git", {tag, "0.7"}}}
{git, "https://github.com/uwiger/edown.git", {tag, "0.8.4"}}}
]}
]}
]}.
Expand Down
89 changes: 89 additions & 0 deletions src/additional_consent_string.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
-module(additional_consent_string).
-include("consent_string.hrl").

-compile([inline]).

-export([parse/1]).
-ifdef(TEST).
-export([parse_version/1, parse_atp_ids/1]).
-endif.

-define(KNOWN_SEPARATOR, "~").
-spec parse(binary()) ->
{ok, addtl_consent()} | {error, invalid_ac_string}.
% @doc Parses an additional consent string, returning the version
% number and the list of numerical ATP ids.
%
% @returns `{ok, #addtl_consent{}}' if the string is valid,
% otherwise an appropriate error is returned.
%
parse(Bin) when is_binary(Bin) ->
case parse_version(Bin) of
{ok, Version, Bin1} ->
case parse_atp_ids(Bin1) of
{ok, AtpIds} ->
{ok, #addtl_consent {
version = Version,
atp_ids = AtpIds
}};
Error -> Error
end;
Error -> Error
end;
parse(_) ->
{error, invalid_ac_string}.
parse_version(Bin) ->
parse_version(Bin, <<"">>).
parse_version(<<Bin, Rest/binary>>, Acc) when Bin >= $0, Bin =< $9 ->
parse_version(Rest, <<Acc/binary, Bin>>);
% Consume the separator token
parse_version(<<?KNOWN_SEPARATOR, Rest/binary>>, Acc) ->
case safe_binary_to_integer(Acc) of
undefined ->
{error, invalid_ac_string};
Version ->
{ok, Version, Rest}
end;
parse_version(_, _) ->
{error, invalid_ac_string}.
parse_atp_ids(Bin) ->
parse_atp_ids(Bin, <<"">>, []).
parse_atp_ids(<<Bin, Rest/binary>>, Acc, Ids) when Bin >= $0, Bin =< $9 ->
parse_atp_ids(Rest, <<Acc/binary, Bin>>, Ids);
parse_atp_ids(<<".", Rest/binary>>, Acc, Ids) when Rest =/= <<"">> ->
case safe_binary_to_integer(Acc) of
undefined ->
{error, invalid_ac_string};
Id ->
parse_atp_ids(Rest, <<"">>, [Id | Ids])
end;
parse_atp_ids(<<_Invalid, _Rest/binary>>, _Acc, _Ids) ->
{error, invalid_ac_string};
parse_atp_ids(<<"">>, <<"">>, Ids) ->
{ok, lists:reverse(Ids)};
parse_atp_ids(<<"">>, Acc, Ids) ->
case safe_binary_to_integer(Acc) of
undefined ->
{error, invalid_ac_string};
Id ->
% Preserve the order of ids
{ok, lists:reverse([Id | Ids])}
end.
-spec safe_binary_to_integer(binary()) -> undefined | integer().
safe_binary_to_integer(Bin) ->
try
binary_to_integer(Bin, 10)
catch
_:_ ->
undefined
end.
75 changes: 75 additions & 0 deletions test/ac_string_tests.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
-module(ac_string_tests).
-include("consent_string.hrl").
-include_lib("eunit/include/eunit.hrl").

-define(_assertMatchAtp(A, B),
?_assertMatch({ok, (A)}, parse_atp_ids((B)))).
-define(_assertMatchVersion(A, B),
?_assertMatch({ok, (A), _}, parse_version((B)))).
-define(_assertErrorVersion(B),
?_assertMatch({error, invalid_ac_string}, parse_version((B)))).
-define(_assertErrorAtp(B),
?_assertMatch({error, invalid_ac_string}, parse_atp_ids((B)))).

parse_version_only_test_() ->
[
?_assertMatchVersion(20, <<"20~">>),
?_assertMatchVersion(0, <<"0~">>),
% Test error clauses
% Make sure that random separators are not recognized
?_assertErrorVersion(<<"1=">>),
% Alphanumeric character in version
?_assertErrorVersion(<<"1a~">>),
% Missing separator at the end
?_assertErrorVersion(<<"1">>),
% Separator at the start
?_assertErrorVersion(<<"~1">>),
% Separator at both eds
?_assertErrorVersion(<<"~1~">>)
].
parse_atps_only_test_() ->
[
% Make sure the order is preserved after parsing
?_assertMatchAtp([1, 10, 100, 20], atps([1, 10, 100, 20])),
?_assertMatchAtp([1], atps([1])),
% Empty list of version should NOT cause an error
?_assertMatchAtp([], atps([])),
% Test error clauses
% Alphanumeric ids are not allowed
?_assertErrorAtp(<<"1.1.a">>),
% Trailing separators are not allowed
?_assertErrorAtp(<<"1.">>),
% Invalid separator
?_assertErrorAtp(<<"1,2,3">>)
].
parse_test_() ->
[
?_assertMatch({ok, #addtl_consent{version = 1, atp_ids = []}}, parse(<<"1~">>)),
?_assertMatch({ok, #addtl_consent{version = 1, atp_ids = [1]}}, parse(<<"1~1">>)),
?_assertMatch({ok, #addtl_consent{version = 1, atp_ids = [1, 3, 2]}}, parse(<<"1~1.3.2">>)),
% Test errors
?_assertMatch({error, invalid_ac_string}, parse(<<"1~a">>)),
?_assertMatch({error, invalid_ac_string}, parse(<<"1a1~">>))
].
%% Helpers
parse(Bin) ->
additional_consent_string:parse(Bin).
parse_version(Bin) ->
additional_consent_string:parse_version(Bin).
parse_atp_ids(Bin) ->
additional_consent_string:parse_atp_ids(Bin).
atps([]) ->
<<"">>;
atps(List) ->
list_to_binary(
lists:join(".", [integer_to_list(X) || X <- List])
).

0 comments on commit 7b85358

Please sign in to comment.