feat: initial commit

This commit is contained in:
l.weber 2025-12-05 12:20:05 +01:00
commit a161b86c9a
705 changed files with 288162 additions and 0 deletions

View file

@ -0,0 +1,368 @@
package longdistance
import (
"sourcery.dny.nu/longdistance/internal/json"
)
// Properties is a key-to-array-of-[Node] map.
//
// It's used to hold any property that's not a JSON-LD keyword.
type Properties map[string][]Node
// Node represents a node in a JSON-LD graph.
//
// Every supported JSON-LD keyword has a field of its own. All remaining
// properties are tracked on the Properties field.
type Node struct {
Direction string // @direction / KeywordDirection
Graph []Node // @graph / KeywordGraph
ID string // @id / KeywordID
Included []Node // @included / KeywordIncluded
Index string // @index / KeywordIndex
Language string // @language / KeywordLanguage
List []Node // @list / KeywordList
Reverse Properties // @reverse / KeywordReverse
Set []Node // @set / KeywordSet
Type []string // @type / KeywordType
Value json.RawMessage // @value / KeywordValue
Properties Properties // everything else
}
// Internal is a generic type that matches the internals of [Node].
//
// This can be used to convert to a [Node] from any type outside this package
// that happens to be a [Node] underneath.
type Internal interface {
~struct {
Direction string
Graph []Node
ID string
Included []Node
Index string
Language string
List []Node
Reverse Properties
Set []Node
Type []string
Value json.RawMessage
Properties Properties
}
}
// PropertySet returns a [Set] with an entry for each property that is set on
// the [Node].
func (n *Node) PropertySet() map[string]struct{} {
if n == nil {
return nil
}
res := make(map[string]struct{}, len(n.Properties)+2)
if n.Has(KeywordDirection) {
res[KeywordDirection] = struct{}{}
}
if n.Has(KeywordGraph) {
res[KeywordGraph] = struct{}{}
}
if n.Has(KeywordID) {
res[KeywordID] = struct{}{}
}
if n.Has(KeywordIncluded) {
res[KeywordIncluded] = struct{}{}
}
if n.Has(KeywordIndex) {
res[KeywordIndex] = struct{}{}
}
if n.Has(KeywordLanguage) {
res[KeywordLanguage] = struct{}{}
}
if n.Has(KeywordList) {
res[KeywordList] = struct{}{}
}
if n.Has(KeywordReverse) {
res[KeywordReverse] = struct{}{}
}
if n.Has(KeywordSet) {
res[KeywordSet] = struct{}{}
}
if n.Has(KeywordType) {
res[KeywordType] = struct{}{}
}
if n.Has(KeywordValue) {
res[KeywordValue] = struct{}{}
}
for p := range n.Properties {
res[p] = struct{}{}
}
return res
}
func (n *Node) propsWithout(props ...string) map[string]struct{} {
nprops := n.PropertySet()
for _, prop := range props {
delete(nprops, prop)
}
return nprops
}
func (n *Node) isNode() bool {
if n == nil {
return false
}
return !n.Has(KeywordList) && !n.Has(KeywordValue) && !n.Has(KeywordSet)
}
// Has returns if a node has the requested property.
//
// Properties must either be a JSON-LD keyword, or an expanded IRI.
func (n *Node) Has(prop string) bool {
if n == nil {
return false
}
switch prop {
case KeywordID:
return n.ID != ""
case KeywordValue:
return n.Value != nil
case KeywordLanguage:
return n.Language != ""
case KeywordDirection:
return n.Direction != ""
case KeywordType:
return n.Type != nil
case KeywordList:
return n.List != nil
case KeywordSet:
return n.Set != nil
case KeywordGraph:
return n.Graph != nil
case KeywordIncluded:
return n.Included != nil
case KeywordIndex:
return n.Index != ""
case KeywordReverse:
return n.Reverse != nil
default:
for key := range n.Properties {
if prop == key {
return true
}
}
return false
}
}
// IsZero returns if this is the zero value of a [Node].
func (n *Node) IsZero() bool {
if n == nil {
return true
}
return len(n.PropertySet()) == 0
}
// IsSubject checks if this node is a subject.
//
// This means:
// - It has an @id.
// - It may have an @type.
// - It has at least one other property.
func (n *Node) IsSubject() bool {
if n == nil {
return false
}
if !n.Has(KeywordID) {
return false
}
return len(n.propsWithout(KeywordID, KeywordIndex)) != 0
}
// IsSubjectReference checks if this node is a subject reference.
//
// This means:
// - It has an @id.
// - It may have an @type.
// - It has no other properties.
func (n *Node) IsSubjectReference() bool {
if n == nil {
return false
}
if !n.Has(KeywordID) {
return false
}
return len(n.propsWithout(KeywordID, KeywordType)) == 0
}
// IsList checks if this node is a list.
//
// This means:
// - It has an @list.
// - It has no other properties.
func (n *Node) IsList() bool {
if n == nil {
return false
}
if !n.Has(KeywordList) {
return false
}
return len(n.propsWithout(KeywordList, KeywordIndex)) == 0
}
// IsValue checks if this is a value node.
//
// This means:
// - It has an @value.
// - It may have an @direction, @index, @langauge and @type.
// - It has no other properties.
//
// Additionally, it's invalid to have @type together with @language or
// @direction.
func (n *Node) IsValue() bool {
if n == nil {
return false
}
if !n.Has(KeywordValue) {
return false
}
return len(n.propsWithout(
KeywordValue,
KeywordDirection,
KeywordIndex,
KeywordLanguage,
KeywordType,
)) == 0
}
// IsGraph returns if the object is a graph.
//
// This requires:
// - It must have an @graph.
// - It may have @id and @index.
// - It has no other properties.
func (n *Node) IsGraph() bool {
if n == nil {
return false
}
if !n.Has(KeywordGraph) {
return false
}
return len(n.propsWithout(KeywordID, KeywordIndex, KeywordGraph)) == 0
}
// IsSimpleGraph returns if the object is a simple graph.
//
// This requires:
// - It must have an @graph.
// - It may have @index.
// - It has no other properties.
func (n *Node) IsSimpleGraph() bool {
if n == nil {
return false
}
if !n.Has(KeywordGraph) {
return false
}
return len(n.propsWithout(KeywordIndex, KeywordGraph)) == 0
}
// MarshalJSON encodes to Expanded Document Form.
func (n *Node) MarshalJSON() ([]byte, error) {
result := map[string]any{}
if n.Has(KeywordID) {
result[KeywordID] = n.ID
}
if n.Has(KeywordIndex) {
result[KeywordIndex] = n.Index
}
if n.Has(KeywordType) {
var data any
if n.Value != nil && len(n.Type) == 1 {
data = n.Type[0]
} else {
data = n.Type
}
result[KeywordType] = data
}
if n.Has(KeywordValue) {
result[KeywordValue] = n.Value
}
if n.Has(KeywordLanguage) {
result[KeywordLanguage] = n.Language
}
if n.Has(KeywordDirection) {
result[KeywordDirection] = n.Direction
}
if n.Has(KeywordList) {
result[KeywordList] = n.List
}
if n.Has(KeywordGraph) {
result[KeywordGraph] = n.Graph
}
if n.Has(KeywordIncluded) {
result[KeywordIncluded] = n.Included
}
if n.Has(KeywordReverse) {
result[KeywordReverse] = n.Reverse
}
for k, v := range n.Properties {
result[k] = v
}
return json.Marshal(result)
}
// GetNodes returns the nodes stored in property.
func (n *Node) GetNodes(property string) []Node {
switch property {
case KeywordGraph:
return n.Graph
case KeywordIncluded:
return n.Included
case KeywordList:
return n.List
case KeywordSet:
return n.Set
default:
if !n.Has(property) {
return nil
}
return n.Properties[property]
}
}
// AddNodes appends the nodes stored in property.
func (n *Node) AddNodes(property string, nodes ...Node) {
n.Properties[property] = append(n.Properties[property], nodes...)
}
// SetNodes overrides the nodes stored in property.
func (n *Node) SetNodes(property string, nodes ...Node) {
n.Properties[property] = nodes
}