diff --git a/cmd/generate_linkedql_client/generate_linkedql_client.go b/cmd/generate_linkedql_client/generate_linkedql_client.go new file mode 100644 index 000000000..59079bc18 --- /dev/null +++ b/cmd/generate_linkedql_client/generate_linkedql_client.go @@ -0,0 +1,318 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "os" + + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/graph/memstore" + "github.com/cayleygraph/cayley/owl" + "github.com/cayleygraph/quad" + "github.com/cayleygraph/quad/jsonld" + "github.com/cayleygraph/quad/voc/rdfs" +) + +const schemaFile = "linkedql.json" +const outputFilePath = "query/linkedql/client/client.go" + +var stepIRI = quad.IRI("http://cayley.io/linkedql#Step") +var pathStepIRI = quad.IRI("http://cayley.io/linkedql#PathStep") +var iteratorStepIRI = quad.IRI("http://cayley.io/linkedql#IteratorStep") + +func main() { + ctx := context.TODO() + qs, err := loadSchema() + + if err != nil { + panic(err) + } + + stepClass, err := owl.GetClass(ctx, qs, stepIRI) + + if err != nil { + panic(err) + } + + stepSubClasses := stepClass.SubClasses() + var decls []ast.Decl + + for _, stepSubClass := range stepSubClasses { + if stepSubClass.Identifier == pathStepIRI || stepSubClass.Identifier == iteratorStepIRI { + continue + } + stepSubClassDecls, err := stepSubClassToDecls(stepSubClass) + if err != nil { + panic(err) + } + decls = append(decls, stepSubClassDecls...) + } + + // Create a FileSet for node. Since the node does not come + // from a real source file, fset will be empty. + fset := token.NewFileSet() + file, err := getFile(fset) + + if err != nil { + panic(err) + } + + file.Decls = append(file.Decls, decls...) + + err = writeFile(fset, file, outputFilePath) + + if err != nil { + panic(err) + } +} + +// loadSchema loads the schema file into an in-memory store +func loadSchema() (graph.QuadStore, error) { + jsonFile, err := os.Open(schemaFile) + if err != nil { + return nil, err + } + var o interface{} + qs := memstore.New() + json.NewDecoder(jsonFile).Decode(&o) + reader := jsonld.NewReaderFromMap(o) + for true { + quad, err := reader.ReadQuad() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + qs.AddQuad(quad) + } + return qs, nil +} + +var xsdString = quad.IRI("http://www.w3.org/2001/XMLSchema#string") +var rdfsResource = quad.IRI(rdfs.Resource).Full() +var stringIdent = ast.NewIdent("string") + +var pathTypeIdent = ast.NewIdent("Path") +var pathIdent = ast.NewIdent("p") + +func stepSubClassToDecls(stepSubClass *owl.Class) ([]ast.Decl, error) { + var decls []ast.Decl + hasFrom := false + iri, ok := stepSubClass.Identifier.(quad.IRI) + if !ok { + return nil, fmt.Errorf("Unexpected class identifier %v of type %T", stepSubClass.Identifier, stepSubClass.Identifier) + } + properties := stepSubClass.Properties() + + var paramsList []*ast.Field + for _, property := range properties { + _type, err := propertyToValueType(stepSubClass, property) + if err != nil { + return nil, err + } + ident := iriToIdent(property.Identifier) + if ident.Name == "from" { + hasFrom = true + continue + } + paramsList = append(paramsList, &ast.Field{ + Names: []*ast.Ident{ident}, + Type: _type, + }) + } + elts := []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.BasicLit{ + Kind: token.STRING, + Value: "\"@type\"", + }, + Value: &ast.BasicLit{ + Kind: token.STRING, + Value: "\"" + string(iri) + "\"", + }, + }, + } + if hasFrom { + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.BasicLit{ + Kind: token.STRING, + Value: "\"from\"", + }, + Value: pathIdent, + }) + } + + for _, property := range properties { + ident := iriToIdent(property.Identifier) + if ident.Name == "from" { + continue + } + var value ast.Expr + value = iriToIdent(property.Identifier) + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.BasicLit{ + Kind: token.STRING, + Value: "\"" + string(property.Identifier) + "\"", + }, + Value: value, + }) + } + + var recv *ast.FieldList + + if hasFrom { + recv = &ast.FieldList{ + List: []*ast.Field{ + &ast.Field{ + Names: []*ast.Ident{pathIdent}, + Type: pathTypeIdent, + }, + }, + } + } + + comment, err := stepSubClass.Comment() + + var doc *ast.CommentGroup + + if err == nil { + doc = &ast.CommentGroup{ + List: []*ast.Comment{ + { + Text: "// " + iriToStringIdent(iri) + " " + comment, + }, + }, + } + } + + decls = append(decls, &ast.FuncDecl{ + Name: iriToIdent(iri), + Doc: doc, + Type: &ast.FuncType{ + Params: &ast.FieldList{List: paramsList}, + Results: &ast.FieldList{ + List: []*ast.Field{ + &ast.Field{ + Names: nil, + Type: pathTypeIdent, + }, + }, + }, + }, + Recv: recv, + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.ReturnStmt{ + Results: []ast.Expr{ + &ast.CompositeLit{ + Type: pathTypeIdent, + Elts: elts, + }, + }, + }, + }, + }, + }) + return decls, nil +} + +var quadValueType = &ast.SelectorExpr{ + Sel: ast.NewIdent("Value"), + X: ast.NewIdent("quad"), +} + +func propertyToValueType(class *owl.Class, property *owl.Property) (ast.Expr, error) { + _range, err := property.Range() + if err != nil { + return nil, err + } + isSlice := true + isPTR := false + cardinality, err := class.CardinalityOf(property) + if cardinality == int64(1) { + isSlice = false + isPTR = false + } + maxCardinality, err := class.MaxCardinalityOf(property) + if maxCardinality == int64(1) { + isSlice = false + isPTR = true + } + var t ast.Expr + if _range == xsdString { + t = stringIdent + } else if _range == pathStepIRI { + t = pathTypeIdent + } else if _range == rdfsResource { + t = quadValueType + } else { + return nil, fmt.Errorf("Unexpected range %v", _range) + } + if isPTR { + t = &ast.StarExpr{ + X: t, + } + } + if isSlice { + t = &ast.ArrayType{ + Elt: t, + } + } + return t, nil +} + +func getFile(fset *token.FileSet) (*ast.File, error) { + src := ` +package client + +import ( + "github.com/cayleygraph/quad" +) + +type Path map[string]interface{} + ` + file, err := parser.ParseFile(fset, "", src, 0) + + if err != nil { + return nil, err + } + + return file, nil +} + +// writeFile writes given file of given fset to given path +func writeFile(fset *token.FileSet, file *ast.File, path string) error { + f, err := os.Create(path) + + if err != nil { + return err + } + + w := bufio.NewWriter(f) + + err = format.Node(w, fset, file) + + if err != nil { + return err + } + + w.Flush() + f.Close() + + return nil +} + +func iriToStringIdent(iri quad.IRI) string { + return string(iri)[26:] +} + +func iriToIdent(iri quad.IRI) *ast.Ident { + return ast.NewIdent(iriToStringIdent(iri)) +} diff --git a/go.mod b/go.mod index b3a5a890f..afe62631d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.12 require ( github.com/badgerodon/peg v0.0.0-20130729175151-9e5f7f4d07ca - github.com/cayleygraph/quad v1.1.0 + github.com/cayleygraph/quad v1.2.0 github.com/cockroachdb/apd v1.1.0 // indirect github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect github.com/coreos/bbolt v1.3.3 // indirect diff --git a/go.sum b/go.sum index a9f06dc1b..75426f589 100644 --- a/go.sum +++ b/go.sum @@ -27,6 +27,8 @@ github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/cayleygraph/quad v1.1.0 h1:w1nXAmn+nz07+qlw89dke9LwWkYpeX+OcvfTvGQRBpM= github.com/cayleygraph/quad v1.1.0/go.mod h1:maWODEekEhrO0mdc9h5n/oP7cH1h/OTgqQ2qWbuI9M4= +github.com/cayleygraph/quad v1.2.0 h1:vqf+71ZINP3eSbtaEzpey0HTr9p4M2xHdmVCda8D7+Q= +github.com/cayleygraph/quad v1.2.0/go.mod h1:maWODEekEhrO0mdc9h5n/oP7cH1h/OTgqQ2qWbuI9M4= github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= diff --git a/gogen.go b/gogen.go index c48953715..76bb7bb8b 100644 --- a/gogen.go +++ b/gogen.go @@ -1,3 +1,3 @@ package cayley -//go:generate go run ./cmd/docgen/docgen.go -i ./docs/GizmoAPI.md.in -o ./docs/GizmoAPI.md +//go:generate go run ./cmd/generate_linkedql_client/generate_linkedql_client.go diff --git a/internal/linkedql/schema/schema.go b/internal/linkedql/schema/schema.go index b7868ebe1..c12e9c0a6 100644 --- a/internal/linkedql/schema/schema.go +++ b/internal/linkedql/schema/schema.go @@ -250,6 +250,23 @@ func (g *generator) Generate() []byte { Range: rng, }) } + graph := []interface{}{ + map[string]string{ + "@id": "linkedql:Step", + "@type": "owl:Class", + }, + map[string]interface{}{ + "@id": "linkedql:PathStep", + "@type": "owl:Class", + "rdfs:subClassOf": map[string]string{"@id": "linkedql:Step"}, + }, + map[string]interface{}{ + "@id": "linkedql:IteratorStep", + "@type": "owl:Class", + "rdfs:subClassOf": map[string]string{"@id": "linkedql:Step"}, + }, + } + graph = append(graph, g.out...) data, err := json.Marshal(map[string]interface{}{ "@context": map[string]interface{}{ "rdf": map[string]string{"@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#"}, @@ -258,7 +275,7 @@ func (g *generator) Generate() []byte { "xsd": map[string]string{"@id": "http://www.w3.org/2001/XMLSchema#"}, "linkedql": map[string]string{"@id": "http://cayley.io/linkedql#"}, }, - "@graph": g.out, + "@graph": graph, }) if err != nil { panic(err) diff --git a/linkedql.json b/linkedql.json new file mode 100644 index 000000000..74a4cd838 --- /dev/null +++ b/linkedql.json @@ -0,0 +1,1171 @@ +{ + "@context": { + "linkedql": { + "@id": "http://cayley.io/linkedql#" + }, + "owl": { + "@id": "http://www.w3.org/2002/07/owl#" + }, + "rdf": { + "@id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + }, + "rdfs": { + "@id": "http://www.w3.org/2000/01/rdf-schema#" + }, + "xsd": { + "@id": "http://www.w3.org/2001/XMLSchema#" + } + }, + "@graph": [ + { + "@id": "linkedql:Step", + "@type": "owl:Class" + }, + { + "@id": "linkedql:PathStep", + "@type": "owl:Class", + "rdfs:subClassOf": { + "@id": "linkedql:Step" + } + }, + { + "@id": "linkedql:IteratorStep", + "@type": "owl:Class", + "rdfs:subClassOf": { + "@id": "linkedql:Step" + } + }, + { + "@id": "linkedql:Is", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to all the values resolved by the from step which are included in provided values.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3613043289672097141", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:FollowReverse", + "@type": "rdfs:Class", + "rdfs:comment": "is the same as follow but follows the chain in the reverse direction. Flips View and ViewReverse where appropriate, the net result being a virtual predicate followed in the reverse direction. Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n9083091194127967913", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8063055307955585418", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:followed" + } + } + ] + }, + { + "@id": "linkedql:Labels", + "@type": "rdfs:Class", + "rdfs:comment": "gets the list of inbound and outbound quad labels", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8459855700265692209", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Unique", + "@type": "rdfs:Class", + "rdfs:comment": "removes duplicate values from the path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n2710973548941612352", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Vertex", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to all the existing objects and primitive values in the graph. If provided with values resolves to a sublist of all the existing values in the graph.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + } + ] + }, + { + "@id": "linkedql:Placeholder", + "@type": "rdfs:Class", + "rdfs:comment": "is like Vertex but resolves to the values in the context it is placed in. It should only be used where a PathStep is expected and can't be resolved on its own.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + } + ] + }, + { + "@id": "linkedql:Count", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to the number of the resolved values of the from step", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3236938820904012665", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Skip", + "@type": "rdfs:Class", + "rdfs:comment": "skips a number of nodes for current path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8331721404640161545", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5928433611785803765", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:offset" + } + } + ] + }, + { + "@id": "linkedql:Back", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to the values of the previous the step or the values assigned to name in a former step.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n7318884210772532587", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5159571487667235037", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:name" + } + } + ] + }, + { + "@id": "linkedql:Properties", + "@type": "rdfs:Class", + "rdfs:comment": "adds tags for all properties of the current entity", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n9018236344034770769", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:PropertyNames", + "@type": "rdfs:Class", + "rdfs:comment": "gets the list of predicates that are pointing out from a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n4445626993086337411", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:ReversePropertyNamesAs", + "@type": "rdfs:Class", + "rdfs:comment": "tags the list of predicates that are pointing in to a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8112324779916985026", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n5338493051676230997", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:tag" + } + } + ] + }, + { + "@id": "linkedql:PropertyNamesAs", + "@type": "rdfs:Class", + "rdfs:comment": "tags the list of predicates that are pointing out from a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n5768812609992315503", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8287472848602919900", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:tag" + } + } + ] + }, + { + "@id": "linkedql:ReverseProperties", + "@type": "rdfs:Class", + "rdfs:comment": "gets all the properties the current entity / value is referenced at", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n443301574217807823", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Select", + "@type": "rdfs:Class", + "rdfs:comment": "Select returns flat records of tags matched in the query", + "rdfs:subClassOf": [ + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8116377088366467444", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Value", + "@type": "rdfs:Class", + "rdfs:comment": "Value returns a single value matched in the query", + "rdfs:subClassOf": [ + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n1211474119861900589", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:HasReverse", + "@type": "rdfs:Class", + "rdfs:comment": "is the same as Has, but sets constraint in reverse direction.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n793190407192836422", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n7208921910592501003", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:property" + } + } + ] + }, + { + "@id": "linkedql:In", + "@type": "rdfs:Class", + "rdfs:comment": "aliases for ViewReverse", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8239793418546475073", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n4982870279148793564", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Order", + "@type": "rdfs:Class", + "rdfs:comment": "sorts the results in ascending order according to the current entity / value", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n4658869256845579334", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:ReversePropertyNames", + "@type": "rdfs:Class", + "rdfs:comment": "gets the list of predicates that are pointing in to a node.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n2234785206717184905", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:ViewBoth", + "@type": "rdfs:Class", + "rdfs:comment": "is like View but resolves to both the object values and references to the values of the given properties in via. It is the equivalent for the Union of View and ViewReverse of the same property.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n1375394481257745149", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8456254640763031822", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Difference", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to all the values resolved by the from step different then the values resolved by the provided steps. Caution: it might be slow to execute.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3325168672420575460", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:SelectFirst", + "@type": "rdfs:Class", + "rdfs:comment": "Like Select but only returns the first result", + "rdfs:subClassOf": [ + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3390240884242909143", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:As", + "@type": "rdfs:Class", + "rdfs:comment": "assigns the resolved values of the from step to a given name. The name can be used with the Select and Documents steps to retrieve the values or to return to the values in further steps with the Back step. It resolves to the values of the from step.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n2526633157694802657", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n3429357401325124988", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:name" + } + } + ] + }, + { + "@id": "linkedql:Filter", + "@type": "rdfs:Class", + "rdfs:comment": "applies constraints to a set of nodes. Can be used to filter values by range or match strings.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n4690243504327224094", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n739931276592480213", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:filter" + } + } + ] + }, + { + "@id": "linkedql:Has", + "@type": "rdfs:Class", + "rdfs:comment": "filters all paths which are, at this point, on the subject for the given predicate and object, but do not follow the path, merely filter the possible paths. Usually useful for starting with all nodes, or limiting to a subset depending on some predicate/value pair.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n4325294036727421714", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n632449057788695932", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:property" + } + } + ] + }, + { + "@id": "linkedql:ViewReverse", + "@type": "rdfs:Class", + "rdfs:comment": "is the inverse of View. Starting with the nodes in `path` on the object, follow the quads with predicates defined by `predicatePath` to their subjects.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n1202338489403740068", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8462290765396416742", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Limit", + "@type": "rdfs:Class", + "rdfs:comment": "limits a number of nodes for current path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n8154336094443820555", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n1095931710072978339", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:limit" + } + } + ] + }, + { + "@id": "linkedql:Union", + "@type": "rdfs:Class", + "rdfs:comment": "returns the combined paths of the two queries. Notice that it's per-path, not per-node. Once again, if multiple paths reach the same destination, they might have had different ways of getting there (and different tags).", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n7940960461887459839", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:View", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to the values of the given property or properties in via of the current objects. If via is a path it's resolved values will be used as properties.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n6177289074236883404", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n4127567549237631394", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Out", + "@type": "rdfs:Class", + "rdfs:comment": "aliases for View", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n301236345131105742", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8700954704201023894", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:properties" + } + } + ] + }, + { + "@id": "linkedql:Intersect", + "@type": "rdfs:Class", + "rdfs:comment": "resolves to all the same values resolved by the from step and the provided steps.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n6461582695449891350", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + } + ] + }, + { + "@id": "linkedql:Follow", + "@type": "rdfs:Class", + "rdfs:comment": "is the way to use a path prepared with Morphism. Applies the path chain on the morphism object to the current path. Starts as if at the g.M() and follows through the morphism path.", + "rdfs:subClassOf": [ + { + "@id": "linkedql:PathStep" + }, + { + "@id": "linkedql:IteratorStep" + }, + { + "@id": "_:n3939936992220835105", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:from" + } + }, + { + "@id": "_:n8565384274678116400", + "@type": "owl:Restriction", + "owl:cardinality": 1, + "owl:onProperty": { + "@id": "linkedql:followed" + } + } + ] + }, + { + "@id": "linkedql:values", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n3487211217454711567", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Is" + }, + { + "@id": "linkedql:Vertex" + }, + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:Has" + } + ] + } + }, + "rdfs:range": { + "@id": "rdfs:Resource" + } + }, + { + "@id": "linkedql:offset", + "@type": "owl:DatatypeProperty", + "rdfs:domain": "linkedql:Skip", + "rdfs:range": { + "@id": "xsd:int" + } + }, + { + "@id": "linkedql:tag", + "@type": "owl:DatatypeProperty", + "rdfs:domain": { + "@id": "_:n6718853691878301872", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:ReversePropertyNamesAs" + }, + { + "@id": "linkedql:PropertyNamesAs" + } + ] + } + }, + "rdfs:range": { + "@id": "xsd:string" + } + }, + { + "@id": "linkedql:tags", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n2724656406774357364", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Select" + }, + { + "@id": "linkedql:SelectFirst" + } + ] + } + }, + "rdfs:range": { + "@id": "xsd:string" + } + }, + { + "@id": "linkedql:steps", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n7733331238921588911", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Difference" + }, + { + "@id": "linkedql:Union" + }, + { + "@id": "linkedql:Intersect" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + }, + { + "@id": "linkedql:filter", + "@type": "owl:ObjectProperty", + "rdfs:domain": "linkedql:Filter", + "rdfs:range": { + "@id": "linkedql:Operator" + } + }, + { + "@id": "linkedql:limit", + "@type": "owl:DatatypeProperty", + "rdfs:domain": "linkedql:Limit", + "rdfs:range": { + "@id": "xsd:int" + } + }, + { + "@id": "linkedql:from", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n6836387890704961384", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:ReversePropertyNamesAs" + }, + { + "@id": "linkedql:Value" + }, + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:Has" + }, + { + "@id": "linkedql:View" + }, + { + "@id": "linkedql:Unique" + }, + { + "@id": "linkedql:Count" + }, + { + "@id": "linkedql:Properties" + }, + { + "@id": "linkedql:ReverseProperties" + }, + { + "@id": "linkedql:ReversePropertyNames" + }, + { + "@id": "linkedql:Filter" + }, + { + "@id": "linkedql:ViewReverse" + }, + { + "@id": "linkedql:In" + }, + { + "@id": "linkedql:Difference" + }, + { + "@id": "linkedql:Out" + }, + { + "@id": "linkedql:FollowReverse" + }, + { + "@id": "linkedql:Labels" + }, + { + "@id": "linkedql:PropertyNames" + }, + { + "@id": "linkedql:Order" + }, + { + "@id": "linkedql:As" + }, + { + "@id": "linkedql:Union" + }, + { + "@id": "linkedql:Back" + }, + { + "@id": "linkedql:PropertyNamesAs" + }, + { + "@id": "linkedql:Intersect" + }, + { + "@id": "linkedql:Is" + }, + { + "@id": "linkedql:Skip" + }, + { + "@id": "linkedql:Select" + }, + { + "@id": "linkedql:ViewBoth" + }, + { + "@id": "linkedql:SelectFirst" + }, + { + "@id": "linkedql:Limit" + }, + { + "@id": "linkedql:Follow" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + }, + { + "@id": "linkedql:followed", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n1351742506042781062", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:FollowReverse" + }, + { + "@id": "linkedql:Follow" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + }, + { + "@id": "linkedql:name", + "@type": "owl:DatatypeProperty", + "rdfs:domain": { + "@id": "_:n7858581684545641474", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Back" + }, + { + "@id": "linkedql:As" + } + ] + } + }, + "rdfs:range": { + "@id": "xsd:string" + } + }, + { + "@id": "linkedql:names", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n9117289071709940667", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:Properties" + }, + { + "@id": "linkedql:ReverseProperties" + } + ] + } + }, + "rdfs:range": { + "@id": "xsd:string" + } + }, + { + "@id": "linkedql:property", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n722017989461747795", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:HasReverse" + }, + { + "@id": "linkedql:Has" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + }, + { + "@id": "linkedql:properties", + "@type": "owl:ObjectProperty", + "rdfs:domain": { + "@id": "_:n5924667912709065573", + "@type": "owl:Class", + "owl:unionOf": { + "@list": [ + { + "@id": "linkedql:In" + }, + { + "@id": "linkedql:ViewBoth" + }, + { + "@id": "linkedql:ViewReverse" + }, + { + "@id": "linkedql:View" + }, + { + "@id": "linkedql:Out" + } + ] + } + }, + "rdfs:range": { + "@id": "linkedql:PathStep" + } + } + ] +} diff --git a/owl/owl.go b/owl/owl.go new file mode 100644 index 000000000..6af8ab28d --- /dev/null +++ b/owl/owl.go @@ -0,0 +1,235 @@ +package owl + +import ( + "context" + "fmt" + + "github.com/cayleygraph/cayley/clog" + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/graph/iterator" + "github.com/cayleygraph/cayley/query/path" + "github.com/cayleygraph/quad" + "github.com/cayleygraph/quad/voc/owl" + "github.com/cayleygraph/quad/voc/rdf" + "github.com/cayleygraph/quad/voc/rdfs" +) + +type Class struct { + ctx context.Context + qs graph.QuadStore + ref graph.Ref + Identifier quad.Value +} + +func (c *Class) path() *path.Path { + return path.StartPath(c.qs, c.Identifier) +} + +// listContainingPath returns a path of lists containing given value +func listContainignPath(qs graph.QuadStore, value quad.Value) *path.Path { + firstPath := path.StartPath(qs, value).In(quad.IRI(rdf.First).Full()) + return firstPath.Or( + firstPath.FollowRecursive(path.StartMorphism().In(quad.IRI(rdf.Rest).Full()), 0, nil), + ) +} + +func classFromRef(ctx context.Context, qs graph.QuadStore, ref graph.Ref) *Class { + val := qs.NameOf(ref) + return &Class{ + ctx: ctx, + qs: qs, + ref: ref, + Identifier: val, + } +} + +var domain = quad.IRI(rdfs.Domain).Full() + +// Properties return all the properties a class instance may have +func (c *Class) Properties() []*Property { + // TODO(@iddan): check for super classes properties + p := c.path().Or(listContainignPath(c.qs, c.Identifier).In(quad.IRI(owl.UnionOf))). + In(domain) + it := p.BuildIterator(c.ctx).Iterate() + var properties []*Property + for it.Next(c.ctx) { + property, err := propertyFromRef(c.ctx, c.qs, it.Result()) + if err != nil { + clog.Warningf(err.Error()) + continue + } + properties = append(properties, property) + } + return properties +} + +func (c *Class) ParentClasses() []*Class { + it := parentClassesPath(c).BuildIterator(c.ctx).Iterate() + var classes []*Class + for it.Next(c.ctx) { + class := classFromRef(c.ctx, c.qs, it.Result()) + classes = append(classes, class) + } + return classes +} + +var rdfsComment = quad.IRI("rdfs:comment").Full() + +// Comment returns classs's comment +func (c *Class) Comment() (string, error) { + it := c.path().Out(rdfsComment).BuildIterator(c.ctx).Iterate() + for it.Next(c.ctx) { + ref := it.Result() + value := c.qs.NameOf(ref) + stringValue, ok := value.(quad.String) + if ok { + return string(stringValue), nil + } + typedStringValue, ok := value.(quad.TypedString) + if ok { + return string(typedStringValue.Value), nil + } + + } + return "", fmt.Errorf("No comment exist for %v", c.Identifier) +} + +func parentClassesPath(c *Class) *path.Path { + return c.path().Out(quad.IRI(rdfs.SubClassOf).Full()) +} + +func restrictionsPath(c *Class) *path.Path { + return parentClassesPath(c). + Has(quad.IRI(rdf.Type).Full(), quad.IRI(owl.Restriction)) +} + +func allPropertyRestrictionsPath(c *Class, property *Property) *path.Path { + return restrictionsPath(c). + Has(quad.IRI(owl.OnProperty), property.Identifier) +} + +func propertyRestrictionPath(c *Class, property *Property, restrictionProperty quad.IRI) *path.Path { + return allPropertyRestrictionsPath(c, property). + Out(restrictionProperty) +} + +func intFromScanner(ctx context.Context, it iterator.Scanner, qs graph.QuadStore) (int64, error) { + for it.Next(ctx) { + ref := it.Result() + value := qs.NameOf(ref) + intValue, ok := value.(quad.Int) + var native interface{} + if ok { + native = intValue.Native() + } + typedString, ok := value.(quad.TypedString) + if ok { + native = typedString.Native() + } + if native == nil { + return -1, fmt.Errorf("Unexpected value %v of type %T", value, value) + } + i, ok := native.(int64) + if !ok { + return -1, fmt.Errorf("Unexpected value %v of type %T", native, native) + } + return i, nil + } + return -1, fmt.Errorf("Iterator has not emitted any value") +} + +// CardinalityOf returns the defined exact cardinality for the property for the class +// If exact cardinality is not defined for the class returns an error +func (c *Class) CardinalityOf(property *Property) (int64, error) { + p := propertyRestrictionPath(c, property, quad.IRI(owl.Cardinality)) + it := p.BuildIterator(c.ctx).Iterate() + cardinality, err := intFromScanner(c.ctx, it, c.qs) + if err != nil { + return -1, fmt.Errorf("No cardinality is defined for property %v for class %v", property.Identifier, c.Identifier) + } + return cardinality, nil +} + +// MaxCardinalityOf returns the defined max cardinality for the property for the class +// If max cardinality is not defined for the class returns an error +func (c *Class) MaxCardinalityOf(property *Property) (int64, error) { + p := propertyRestrictionPath(c, property, quad.IRI(owl.MaxCardinality)) + it := p.BuildIterator(c.ctx).Iterate() + cardinality, err := intFromScanner(c.ctx, it, c.qs) + if err != nil { + return -1, fmt.Errorf("No maxCardinality is defined for property %v for class %v", property.Identifier, c.Identifier) + } + return cardinality, nil +} + +var subClassOf = quad.IRI(rdfs.SubClassOf).Full() + +// SubClasses returns all the classes defined as sub classes of the class +func (c *Class) SubClasses() []*Class { + p := c.path().FollowRecursive(path.StartMorphism().In(subClassOf), 0, nil) + it := p.BuildIterator(c.ctx).Iterate() + var subClasses []*Class + for it.Next(c.ctx) { + class := classFromRef(c.ctx, c.qs, it.Result()) + subClasses = append(subClasses, class) + } + return subClasses +} + +// GetClass returns for given identifier a class object representing a class defined in given store. +// If the identifier is not of a class in the store returns an error. +func GetClass(ctx context.Context, qs graph.QuadStore, identifier quad.IRI) (*Class, error) { + ref := qs.ValueOf(identifier) + if ref == nil { + return nil, fmt.Errorf("Identifier %v does not exist in the store", identifier) + } + // TODO(iddan): validate given identifier is an OWL class + return &Class{Identifier: identifier, ref: ref, qs: qs, ctx: ctx}, nil +} + +type Property struct { + ctx context.Context + qs graph.QuadStore + ref graph.Ref + Identifier quad.IRI +} + +func GetProperty(ctx context.Context, qs graph.QuadStore, identifier quad.IRI) (*Property, error) { + ref := qs.ValueOf(identifier) + if ref == nil { + return nil, fmt.Errorf("Identifier %v does not exist in the store", identifier) + } + // TODO(iddan): validate given identifier is an OWL property + return &Property{ + ctx: ctx, + qs: qs, + ref: ref, + Identifier: identifier, + }, nil +} + +func propertyFromRef(ctx context.Context, qs graph.QuadStore, ref graph.Ref) (*Property, error) { + val := qs.NameOf(ref) + iri, ok := val.(quad.IRI) + if !ok { + return nil, fmt.Errorf("Predicate of unexpected type %T. Predicates should be IRIs", val) + } + return &Property{ + ctx: ctx, + qs: qs, + ref: ref, + Identifier: iri, + }, nil +} + +// Range returns the expected target type of a property +func (p *Property) Range() (quad.Value, error) { + rangePath := path.StartPath(p.qs, p.Identifier).Out(quad.IRI(rdfs.Range).Full()) + it := rangePath.BuildIterator(p.ctx).Iterate() + for it.Next(p.ctx) { + ref := it.Result() + value := p.qs.NameOf(ref) + return value, nil + } + return nil, fmt.Errorf("No range was defined for property %v", p) +} diff --git a/owl/owl_test.go b/owl/owl_test.go new file mode 100644 index 000000000..c1d3481bc --- /dev/null +++ b/owl/owl_test.go @@ -0,0 +1,302 @@ +package owl + +import ( + "context" + "testing" + + "github.com/cayleygraph/cayley/graph" + "github.com/cayleygraph/cayley/graph/memstore" + "github.com/cayleygraph/cayley/query/path" + "github.com/cayleygraph/quad" + "github.com/cayleygraph/quad/voc/owl" + "github.com/cayleygraph/quad/voc/rdf" + "github.com/cayleygraph/quad/voc/rdfs" + "github.com/stretchr/testify/require" +) + +var ( + fooID = quad.IRI("ex:Foo").Full() + barID = quad.IRI("ex:Bar").Full() + garID = quad.IRI("ex:Gar").Full() + bazID = quad.IRI("ex:baz").Full() + fooBarGarUnion = quad.RandomBlankNode() + fooBazCardinalityRestriction = quad.RandomBlankNode() + barBazMaxCardinalityRestriction = quad.RandomBlankNode() + exampleGraph = quad.IRI("ex:graph") +) +var fooClassQuads = []quad.Quad{ + { + Subject: fooID, + Predicate: quad.IRI(rdf.Type).Full(), + Object: quad.IRI(rdfs.Class).Full(), + Label: exampleGraph, + }, +} +var bazPropertyQuads = []quad.Quad{ + { + Subject: barID, + Predicate: quad.IRI(rdfs.SubClassOf).Full(), + Object: fooID, + Label: exampleGraph, + }, + + { + Subject: bazID, + Predicate: quad.IRI(rdfs.Domain).Full(), + Object: fooID, + Label: exampleGraph, + }, + { + Subject: bazID, + Predicate: quad.IRI(rdfs.Range).Full(), + Object: barID, + Label: exampleGraph, + }, +} +var fooBazCardinalityRestrictionQuads = []quad.Quad{ + { + Subject: fooBazCardinalityRestriction, + Predicate: quad.IRI(rdf.Type).Full(), + Object: quad.IRI(owl.Restriction), + Label: exampleGraph, + }, + { + Subject: fooBazCardinalityRestriction, + Predicate: quad.IRI(owl.OnProperty), + Object: bazID, + Label: exampleGraph, + }, + { + Subject: fooBazCardinalityRestriction, + Predicate: quad.IRI(owl.Cardinality), + Object: quad.Int(1), + Label: exampleGraph, + }, + { + Subject: fooID, + Predicate: quad.IRI(rdfs.SubClassOf).Full(), + Object: fooBazCardinalityRestriction, + Label: exampleGraph, + }, +} +var barBazCardinalityRestrictionQuad = []quad.Quad{ + { + Subject: barBazMaxCardinalityRestriction, + Predicate: quad.IRI(rdf.Type).Full(), + Object: quad.IRI(owl.Restriction), + Label: exampleGraph, + }, + { + Subject: barBazMaxCardinalityRestriction, + Predicate: quad.IRI(owl.OnProperty), + Object: bazID, + Label: exampleGraph, + }, + { + Subject: barBazMaxCardinalityRestriction, + Predicate: quad.IRI(owl.MaxCardinality), + Object: quad.Int(1), + Label: exampleGraph, + }, + { + Subject: barID, + Predicate: quad.IRI(rdfs.SubClassOf).Full(), + Object: barBazMaxCardinalityRestriction, + Label: exampleGraph, + }, +} + +func listQuads(items []quad.Value, label quad.Value) (quad.Value, []quad.Quad) { + var quads []quad.Quad + list := quad.RandomBlankNode() + cursor := list + for i, item := range items { + first := quad.Quad{ + Subject: cursor, + Predicate: quad.IRI(rdf.First).Full(), + Object: item, + Label: label, + } + var rest quad.Quad + if i < len(items)-1 { + rest = quad.Quad{ + Subject: cursor, + Predicate: quad.IRI(rdf.Rest).Full(), + Object: quad.IRI(rdf.Nil).Full(), + Label: label, + } + } else { + nextCursor := quad.RandomBlankNode() + rest = quad.Quad{ + Subject: cursor, + Predicate: quad.IRI(rdf.Rest).Full(), + Object: nextCursor, + Label: label, + } + cursor = nextCursor + } + quads = append(quads, first, rest) + } + return list, quads +} + +func getUnionQuads() []quad.Quad { + var unionQuads []quad.Quad + membersList, membersQuads := listQuads( + []quad.Value{fooID, barID, garID}, + exampleGraph, + ) + unionQuads = append(unionQuads, membersQuads...) + unionQuads = append(unionQuads, quad.Quad{ + Subject: fooBarGarUnion, + Predicate: quad.IRI(owl.UnionOf), + Object: membersList, + Label: exampleGraph, + }) + return unionQuads +} + +func getTestSet() []quad.Quad { + var testSet []quad.Quad + testSet = append(testSet, fooBazCardinalityRestrictionQuads...) + testSet = append(testSet, barBazCardinalityRestrictionQuad...) + testSet = append(testSet, bazPropertyQuads...) + testSet = append(testSet, getUnionQuads()...) + return testSet +} + +func TestListContainingPath(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + p := listContainignPath(qs, fooID).In(quad.IRI(owl.UnionOf)) + values := collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBarGarUnion, + }, values) + p = listContainignPath(qs, barID).In(quad.IRI(owl.UnionOf)) + values = collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBarGarUnion, + }, values) + p = listContainignPath(qs, garID).In(quad.IRI(owl.UnionOf)) + values = collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBarGarUnion, + }, values) + p = listContainignPath(qs, bazID).In(quad.IRI(owl.UnionOf)) + values = collectPath(ctx, qs, p) + require.Equal(t, []quad.Value(nil), values) +} + +func TestGetClass(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + class, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + require.Equal(t, class.Identifier, fooID) +} + +func TestSubClasses(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + barClass, err := GetClass(ctx, qs, barID) + require.NoError(t, err) + subClasses := fooClass.SubClasses() + require.Len(t, subClasses, 1) + require.Contains(t, subClasses, barClass) +} + +func TestProperties(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + properties := fooClass.Properties() + require.Len(t, properties, 1) + require.Contains(t, properties, bazProperty) +} + +func TestParentClasses(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + properties := fooClass.Properties() + require.Len(t, properties, 1) + require.Contains(t, properties, bazProperty) +} + +func TestCardinalityOf(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + cardinality, err := fooClass.CardinalityOf(bazProperty) + require.NoError(t, err) + require.Equal(t, cardinality, int64(1)) +} + +func TestMaxCardinalityOf(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, barID) + require.NoError(t, err) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + cardinality, err := fooClass.MaxCardinalityOf(bazProperty) + require.NoError(t, err) + require.Equal(t, cardinality, int64(1)) +} + +func TestRange(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + bazProperty, err := GetProperty(ctx, qs, bazID) + require.NoError(t, err) + _range, err := bazProperty.Range() + require.NoError(t, err) + require.Equal(t, _range, barID) +} + +func collectPath(ctx context.Context, qs graph.QuadStore, p *path.Path) []quad.Value { + var values []quad.Value + it := p.BuildIterator(ctx).Iterate() + for it.Next(ctx) { + ref := it.Result() + value := qs.NameOf(ref) + values = append(values, value) + } + return values +} + +func TestParentClassesPath(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + p := parentClassesPath(fooClass) + values := collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBazCardinalityRestriction, + }, values) +} + +func TestRestrictionsPath(t *testing.T) { + ctx := context.TODO() + qs := memstore.New(getTestSet()...) + fooClass, err := GetClass(ctx, qs, fooID) + require.NoError(t, err) + p := restrictionsPath(fooClass) + values := collectPath(ctx, qs, p) + require.Equal(t, []quad.Value{ + fooBazCardinalityRestriction, + }, values) +} diff --git a/query/linkedql/client/client.go b/query/linkedql/client/client.go new file mode 100644 index 000000000..c491eef72 --- /dev/null +++ b/query/linkedql/client/client.go @@ -0,0 +1,172 @@ +package client + +import ( + "github.com/cayleygraph/quad" +) + +type Path map[string]interface{} + +// As assigns the resolved values of the from step to a given name. The name can be used with the Select and Documents steps to retrieve the values or to return to the values in further steps with the Back step. It resolves to the values of the from step. +func (p Path) As(name string) Path { + return Path{"@type": "http://cayley.io/linkedql#As", "from": p, "http://cayley.io/linkedql#name": name} +} + +// Back resolves to the values of the previous the step or the values assigned to name in a former step. +func (p Path) Back(name string) Path { + return Path{"@type": "http://cayley.io/linkedql#Back", "from": p, "http://cayley.io/linkedql#name": name} +} + +// Count resolves to the number of the resolved values of the from step +func (p Path) Count() Path { + return Path{"@type": "http://cayley.io/linkedql#Count", "from": p} +} + +// Difference resolves to all the values resolved by the from step different then the values resolved by the provided steps. Caution: it might be slow to execute. +func (p Path) Difference(steps []Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Difference", "from": p, "http://cayley.io/linkedql#steps": steps} +} + +// Filter applies constraints to a set of nodes. Can be used to filter values by range or match strings. +func (p Path) Filter() Path { + return Path{"@type": "http://cayley.io/linkedql#Filter", "from": p} +} + +// Follow is the way to use a path prepared with Morphism. Applies the path chain on the morphism object to the current path. Starts as if at the g.M() and follows through the morphism path. +func (p Path) Follow(followed Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Follow", "from": p, "http://cayley.io/linkedql#followed": followed} +} + +// FollowReverse is the same as follow but follows the chain in the reverse direction. Flips View and ViewReverse where appropriate, the net result being a virtual predicate followed in the reverse direction. Starts at the end of the morphism and follows it backwards (with appropriate flipped directions) to the g.M() location. +func (p Path) FollowReverse(followed Path) Path { + return Path{"@type": "http://cayley.io/linkedql#FollowReverse", "from": p, "http://cayley.io/linkedql#followed": followed} +} + +// Has filters all paths which are, at this point, on the subject for the given predicate and object, but do not follow the path, merely filter the possible paths. Usually useful for starting with all nodes, or limiting to a subset depending on some predicate/value pair. +func (p Path) Has(property Path, values []quad.Value) Path { + return Path{"@type": "http://cayley.io/linkedql#Has", "from": p, "http://cayley.io/linkedql#property": property, "http://cayley.io/linkedql#values": values} +} + +// HasReverse is the same as Has, but sets constraint in reverse direction. +func (p Path) HasReverse(property Path, values []quad.Value) Path { + return Path{"@type": "http://cayley.io/linkedql#HasReverse", "from": p, "http://cayley.io/linkedql#property": property, "http://cayley.io/linkedql#values": values} +} + +// In aliases for ViewReverse +func (p Path) In(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#In", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// Intersect resolves to all the same values resolved by the from step and the provided steps. +func (p Path) Intersect(steps []Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Intersect", "from": p, "http://cayley.io/linkedql#steps": steps} +} + +// Is resolves to all the values resolved by the from step which are included in provided values. +func (p Path) Is(values []quad.Value) Path { + return Path{"@type": "http://cayley.io/linkedql#Is", "from": p, "http://cayley.io/linkedql#values": values} +} + +// Labels gets the list of inbound and outbound quad labels +func (p Path) Labels() Path { + return Path{"@type": "http://cayley.io/linkedql#Labels", "from": p} +} + +// Limit limits a number of nodes for current path. +func (p Path) Limit() Path { + return Path{"@type": "http://cayley.io/linkedql#Limit", "from": p} +} + +// Order sorts the results in ascending order according to the current entity / value +func (p Path) Order() Path { + return Path{"@type": "http://cayley.io/linkedql#Order", "from": p} +} + +// Out aliases for View +func (p Path) Out(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Out", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// Properties adds tags for all properties of the current entity +func (p Path) Properties(names []string) Path { + return Path{"@type": "http://cayley.io/linkedql#Properties", "from": p, "http://cayley.io/linkedql#names": names} +} + +// PropertyNames gets the list of predicates that are pointing out from a node. +func (p Path) PropertyNames() Path { + return Path{"@type": "http://cayley.io/linkedql#PropertyNames", "from": p} +} + +// PropertyNamesAs tags the list of predicates that are pointing out from a node. +func (p Path) PropertyNamesAs(tag string) Path { + return Path{"@type": "http://cayley.io/linkedql#PropertyNamesAs", "from": p, "http://cayley.io/linkedql#tag": tag} +} + +// ReverseProperties gets all the properties the current entity / value is referenced at +func (p Path) ReverseProperties(names []string) Path { + return Path{"@type": "http://cayley.io/linkedql#ReverseProperties", "from": p, "http://cayley.io/linkedql#names": names} +} + +// ReversePropertyNames gets the list of predicates that are pointing in to a node. +func (p Path) ReversePropertyNames() Path { + return Path{"@type": "http://cayley.io/linkedql#ReversePropertyNames", "from": p} +} + +// ReversePropertyNamesAs tags the list of predicates that are pointing in to a node. +func (p Path) ReversePropertyNamesAs(tag string) Path { + return Path{"@type": "http://cayley.io/linkedql#ReversePropertyNamesAs", "from": p, "http://cayley.io/linkedql#tag": tag} +} + +// Select Select returns flat records of tags matched in the query +func (p Path) Select(tags []string) Path { + return Path{"@type": "http://cayley.io/linkedql#Select", "from": p, "http://cayley.io/linkedql#tags": tags} +} + +// SelectFirst Like Select but only returns the first result +func (p Path) SelectFirst(tags []string) Path { + return Path{"@type": "http://cayley.io/linkedql#SelectFirst", "from": p, "http://cayley.io/linkedql#tags": tags} +} + +// Skip skips a number of nodes for current path. +func (p Path) Skip() Path { + return Path{"@type": "http://cayley.io/linkedql#Skip", "from": p} +} + +// Union returns the combined paths of the two queries. Notice that it's per-path, not per-node. Once again, if multiple paths reach the same destination, they might have had different ways of getting there (and different tags). +func (p Path) Union(steps []Path) Path { + return Path{"@type": "http://cayley.io/linkedql#Union", "from": p, "http://cayley.io/linkedql#steps": steps} +} + +// Unique removes duplicate values from the path. +func (p Path) Unique() Path { + return Path{"@type": "http://cayley.io/linkedql#Unique", "from": p} +} + +// Value Value returns a single value matched in the query +func (p Path) Value() Path { + return Path{"@type": "http://cayley.io/linkedql#Value", "from": p} +} + +// Vertex resolves to all the existing objects and primitive values in the graph. If provided with values resolves to a sublist of all the existing values in the graph. +func Vertex(values []quad.Value) Path { + return Path{"@type": "http://cayley.io/linkedql#Vertex", "http://cayley.io/linkedql#values": values} +} + +// View resolves to the values of the given property or properties in via of the current objects. If via is a path it's resolved values will be used as properties. +func (p Path) View(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#View", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// ViewBoth is like View but resolves to both the object values and references to the values of the given properties in via. It is the equivalent for the Union of View and ViewReverse of the same property. +func (p Path) ViewBoth(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#ViewBoth", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// ViewReverse is the inverse of View. Starting with the nodes in `path` on the object, follow the quads with predicates defined by `predicatePath` to their subjects. +func (p Path) ViewReverse(properties Path) Path { + return Path{"@type": "http://cayley.io/linkedql#ViewReverse", "from": p, "http://cayley.io/linkedql#properties": properties} +} + +// Placeholder is like Vertex but resolves to the values in the context it is placed in. It should only be used where a PathStep is expected and can't be resolved on its own. +func Placeholder() Path { + return Path{"@type": "http://cayley.io/linkedql#Placeholder"} +}