From 3cba749945d4028beab879b7060f60e8f1fbc6af Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Sat, 9 Apr 2022 21:15:34 +0200 Subject: [PATCH] Add ~i, ~b and ~l sigils --- CHANGELOG.md | 4 +- lib/rdf/sigils.ex | 101 +++++++++++++++++++++++++++++++++++++- test/unit/sigils_test.exs | 56 ++++++++++++++++++--- 3 files changed, 152 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c3fd99..8632d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ This project adheres to [Semantic Versioning](http://semver.org/) and ### Added - a `RDF.Graph` builder DSL available under the `RDF.Graph.build/2` function +- new `RDF.Sigils` `~i`, `~b` and `~l` as variants of the `~I`, `~B` and `~L` + sigils, which support string interpolation - `RDF.Graph.new/2` and `RDF.Graph.add/2` support the addition of `RDF.Dataset`s - `RDF.Description.empty?/1`, `RDF.Graph.empty?/1`, `RDF.Dataset.empty?/1` and `RDF.Data.empty?/1` which are significantly faster than `Enum.empty?/1` - - By replacing all `Enum.empty?/1` uses over the RDF data structures with these + - By replacing all `Enum.empty?/1` uses over the RDF data structures with these new `empty?/1` functions throughout the code base, several functions benefit from this performance improvement. - `RDF.Description.first/2` now has a `RDF.Description.first/3` variant which diff --git a/lib/rdf/sigils.ex b/lib/rdf/sigils.ex index 427b08e..0a43c97 100644 --- a/lib/rdf/sigils.ex +++ b/lib/rdf/sigils.ex @@ -6,7 +6,9 @@ defmodule RDF.Sigils do @doc ~S""" Handles the sigil `~I` for IRIs. - Note: The given IRI string is precompiled into an `RDF.IRI` struct. + It returns an `RDF.IRI` from the given string without interpolations and + without escape characters, except for the escaping of the closing sigil + character itself. ## Examples @@ -19,9 +21,36 @@ defmodule RDF.Sigils do Macro.escape(RDF.iri!(iri)) end + @doc ~S""" + Handles the sigil `~i` for IRIs. + + It returns an `RDF.IRI` from the given string as if it was a double quoted + string, unescaping characters and replacing interpolations. + + ## Examples + + iex> import RDF.Sigils + iex> ~i + RDF.iri("http://example.com/foo") + + """ + defmacro sigil_i({:<<>>, _, [iri]}, []) when is_binary(iri) do + Macro.escape(RDF.iri!(iri)) + end + + defmacro sigil_i({:<<>>, line, pieces}, []) do + quote do + RDF.iri!(unquote({:<<>>, line, unescape_tokens(pieces)})) + end + end + @doc ~S""" Handles the sigil `~B` for blank nodes. + It returns an `RDF.BlankNode` from the given string without interpolations + and without escape characters, except for the escaping of the closing sigil + character itself. + ## Examples iex> import RDF.Sigils @@ -33,9 +62,34 @@ defmodule RDF.Sigils do Macro.escape(RDF.BlankNode.new(bnode)) end + @doc ~S""" + Handles the sigil `~b` for blank nodes. + + It returns an `RDF.BlankNode` from the given string as if it was a double quoted + string, unescaping characters and replacing interpolations. + + ## Examples + + iex> import RDF.Sigils + iex> ~b + RDF.bnode("foobar") + + """ + defmacro sigil_b({:<<>>, _, [bnode]}, []) when is_binary(bnode) do + Macro.escape(RDF.BlankNode.new(bnode)) + end + + defmacro sigil_b({:<<>>, line, pieces}, []) do + quote do + RDF.BlankNode.new(unquote({:<<>>, line, unescape_tokens(pieces)})) + end + end + @doc ~S""" Handles the sigil `~L` for plain Literals. + It returns an `RDF.Literal` from the given string without interpolations and without escape characters, except for the escaping of the closing sigil character itself. + The sigil modifier can be used to specify a language tag. Note: Languages with subtags are not supported. @@ -58,4 +112,49 @@ defmodule RDF.Sigils do defmacro sigil_L({:<<>>, _, [value]}, language) when is_binary(value) do Macro.escape(RDF.LangString.new(value, language: to_string(language))) end + + @doc ~S""" + Handles the sigil `~l` for blank nodes. + + It returns an `RDF.Literal` from the given string as if it was a double quoted + string, unescaping characters and replacing interpolations. + + ## Examples + + iex> import RDF.Sigils + iex> ~l"foo #{String.downcase("Bar")}" + RDF.literal("foo bar") + iex> ~l"foo #{String.downcase("Bar")}"en + RDF.literal("foo bar", language: "en") + + """ + defmacro sigil_l(value, language) + + defmacro sigil_l({:<<>>, _, [value]}, []) when is_binary(value) do + Macro.escape(RDF.XSD.String.new(value)) + end + + defmacro sigil_l({:<<>>, _, [value]}, language) when is_binary(value) do + Macro.escape(RDF.LangString.new(value, language: to_string(language))) + end + + defmacro sigil_l({:<<>>, line, pieces}, []) do + quote do + RDF.XSD.String.new(unquote({:<<>>, line, unescape_tokens(pieces)})) + end + end + + defmacro sigil_l({:<<>>, line, pieces}, language) do + quote do + RDF.LangString.new(unquote({:<<>>, line, unescape_tokens(pieces)}), + language: to_string(unquote(language)) + ) + end + end + + defp unescape_tokens(tokens) do + for token <- tokens do + if is_binary(token), do: Macro.unescape_string(token), else: token + end + end end diff --git a/test/unit/sigils_test.exs b/test/unit/sigils_test.exs index 718f8ea..0051741 100644 --- a/test/unit/sigils_test.exs +++ b/test/unit/sigils_test.exs @@ -5,25 +5,67 @@ defmodule RDF.SigilsTest do doctest RDF.Sigils - describe "IRI sigil without interpolation" do - test "creating an IRI" do + describe "~I sigil" do + test "creates an IRI" do assert ~I == RDF.iri("http://example.com") end + + test "escaping" do + assert ~I == RDF.iri("http://example.com/f\\no") + end end - describe "Blank node sigil without interpolation" do - test "creating a blank node" do + describe "~i sigil" do + test "without interpolation" do + assert ~i == RDF.iri("http://example.com") + end + + test "with interpolation" do + assert ~i == RDF.iri("http://example.com/3") + assert ~i == RDF.iri("http://example.com/foo") + assert ~i == RDF.iri("http://example.com/foo") + end + + test "escaping" do + assert ~i == RDF.iri("http://example.com/f\\no") + end + end + + describe "~B sigil" do + test "creates a blank node" do assert ~B == RDF.bnode("foo") end end - describe "Literal sigil without interpolation" do - test "creating a plain Literal" do + describe "~b sigil" do + test "without interpolation" do + assert ~b == RDF.bnode("foo") + end + + test "with interpolation" do + assert ~b == RDF.bnode("foo3") + end + end + + describe "~L sigil" do + test "creates a plain Literal" do assert ~L"foo" == RDF.literal("foo") end - test "creating a language-tagged Literal" do + test "creates a language-tagged Literal" do assert ~L"foo"en == RDF.literal("foo", language: "en") end end + + describe "~l sigil" do + test "without interpolation" do + assert ~l"foo" == RDF.literal("foo") + assert ~l"foo"en == RDF.literal("foo", language: "en") + end + + test "with interpolation" do + assert ~l"foo#{1 + 2}" == RDF.literal("foo3") + assert ~l"foo#{1 + 2}"en == RDF.literal("foo3", language: "en") + end + end end