Skip to content

Commit

Permalink
Add simple RDF graph.
Browse files Browse the repository at this point in the history
  • Loading branch information
q-uint committed Sep 10, 2023
1 parent 3a945f3 commit 4438f8b
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 17 deletions.
58 changes: 45 additions & 13 deletions datatypes.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
package rdf

import "fmt"
import (
"fmt"
"math/big"
"strings"
)

type DataType string

// INFO on datatypes: https://www.w3.org/TR/xmlschema11-2/type-hierarchy-201104.longdesc.html
const (
XSD = "http://www.w3.org/2001/XMLSchema#"
XSDAnyType DataType = XSD + "anyType"
XSDAnyURI DataType = XSD + "anyURI"
XSDBase64Binary DataType = XSD + "base64Binary"
XSDBoolean DataType = XSD + "boolean"
XSDDate DataType = XSD + "date"
XDSDecimal DataType = XSD + "decimal"
XSDDouble DataType = XSD + "double"
XSDDuration DataType = XSD + "duration"
XSDFloat DataType = XSD + "float"
XSDString DataType = XSD + "string"
XSD = "http://www.w3.org/2001/XMLSchema#"
XSDAnyType DataType = XSD + "anyType"
XSDBoolean DataType = XSD + "boolean"
XSDInteger DataType = XSD + "integer"
XSDDecimal DataType = XSD + "decimal"
XSDDouble DataType = XSD + "double"
XSDString DataType = XSD + "string"
// TODO: add "other" build-in atomic types, e.g. dateTimeStamp etc.

XSDNS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
Expand All @@ -35,6 +35,27 @@ func (d DataType) NativeType(value string) (any, bool, error) {
return value[0] == 't', true, nil
}
return nil, false, fmt.Errorf("invalid boolean value: %q", value)
case XSDInteger:
if i, ok := new(big.Int).SetString(value, 10); ok {
return i, true, nil
}
return nil, false, fmt.Errorf("invalid integer value: %q", value)
case XSDDecimal:
if strings.ContainsAny(value, "eE") {
return nil, false, fmt.Errorf("invalid decimal value: %q", value)
}
if f, ok := new(big.Float).SetString(value); ok {
return f, true, nil
}
return nil, false, fmt.Errorf("invalid decimal value: %q", value)
case XSDDouble:
if value == "INF" || value == "-INF" || value == "NaN" {
return value, true, nil
}
if f, ok := new(big.Float).SetString(value); ok {
return f, true, nil
}
return nil, false, fmt.Errorf("invalid decimal value: %q", value)
default:
return value, false, nil
}
Expand All @@ -46,7 +67,6 @@ func (d DataType) Validate(v any, acceptString bool) bool {
// TODO: validate string against the datatype.
return true
}

switch d {
case XSDAnyType:
return true
Expand All @@ -56,6 +76,18 @@ func (d DataType) Validate(v any, acceptString bool) bool {
case XSDBoolean:
_, ok := v.(bool)
return ok
case XSDInteger:
_, ok := v.(*big.Int)
return ok
case XSDDecimal:
_, ok := v.(*big.Float)
return ok
case XSDDouble:
if _, ok := v.(*big.Float); ok {
return true
}
_, ok := v.(string)
return ok && (v == "INF" || v == "-INF" || v == "NaN")
default:
return false
}
Expand Down
54 changes: 54 additions & 0 deletions datatypes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package rdf

import "testing"

// https://www.w3.org/TR/xmlschema-2/#decimal
func TestDataType_NativeType_decimal(t *testing.T) {
for _, test := range []string{
"-1", "12678967", "+100000", "210",
"-1.23", "12678967.543233", "+100000.00", "210",
} {
if _, ok, err := XSDDecimal.NativeType(test); !ok || err != nil {
t.Errorf("XSDDecimal.NativeType(%q) = %v, %v; want %v, nil", test, ok, err, true)
}
}
for _, test := range []string{
"-1E4", "1267.43233E12", "12.78e-2", "INF",
} {
if _, ok, err := XSDDecimal.NativeType(test); ok || err == nil {
t.Errorf("XSDDecimal.NativeType(%q) = %v, %v; want %v, nil", test, ok, err, false)
}
}
}

// https://www.w3.org/TR/xmlschema-2/#double
func TestDataType_NativeType_double(t *testing.T) {
for _, test := range []string{
"-1", "12678967", "+100000", "210",
"-1.23", "12678967.543233", "+100000.00", "210",
"-1E4", "1267.43233E12", "12.78e-2", "12", "-0", "0", "INF",
} {
if _, ok, err := XSDDouble.NativeType(test); !ok || err != nil {
t.Errorf("XSDDouble.NativeType(%q) = %v, %v; want %v, nil", test, ok, err, true)
}
}
}

// https://www.w3.org/TR/xmlschema-2/#integer
func TestDataType_NativeType_integer(t *testing.T) {
for _, test := range []string{
"-1", "12678967", "+100000", "210",
} {
if _, ok, err := XSDInteger.NativeType(test); !ok || err != nil {
t.Errorf("XSDInteger.NativeType(%q) = %v, %v; want %v, nil", test, ok, err, true)
}
}
for _, test := range []string{
"-1.23", "12678967.543233", "+100000.00",
"-1E4", "1267.43233E12", "12.78e-2", "INF",
} {
if _, ok, err := XSDInteger.NativeType(test); ok || err == nil {
t.Errorf("XSDInteger.NativeType(%q) = %v, %v; want %v, nil", test, ok, err, false)
}
}
}
72 changes: 72 additions & 0 deletions graph.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package rdf

type Graph struct {
triples map[*Triple]struct{}
}

func NewGraph(ts ...*Triple) *Graph {
triples := make(map[*Triple]struct{})
for _, t := range ts {
triples[t] = struct{}{}
}
return &Graph{triples}
}

func (g *Graph) Add(s, p, o Node) {
t := &Triple{s, p, o}
g.triples[t] = struct{}{}
}

// Find returns the first triple matching the given pattern.
func (g *Graph) Find(s, p, o Node) *Triple {
for t := range g.iter() {
if (s == nil || t.Subject == s) &&
(p == nil || t.Predicate == p) &&
(o == nil || t.Object == o) {
return t
}
}
return nil
}

// FindAll returns all triples matching the given pattern.
func (g *Graph) FindAll(s, p, o Node) []*Triple {
var triples []*Triple
for t := range g.iter() {
if (s == nil || t.Subject == s) &&
(p == nil || t.Predicate == p) &&
(o == nil || t.Object == o) {
triples = append(triples, t)
}
}
return triples
}

func (g *Graph) Remove(t *Triple) {
delete(g.triples, t)
}

func (g *Graph) iter() chan *Triple {
ch := make(chan *Triple)
go func() {
for t := range g.triples {
ch <- t
}
close(ch)
}()
return ch
}

type Triple struct {
Subject Node
Predicate Node
Object Node
}

func NewTriple(s, p, o Node) *Triple {
return &Triple{s, p, o}
}

func (t *Triple) Equal(other *Triple) bool {
return t.Subject.Equal(other.Subject) && t.Predicate.Equal(other.Predicate) && t.Object.Equal(other.Object)
}
37 changes: 37 additions & 0 deletions graph_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package rdf

import "testing"

func TestGraph(t *testing.T) {
var (
a = &BlankNode{"a"}
b = &BlankNode{"b"}
c = &BlankNode{"c"}
)

abc := NewTriple(a, b, c)
abb := NewTriple(a, b, b)
aaa := NewTriple(a, a, a)

g := NewGraph(abc, abb, aaa)

if triple := g.Find(nil, nil, nil); triple != abc {
t.Error("expected abc")
}
if triple := g.Find(a, b, c); triple != abc {
t.Error("expected abc")
}
if triple := g.Find(a, a, a); t == nil || triple != aaa {
t.Error("expected aaa")
}
if triple := g.Find(nil, a, nil); triple != aaa {
t.Error("expected aaa")
}

if len(g.FindAll(a, nil, nil)) != 3 {
t.Error("expected 3 triples")
}
if len(g.FindAll(nil, b, nil)) != 2 {
t.Error("expected 2 triples")
}
}
56 changes: 54 additions & 2 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ type BlankNode struct {
Attribute string
}

func (b *BlankNode) Equal(other Node) bool {
if other, ok := other.(*BlankNode); ok {
return b.Attribute == other.Attribute
}
return false
}

func (b *BlankNode) GetValue() string {
return b.Attribute
}

func (b *BlankNode) toObject(_ bool) (map[string]any, error) {
return map[string]any{
"@id": b.Attribute,
Expand All @@ -20,6 +31,17 @@ type IRIReference struct {
Value string
}

func (r *IRIReference) Equal(other Node) bool {
if other, ok := other.(*IRIReference); ok {
return r.Value == other.Value
}
return false
}

func (r *IRIReference) GetValue() string {
return r.Value
}

func (r *IRIReference) toObject(_ bool) (map[string]any, error) {
return map[string]any{
"@id": r.Value,
Expand All @@ -36,6 +58,17 @@ type Literal struct {
Language string
}

func (l *Literal) Equal(other Node) bool {
if other, ok := other.(*Literal); ok {
return l.Value == other.Value && l.Datatype == other.Datatype && l.Language == other.Language
}
return false
}

func (l *Literal) GetValue() string {
return l.Value
}

func (l *Literal) toObject(nativeTypes bool) (map[string]any, error) {
if l.Language != "" {
if l.Datatype != "" && l.Datatype != XSDNSString {
Expand Down Expand Up @@ -68,10 +101,15 @@ func (l *Literal) toObject(nativeTypes bool) (map[string]any, error) {
}

type Node interface {
// GetValue returns the value of the node.
GetValue() string
// Equal returns true if the two nodes are equal.
Equal(other Node) bool

toObject(nativeTypes bool) (map[string]any, error)
}

func FromObject(obj any, acceptString bool) (Node, error) {
func fromObject(obj any, acceptString bool) (Node, error) {
var id string
switch obj := obj.(type) {
case map[string]any:
Expand All @@ -80,7 +118,7 @@ func FromObject(obj any, acceptString bool) (Node, error) {
Value: fmt.Sprintf("%v", v),
Datatype: XSDString,
}
if datatype, ok := obj["@datatype"]; ok {
if datatype, ok := obj["@type"]; ok {
s, ok := datatype.(string)
if !ok {
return nil, fmt.Errorf("invalid @datatype attribute: %T", datatype)
Expand All @@ -90,6 +128,20 @@ func FromObject(obj any, acceptString bool) (Node, error) {
if !literal.Datatype.Validate(v, acceptString) {
return nil, fmt.Errorf("invalid @value for datatype %s: %v", literal.Datatype, v)
}

if language, ok := obj["@language"]; ok {
s, ok := language.(string)
if !ok {
return nil, fmt.Errorf("invalid @language attribute: %T", language)
}
literal.Language = s
if literal.Datatype != "" && literal.Datatype != XSDNSString {
return nil, fmt.Errorf("invalid datatype for language literal: %s", literal.Datatype)
}
if literal.Datatype == "" {
literal.Datatype = XSDNSString
}
}
return literal, nil
}

Expand Down
4 changes: 2 additions & 2 deletions node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestFromObject_blankNode(t *testing.T) {
if err := json.Unmarshal([]byte(test), &m); err != nil {
t.Fatal(err)
}
n, err := FromObject(m, false)
n, err := fromObject(m, false)
if err != nil {
t.Fatal(err)
}
Expand All @@ -43,7 +43,7 @@ func TestFromObject_iriReference(t *testing.T) {
if err := json.Unmarshal([]byte(test), &m); err != nil {
t.Fatal(err)
}
n, err := FromObject(m, false)
n, err := fromObject(m, false)
if err != nil {
t.Fatal(err)
}
Expand Down

0 comments on commit 4438f8b

Please sign in to comment.