;;; rdf-model.el --- RDF term/quad/dataset data model  -*- lexical-binding: t; -*-

;; Author: Eric Prud'hommeaux <eric@w3.org>
;; Assisted-by: Claude:claude-sonnet-4-6
;; Package-Requires: ((emacs "29.1"))
;; Keywords: languages
;; URL: https://github.com/ericprud/shexc-mode-for-emacs
;; SPDX-License-Identifier: MIT

;;; Commentary:

;; An Emacs Lisp equivalent of the RDF/JS Data Model spec
;; (https://rdf.js.org/data-model-spec/): the term types (NamedNode,
;; BlankNode, Literal, Variable, DefaultGraph), `Quad', and a `Dataset'
;; protocol -- but no concrete `Dataset' implementation (see
;; `rdf-store.el' for that), mirroring RDF/JS's own split between the
;; spec (`@rdfjs/types') and concrete implementations
;; (`@rdfjs/dataset', N3.js's `Store', ...).
;;
;; Deliberate departures from RDF/JS, where Lisp idiom serves better
;; than a literal transliteration:
;; - No `.equals()' method on terms/quads.  `cl-defstruct' records
;;   already compare structurally with plain `equal' (verified: two
;;   structs of the same type with `equal' slots are themselves
;;   `equal'), which is simpler than RDF/JS's JS-driven `.equals()'
;;   convention.
;; - No `termType' string discriminator stored on each term -- the
;;   struct type itself *is* the discriminator (`rdf-model-named-node-p'
;;   etc., auto-generated by `cl-defstruct').  `rdf-model-term-type'
;;   below provides the same information as a symbol, for callers that
;;   want to dispatch/print generically.
;;
;; This file is currently bundled inside the `shexc-ts-mode' MELPA
;; package rather than shipped as its own package -- see the README's
;; "Packaging" section.  Named after the package it's meant to become.

;;; Code:

(require 'cl-lib)

;;; Terms

(cl-defstruct (rdf-model-named-node (:constructor rdf-model-named-node-create))
  "An IRI.  VALUE is the IRI as a string, e.g. \"http://example.org/Alice\"."
  value)

(cl-defstruct (rdf-model-blank-node (:constructor rdf-model-blank-node-create))
  "A blank node.  VALUE is its label *without* the leading \"_:\",
e.g. \"b0\" for `_:b0'."
  value)

(cl-defstruct (rdf-model-literal (:constructor rdf-model-literal-create))
  "A literal.  VALUE is its lexical form (a string).  LANGUAGE is a
BCP47 language tag string, or nil.  DATATYPE is an `rdf-model-named-node',
or nil only transiently before a caller has assigned the implicit
datatype (RDF 1.1 gives every well-formed literal a datatype: plain
literals are `xsd:string', language-tagged literals are
`rdf:langString' -- this struct does not enforce that, callers
constructing literals are expected to set DATATYPE accordingly)."
  value language datatype)

(cl-defstruct (rdf-model-variable (:constructor rdf-model-variable-create))
  "A SPARQL-style variable.  VALUE is its name, without the leading `?'."
  value)

(cl-defstruct (rdf-model--default-graph)
  "The default graph.  Empty -- every instance is `equal'; use the
shared `rdf-model-default-graph' constant rather than constructing
your own.")

(defconst rdf-model-default-graph (make-rdf-model--default-graph)
  "The (sole) default graph, RDF/JS's `DefaultGraph' singleton.")

(defun rdf-model-term-type (term)
  "Return a symbol naming TERM's type: `named-node', `blank-node',
`literal', `variable', or `default-graph'.  Signals `wrong-type-argument'
if TERM isn't one of `rdf-model''s term structs."
  (cond
   ((rdf-model-named-node-p term) 'named-node)
   ((rdf-model-blank-node-p term) 'blank-node)
   ((rdf-model-literal-p term) 'literal)
   ((rdf-model-variable-p term) 'variable)
   ((rdf-model--default-graph-p term) 'default-graph)
   (t (signal 'wrong-type-argument (list 'rdf-model-term-p term)))))

;;; Quads

(cl-defstruct (rdf-model-quad (:constructor rdf-model-quad-create))
  "A quad.  SUBJECT is an `rdf-model-named-node'/`-blank-node'.
PREDICATE is an `rdf-model-named-node'.  OBJECT is any term type.
GRAPH is an `rdf-model-named-node'/`-blank-node'/`rdf-model-default-graph'
\(defaults to `rdf-model-default-graph', for a plain triple)."
  subject predicate object (graph rdf-model-default-graph))

;;; Dataset protocol
;;
;; No concrete implementation here -- see `rdf-store.el'.  All generics
;; take the dataset first, mirroring RDF/JS's `dataset.add(quad)' etc.
;; called as methods on the dataset object.

(cl-defgeneric rdf-model-dataset-add (dataset quad)
  "Add QUAD to DATASET (a no-op if it's already present).  Returns DATASET.")

(cl-defgeneric rdf-model-dataset-delete (dataset quad)
  "Remove QUAD from DATASET, if present.  Returns DATASET.")

(cl-defgeneric rdf-model-dataset-has (dataset quad)
  "Return non-nil if QUAD is in DATASET.")

(cl-defgeneric rdf-model-dataset-match (dataset &optional subject predicate object graph)
  "Return a list of every quad in DATASET matching the given terms.
A nil SUBJECT/PREDICATE/OBJECT/GRAPH matches any term in that
position; non-nil arguments must match via `equal'.")

(cl-defgeneric rdf-model-dataset-quads (dataset)
  "Return a list of every quad in DATASET, in unspecified order.")

(cl-defgeneric rdf-model-dataset-size (dataset)
  "Return the number of quads in DATASET.")

(provide 'rdf-model)

;;; rdf-model.el ends here
