From 3f0c6635c1d43ff828ea56525e0a501862cbd25f Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Sun, 19 Jun 2022 19:55:10 +0200 Subject: [PATCH] Allow to pass bindings to RDF.Graph.build blocks --- CHANGELOG.md | 3 +- lib/rdf/graph.ex | 4 +- lib/rdf/graph_builder.ex | 29 ++++++--- test/unit/graph_builder_test.exs | 101 ++++++++++++++++++++++++++++++- 4 files changed, 125 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea3499..d2ecffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,8 @@ The generated namespaces are much more flexible now and compile faster. import no longer affect the caller context. `alias`es in the caller context are still available in the build block, but `import`s not and must be reimported in the build block. Variables in the caller context are also no longer available - build block. + build block, but must be passed explicitly with the new optional `bindings` + argument of `RDF.Graph.build/3`. - `RDF.Data.merge/2` and `RDF.Data.equal?/2` are now commutative, i.e. structs which implement the `RDF.Data` protocol can be given also as the second argument (previously custom structs with `RDF.Data` protocol implementations always diff --git a/lib/rdf/graph.ex b/lib/rdf/graph.ex index 0a1c212..d80be01 100644 --- a/lib/rdf/graph.ex +++ b/lib/rdf/graph.ex @@ -144,8 +144,8 @@ defmodule RDF.Graph do For a description of the DSL see [this guide](https://rdf-elixir.dev/rdf-ex/description-and-graph-dsl.html). """ - defmacro build(opts \\ [], do: block) do - Builder.build(block, __CALLER__, Builder.builder_mod(__CALLER__), opts) + defmacro build(bindings \\ [], opts \\ [], do: block) do + Builder.build(block, __CALLER__, Builder.builder_mod(__CALLER__), bindings, opts) end @doc """ diff --git a/lib/rdf/graph_builder.ex b/lib/rdf/graph_builder.ex index 2180fb3..d3d82c8 100644 --- a/lib/rdf/graph_builder.ex +++ b/lib/rdf/graph_builder.ex @@ -17,13 +17,15 @@ defmodule RDF.Graph.Builder do def exclude(_), do: nil end - def build(do_block, env, opts) do - build(do_block, env, builder_mod(env), opts) + def build(do_block, env, bindings, opts) do + build(do_block, env, builder_mod(env), bindings, opts) end - def build({:__block__, _, block}, env, builder_mod, opts) do + def build({:__block__, _, block}, env, builder_mod, bindings, opts) do env_aliases = env_aliases(env) block = expand_aliased_modules(block, env_aliases) + {bindings_vars, bindings_pattern} = bindings_vars(bindings, builder_mod) + block = bind_vars(block, bindings_vars, builder_mod) non_strict_ns = extract_non_strict_ns(block) {declarations, data} = Enum.split_with(block, &declaration?/1) {base, declarations} = extract_base(declarations) @@ -39,7 +41,7 @@ defmodule RDF.Graph.Builder do @compile {:no_warn_undefined, mod} end - def build(opts) do + def build(unquote(bindings_pattern), opts) do alias RDF.XSD alias RDF.NS.{RDFS, OWL} @@ -60,12 +62,12 @@ defmodule RDF.Graph.Builder do Module.create(builder_mod, mod_body, Macro.Env.location(env)) quote do - apply(unquote(builder_mod), :build, [unquote(opts)]) + apply(unquote(builder_mod), :build, [Map.new(unquote(bindings)), unquote(opts)]) end end - def build(single, env, builder_mod, opts) do - build({:__block__, [], List.wrap(single)}, env, builder_mod, opts) + def build(single, env, builder_mod, bindings, opts) do + build({:__block__, [], List.wrap(single)}, env, builder_mod, bindings, opts) end @doc false @@ -247,6 +249,19 @@ defmodule RDF.Graph.Builder do end end + defp bindings_vars(bindings, mod) do + vars = Enum.map(bindings, fn {key, _} -> key end) + + {vars, {:%{}, [], Enum.map(vars, &{&1, Macro.var(&1, mod)})}} + end + + defp bind_vars(block, bindings_vars, mod) do + Macro.prewalk(block, fn + {var, line, nil} = expr -> if var in bindings_vars, do: {var, line, mod}, else: expr + other -> other + end) + end + defp env_aliases(env) do Map.new(env.aliases, fn {short, full} -> { diff --git a/test/unit/graph_builder_test.exs b/test/unit/graph_builder_test.exs index a733a13..0d0193a 100644 --- a/test/unit/graph_builder_test.exs +++ b/test/unit/graph_builder_test.exs @@ -495,7 +495,7 @@ defmodule RDF.Graph.BuilderTest do test "merge with prefixes opt" do graph = - RDF.Graph.build prefixes: [custom: EX] do + RDF.Graph.build [], prefixes: [custom: EX] do @prefix custom: TestNS.Custom Custom.S |> Custom.p(Custom.O) @@ -604,6 +604,103 @@ defmodule RDF.Graph.BuilderTest do assert graph == RDF.graph(EX.S |> EX.p(EX.O)) end + describe "bindings" do + test "passing a variable from the outer context" do + literal = "foo" + + graph = + RDF.Graph.build literal: literal do + EX.S1 |> EX.p1(EX.O1) + EX.S2 |> EX.p2(literal) + end + + assert graph == + RDF.graph([ + EX.S1 |> EX.p1(EX.O1), + EX.S2 |> EX.p2("foo") + ]) + end + + test "passing a complex expression from the outer context" do + literal = "foo" + + graph = + RDF.Graph.build literal: String.upcase(literal <> "bar") do + EX.S1 |> EX.p1(EX.O1) + EX.S2 |> EX.p2(literal) + end + + assert graph == + RDF.graph([ + EX.S1 |> EX.p1(EX.O1), + EX.S2 |> EX.p2("FOOBAR") + ]) + end + + test "passing a graph from a previous build step" do + graph = + (fn -> + graph1 = + RDF.Graph.build do + @prefix ad: "http://example.com/ad-hoc/" + @prefix ex1: "http://example.com/ad-hoc/ex1#" + + Ad.S |> Ad.p(Ex1.O) + end + + RDF.Graph.build graph: graph1 do + @prefix ad: "http://example.com/ad-hoc/" + @prefix ex2: "http://example.com/ad-hoc/ex2#" + + graph + + Ad.S |> Ad.p(Ex2.O) + end + end).() + + assert graph == + RDF.graph( + [ + { + RDF.iri("http://example.com/ad-hoc/S"), + RDF.iri("http://example.com/ad-hoc/p"), + RDF.iri("http://example.com/ad-hoc/ex1#O") + }, + { + RDF.iri("http://example.com/ad-hoc/S"), + RDF.iri("http://example.com/ad-hoc/p"), + RDF.iri("http://example.com/ad-hoc/ex2#O") + } + ], + prefixes: + RDF.default_prefixes( + ad: "http://example.com/ad-hoc/", + ex1: "http://example.com/ad-hoc/ex1#", + ex2: "http://example.com/ad-hoc/ex2#" + ) + ) + end + + test "with opts" do + literal = "foo" + + graph = + RDF.Graph.build [literal: literal], name: EX.Graph do + EX.S1 |> EX.p1(EX.O1) + EX.S2 |> EX.p2(literal) + end + + assert graph == + RDF.graph( + [ + EX.S1 |> EX.p1(EX.O1), + EX.S2 |> EX.p2("foo") + ], + name: EX.Graph + ) + end + end + test "opts" do initial = {EX.S, EX.p(), "init"} @@ -615,7 +712,7 @@ defmodule RDF.Graph.BuilderTest do ] graph = - RDF.Graph.build opts do + RDF.Graph.build [], opts do EX.S |> EX.p(EX.O) end