%% xmlrat %% %% Copyright 2021 The University of Queensland %% Author: Alex Wilson %% %% Redistribution and use in source and binary forms, with or without %% modification, are permitted provided that the following conditions %% are met: %% 1. Redistributions of source code must retain the above copyright %% notice, this list of conditions and the following disclaimer. %% 2. Redistributions in binary form must reproduce the above copyright %% notice, this list of conditions and the following disclaimer in the %% documentation and/or other materials provided with the distribution. %% %% THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR %% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES %% OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. %% IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, %% INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT %% NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, %% DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY %% THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF %% THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% %% @doc Compile and execute XPath 1.0 expressions at runtime. %% %% This module provides support for compiling a subset of XPath 1.0 to Erlang %% code. The generated code consists of successive pattern matches and list %% comprehensions, plus some calls to runtime helpers as needed for predicates %% with position matches in them. %% %% The subset of XPath 1.0 implemented is chosen to be as "safe" as possible -- %% it should avoid any accidental exponential expansion in memory usage or %% time, and compiles to simple pattern matches as much as possible. Being a %% fully compliant XPath implementation is a non-goal. %% %% It's important to note that every XPath expression this module compiles %% will result in a new Erlang module being loaded by the code server. These %% are a finite resource, so you should avoid dynamically compiling an arbitray %% number of input expressions (e.g. don't accept XPath expressions from user %% input). %% %% Expressions support being parametrised by using variables (see the type %% {@link varbinds()}). %% %% Supported XPath 1.0 syntax features: %% -module(xmlrat_xpath). -export([ compile/1, compile/2, run/2, run/3 ]). -export_type([xpath_ref/0]). -record(xpath_ref, { mod :: module() }). -opaque xpath_ref() :: #xpath_ref{}. -type nsmap() :: #{xmlrat:nsname() => xmlrat:uri()}. %% Map of namespaces used in the XPath expression itself (does not have to match %% the namespace set used by the document). -type compile_options() :: #{namespaces => nsmap()}. -type xpath() :: string() | binary(). %% An XPath 1.0 expression. parse(XPath) -> case (catch xmlrat_xpath_parse:parse(XPath)) of {'EXIT', Why} -> {error, Why}; {fail, Why} -> {error, Why}; Expr -> {ok, Expr} end. %% @doc Compiles an XPath expression to Erlang code. %% %% Returns an xpath_ref() which can be passed to %% run(). -spec compile(xpath()) -> {ok, xpath_ref()} | {error, term()}. compile(XPath) -> compile(XPath, #{}). %% @doc Compiles an XPath expression to Erlang code, with options. -spec compile(xpath(), compile_options()) -> {ok, xpath_ref()} | {error, term()}. compile(XPath, Opts) -> NS = maps:get(namespaces, Opts, #{}), case parse(XPath) of {ok, Expr} -> case (catch xmlrat_xpath_compile:compile_module(Expr, NS)) of {ok, Mod} -> {ok, #xpath_ref{mod = Mod}}; {'EXIT', Why} -> {error, Why} end; E = {error, _} -> E end. -type xpath_result() :: [xmlrat:element()] | [xmlrat:attribute()] | binary() | [xmlrat:content()]. %% @doc Executes an XPath expression over an input document. -spec run(xpath() | xpath_ref(), xmlrat:document()) -> {ok, xpath_result()} | {error, term()}. run(#xpath_ref{mod = Mod}, Doc) -> {ok, Mod:match(Doc)}; run(XPath, Doc) -> case compile(XPath) of {ok, #xpath_ref{mod = Mod}} -> Res = Mod:match(Doc), code:delete(Mod), code:purge(Mod), {ok, Res}; E = {error, _} -> E end. -type varbinds() :: #{binary() => binary() | integer()}. %% Bound variables for use within an XPath expression. %% %% For example, after compiling the XPath expression %% /foo[@bar = $var], you could execute it with different values %% in place of $var by using: %%
%% xmlrat_xpath:run(Expr, Doc, #{variables =>
%%   #{<<"var">> => <<"1">>}})
%% 
%% etc. -type run_options() :: #{ namespaces => nsmap(), variables => varbinds()}. %% Options used with run/3. %% @doc Executes an XPath expression over an input document, with options. -spec run(xpath() | xpath_ref(), xmlrat:document(), run_options()) -> {ok, xpath_result()} | {error, term()}. run(#xpath_ref{mod = Mod}, Doc, Opts) -> VarBinds = maps:get(variables, Opts, #{}), {ok, Mod:match(Doc, VarBinds)}; run(XPath, Doc, Opts) -> VarBinds = maps:get(variables, Opts, #{}), case compile(XPath, Opts) of {ok, #xpath_ref{mod = Mod}} -> Res = Mod:match(Doc, VarBinds), code:delete(Mod), code:purge(Mod), {ok, Res}; E = {error, _} -> E end.