% vim: sw=4 ts=4 et ft=erlang % Nitrogen Web Framework for Erlang % Copyright (c) 2008-2013 Rusty Klophaus % See MIT-LICENSE for licensing information. -module (wf_utils). -include("wf.hrl"). -export ([ f/1, f/2, guid/0, short_guid/0, path_search/3, replace/3, coalesce/1, is_process_alive/1, debug/0, break/0, get_elementbase/1, get_actionbase/1, get_validatorbase/1, replace_with_base/2, fast_copy_fields/2, indexof/2, replace_field/4, get_field/3, copy_fields/2, is_iolist_empty/1, has_behaviour/2, app_modules/1, ensure_loaded/1, write_debug/2, profile/3, pterm/1, pterm/2 ]). -define(COPY_TO_BASERECORD(Name, Size, Record), list_to_tuple([Name | lists:sublist(tuple_to_list(Record), 2, Size-1)])). %%% FORMAT %%% f(S) -> f(S, []). f(S, Args) when is_binary(S) -> wf:to_unicode_binary(io_lib:format(S, Args)); f(S, Args) when is_list(S) -> wf:to_unicode_list(lists:flatten(io_lib:format(S,Args))). %%% IDS %%% % guid/0 - Return a guid like object. guid() -> MD5 = erlang:md5(term_to_binary({node(), os:timestamp(), make_ref()})), MD5List = lists:nthtail(8, binary_to_list(MD5)), F = fun(N) -> wf:f("~2.16.0B", [N]) end, L = [F(N) || N <- MD5List], lists:flatten(L). % short_guid/0 - Return a shorter guid like object. short_guid() -> MD5 = erlang:md5(term_to_binary({node(), os:timestamp(), make_ref()})), MD5List = lists:nthtail(14, binary_to_list(MD5)), F = fun(N) -> wf:f("~2.16.0B", [N]) end, L = [F(N) || N <- MD5List], lists:flatten(L). is_process_alive(Pid) -> case is_pid(Pid) of true -> % If node(Pid) is down, rpc:call returns something other than % true or false. case rpc:call(node(Pid), erlang, is_process_alive, [Pid]) of true -> true; _ -> false end; _ -> false end. %%% XPATH STYLE QUERY LOOKUPS %%% % path_search/2 - search for part of a specified path within a list of paths. % Partial = [atom3, atom2, atom1] % Paths=[[atom3, atom2, atom1], [atom5, atom4...]] % Conducts path_search(Partial, N, Paths) -> path_search(Partial, N, Paths, 1). path_search(_, _, [], _) -> []; path_search([], _, Paths, _) -> Paths; path_search(['*'], _, Paths, _) -> Paths; path_search(_, _, _, 10) -> []; path_search(['*'|T], N, Paths, Pos) -> % We have a wildcard so everything matches. % Split into two new searches. path_search(['*'|T], N, Paths, Pos + 1) ++ path_search(T, N, Paths, Pos + 1); path_search([H|T], N, Paths, Pos) -> % Return all Paths for which H matches the Nth element. F = fun(Tuple) -> Path = erlang:element(N, Tuple), (Pos =< length(Path)) andalso (H == lists:nth(Pos, Path)) end, Paths1 = lists:filter(F, Paths), path_search(T, N, Paths1, Pos + 1). %%% STRING REPLACE %%% replace([], _, _) -> []; replace(String, S1, S2) when is_list(String), is_list(S1), is_list(S2) -> Length = length(S1), case string:substr(String, 1, Length) of S1 -> S2 ++ replace(string:substr(String, Length + 1), S1, S2); _ -> [hd(String)|replace(tl(String), S1, S2)] end. %%% COALESCE %%% coalesce([]) -> undefined; coalesce([H]) -> H; coalesce([undefined|T]) -> coalesce(T); coalesce([[]|T]) -> coalesce(T); coalesce([H|_]) -> H. %%% BASE RECORDS %%% get_actionbase(Term) -> ?COPY_TO_BASERECORD(actionbase, size(#actionbase{}), Term). get_elementbase(Term) -> ?COPY_TO_BASERECORD(elementbase, size(#elementbase{}), Term). get_validatorbase(Term) -> ?COPY_TO_BASERECORD(validatorbase, size(#validatorbase{}), Term). replace_with_base(Base, Record) -> RecordType = element(1, Record), BaseMiddle = tl(tuple_to_list(Base)), Start = size(Base) + 1, Len = size(Record) - Start + 1, RecordEnd = lists:sublist(tuple_to_list(Record), Start, Len), list_to_tuple([RecordType] ++ BaseMiddle ++ RecordEnd). %%% COPY ELEMENT FIELDS %%% -spec copy_fields(tuple(), tuple()) -> tuple(). %% @doc Copies any fields from FromElement to ToElement if the record %% fields have EXACTLY the same name. copy_fields(FromElement, ToElement) -> FromModule = element(3,FromElement), ToModule = element(3,ToElement), FromFieldList = FromModule:reflect(), ToFieldList = ToModule:reflect(), %% get tail because reflect() doesn't include first element (record tag) FromValueList = tl(tuple_to_list(FromElement)), lists:foldl(fun({Field, Value}, NewElement) -> case indexof(Field, ToFieldList) of undefined -> NewElement; Index -> setelement(Index, NewElement, Value) end %% Here we use tl(tl( to ignore the first 2 fields from reflect() end, ToElement, tl(tl(lists:zip(FromFieldList,FromValueList)))). -spec fast_copy_fields(tuple(), tuple()) -> tuple(). %% @doc This is a shortcut converter from one element to another. It expects %% that the element that's being copied was extended using ?WF_EXTEND (which %% uses the `rekt` parsetransform). %% It just uses replace_with_base to copy all fields from FromElement to ToElement, fast_copy_fields(FromElement, ToElement) when tuple_size(FromElement) =< tuple_size(ToElement) -> Mod2 = element(3, ToElement), New = replace_with_base(FromElement, ToElement), setelement(3, New, Mod2); fast_copy_fields(FromElement, ToElement) -> %% Get record tag and callback module Tag = element(1, ToElement), Mod = element(3, ToElement), %% Get size of target tuple FinalSize = tuple_size(ToElement), %% Convert from tuple to a list AllFields = tuple_to_list(FromElement), %% and extract only the truncated number of fields (ignoring record tag and callback module - elements 1 and 3 respectively) [_, Type, _ | TruncFields] = lists:sublist(AllFields, FinalSize), %% replace tag and module in field list NewList = [Tag, Type, Mod | TruncFields], %% and convert to completed tuple list_to_tuple(NewList). %%% EMPTY LIST/BINARY CHECK -spec is_iolist_empty(iolist()) -> boolean(). %% @doc Without flattening the whole list, this will check to make sure there %% exists *something* other than empty lists or binaries of length 0. It takes %% a list, binary, or iolist and returns true if iolist_to_binary would end up %% returning <<>>, but it does so by short circuiting as soon as it encounters %% non-empty token (atom, character, tuple, etc). is_iolist_empty([]) -> true; is_iolist_empty(<<>>) -> true; is_iolist_empty([[]|T]) -> is_iolist_empty(T); is_iolist_empty([<<>>|T]) -> is_iolist_empty(T); is_iolist_empty([ListH | T]) when is_list(ListH) -> case is_iolist_empty(ListH) of true -> is_iolist_empty(T); false -> false end; is_iolist_empty(_) -> false. %%% DEBUG %%% debug() -> % Get all web and wf modules. F = fun(X) -> {source, Path} = lists:keyfind(source, 1, X:module_info(compile)), Path end, L = [list_to_binary(atom_to_list(X)) || X <- erlang:loaded()], ModulePaths = [F(wf)] ++ [F(list_to_atom(binary_to_list(X))) || <<"web_", _/binary>>=X <- L] ++ [F(list_to_atom(binary_to_list(X))) || <<"wf_", _/binary>>=X <- L], i:im(), i:ii(ModulePaths), i:iaa([break]), i:ib(?MODULE, break, 0). break() -> ok. indexof(Key, Fields) -> indexof(Key, Fields, 2). indexof(_Key, [], _N) -> undefined; indexof(Key, [Key|_T], N) -> N; indexof(Key, [_|T], N) -> indexof(Key, T, N + 1). replace_field(Key, Value, Fields, Rec) -> N = indexof(Key, Fields), setelement(N, Rec, Value). get_field(Key, Fields, Rec) -> case indexof(Key, Fields) of undefined -> undefined; N -> element(N, Rec) end. %% HAS BEHAVIOUR has_behaviour(Module, Behaviour) -> Behaviours = get_behaviours(Module), lists:member(Behaviour, Behaviours). %% Modules can have more than one behaviour, and it's perfectly legit to use %% -behaviour or -behavior (US Spelling), which Erlang trreats them as different %% module attributes. This makes it tolerant of both types. get_behaviours(Module) -> Attributes = try Module:module_info(attributes) catch error:undef -> [] end, lists:foldl(fun(Att, Acc) -> case Att of {behavior, [B]} -> [B | Acc]; {behaviour, [B]} -> [B | Acc]; _ -> Acc end end, [], Attributes). ensure_loaded(Module) -> wf:cache({ensure_loaded, Module}, 1000, fun() -> code:ensure_loaded(Module) end). app_modules(App) -> {ok, Modules} = application:get_key(App, modules), Modules. write_debug(Tag, Term) -> Output = wf:f("NITROGEN DEBUG: ~p~n~p~n*****************************************~n", [Tag, Term]), ok = file:write_file("nitrogen.debug", Output, [append]). -define(PROFILE_FORMAT_STRING, "~p, ~p, ~p, ~p, ~p~n"). -spec profile(Tag :: any(), Fun :: fun(), To :: undefined | pid() | string()) -> any(). profile(Tag, Fun, To) -> Pid = self(), {_, StartRed} = erlang:process_info(Pid, reductions), {_, StartHeap} = erlang:process_info(Pid, heap_size), {_, StartMem} = erlang:process_info(Pid, memory), {Time, Res} = timer:tc(Fun), {_, EndRed} = erlang:process_info(Pid, reductions), {_, EndHeap} = erlang:process_info(Pid, heap_size), {_, EndMem} = erlang:process_info(Pid, memory), Reductions = EndRed - StartRed, Heap = EndHeap - StartHeap, Mem = EndMem - StartMem, Terms = [Tag, Time, Reductions, Heap, Mem], case To of undefined -> io:format(?PROFILE_FORMAT_STRING, Terms); ToPid when is_pid(ToPid) -> ToPid ! {profile, Terms}; _ -> Output = io_lib:format(?PROFILE_FORMAT_STRING, Terms), file:write_file(To, Output, [append]) end, Res. pterm(Key) -> persistent_term:get({nitrogen_core, Key}, undefined). pterm(Key, Value) -> persistent_term:put({nitrogen_core, Key}, Value).