%%%-------------------------------------------------------------------------
%%% @copyright (C) 2023, meppu
%%% @doc Connection configuration module for SurrealDB Erlang.
%%%
%%% == SurrealDB URI Format ==
%%%
%%% The SurrealDB URI format is an unofficial way to represent connection information for accessing SurrealDB.
%%% It allows users to define various parameters necessary for establishing a connection to the database server.
%%%
%%% This format has been adopted and enhanced from
%%% <a href="https://github.com/ri-nat/mylk#database-connection-string-format" target="_blank">mylk</a>,
%%% a SurrealDB driver for Ruby.
%%%
%%% The URI format follows this format:
%%%
%%% - <strong>Plain TCP</strong>:
%%% `surrealdb://username:password@host:port/namespace/database'
%%%
%%% - <strong>TLS</strong>:
%%% `surrealdb+tls://username:password@host:port/namespace/database'
%%%
%%% === Scheme ===
%%%
%%% This is the scheme used to identify SurrealDB connections. The optional "tls" part
%%% indicates that the connection should be made over TLS (Transport Layer Security),
%%% providing a secure and encrypted communication channel.
%%%
%%% === Credentials ===
%%%
%%% The username and password to authenticate with the SurrealDB server.
%%%
%%% === Host ===
%%%
%%% The hostname or IP address of the SurrealDB server.
%%%
%%% === Namespace ===
%%%
%%% The name of the SurrealDB namespace that you want to use.
%%%
%%% === Database ===
%%%
%%% The name of the SurrealDB database that you want to use.
%%%
%%% === Timeout ===
%%%
%%% Timeout for the operations in milliseconds. Default is `5000' (5 seconds).
%%%
%%% == Example URI ==
%%%
%%% <strong>Default for SurrealDB</strong>:
%%% `surrealdb://root:root@localhost:8000/test/test'
%%%
%%% <strong>Default for SurrealDB with 10s timeout</strong>:
%%% `surrealdb://root:root@localhost:8000/test/test?timeout=10000'
%%%
%%% @author meppu
%%% @end
%%%-------------------------------------------------------------------------
-module(surreal_config).
-export([parse/1]).
-export_type([connection_map/0]).
%%%==========================================================================
%%%
%%% Public types
%%%
%%%==========================================================================
-type connection_map() :: #{
host => string(),
username => string(),
password => string(),
namespace => string(),
database => string(),
port => non_neg_integer(),
tls => boolean(),
timeout => timeout()
}.
%%%==========================================================================
%%%
%%% Public functions
%%%
%%%==========================================================================
%%-------------------------------------------------------------------------
%% @doc Parse SurrealDB URI.
%%
%% ```
%1> surreal_config:parse("surrealdb://root:root@localhost:8000/test/test").
%% % {ok,#{database => "test",host => "localhost",namespace => "test",
%% % password => "root",port => 8000,timeout => 5000,
%% % tls => false,username => "root"}}
%% '''
%% @end
%%-------------------------------------------------------------------------
-spec parse(Uri :: nonempty_string()) -> {ok, connection_map()} | {error, atom(), term()}.
parse(Uri) ->
case uri_string:parse(Uri) of
#{
host := Host,
path := Path,
port := Port,
scheme := Scheme,
userinfo := UserInfo
} = Parsed ->
try
{ok, Tls} = parse_scheme(Scheme),
{ok, [Username, Password]} = parse_userinfo(UserInfo),
{ok, [Namespace, Database]} = parse_path(Path),
{ok, Timeout} = parse_timeout(maps:get(query, Parsed, "timeout=5000")),
{ok, #{
host => Host,
username => Username,
password => Password,
namespace => Namespace,
database => Database,
port => Port,
tls => Tls,
timeout => Timeout
}}
catch
error:{badmatch, Error} ->
Error
end;
{error, _, _} = Error ->
Error;
_ ->
{error, invalid_uri, []}
end.
%%%==========================================================================
%%%
%%% Private functions
%%%
%%%==========================================================================
%% @private
parse_userinfo(UserInfo) ->
case string:split(UserInfo, ":", all) of
[_, _] = Data -> {ok, Data};
_ -> {error, invalid_userinfo}
end.
%% @private
parse_path(Path) ->
case string:split(Path, "/", all) of
[[], Namespace, Database] -> {ok, [Namespace, Database]};
_ -> {error, invalid_path}
end.
%% @private
parse_scheme("surrealdb") ->
{ok, false};
parse_scheme("surrealdb+tls") ->
{ok, true};
parse_scheme(_Other) ->
{error, invalid_scheme}.
%% @private
parse_timeout(RawQuery) ->
ParsedQuery = uri_string:dissect_query(RawQuery),
case lists:keysearch("timeout", 1, ParsedQuery) of
{value, {"timeout", Timeout}} ->
case string:to_integer(Timeout) of
{Int, []} ->
{ok, Int};
_ ->
{error, invalid_timeout}
end;
_Other ->
{error, invalid_query}
end.