-module(lee).
%% API exports
-export([ namespace/2
, base_metamodel/0
, get/3
, list/3
, validate/2
, validate/3
, meta_validate/1
, meta_validate/2
, from_string/3
, from_strings/3
]).
-export_type([ node_id/0
, model_key/0
, key/0
, metatype/0
, type/0
, mnode/0
, model/0
, lee_module/0
, cooked_module/0
, properties/0
, data/0
, patch/0
]).
-include("lee_internal.hrl").
%%====================================================================
%% Types
%%====================================================================
-type data() :: lee_storage:data() | [lee_storage:data()].
-type patch() :: lee_storage:patch(term()).
-type namespace() :: #{node_id() => mnode() | namespace()}.
-type lee_module() :: namespace().
-type cooked_module() :: lee_storage:data(#mnode{}).
-type model() :: #model{}.
-type validate_result() :: {ok, Warnings :: [string()]}
| {error, Errors :: [term()], Warnings :: [term()]}.
-type validate_callback() :: fun((model(), data(), key(), #mnode{}) ->
validate_result()).
-type validation_type() :: validate_node | meta_validate.
-type metatype() :: atom().
-type node_id() :: atom() %% Should not begin with `$', these are reserved
| tuple()
| integer()
.
-type model_key() :: [ node_id() | ?children ].
-type key() :: [ node_id() | ?lcl(term()) | ?children ].
-type properties() :: #{atom() => term()}.
-type type() :: #type{} | atom() | {var, term()}.
%% Model node
-type mnode() :: {[metatype()], properties(), lee_module()}
| {[metatype()], properties()} %% Shortcut for child-free MOs
.
%%====================================================================
%% Macros
%%====================================================================
%%====================================================================
%% API functions
%%====================================================================
%% @doc Put model to a namespace.
%%
%% `namespace([foo, bar], A)' is equivalent to `#{foo => #{bar => A}}'
-spec namespace(lee:key(), lee:module()) ->
lee:module().
namespace(Key, M) ->
lists:foldl( fun(NodeId, Acc) ->
#{NodeId => Acc}
end
, M
, lists:reverse(Key)
).
%% @doc Model module containing basic metatypes:
%%
%% == value ==
%%
%% `value' is a leaf node, i.e. a concrete value.
%%
%% === Metaparameters ===
%%
- `type' of type `typerefl:type()':
%% Type of the value
%%
%% - `default' (optional):
%% Default value, if present
%% - `oneliner' of type `string()' (optional, but highly desired):
%% One-sentence doc string
%% - `doc' of type `string()' (optional, but highly desired):
%% Long description using DocBook
%% markup
%%
%%
%% == map ==
%% `map' denotes a container node that groups multiple values together.
%%
%% Metaparameters:
%% - `key_elements' of type `[lee:key()]':
%% List of child element keys that identify the map. Optional,
%% defaults to `[]'
%%
%%
%%
-spec base_metamodel() -> lee:module().
base_metamodel() ->
namespace([metatype]
, #{ value =>
{[metatype, documented],
#{ validate_node => fun validate_value/4
, meta_validate => fun meta_validate_value/4
, doc_chapter_title => "Values"
, doc_gen => fun lee_doc:document_values/2
}}
, map =>
{[metatype],
#{ meta_validate => fun meta_validate_map/4
}}
, doc_root =>
{[metatype],
#{ meta_validate => fun lee_doc:validate_doc_root/4
}}
}
).
%% @doc Get a value from the config
%%
%% The return value is guaranteed safe, as long as `Data' has been
%% validated against the `Model', and `Key' is a valid `Model' key
-spec get(lee:model() | lee:cooked_module(), data(), lee:key()) -> term().
get(Model, Data, Key) when ?is_storage(Data) ->
case lee_storage:get(Key, Data) of
{ok, Val} ->
Val;
_ ->
MKey = lee_model:get_model_key(Key),
#mnode{metaparams = Attrs} = lee_model:get(MKey, Model),
case Attrs of
#{default := Val} ->
Val;
_ ->
%% Data shouldn't have passed validation without
%% the default, so just crash here
error({missing_data, Key})
end
end;
get(Model, [Data], Key) ->
get(Model, Data, Key);
get(Model, [Data|Rest], Key) ->
case lee_storage:get(Key, Data) of
{ok, Val} ->
Val;
undefined ->
get(Model, Rest, Key)
end.
%% @doc List objects in `Data' that can match `Key'
%%
%% `?children' nodes in the key will be replaced with actual child
%% keys, e.g. ```list(Model, Data, [foo, ?children])''' will return
%% ```[[foo, ?lcl(1)], [foo, ?lcl(2)]]''' when map `[foo]' contains
%% two children with keys `1' and `2'.
-spec list(model() | cooked_module(), data(), lee:key()) -> [lee:key()].
list(_Model, Data, Key) when ?is_storage(Data) ->
%% TODO: Include default values in the list
lee_storage:list(Key, Data);
list(Model, Data, Pattern) when is_list(Data) ->
lists:usort(lists:foldl( fun(I, Acc) ->
list(Model, I, Pattern) ++ Acc
end
, []
, Data)).
%% @equiv validate(all, Model, Data)
-spec validate(lee:model(), data()) -> validate_result().
validate(Model, Data) ->
validate(all, Model, Data).
%% @doc Validate `Data' against `Model'. `MetaTypes' is a list of
%% metatypes that should be validated, or atom `all'
-spec validate([metatype()] | all, lee:model(), data()) -> validate_result().
validate(MetaTypes, Model, Data) ->
validate(validate_node, MetaTypes, Model, Data).
%% @equiv meta_validate(all, Model)
-spec meta_validate(lee:model()) -> validate_result().
meta_validate(Model) ->
meta_validate(all, Model).
%% @doc Validate model itself against its metamodel. Useful for
%% spotting bugs in the model definition
-spec meta_validate([metatype()] | all, lee:model()) -> validate_result().
meta_validate(MetaTypes, Model) ->
Data = lee_storage:new(lee_map_storage), %% Mostly to make dialyzer happy
validate(meta_validate, MetaTypes, Model, Data).
-spec from_string(lee:model(), lee:model_key(), string()) ->
{ok, term()} | {error, string()}.
from_string(Model, Key, String) ->
#mnode{metaparams = MP} = lee_model:get(Key, Model),
Type = maps:get(type, MP),
Default = fun(Str) ->
case typerefl:from_string(Type, Str) of
Ok = {ok, _} ->
Ok;
{error, Err} ->
{error, Err}
end
end,
Fun = maps:get(from_string, MP, Default),
Fun(String).
%% @doc Temporary function, don't use it.
-spec from_strings(lee:model(), lee:model_key(), [string()]) ->
{ok, [term()]} | {error, string()}.
from_strings(Model, Key, Strings) ->
#mnode{metaparams = MP} = lee_model:get(Key, Model),
Type = ?m_attr(value, type, MP),
try
case MP of
#{?m_valid(value, from_string) := Fun} ->
%% Custom from_string callback is defined:
{ok, [case Fun(I) of
{ok, T} -> T;
Err = {error, _} -> throw(Err)
end || I <- Strings]};
_ ->
%% Fallback to typerefl:
{ok, typerefl:from_string_(Type, Strings)}
end
catch
Err = {error, _} -> Err
end.
%%====================================================================
%% Internal functions
%%====================================================================
%% Validate all instances of certain metatypes against the model
-spec validate(validation_type(), [metatype()] | all, lee:model(), data()) ->
validate_result().
validate(ValidationType, MetaTypes, Model, Data) ->
ModelIdx0 = Model#model.meta_class_idx,
ModelIdx = case MetaTypes of
all -> ModelIdx0;
L -> maps:with(L, ModelIdx0)
end,
{Errors, Warnings} =
maps:fold( fun(MetaType, Nodes, {Err0, Warn0}) ->
{Err1, Warn1} = validate_nodes( ValidationType
, Model
, Data
, MetaType
, Nodes
),
{Err1 ++ Err0, Warn1 ++ Warn0}
end
, {[], []}
, ModelIdx
),
case Errors of
[] ->
{ok, Warnings};
_ ->
{error, Errors, Warnings}
end.
%% Validate all nodes belonging to a metatype
-spec validate_nodes( validation_type()
, lee:model()
, lee:data()
, atom() | integer() | tuple()
, ordsets:set(lee:model_key())
) -> lee_lib:check_result().
validate_nodes(ValidationType, Model, Data, MetaTypeId, Nodes) ->
#model{metamodel = Meta} = Model,
#mnode{metaparams = Attrs} = lee_model:get([metatype, MetaTypeId], Meta),
Default = fun(_, _, _, _) -> {[], []} end,
ValidateFun = ?m_attr(metatype, ValidationType, Attrs, Default),
ordsets:fold( fun(NodeId, {E0, W0}) ->
{E1, W1} = validate_node(Model, Data, NodeId, ValidateFun),
{E1 ++ E0, W1 ++ W0}
end
, {[], []}
, Nodes
).
-spec validate_node( lee:model()
, lee:data()
, lee:key()
, validate_callback()
) -> lee_lib:check_result().
validate_node(Model, Data, NodeId, ValidateFun) ->
MNode = lee_model:get(NodeId, Model),
Instances = case lee_model:split_key(NodeId) of
{[], _} ->
[NodeId];
{Base, Required} ->
[I ++ Required || I <- lee_storage:list(Base, Data)]
end,
lists:foldl( fun(Id, {Err0, Warn0}) ->
{Err1, Warn1} = ValidateFun(Model, Data, Id, MNode),
{Err1 ++ Err0, Warn1 ++ Warn0}
end
, {[], []}
, Instances
).
%% Validate nodes of `value' metatype
-spec validate_value(lee:model(), lee:data(), lee:key(), #mnode{}) ->
lee_lib:check_result().
validate_value(Model, Data, Key, #mnode{metaparams = Attrs}) ->
Type = ?m_attr(value, type, Attrs),
Default = case Attrs of
#{default := Default0} ->
{ok, Default0};
_ ->
undefined
end,
case {lee_storage:get(Key, Data), Default} of
{{ok, Term}, _} ->
Result = case typerefl:typecheck(Type, Term) of
ok -> {[], []};
{error, Err} -> {[Err], []}
end,
lee_lib:inject_error_location(Key, Result);
{undefined, {ok, _}} ->
{[], []};
{undefined, undefined} ->
Err = lee_lib:format("~p: Mandatory value is missing in the config", [Key]),
{[Err] , []}
end.
-spec meta_validate_value(lee:model(), _, lee:key(), #mnode{}) ->
lee_lib:check_result().
meta_validate_value(_Model, _, Key, #mnode{metaparams = Attrs}) ->
lee_lib:perform_checks(Key, Attrs, [ fun lee_doc:check_docstrings/1
, fun check_type_and_default/1
]).
-spec meta_validate_map(lee:model(), _, lee:key(), #mnode{}) ->
lee_lib:check_result().
meta_validate_map(#model{model = Model}, _, Key, #mnode{metaparams = Params}) ->
io:format("OHAYO: ~p~n", [Model]),
ValidateKey =
fun(ChildKey) ->
case lee_storage:get(Key ++ [?children|ChildKey], Model) of
undefined ->
Error = lee_lib:format( "~p is not a valid child key of ~p"
, [ChildKey, Key]
),
{true, Error};
{ok, _} ->
false
end
end,
{ case Params of
#{key_elements := KeyElems} when is_list(KeyElems) ->
lists:filtermap(ValidateKey, KeyElems);
#{key_elements := _} ->
["`key_elements' should be a list of valid child keys"];
_ ->
[]
end
, []
}.
check_type_and_default(Attrs) ->
case Attrs of
#{type := Type, default := Default} ->
case typerefl:typecheck(Type, Default) of
ok -> {[], []};
{error, Err} -> {["Mistyped default value: " ++ Err], []}
end;
#{type := _Type} ->
%% TODO: typerefl:is_type?
{[], []};
_ ->
{["missing `type' metaparamter"], []}
end.