From 99c5313697a17530ca62cf58b2766a8a6a5183dd Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Thu, 5 Dec 2024 15:41:10 +0100 Subject: [PATCH] Add RDF.JSON datatype --- CHANGELOG.md | 4 + lib/rdf/model/literal/datatype.ex | 6 +- lib/rdf/model/literal/datatypes/json.ex | 156 +++++++ mix.exs | 3 +- mix.lock | 1 + priv/vocabs/rdf.ttl | 9 + test/unit/datatypes/json_test.exs | 515 ++++++++++++++++++++++++ 7 files changed, 691 insertions(+), 3 deletions(-) create mode 100644 lib/rdf/model/literal/datatypes/json.ex create mode 100644 test/unit/datatypes/json_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8234f8..108a9234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ This project adheres to [Semantic Versioning](http://semver.org/) and Elixir versions < 1.14 and OTP version < 24 are no longer supported +### Added + +- `RDF.JSON` datatype + ### Fixed - Fixed compilation error when defining `RDF.Vocabulary.Namespace`s for large diff --git a/lib/rdf/model/literal/datatype.ex b/lib/rdf/model/literal/datatype.ex index c841a8cb..db3ef8a7 100644 --- a/lib/rdf/model/literal/datatype.ex +++ b/lib/rdf/model/literal/datatype.ex @@ -11,8 +11,10 @@ defmodule RDF.Literal.Datatype do builtin implementations of this behaviour for the most important XSD datatypes, but you define your own custom datatypes by deriving from these builtin datatypes and constraining them via `RDF.XSD.Facet`s. - - Non-XSD datatypes which implement the `RDF.Literal.Datatype` directly: There's currently only one - builtin datatype of this category - `RDF.LangString` for language tagged RDF literals. + - Non-XSD datatypes which implement the `RDF.Literal.Datatype` directly. + There's currently only two builtin datatypes of this category + - `RDF.LangString` for language tagged RDF literals and + - `RDF.JSON` for JSON content - `RDF.Literal.Generic`: This is a generic implementation which is used for `RDF.Literal`s with a datatype that has no own `RDF.Literal.Datatype` implementation defining its semantics. """ diff --git a/lib/rdf/model/literal/datatypes/json.ex b/lib/rdf/model/literal/datatypes/json.ex new file mode 100644 index 00000000..c6a38a6e --- /dev/null +++ b/lib/rdf/model/literal/datatypes/json.ex @@ -0,0 +1,156 @@ +defmodule RDF.JSON do + defstruct [:lexical] + + use RDF.Literal.Datatype, + name: "JSON", + id: RDF.Utils.Bootstrapping.rdf_iri("JSON") + + alias RDF.Literal.Datatype + alias RDF.Literal + + @type lexical :: String.t() + @type value :: String.t() | number | boolean | map | list | nil + + @type t :: %__MODULE__{lexical: lexical()} + + @impl RDF.Literal.Datatype + @spec new(t() | lexical() | value(), keyword) :: Literal.t() + def new(value_or_lexical, opts \\ []) + + def new(%__MODULE__{} = json, _opts), do: %Literal{literal: json} + + def new(value, _opts) + when is_number(value) or is_boolean(value) or is_nil(value) or is_list(value) or + is_map(value) do + from_value(value) + end + + def new(value_or_lexical, opts) when is_binary(value_or_lexical) do + if Keyword.get(opts, :as_value, false) do + from_value(value_or_lexical) + else + literal = %Literal{literal: %__MODULE__{lexical: value_or_lexical}} + + if Keyword.get(opts, :canonicalize, false) do + canonical(literal) + else + literal + end + end + end + + def new(value, _opts), do: from_invalid(value) + + defp from_value(value) do + %Literal{literal: %__MODULE__{lexical: Jcs.encode(value)}} + rescue + _ -> from_invalid(value) + end + + defp from_invalid(value) when is_binary(value), + do: %Literal{literal: %__MODULE__{lexical: value}} + + defp from_invalid(value), do: value |> inspect() |> from_invalid() + + @impl RDF.Literal.Datatype + @spec new!(lexical() | value(), keyword) :: Literal.t() + def new!(value_or_lexical, opts \\ []) do + literal = new(value_or_lexical, opts) + + if valid?(literal) do + literal + else + raise ArgumentError, "#{inspect(value_or_lexical)} is not a valid #{inspect(__MODULE__)}" + end + end + + @impl Datatype + @spec value(Literal.t() | t()) :: value() | :invalid + def value(%Literal{literal: literal}), do: value(literal) + + def value(%__MODULE__{} = json) do + case Jason.decode(json.lexical) do + {:ok, value} -> value + _ -> :invalid + end + end + + @impl Datatype + def lexical(%Literal{literal: literal}), do: lexical(literal) + def lexical(%__MODULE__{} = json), do: json.lexical + + @impl Datatype + def canonical(%Literal{literal: literal}), do: canonical(literal) + + def canonical(%__MODULE__{} = json) do + case value(json) do + :invalid -> new(json) + value -> from_value(value) + end + end + + @impl Datatype + def canonical?(%Literal{literal: literal}), do: canonical?(literal) + + def canonical?(%__MODULE__{} = json) do + if valid?(json) do + json.lexical == json |> canonical() |> lexical() + end + end + + @impl Datatype + def valid?(%Literal{literal: %__MODULE__{} = literal}), do: valid?(literal) + def valid?(%__MODULE__{} = json), do: value(json) != :invalid + def valid?(_), do: false + + @impl Datatype + def language(%Literal{literal: literal}), do: language(literal) + def language(%__MODULE__{}), do: nil + + @impl Datatype + def do_cast(_), do: nil + + @impl Datatype + def do_equal_value_same_or_derived_datatypes?(%__MODULE__{} = left, %__MODULE__{} = right) do + canonical(left).literal == canonical(right).literal + end + + def do_equal_value_same_or_derived_datatypes?(_, _), do: nil + + @impl Datatype + def do_compare(%__MODULE__{} = left, %__MODULE__{} = right) do + case {value(left), value(right)} do + {:invalid, _} -> + nil + + {_, :invalid} -> + nil + + {value, value} -> + :eq + + {left_value, right_value} -> + left_jcs = Jcs.encode(left_value) + right_jcs = Jcs.encode(right_value) + + cond do + left_jcs < right_jcs -> :lt + left_jcs > right_jcs -> :gt + true -> :eq + end + end + end + + def do_compare(_, _), do: nil + + @impl Datatype + def update(literal, fun, opts \\ []) + def update(%Literal{literal: literal}, fun, opts), do: update(literal, fun, opts) + + def update(%__MODULE__{} = literal, fun, _opts) do + literal + |> value() + |> fun.() + |> new(as_value: true) + end +end diff --git a/mix.exs b/mix.exs index c76fa75b..bdbacaa2 100644 --- a/mix.exs +++ b/mix.exs @@ -68,11 +68,12 @@ defmodule RDF.Mixfile do [ {:decimal, "~> 1.5 or ~> 2.0"}, {:uniq, "~> 0.6"}, + {:jason, "~> 1.4"}, + {:jcs, "~> 0.1"}, {:protocol_ex, "~> 0.4.4"}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.34", only: :dev, runtime: false}, - {:jason, "~> 1.4", only: [:dev, :test]}, {:excoveralls, "~> 0.18", only: :test}, # This dependency is needed for ExCoveralls when OTP < 25 {:castore, "~> 1.0", only: :test}, diff --git a/mix.lock b/mix.lock index b85bf869..77959072 100644 --- a/mix.lock +++ b/mix.lock @@ -12,6 +12,7 @@ "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "jcs": {:hex, :jcs, "0.1.1", "369e5a8a1697dd0856eb5c214dfae97d2b9c5d848dba9f6982d98b0ff8baa9f3", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b451f24d7220db89b3e6c0bd8271f32bb37bf5354eb36961bb24402a6dcf58ef"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, diff --git a/priv/vocabs/rdf.ttl b/priv/vocabs/rdf.ttl index 18d4f293..12ac07d4 100644 --- a/priv/vocabs/rdf.ttl +++ b/priv/vocabs/rdf.ttl @@ -14,6 +14,15 @@ rdf:HTML a rdfs:Datatype ; rdfs:label "HTML" ; rdfs:comment "The datatype of RDF literals storing fragments of HTML content" . +# TODO: use the official RDF 1.2 vocabulary as soon as it is available +# see https://github.com/w3c/rdf-concepts/issues/119 +# Note: This definition of rdf:JSON is not part of the official vocabulary. +rdf:JSON a rdfs:Datatype ; + rdfs:subClassOf rdfs:Literal ; + rdfs:seeAlso ; + rdfs:label "JSON" ; + rdfs:comment "The datatype of RDF literals storing JSON content" . + rdf:langString a rdfs:Datatype ; rdfs:subClassOf rdfs:Literal ; rdfs:isDefinedBy ; diff --git a/test/unit/datatypes/json_test.exs b/test/unit/datatypes/json_test.exs new file mode 100644 index 00000000..5025698f --- /dev/null +++ b/test/unit/datatypes/json_test.exs @@ -0,0 +1,515 @@ +if String.to_integer(System.otp_release()) >= 25 do + defmodule RDF.JSONTest do + use ExUnit.Case + + alias RDF.{Literal, XSD} + + @valid_values [ + true, + false, + nil, + 0, + 42, + 3.14, + %{}, + %{"a" => 42}, + %{"a" => [1, 2, 3]}, + %{"a" => %{"b" => 1}}, + [], + [1, "foo", true], + [%{"a" => 1}, %{"b" => 2}] + ] + + @canonical_json [ + ~s(42), + ~s(3.14), + ~s(true), + ~s(false), + ~s(null), + ~s("null"), + ~s("a string"), + ~s({"a":1}), + ~s([1,2,3]) + ] + + @non_canonical_json [ + ~s([1, 2, 3]), + ~s({"a": 1}), + ~s( {"a":1} ), + ~s({"b":2, "a":1}), + """ + { + "a": 1, + "b": 2 + } + """ + ] + + @valid_json @canonical_json ++ @non_canonical_json + + @invalid_json [ + ~s(invalid JSON), + ~s([1, 2, 3), + ~s([1, 2, 3), + ~s({"a": 1) + ] + + @invalid_values [ + "\xFF", + %{%{a: 1} => 2} + ] + + @invalid_typed [ + :atom, + {"tuple"} + ] + + @all_valid @valid_values ++ @valid_json + @all_invalid @invalid_json ++ @invalid_typed ++ @invalid_values + @all @all_valid ++ @all_invalid + + describe "new/2" do + test "with non-string JSON-compatible values" do + Enum.each(@valid_values, fn value -> + assert %Literal{} = RDF.JSON.new(value) + end) + end + + test "with JSON-encoded strings (default behavior)" do + Enum.each(@valid_json, fn json -> + assert %Literal{} = literal = RDF.JSON.new(json) + assert RDF.JSON.valid?(literal) + assert RDF.JSON.value(literal) == Jason.decode!(json) + assert RDF.JSON.lexical(literal) == json + end) + end + + test "with strings as values (using as_value: true)" do + string = "a string" + literal = RDF.JSON.new(string, as_value: true) + assert RDF.JSON.valid?(literal) + assert RDF.JSON.value(literal) == string + end + + test "with invalid values" do + Enum.each(@invalid_values ++ @invalid_json, fn value -> + assert %Literal{} = RDF.JSON.new(value) + end) + end + + test "with :canonicalize opt" do + Enum.each(@valid_values, fn value -> + assert RDF.JSON.new(value, canonicalize: true) == + value |> RDF.JSON.new() |> RDF.JSON.canonical() + end) + + Enum.each(@non_canonical_json, fn value -> + assert RDF.JSON.new(value, canonicalize: true) == + value |> RDF.JSON.new() |> RDF.JSON.canonical() + + assert RDF.JSON.new(value, as_value: true, canonicalize: true) == + value |> RDF.JSON.new(as_value: true) |> RDF.JSON.canonical() + end) + end + end + + describe "new!/2" do + test "with valid values, it behaves the same as new" do + Enum.each(@all_valid, fn value -> + assert RDF.JSON.new!(value) == + RDF.JSON.new(value) + + assert RDF.JSON.new!(value, canonicalize: true) == + RDF.JSON.new(value, canonicalize: true) + end) + + string = "some string" + + assert RDF.JSON.new!(string, as_value: true) == + RDF.JSON.new(string, as_value: true) + + assert RDF.JSON.new!("null") == RDF.JSON.new("null") + end + + test "with invalid values" do + Enum.each(@all_invalid, fn value -> + assert_raise ArgumentError, fn -> RDF.JSON.new!(value) end + end) + end + end + + describe "value/1" do + test "with valid literals" do + Enum.each(@valid_values, fn value -> + assert RDF.JSON.new!(value) |> RDF.JSON.value() == value + end) + + string = "some string" + assert RDF.JSON.new(string, as_value: true) |> RDF.JSON.value() == string + + Enum.each(@valid_json, fn json -> + assert RDF.JSON.new!(json) |> RDF.JSON.value() == Jason.decode!(json) + assert RDF.JSON.new!(json, as_value: true) |> RDF.JSON.value() == json + end) + end + + test "with invalid literals" do + Enum.each(@all_invalid, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.value() == :invalid + end) + end + + test "special case: null" do + assert RDF.JSON.new(nil) |> RDF.JSON.value() == nil + assert RDF.JSON.new("null") |> RDF.JSON.value() == nil + assert RDF.JSON.new("null", as_value: true) |> RDF.JSON.value() == "null" + end + end + + describe "lexical/1" do + test "with valid literals from values" do + Enum.each(@valid_values, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.lexical() == Jcs.encode(value) + end) + end + + test "with string values (as_value: true)" do + string = "some string" + assert RDF.JSON.new(string, as_value: true) |> RDF.JSON.lexical() == Jcs.encode(string) + + Enum.each(@valid_json, fn json -> + assert RDF.JSON.new(json, as_value: true) |> RDF.JSON.lexical() == Jcs.encode(json) + end) + end + + test "with valid literals from JSON strings" do + Enum.each(@valid_json, fn json -> + assert RDF.JSON.new(json) |> RDF.JSON.lexical() == json + end) + end + + test "with invalid literals" do + Enum.each(@all_invalid, fn + value when is_binary(value) -> + assert RDF.JSON.new(value) |> RDF.JSON.lexical() == value + + value -> + assert RDF.JSON.new(value) |> RDF.JSON.lexical() == inspect(value) + end) + end + + test "special case: null" do + assert RDF.JSON.new(nil) |> RDF.JSON.lexical() == "null" + assert RDF.JSON.new("null") |> RDF.JSON.lexical() == "null" + assert RDF.JSON.new("null", as_value: true) |> RDF.JSON.lexical() == ~s("null") + end + end + + describe "canonical/1" do + test "with valid literals from values" do + Enum.each(@valid_values, fn value -> + assert %Literal{} = canonical = RDF.JSON.new(value) |> RDF.JSON.canonical() + assert RDF.JSON.lexical(canonical) == Jcs.encode(value) + assert RDF.JSON.value(canonical) == value + end) + end + + test "with string values (as_value: true)" do + string = "some string" + + assert %Literal{} = + canonical = RDF.JSON.new(string, as_value: true) |> RDF.JSON.canonical() + + assert RDF.JSON.lexical(canonical) == Jcs.encode(string) + assert RDF.JSON.value(canonical) == string + + Enum.each(@valid_json, fn json -> + assert %Literal{} = + canonical = RDF.JSON.new(json, as_value: true) |> RDF.JSON.canonical() + + assert RDF.JSON.lexical(canonical) == Jcs.encode(json) + assert RDF.JSON.value(canonical) == json + end) + end + + test "with valid literals from JSON strings" do + Enum.each(@canonical_json, fn json -> + assert RDF.JSON.new(json) |> RDF.JSON.canonical() == RDF.JSON.new(json) + end) + + Enum.each(@non_canonical_json, fn json -> + value = Jason.decode!(json) + assert %Literal{} = canonical = RDF.JSON.new(json) |> RDF.JSON.canonical() + assert RDF.JSON.lexical(canonical) == Jcs.encode(value) + assert RDF.JSON.value(canonical) == value + end) + end + + test "with invalid literals" do + Enum.each(@all_invalid, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.canonical() == RDF.JSON.new(value) + end) + end + + test "special case: null" do + assert %Literal{} = canonical = RDF.JSON.new(nil) |> RDF.JSON.canonical() + assert RDF.JSON.lexical(canonical) == "null" + assert RDF.JSON.value(canonical) == nil + + assert %Literal{} = canonical = RDF.JSON.new("null") |> RDF.JSON.canonical() + assert RDF.JSON.lexical(canonical) == "null" + assert RDF.JSON.value(canonical) == nil + + assert %Literal{} = + canonical = RDF.JSON.new("null", as_value: true) |> RDF.JSON.canonical() + + assert RDF.JSON.lexical(canonical) == ~s("null") + assert RDF.JSON.value(canonical) == "null" + end + end + + describe "canonical?/1" do + test "with valid literals from values" do + Enum.each(@valid_values, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.canonical?() == true + end) + end + + test "with string values (as_value: true)" do + string = "some string" + assert RDF.JSON.new(string, as_value: true) |> RDF.JSON.canonical?() == true + + Enum.each(@valid_json, fn json -> + assert RDF.JSON.new(json, as_value: true) |> RDF.JSON.canonical?() == true + end) + end + + test "with canonical literals from JSON strings" do + Enum.each(@canonical_json, fn json -> + assert RDF.JSON.new(json) |> RDF.JSON.canonical?() == true + end) + end + + test "with non-canonical literals from JSON strings" do + Enum.each(@non_canonical_json, fn json -> + assert RDF.JSON.new(json) |> RDF.JSON.canonical?() == false + end) + end + + test "with invalid literals" do + Enum.each(@all_invalid, fn value -> + refute RDF.JSON.new(value) |> RDF.JSON.canonical?() + end) + end + + test "special case: null" do + assert RDF.JSON.new(nil) |> RDF.JSON.canonical?() == true + assert RDF.JSON.new("null") |> RDF.JSON.canonical?() == true + assert RDF.JSON.new("null", as_value: true) |> RDF.JSON.canonical?() == true + end + end + + describe "valid?/1" do + test "with valid values" do + Enum.each(@all_valid, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.valid?() == true + end) + + Enum.each(@all_valid, fn value -> + assert RDF.JSON.new(value, as_value: true) |> RDF.JSON.valid?() == true + end) + end + + test "with invalid_values" do + Enum.each(@all_invalid, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.valid?() == false + end) + end + end + + test "datatype?/1" do + assert RDF.JSON.datatype?(RDF.JSON) == true + + Enum.each(@all, fn value -> + literal = RDF.JSON.new(value) + assert RDF.JSON.datatype?(literal) == true + assert RDF.JSON.datatype?(literal.literal) == true + end) + end + + test "datatype_id/1" do + Enum.each(@all, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.datatype_id() == + RDF.iri(RDF.JSON.id()) + end) + end + + test "language/1" do + Enum.each(@all, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.language() == nil + end) + end + + describe "cast/1" do + test "when given a valid RDF.JSON literal" do + Enum.each(@all_valid, fn value -> + assert RDF.JSON.new(value) |> RDF.JSON.cast() == + RDF.JSON.new(value) + end) + end + + test "when given a literal with a datatype which is not castable" do + assert XSD.String.new("foo") |> RDF.JSON.cast() == nil + assert XSD.Integer.new(12_345) |> RDF.JSON.cast() == nil + end + + test "with invalid literals" do + assert XSD.Integer.new(3.14) |> RDF.JSON.cast() == nil + end + + test "with non-coercible value" do + assert RDF.JSON.cast(:foo) == nil + assert RDF.JSON.cast(make_ref()) == nil + end + end + + describe "equal_value?/2" do + test "with valid equal values" do + Enum.each(@all_valid, fn value -> + literal = RDF.JSON.new(value) + assert RDF.JSON.equal_value?(literal, literal) == true + end) + end + + test "with valid unequal values" do + assert RDF.JSON.equal_value?(RDF.JSON.new("a"), RDF.JSON.new("b")) == false + assert RDF.JSON.equal_value?(RDF.JSON.new(1), RDF.JSON.new(2)) == false + end + + test "with different lexical forms but same value" do + assert RDF.JSON.equal_value?( + RDF.JSON.new(~s({"a": 1})), + RDF.JSON.new(%{"a" => 1}) + ) == true + + assert RDF.JSON.equal_value?( + RDF.JSON.new(~s({"x": {"a": 1, "b": 2}})), + RDF.JSON.new(~s({"x": {"b": 2, "a": 1}})) + ) == true + + assert RDF.JSON.equal_value?(RDF.JSON.new(1), RDF.JSON.new(1.0)) == true + + assert RDF.JSON.equal_value?( + RDF.JSON.new("1.23456789"), + RDF.JSON.new("1.234567890000") + ) == true + + # not actually different lexical forms, but different forms of initialization + assert RDF.JSON.equal_value?( + RDF.JSON.new("42"), + RDF.JSON.new(42) + ) == true + end + + test "with invalid values" do + Enum.each(@all_invalid, fn value -> + literal = RDF.JSON.new(value) + assert RDF.JSON.equal_value?(literal, literal) == true + end) + end + end + + describe "compare/2" do + test "with equal values" do + Enum.each(@all_valid, fn value -> + assert RDF.JSON.compare( + RDF.JSON.new(value), + RDF.JSON.new(value) + ) == :eq + end) + end + + test "ordering based on JCS representation" do + ordered = [ + ~s("a string"), + -1, + 42, + [1, 2, 3], + false, + nil, + true, + %{"a" => 1} + ] + + ordered_literals = Enum.map(ordered, &RDF.JSON.new/1) + + Enum.chunk_every(ordered_literals, 2, 1, :discard) + |> Enum.each(fn [a, b] -> + assert RDF.JSON.compare(a, b) == :lt, "expected #{inspect(a)} < #{inspect(b)}" + assert RDF.JSON.compare(b, a) == :gt, "expected #{inspect(b)} > #{inspect(a)}" + end) + end + + test "different values with same JCS representation" do + examples = [ + {1, 1.0}, + {~s({"a": 1}), ~s({"a":1})} + ] + + Enum.each(examples, fn {value1, value2} -> + assert RDF.JSON.compare(RDF.JSON.new(value1), RDF.JSON.new(value2)) == :eq + end) + end + + test "comparing same type values" do + assert RDF.JSON.compare( + RDF.JSON.new("a", as_value: true), + RDF.JSON.new("b", as_value: true) + ) == :lt + + assert RDF.JSON.compare(RDF.JSON.new(1), RDF.JSON.new(2)) == :lt + + assert RDF.JSON.compare(RDF.JSON.new([1]), RDF.JSON.new([2])) == :lt + + assert RDF.JSON.compare( + RDF.JSON.new(%{"a" => 1}), + RDF.JSON.new(%{"b" => 1}) + ) == :lt + end + + test "with invalid values" do + Enum.each(@all_invalid, fn value -> + literal1 = RDF.JSON.new(value) + literal2 = RDF.JSON.new(value) + assert RDF.JSON.compare(literal1, literal2) == nil + + Enum.each(@valid_values, fn valid -> + assert RDF.JSON.compare(literal1, RDF.JSON.new(valid)) == nil + assert RDF.JSON.compare(RDF.JSON.new(valid), literal1) == nil + end) + end) + end + + test "special cases" do + assert RDF.JSON.compare(RDF.JSON.new("null", as_value: true), RDF.JSON.new(nil)) == :lt + assert RDF.JSON.compare(RDF.JSON.new(nil), RDF.JSON.new(nil)) == :eq + assert RDF.JSON.compare(RDF.JSON.new("null"), RDF.JSON.new(nil)) == :eq + end + end + + describe "update/2" do + test "with map" do + assert %{a: 1} + |> RDF.JSON.new() + |> Literal.update(fn map -> + Map.put(map, :b, 2) + end) == RDF.JSON.new(%{a: 1, b: 2}) + end + + test "result is interpreted as value" do + assert RDF.JSON.new(1) |> Literal.update(fn _ -> "foo" end) == + RDF.JSON.new("foo", as_value: true) + end + end + end +end