diff --git a/CHANGELOG.md b/CHANGELOG.md index 8562880..fd0095b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ This project adheres to [Semantic Versioning](http://semver.org/) and - When triples with an empty object list where added to an `RDF.Graph`, it included empty descriptions, which lead to inconsistent behaviour (for example it would be counted in `RDF.Graph.subject_count/1`). +- When an `RDF.Graph` contained empty descriptions these were rendered by + the `RDF.Turtle.Encoder` to a subject without predicates and objects, i.e. + invalid Turtle. This actually shouldn't happen and is either caused by + misuse or a bug. So instead, a `RDF.Graph.EmptyDescriptionError` with a + detailed message will be raised now when this case is detected. [Compare v0.11.0...HEAD](https://github.com/rdf-elixir/rdf-ex/compare/v0.11.0...HEAD) diff --git a/lib/rdf/exceptions.ex b/lib/rdf/exceptions.ex index 06dcea0..f4f7330 100644 --- a/lib/rdf/exceptions.ex +++ b/lib/rdf/exceptions.ex @@ -22,14 +22,6 @@ defmodule RDF.Triple.InvalidPredicateError do end end -defmodule RDF.XSD.Datatype.Mismatch do - defexception [:value, :expected_type] - - def message(%{value: value, expected_type: expected_type}) do - "'#{inspect(value)}' is not a #{expected_type}" - end -end - defmodule RDF.Quad.InvalidGraphContextError do defexception [:graph_context] @@ -38,6 +30,31 @@ defmodule RDF.Quad.InvalidGraphContextError do end end +defmodule RDF.Graph.EmptyDescriptionError do + defexception [:subject] + + def message(%{subject: subject}) do + """ + RDF.Graph with empty description about '#{inspect(subject)}' detected. + Empty descriptions in a graph lead to inconsistent behaviour. The RDF.Graph API + should ensure that this never happens. So this probably happened by changing the + contents of the RDF.Graph struct directly, which is strongly discouraged. + You should always use the RDF.Graph API to change the content of a graph. + If this happened while using the RDF.Graph API, this is a bug. + Please report this at https://github.com/rdf-elixir/rdf-ex/issues and describe the + circumstances how this happened. + """ + end +end + +defmodule RDF.XSD.Datatype.Mismatch do + defexception [:value, :expected_type] + + def message(%{value: value, expected_type: expected_type}) do + "'#{inspect(value)}' is not a #{expected_type}" + end +end + defmodule RDF.Namespace.InvalidVocabBaseIRIError do defexception [:message] end diff --git a/lib/rdf/serializations/turtle_encoder.ex b/lib/rdf/serializations/turtle_encoder.ex index 0f25be7..6f2a837 100644 --- a/lib/rdf/serializations/turtle_encoder.ex +++ b/lib/rdf/serializations/turtle_encoder.ex @@ -232,11 +232,15 @@ defmodule RDF.Turtle.Encoder do defp description_order(%{subject: s1}, %{subject: s2}), do: to_string(s1) < to_string(s2) defp description_statements(description, state, nesting) do - with %BlankNode{} <- description.subject, - ref_count when ref_count < 2 <- State.bnode_ref_counter(state, description.subject) do - unrefed_bnode_subject_term(description, ref_count, state, nesting) + if Description.empty?(description) do + raise Graph.EmptyDescriptionError, subject: description.subject else - _ -> full_description_statements(description, state, nesting) + with %BlankNode{} <- description.subject, + ref_count when ref_count < 2 <- State.bnode_ref_counter(state, description.subject) do + unrefed_bnode_subject_term(description, ref_count, state, nesting) + else + _ -> full_description_statements(description, state, nesting) + end end end diff --git a/test/unit/turtle_encoder_test.exs b/test/unit/turtle_encoder_test.exs index 1ea97d2..fecf9f6 100644 --- a/test/unit/turtle_encoder_test.exs +++ b/test/unit/turtle_encoder_test.exs @@ -416,13 +416,36 @@ defmodule RDF.Turtle.EncoderTest do ] . """ end + + test "serializing a pathological graph with an empty description" do + description = RDF.description(EX.S) + graph = %Graph{Graph.new() | descriptions: %{description.subject => description}} + + assert_raise Graph.EmptyDescriptionError, fn -> + Turtle.Encoder.encode!(graph) + end + end end - test "serializing a description" do - description = EX.S |> EX.p(EX.O) + describe "serializing a description" do + test "a non-empty description" do + description = EX.S |> EX.p(EX.O) - assert Turtle.Encoder.encode!(description) == - description |> Graph.new() |> Turtle.Encoder.encode!() + assert Turtle.Encoder.encode!(description) == + description |> Graph.new() |> Turtle.Encoder.encode!() + end + + test "an empty description" do + description = RDF.description(EX.S) + + assert Turtle.Encoder.encode!(description) |> String.trim() == + """ + @prefix rdf: . + @prefix rdfs: . + @prefix xsd: . + """ + |> String.trim() + end end describe "prefixed_name/2" do