Replace the protobuf hex package (agent.pb.ex) and hand-written validator
(validator.ex) with a pure Gleam implementation: wire format primitives,
all 23 message types, encode/decode functions, and validation merged into
decode. Thin Elixir wrappers in agent.ex preserve the existing struct API.
- Wire format: varint, length-delimited, double (IEEE 754 via FFI), tag encode/decode
- Types: all proto3 message types as Gleam custom types with enums and oneof
- Decode: field-level parsing with integrated validation (UUID, IP, hostname, limits)
- Encode: proto3 default omission, bytes_tree concatenation, map/repeated fields
- Wrappers: Elixir structs with encode/decode delegating to Gleam, enum atom conversion
- Remove {:protobuf, "~> 0.12"} dependency
Reviewed-on: graham/towerops-web#104
79 lines
2.7 KiB
Erlang
79 lines
2.7 KiB
Erlang
-module(towerops_proto_decode_ffi).
|
|
-export([bits_to_string/1, is_valid_uuid/1, is_valid_version/1,
|
|
is_valid_hostname/1, parse_ip_address/1, int_to_string/1,
|
|
int_to_float/1, float_gte/2, float_lte/2]).
|
|
|
|
%% Convert binary to string (identity on BEAM, but validates UTF-8).
|
|
-spec bits_to_string(binary()) -> {ok, binary()} | {error, nil}.
|
|
bits_to_string(Data) when is_binary(Data) ->
|
|
case unicode:characters_to_binary(Data, utf8) of
|
|
Bin when is_binary(Bin) -> {ok, Bin};
|
|
_ -> {error, nil}
|
|
end.
|
|
|
|
%% Check if a string is a valid UUID (lowercase or uppercase hex).
|
|
-spec is_valid_uuid(binary()) -> boolean().
|
|
is_valid_uuid(<<A:8/binary, $-, B:4/binary, $-, C:4/binary, $-, D:4/binary, $-, E:12/binary>>) ->
|
|
is_hex(A) andalso is_hex(B) andalso is_hex(C) andalso is_hex(D) andalso is_hex(E);
|
|
is_valid_uuid(_) ->
|
|
false.
|
|
|
|
is_hex(<<>>) -> true;
|
|
is_hex(<<C, Rest/binary>>) when
|
|
(C >= $0 andalso C =< $9) orelse
|
|
(C >= $a andalso C =< $f) orelse
|
|
(C >= $A andalso C =< $F) ->
|
|
is_hex(Rest);
|
|
is_hex(_) -> false.
|
|
|
|
%% Check if a version string is valid.
|
|
%% Accepts: semver (1.2.3, 1.2.3-rc.1), RFC 3339 timestamps,
|
|
%% git short SHA (7-40 hex chars), short identifiers like "dev".
|
|
-spec is_valid_version(binary()) -> boolean().
|
|
is_valid_version(Version) ->
|
|
%% Try patterns in order
|
|
case re:run(Version, <<"^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.\\-]+)?$">>) of
|
|
{match, _} -> true;
|
|
nomatch ->
|
|
case re:run(Version, <<"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$">>) of
|
|
{match, _} -> true;
|
|
nomatch ->
|
|
case re:run(Version, <<"^[a-zA-Z0-9]{1,40}$">>) of
|
|
{match, _} -> true;
|
|
nomatch -> false
|
|
end
|
|
end
|
|
end.
|
|
|
|
%% Check if a hostname is valid RFC 1123.
|
|
-spec is_valid_hostname(binary()) -> boolean().
|
|
is_valid_hostname(Hostname) ->
|
|
case re:run(Hostname, <<"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$">>) of
|
|
{match, _} -> true;
|
|
nomatch -> false
|
|
end.
|
|
|
|
%% Parse an IP address (IPv4 or IPv6).
|
|
-spec parse_ip_address(binary()) -> {ok, nil} | {error, nil}.
|
|
parse_ip_address(Ip) ->
|
|
case inet:parse_address(binary_to_list(Ip)) of
|
|
{ok, _} -> {ok, nil};
|
|
{error, _} -> {error, nil}
|
|
end.
|
|
|
|
%% Convert integer to string.
|
|
-spec int_to_string(integer()) -> binary().
|
|
int_to_string(I) ->
|
|
integer_to_binary(I).
|
|
|
|
%% Convert integer to float.
|
|
-spec int_to_float(integer()) -> float().
|
|
int_to_float(I) ->
|
|
float(I).
|
|
|
|
%% Float comparison helpers.
|
|
-spec float_gte(float(), float()) -> boolean().
|
|
float_gte(A, B) -> A >= B.
|
|
|
|
-spec float_lte(float(), float()) -> boolean().
|
|
float_lte(A, B) -> A =< B.
|