diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd6804..d7f0351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a CHANGELOG](http://keepachangelog.com). +## Unreleased + +### Added + +- the `JSON.LD.Encoder` now supports implicit compaction by providing a context + as a map or a URL string for a remote context with the new `:context` option + +[Compare v0.3.4...HEAD](https://github.com/rdf-elixir/jsonld-ex/compare/v0.3.4...HEAD) + + + ## 0.3.4 - 2021-12-13 Elixir versions < 1.10 are no longer supported diff --git a/VERSION b/VERSION index 42045ac..6a92166 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.4 +0.3.5-pre diff --git a/lib/json/ld/compaction.ex b/lib/json/ld/compaction.ex index b163a90..25d30c8 100644 --- a/lib/json/ld/compaction.ex +++ b/lib/json/ld/compaction.ex @@ -25,9 +25,11 @@ defmodule JSON.LD.Compaction do result end - if Context.empty?(active_context), - do: result, - else: Map.put(result, "@context", context["@context"] || context) + cond do + Context.empty?(active_context) -> result + is_binary(context) -> Map.put(result, "@context", context) + true -> Map.put(result, "@context", context["@context"] || context) + end end @spec do_compact(any, Context.t(), map, String.t() | nil, boolean) :: any diff --git a/lib/json/ld/decoder.ex b/lib/json/ld/decoder.ex index b64aca3..a57c779 100644 --- a/lib/json/ld/decoder.ex +++ b/lib/json/ld/decoder.ex @@ -1,5 +1,10 @@ defmodule JSON.LD.Decoder do @moduledoc """ + A decoder for JSON-LD serializations to `RDF.Dataset`s. + + As for all decoders of `RDF.Serialization.Format`s, you normally won't use these + functions directly, but via one of the `read_` functions on the `JSON.LD` format + module or the generic `RDF.Serialization` module. """ use RDF.Serialization.Decoder diff --git a/lib/json/ld/encoder.ex b/lib/json/ld/encoder.ex index a14c646..257f885 100644 --- a/lib/json/ld/encoder.ex +++ b/lib/json/ld/encoder.ex @@ -1,10 +1,34 @@ defmodule JSON.LD.Encoder do @moduledoc """ + An encoder for JSON-LD serializations of RDF.ex data structures. + + As for all encoders of `RDF.Serialization.Format`s, you normally won't use these + functions directly, but via one of the `write_` functions on the `JSON.LD` + format module or the generic `RDF.Serialization` module. + + + ## Options + + - `:context`: When a context map or remote context URL string is given, + compaction is performed using this context + - `:base`: : Allows to specify a base URI to be used during compaction + (only when `:context` is provided). + - `:use_native_types`: If this flag is set to `true`, RDF literals with a datatype IRI + that equals `xsd:integer` or `xsd:double` are converted to a JSON numbers and + RDF literals with a datatype IRI that equals `xsd:boolean` are converted to `true` + or `false` based on their lexical form. (default: `false`) + - `:use_rdf_type`: Unless this flag is set to `true`, `rdf:type` predicates will be + serialized as `@type` as long as the associated object is either an IRI or blank + node identifier. (default: `false`) + + The given options are also passed through to `Jason.encode/2`, so you can also + provide any the options this function supports, most notably the `:pretty` option. + """ use RDF.Serialization.Encoder - alias JSON.LD.Options + alias JSON.LD.{Compaction, Options} alias RDF.{ BlankNode, @@ -30,7 +54,8 @@ defmodule JSON.LD.Encoder do @impl RDF.Serialization.Encoder @spec encode(RDF.Data.t(), Options.t() | Enum.t()) :: {:ok, String.t()} | {:error, any} def encode(data, opts \\ []) do - with {:ok, json_ld_object} <- from_rdf(data, opts) do + with {:ok, json_ld_object} <- from_rdf(data, opts), + {:ok, json_ld_object} <- maybe_compact(json_ld_object, opts) do encode_json(json_ld_object, opts) end end @@ -40,9 +65,18 @@ defmodule JSON.LD.Encoder do @spec encode!(RDF.Data.t(), Options.t() | Enum.t()) :: String.t() @dialyzer {:nowarn_function, encode!: 1} def encode!(data, opts \\ []) do - data - |> from_rdf!(opts) - |> encode_json!(opts) + case encode(data, opts) do + {:ok, result} -> result + {:error, error} -> raise error + end + end + + defp maybe_compact(json_ld_object, opts) do + if context = Keyword.get(opts, :context) do + {:ok, Compaction.compact(json_ld_object, context, opts)} + else + {:ok, json_ld_object} + end end @spec from_rdf(RDF.Data.t(), Options.t() | Enum.t()) :: {:ok, [map]} | {:error, any} @@ -388,9 +422,4 @@ defmodule JSON.LD.Encoder do defp encode_json(value, opts) do Jason.encode(value, opts) end - - @spec encode_json!(any, [Jason.encode_opt()]) :: String.t() - defp encode_json!(value, opts) do - Jason.encode!(value, opts) - end end diff --git a/test/unit/encoder_test.exs b/test/unit/encoder_test.exs index 40d2efa..0b2f3f4 100644 --- a/test/unit/encoder_test.exs +++ b/test/unit/encoder_test.exs @@ -572,6 +572,131 @@ defmodule JSON.LD.EncoderTest do end) end + describe "encode options" do + test ":context with a context map" do + graph = + ~I + |> S.givenName("Manu") + |> S.familyName("Sporny") + |> S.url(~I) + |> Graph.new() + + context = %{ + "givenName" => "http://schema.org/givenName", + "familyName" => "http://schema.org/familyName", + "homepage" => %{ + "@id" => "http://schema.org/url", + "@type" => "@id" + } + } + + assert JSON.LD.Encoder.encode!(graph, context: context, pretty: true) == + """ + { + "@context": { + "familyName": "http://schema.org/familyName", + "givenName": "http://schema.org/givenName", + "homepage": { + "@id": "http://schema.org/url", + "@type": "@id" + } + }, + "@id": "http://manu.sporny.org/about#manu", + "familyName": "Sporny", + "givenName": "Manu", + "homepage": "http://manu.sporny.org/" + } + """ + |> String.trim() + end + + test ":context with a remote context" do + bypass = Bypass.open() + + Bypass.expect(bypass, fn conn -> + assert "GET" == conn.method + assert "/test-context" == conn.request_path + + context = %{ + "@context" => %{ + "givenName" => "http://schema.org/givenName", + "familyName" => "http://schema.org/familyName", + "homepage" => %{ + "@id" => "http://schema.org/url", + "@type" => "@id" + } + } + } + + Plug.Conn.resp(conn, 200, Jason.encode!(context)) + end) + + remote_context = "http://localhost:#{bypass.port}/test-context" + + graph = + ~I + |> S.givenName("Manu") + |> S.familyName("Sporny") + |> S.url(~I) + |> Graph.new() + + assert JSON.LD.Encoder.encode!(graph, context: remote_context, pretty: true) == + """ + { + "@context": "#{remote_context}", + "@id": "http://manu.sporny.org/about#manu", + "familyName": "Sporny", + "givenName": "Manu", + "homepage": "http://manu.sporny.org/" + } + """ + |> String.trim() + end + + test "compaction options" do + graph = + ~I + |> S.givenName("Manu") + |> S.familyName("Sporny") + |> RDF.type(S.Person) + |> EX.foo(3.14) + |> EX.bar(EX.Bar) + |> Graph.new() + + context = %{ + "givenName" => "http://schema.org/givenName", + "familyName" => "http://schema.org/familyName" + } + + assert JSON.LD.Encoder.encode!(graph, + context: context, + base: EX.__base_iri__(), + use_native_types: true, + use_rdf_type: true, + pretty: true + ) == + """ + { + "@context": { + "familyName": "http://schema.org/familyName", + "givenName": "http://schema.org/givenName" + }, + "@id": "http://manu.sporny.org/about#manu", + "familyName": "Sporny", + "givenName": "Manu", + "http://example.com/bar": { + "@id": "Bar" + }, + "http://example.com/foo": 3.14, + "http://www.w3.org/1999/02/22-rdf-syntax-ns#type": { + "@id": "http://schema.org/Person" + } + } + """ + |> String.trim() + end + end + describe "problems" do %{ "xsd:boolean as value" => {