Current section

Files

Jump to
locus src locus.erl
Raw

src/locus.erl

%% Copyright (c) 2017-2026 Guilherme Andrade
%%
%% Permission is hereby granted, free of charge, to any person obtaining a
%% copy of this software and associated documentation files (the "Software"),
%% to deal in the Software without restriction, including without limitation
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%% and/or sell copies of the Software, and to permit persons to whom the
%% Software is furnished to do so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%% DEALINGS IN THE SOFTWARE.
%%
%% locus is an independent project and has not been authorized, sponsored,
%% or otherwise approved by MaxMind.
-module(locus).
-ifdef(E48).
-moduledoc "The main API.".
-endif.
%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------
-export([
start_loader/2,
start_loader/3,
stop_loader/1,
loader_child_spec/2,
loader_child_spec/3,
loader_child_spec/4,
await_loader/1,
await_loader/2,
await_loaders/2,
lookup/2,
get_info/1,
get_info/2,
check/1
]).
-ignore_xref([
start_loader/2,
start_loader/3,
stop_loader/1,
loader_child_spec/2,
loader_child_spec/3,
loader_child_spec/4,
await_loader/1,
await_loader/2,
await_loaders/2,
lookup/2,
get_info/1,
get_info/2,
check/1
]).
-ifdef(TEST).
-export([parse_database_edition/1]).
-endif.
%% ------------------------------------------------------------------
%% CLI-only Function Exports
%% ------------------------------------------------------------------
-ifdef(ESCRIPTIZING).
-export([main/1]).
-ignore_xref(main/1).
-endif.
%% ------------------------------------------------------------------
%% Macro Definitions
%% ------------------------------------------------------------------
-define(might_be_chardata(V), (is_binary((V)) orelse ?is_proper_list((V)))).
-define(is_proper_list(V), (length((V)) >= 0)).
%% ------------------------------------------------------------------
%% Type Definitions
%% ------------------------------------------------------------------
-type database_edition() :: maxmind_database_edition().
-export_type([database_edition/0]).
-type maxmind_database_edition() ::
{maxmind, atom() | unicode:chardata()}
| legacy_maxmind_database_edition().
-export_type([maxmind_database_edition/0]).
-type legacy_maxmind_database_edition() :: atom().
-export_type([legacy_maxmind_database_edition/0]).
-type database_url() :: unicode:chardata().
-export_type([database_url/0]).
-type custom_fetcher() :: {custom_fetcher, module(), Args :: term()}.
-export_type([custom_fetcher/0]).
-type database_error() :: database_unknown | database_not_loaded.
-export_type([database_error/0]).
-type database_entry() :: locus_mmdb_data:value().
-export_type([database_entry/0]).
-type ip_address_prefix() :: locus_mmdb_tree:ip_address_prefix().
-export_type([ip_address_prefix/0]).
-type database_info() ::
#{
metadata := database_metadata(),
source := database_source(),
version := database_version()
}.
-export_type([database_info/0]).
-type database_metadata() :: locus_mmdb_metadata:t().
-export_type([database_metadata/0]).
-type database_source() :: locus_loader:source().
-export_type([database_source/0]).
-type database_version() :: calendar:datetime().
-export_type([database_version/0]).
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
-ifdef(E48).
-doc """
Like `start_loader/3` but with default options.
- `DatabaseId` must be an atom.
- `LoadFrom` must be either:
- a `t:database_edition/0` tuple, or
- a `DatabaseURL` containing either a string or binary representation
of a HTTP(s) URL or local path, or
- a `{custom_fetcher, Module, Args}` tuple, with `Module` implementing
the `m:locus_custom_fetcher` behaviour
Returns:
- `ok` in case of success.
- `{error, invalid_url}` if the source is invalid.
- `{error, already_started}` if the loader under `DatabaseId` has already been started.
See also: `await_loader/1`, `await_loader/2`, `start_loader/3`.
""".
-endif.
-spec start_loader(DatabaseId, LoadFrom) -> ok | {error, Error} when
DatabaseId :: atom(),
LoadFrom :: DatabaseEdition | DatabaseURL | CustomFetcher,
DatabaseEdition :: database_edition(),
DatabaseURL :: database_url(),
CustomFetcher :: custom_fetcher(),
Error :: invalid_url | already_started | application_not_running.
start_loader(DatabaseId, LoadFrom) ->
start_loader(DatabaseId, LoadFrom, []).
%%
-ifdef(E48).
-doc """
Starts a database loader under id `DatabaseId` with options `Opts`.
- `DatabaseId` must be an atom.
- `LoadFrom` must be either:
- a `t:database_edition/0` tuple, or
- a `DatabaseURL` containing either a string or binary representation
of a HTTP(s) URL or local path, or
- a `{custom_fetcher, Module, Args}` tuple, with `Module` implementing
the `m:locus_custom_fetcher` behaviour
- `Opts` must be a list of `t:locus_database:opt/0` values
Returns:
- `ok` in case of success.
- `{error, invalid_url}` if the source is invalid.
- `{error, already_started}` if the loader under `DatabaseId` has already been started.
See also: `await_loader/1`, `await_loader/2`, `start_loader/2`.
""".
-endif.
-spec start_loader(DatabaseId, LoadFrom, Opts) -> ok | {error, Error} when
DatabaseId :: atom(),
LoadFrom :: DatabaseEdition | DatabaseURL | CustomFetcher,
DatabaseEdition :: database_edition(),
DatabaseURL :: database_url(),
CustomFetcher :: custom_fetcher(),
Opts :: [locus_database:opt()],
Error ::
(invalid_url
| already_started
| {invalid_opt, term()}
| application_not_running).
start_loader(DatabaseId, {maxmind, _} = DatabaseEdition, Opts) ->
Origin = parse_database_edition(DatabaseEdition),
OptsWithDefaults = opts_with_defaults(Opts),
locus_database:start(DatabaseId, Origin, OptsWithDefaults);
start_loader(DatabaseId, DatabaseEdition, Opts) when
is_atom(DatabaseEdition)
->
% Deprecated edition format
Origin = parse_database_edition(DatabaseEdition),
OptsWithDefaults = opts_with_defaults(Opts),
locus_database:start(DatabaseId, Origin, OptsWithDefaults);
start_loader(DatabaseId, DatabaseURL, Opts) when
?might_be_chardata(DatabaseURL)
->
case parse_url(DatabaseURL) of
false ->
{error, invalid_url};
Origin ->
OptsWithDefaults = opts_with_defaults(Opts),
locus_database:start(DatabaseId, Origin, OptsWithDefaults)
end;
start_loader(DatabaseId, {custom_fetcher, Module, _Args} = CustomFetcher, Opts) when
is_atom(Module)
->
Origin = CustomFetcher,
OptsWithDefaults = opts_with_defaults(Opts),
locus_database:start(DatabaseId, Origin, OptsWithDefaults).
%%
-ifdef(E48).
-doc """
Stops the database loader under id `DatabaseId`.
- `DatabaseId` must be an atom and refer to a database loader.
Returns `ok` in case of success, `{error, not_found}` otherwise.
""".
-endif.
-spec stop_loader(DatabaseId) -> ok | {error, Error} when
DatabaseId :: atom(),
Error :: not_found.
stop_loader(DatabaseId) ->
locus_database:stop(DatabaseId, _Reason = normal).
%%
-ifdef(E48).
-doc """
Like `loader_child_spec/3` but with default options.
- `DatabaseId` must be an atom.
- `LoadFrom` must be either:
- a `t:database_edition/0` tuple, or
- a `DatabaseURL` containing either a string or binary representation
of a HTTP(s) URL or local path, or
- a `{custom_fetcher, Module, Args}` tuple, with `Module` implementing
the `m:locus_custom_fetcher` behaviour
Returns:
- A `t:supervisor:child_spec/0`.
See also: `loader_child_spec/3`, `await_loader/1`, `await_loader/2`, `start_loader/2`.
""".
-endif.
-spec loader_child_spec(DatabaseId, LoadFrom) -> ChildSpec | no_return() when
DatabaseId :: atom(),
LoadFrom :: DatabaseEdition | DatabaseURL | CustomFetcher,
DatabaseEdition :: database_edition(),
DatabaseURL :: database_url(),
CustomFetcher :: custom_fetcher(),
ChildSpec :: locus_database:static_child_spec().
loader_child_spec(DatabaseId, LoadFrom) ->
loader_child_spec(DatabaseId, LoadFrom, []).
%%
-ifdef(E48).
-doc """
Like `loader_child_spec/4` but with default child id.
- `DatabaseId` must be an atom.
- `LoadFrom` must be either:
- a `t:database_edition/0` tuple, or
- a `DatabaseURL` containing either a string or binary representation
of a HTTP(s) URL or local path, or
- a `{custom_fetcher, Module, Args}` tuple, with `Module` implementing
the `m:locus_custom_fetcher` behaviour
- `Opts` must be a list of `t:locus_database:opt/0` values
Returns:
- A `t:supervisor:child_spec/0`.
See also: `loader_child_spec/2`, `loader_child_spec/4`, `await_loader/1`,
`await_loader/2`, `start_loader/3`.
""".
-endif.
-spec loader_child_spec(DatabaseId, LoadFrom, Opts) -> ChildSpec | no_return() when
DatabaseId :: atom(),
LoadFrom :: DatabaseEdition | DatabaseURL | CustomFetcher,
DatabaseEdition :: database_edition(),
DatabaseURL :: database_url(),
CustomFetcher :: custom_fetcher(),
Opts :: [locus_database:opt()],
ChildSpec :: locus_database:static_child_spec().
loader_child_spec(DatabaseId, LoadFrom, Opts) ->
loader_child_spec({locus_database, DatabaseId}, DatabaseId, LoadFrom, Opts).
%%
-ifdef(E48).
-doc """
Returns a supervisor child spec for a database loader under id `DatabaseId` with options `Opts`.
- `DatabaseId` must be an atom.
- `LoadFrom` must be either:
- a `t:database_edition/0` tuple, or
- a `DatabaseURL` containing either a string or binary representation
of a HTTP(s) URL or local path, or
- a `{custom_fetcher, Module, Args}` tuple, with `Module` implementing
the `m:locus_custom_fetcher` behaviour
- `Opts` must be a list of `t:locus_database:opt/0` values
Returns:
- A `t:supervisor:child_spec/0`.
See also: `loader_child_spec/3`, `await_loader/1`, `await_loader/2`, `start_loader/3`.
""".
-endif.
-spec loader_child_spec(ChildId, DatabaseId, LoadFrom, Opts) ->
ChildSpec | no_return()
when
ChildId :: term(),
DatabaseId :: atom(),
LoadFrom :: DatabaseEdition | DatabaseURL | CustomFetcher,
DatabaseEdition :: database_edition(),
DatabaseURL :: database_url(),
CustomFetcher :: custom_fetcher(),
Opts :: [locus_database:opt()],
ChildSpec :: locus_database:static_child_spec().
loader_child_spec(ChildId, DatabaseId, {maxmind, _} = DatabaseEdition, Opts) ->
Origin = parse_database_edition(DatabaseEdition),
OptsWithDefaults = opts_with_defaults(Opts),
locus_database:static_child_spec(ChildId, DatabaseId, Origin, OptsWithDefaults);
loader_child_spec(ChildId, DatabaseId, DatabaseEdition, Opts) when
is_atom(DatabaseEdition)
->
% Deprecated edition format
Origin = parse_database_edition(DatabaseEdition),
OptsWithDefaults = opts_with_defaults(Opts),
locus_database:static_child_spec(ChildId, DatabaseId, Origin, OptsWithDefaults);
loader_child_spec(ChildId, DatabaseId, DatabaseURL, Opts) when
?might_be_chardata(DatabaseURL)
->
case parse_url(DatabaseURL) of
false ->
error(invalid_url);
Origin ->
OptsWithDefaults = opts_with_defaults(Opts),
locus_database:static_child_spec(ChildId, DatabaseId, Origin, OptsWithDefaults)
end;
loader_child_spec(ChildId, DatabaseId, {custom_fetcher, Module, _Args} = CustomFetcher, Opts) when
is_atom(Module)
->
Origin = CustomFetcher,
OptsWithDefaults = opts_with_defaults(Opts),
locus_database:static_child_spec(ChildId, DatabaseId, Origin, OptsWithDefaults).
%%
-ifdef(E48).
-doc """
Like `await_loader/2` but with a default timeout of 30 seconds.
- `DatabaseId` must be an atom and refer to a database loader.
Returns:
- `{ok, LoadedVersion}` when the database is ready to use.
- `{error, database_unknown}` if the database loader for `DatabaseId` hasn't been started.
- `{error, {database_stopped, _}}` if the database loader for `DatabaseId` stopped while we waited.
- `{error, {timeout, [_]}}` if all the load attempts performed before timing out have failed.
See also: `await_loader/2`.
""".
-endif.
-spec await_loader(DatabaseId) -> {ok, LoadedVersion} | {error, Reason} when
DatabaseId :: atom(),
LoadedVersion :: database_version(),
Reason ::
(database_unknown
| {database_stopped, term()}
| {timeout, LoadAttemptFailures}),
LoadAttemptFailures :: [term()].
await_loader(DatabaseId) ->
await_loader(DatabaseId, 30000).
%%
-ifdef(E48).
-doc """
Blocks caller execution until either readiness is achieved or the given timeout is triggered.
- `DatabaseId` must be an atom and refer to a database loader.
- `Timeout` must be either a non-negative integer (milliseconds) or `infinity`.
Returns:
- `{ok, LoadedVersion}` when the database is ready to use.
- `{error, database_unknown}` if the database loader for `DatabaseId` hasn't been started.
- `{error, {database_stopped, _}}` if the database loader for `DatabaseId` stopped while we waited.
- `{error, {timeout, [_]}}` if all the load attempts performed before timing out have failed.
See also: `await_loader/1`, `await_loaders/2`.
""".
-endif.
-spec await_loader(DatabaseId, Timeout) -> {ok, LoadedVersion} | {error, Reason} when
DatabaseId :: atom(),
Timeout :: timeout(),
LoadedVersion :: database_version(),
Reason ::
(database_unknown
| {database_stopped, term()}
| {timeout, LoadAttemptFailures}),
LoadAttemptFailures :: [term()].
await_loader(DatabaseId, Timeout) ->
case await_loaders([DatabaseId], Timeout) of
{ok, #{DatabaseId := LoadedVersion}} ->
{ok, LoadedVersion};
{error, {#{DatabaseId := Reason}, _}} ->
{error, Reason}
end.
%%
-ifdef(E48).
-doc """
Like `await_loader/2` but concurrently awaits status from more than one database.
- `DatabaseIds` must be a list of atoms referring to database loaders.
- `Timeout` must be either a non-negative integer (milliseconds) or `infinity`.
Returns:
- `{ok, #{DatabaseId => LoadedVersion}}` when all the databases are ready to use.
- `{error, {#{DatabaseId => ErrorReason}, _}}` in case of errors.
See also: `await_loader/2`.
""".
-endif.
-spec await_loaders(DatabaseIds, Timeout) ->
({ok, Successes}
| {error, {ErrorPerDatabase, PartialSuccesses}})
when
DatabaseIds :: [DatabaseId],
Timeout :: timeout(),
Successes :: LoadedVersionPerDatabase,
PartialSuccesses :: LoadedVersionPerDatabase,
LoadedVersionPerDatabase :: #{DatabaseId => LoadedVersion},
LoadedVersion :: database_version(),
ErrorPerDatabase :: #{DatabaseId := Reason},
Reason ::
(database_unknown
| {database_stopped, term()}
| {timeout, LoadAttemptFailures}),
LoadAttemptFailures :: [term()].
await_loaders(DatabaseIds, Timeout) ->
ReplyRef = make_ref(),
UniqueDatabaseIds = lists:usort(DatabaseIds),
Waiters = launch_waiters(ReplyRef, Timeout, UniqueDatabaseIds),
perform_wait(ReplyRef, Waiters, #{}, #{}).
%%
-ifdef(E48).
-doc """
Looks up info on IPv4 and IPv6 addresses.
- `DatabaseId` must be an atom and refer to a database loader.
- `Address` must be either a `t:inet:ip_address/0` tuple, or a string/binary
containing a valid representation of the address.
Returns:
- `{ok, Entry}` in case of success.
- `not_found` if no data was found for this `Address`.
- `{error, invalid_address}` if `Address` is not either a `t:inet:ip_address/0`
tuple or a valid textual representation of an IP address.
- `{error, database_unknown}` if the database loader for `DatabaseId` hasn't been started.
- `{error, database_not_loaded}` if the database hasn't yet been loaded.
- `{error, ipv4_database}` if `Address` represents an IPv6 address and the database
only supports IPv4 addresses.
""".
-endif.
-spec lookup(DatabaseId, Address) -> {ok, Entry} | not_found | {error, Error} when
DatabaseId :: atom(),
Address :: inet:ip_address() | string() | binary(),
Entry :: database_entry(),
Error ::
(database_unknown
| database_not_loaded
| {invalid_address, Address}
| ipv4_database).
lookup(DatabaseId, Address) ->
case locus_database:find(DatabaseId) of
{ok, Database, _Source, _Version} ->
locus_mmdb:lookup_address(Address, Database);
Error ->
Error
end.
%%
-ifdef(E48).
-doc """
Returns the properties of a currently loaded database.
- `DatabaseId` must be an atom and refer to a database loader.
Returns:
- `{ok, t:database_info/0}` in case of success.
- `{error, database_unknown}` if the database loader for `DatabaseId` hasn't been started.
- `{error, database_not_loaded}` if the database hasn't yet been loaded.
See also: `get_info/2`.
""".
-endif.
-spec get_info(DatabaseId) -> {ok, Info} | {error, Error} when
DatabaseId :: atom(),
Info :: database_info(),
Error :: database_unknown | database_not_loaded.
get_info(DatabaseId) ->
case locus_database:find(DatabaseId) of
{ok, Database, Source, Version} ->
Info = database_info(Database, Source, Version),
{ok, Info};
Error ->
Error
end.
%%
-ifdef(E48).
-doc """
Returns a specific property of a currently loaded database.
- `DatabaseId` must be an atom and refer to a database loader.
- `Property` must be either `metadata`, `source` or `version`.
Returns:
- `{ok, Value}` in case of success.
- `{error, database_unknown}` if the database loader for `DatabaseId` hasn't been started.
- `{error, database_not_loaded}` if the database hasn't yet been loaded.
See also: `get_info/1`.
""".
-endif.
-spec get_info(DatabaseId, Property) -> {ok, Value} | {error, Error} when
DatabaseId :: atom(),
Property :: metadata | source | version,
Value :: database_metadata() | database_source() | database_version(),
Error :: database_unknown | database_not_loaded.
get_info(DatabaseId, Property) ->
case get_info(DatabaseId) of
{ok, Info} ->
Value = maps:get(Property, Info),
{ok, Value};
{error, Error} ->
{error, Error}
end.
%%
-ifdef(E48).
-doc """
Analyzes a loaded database for corruption or incompatibility.
- `DatabaseId` must be an atom and refer to a database loader.
Returns:
- `ok` if the database is wholesome.
- `{error, database_unknown}` if the database loader for `DatabaseId` hasn't been started.
- `{error, database_not_loaded}` if the database hasn't yet been loaded.
- `{validation_warnings, [CheckWarning, ...]}` in case something smells within the database
(check the definitions in `m:locus_mmdb_check`).
- `{validation_errors, [CheckError], [...]}` in case of corruption or incompatibility
(check the definitions in `m:locus_mmdb_check`).
""".
-endif.
-spec check(DatabaseId) ->
ok
| {error, Error}
| {validation_warnings, [ValidationWarning, ...]}
| {validation_errors, [ValidationError, ...], [ValidationWarning]}
when
DatabaseId :: atom(),
Error :: database_unknown | database_not_loaded,
ValidationWarning :: locus_mmdb_check:warning(),
ValidationError :: locus_mmdb_check:error().
check(DatabaseId) ->
case locus_database:find(DatabaseId) of
{ok, Database, _Source, _Version} ->
check_database(Database);
{error, _} = Error ->
Error
end.
%% ------------------------------------------------------------------
%% CLI-only Function Definitions
%% ------------------------------------------------------------------
-ifdef(ESCRIPTIZING).
-spec main([string()]) -> no_return().
%% @private
main(Args) ->
locus_cli:main(Args).
-endif.
%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------
-spec parse_database_edition(database_edition()) -> {maxmind, atom()}.
parse_database_edition({maxmind, Atom}) when
is_atom(Atom)
->
{maxmind, Atom};
parse_database_edition({maxmind, Chardata}) when
?might_be_chardata(Chardata)
->
Charlist = unicode:characters_to_list(Chardata),
Atom = list_to_atom(Charlist),
{maxmind, Atom};
parse_database_edition(LegacyMaxMindDatabaseEdition) when
is_atom(LegacyMaxMindDatabaseEdition)
->
{maxmind, LegacyMaxMindDatabaseEdition}.
-spec parse_url(database_url()) -> locus_database:origin() | false.
parse_url(DatabaseURL) ->
case parse_http_url(DatabaseURL) of
Origin when is_tuple(Origin) ->
Origin;
false ->
parse_filesystem_url(DatabaseURL)
end.
parse_http_url(DatabaseURL) when is_list(DatabaseURL) ->
try unicode:characters_to_binary(DatabaseURL) of
<<BinaryChardata/bytes>> ->
parse_http_url(BinaryChardata);
_ ->
false
catch
_:_ -> false
end;
parse_http_url(DatabaseURL) ->
ByteList = binary_to_list(DatabaseURL),
try
io_lib:printable_latin1_list(ByteList) andalso
locus_util:parse_absolute_http_url(ByteList)
of
false ->
false;
{ok, _Result} ->
{http, ByteList};
{error, _Reason} ->
false
catch
error:badarg -> false
end.
parse_filesystem_url(DatabaseURL) ->
try unicode:characters_to_list(DatabaseURL) of
Path when is_list(Path) ->
{filesystem, filename:absname(Path)};
{error, _Parsed, _RestData} ->
false;
{incomplete, _Parsed, _RestData} ->
false
catch
error:badarg -> false
end.
-spec database_info(locus_mmdb:database(), locus_loader:source(), calendar:datetime()) ->
database_info().
database_info(Database, Source, Version) ->
#{metadata := Metadata} = Database,
#{metadata => Metadata, source => Source, version => Version}.
opts_with_defaults(Opts) ->
[{event_subscriber, locus_logger} | Opts].
launch_waiters(ReplyRef, Timeout, UniqueDatabaseIds) ->
[
{DatabaseId, locus_waiter:start(ReplyRef, DatabaseId, Timeout)}
|| DatabaseId <- UniqueDatabaseIds
].
perform_wait(_ReplyRef, [], Successes, Failures) ->
case map_size(Failures) =:= 0 of
true ->
{ok, Successes};
false ->
{error, {Failures, Successes}}
end;
perform_wait(ReplyRef, WaitersLeft, Successes, Failures) ->
case receive_waiter_reply(ReplyRef) of
{DatabaseId, {ok, Version}} ->
{value, _, RemainingWaitersLeft} = lists:keytake(DatabaseId, 1, WaitersLeft),
UpdatedSuccesses = Successes#{DatabaseId => Version},
perform_wait(ReplyRef, RemainingWaitersLeft, UpdatedSuccesses, Failures);
{DatabaseId, {error, Reason}} ->
{value, _, RemainingWaitersLeft} = lists:keytake(DatabaseId, 1, WaitersLeft),
UpdatedFailures = Failures#{DatabaseId => Reason},
perform_wait(ReplyRef, RemainingWaitersLeft, Successes, UpdatedFailures)
end.
receive_waiter_reply(ReplyRef) ->
receive
{ReplyRef, DatabaseId, Reply} ->
{DatabaseId, Reply}
end.
check_database(Database) ->
case locus_mmdb_check:run(Database) of
ok ->
ok;
{warnings, Warnings} ->
{validation_warnings, Warnings};
{errors, Errors, Warnings} ->
{validation_errors, Errors, Warnings}
end.