Skip to content

Commit

Permalink
Add RDF.JSON datatype
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelotto committed Dec 5, 2024
1 parent 68e119c commit 99c5313
Show file tree
Hide file tree
Showing 7 changed files with 691 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions lib/rdf/model/literal/datatype.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down
156 changes: 156 additions & 0 deletions lib/rdf/model/literal/datatypes/json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
defmodule RDF.JSON do

Check warning on line 1 in lib/rdf/model/literal/datatypes/json.ex

View workflow job for this annotation

GitHub Actions / Formatting and Unused Deps (1.17.2, 27.0.1)

Modules should have a @moduledoc tag.
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
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
9 changes: 9 additions & 0 deletions priv/vocabs/rdf.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://www.w3.org/TR/rdf12-concepts/#section-json> ;
rdfs:label "JSON" ;
rdfs:comment "The datatype of RDF literals storing JSON content" .

rdf:langString a rdfs:Datatype ;
rdfs:subClassOf rdfs:Literal ;
rdfs:isDefinedBy <http://www.w3.org/1999/02/22-rdf-syntax-ns#> ;
Expand Down
Loading

0 comments on commit 99c5313

Please sign in to comment.