twink/vendor/sourcery.dny.nu/longdistance/term.go

772 lines
15 KiB
Go
Raw Normal View History

2025-12-05 12:20:05 +01:00
package longdistance
import (
"bytes"
"log/slog"
"maps"
"slices"
"strings"
"sourcery.dny.nu/longdistance/internal/json"
"sourcery.dny.nu/longdistance/internal/url"
)
// Term represents a term definition in a JSON-LD context.
type Term struct {
IRI string
Prefix bool
Protected bool
Reverse bool
BaseIRI string
Context json.RawMessage
Container []string
Direction string
Index string
Language string
Nest string
Type string
}
func (t *Term) equalWithoutProtected(ot *Term) bool {
if t == nil && ot == nil {
return true
}
if t == nil || ot == nil {
return false
}
if t.IRI != ot.IRI {
return false
}
if t.Prefix != ot.Prefix {
return false
}
if t.Reverse != ot.Reverse {
return false
}
if t.BaseIRI != ot.BaseIRI {
return false
}
if !bytes.Equal(t.Context, ot.Context) {
return false
}
if !slices.Equal(t.Container, ot.Container) {
return false
}
if t.Direction != ot.Direction {
return false
}
if t.Index != ot.Index {
return false
}
if t.Language != ot.Language {
return false
}
if t.Nest != ot.Nest {
return false
}
if t.Type != ot.Type {
return false
}
return true
}
func (t *Term) IsZero() bool {
if t == nil {
return true
}
return t.IRI == "" && !t.Prefix && !t.Protected &&
!t.Reverse && t.BaseIRI == "" && t.Context == nil &&
t.Container == nil && t.Direction == "" &&
t.Index == "" && t.Language == "" && t.Nest == "" &&
t.Type == ""
}
type createTermOptions struct {
baseURL string
protected bool
override bool
remotes []string
validate bool
}
func newCreateTermOptions() createTermOptions {
return createTermOptions{
validate: true,
}
}
func (p *Processor) createTerm(
activeContext *Context,
localContext map[string]json.RawMessage,
term string,
defined map[string]*bool,
opts createTermOptions,
) error {
// 1)
if v := defined[term]; v != nil {
if *v {
return nil
}
return ErrCyclicIRIMapping
}
// 2)
if term == "" {
return ErrInvalidTermDefinition
} else {
b := false
defined[term] = &b
}
// 3)
value := localContext[term]
// 4)
if term == KeywordType {
if p.modeLD10 {
return ErrKeywordRedefinition
}
var obj map[string]json.RawMessage
if err := json.Unmarshal(value, &obj); err != nil {
return ErrKeywordRedefinition
}
if len(obj) == 0 {
return ErrKeywordRedefinition
}
objCopy := maps.Clone(obj)
delete(objCopy, KeywordContainer)
delete(objCopy, KeywordProtected)
if len(objCopy) != 0 {
return ErrKeywordRedefinition
}
if v, ok := obj[KeywordContainer]; ok {
var s string
if err := json.Unmarshal(v, &s); err != nil {
return ErrKeywordRedefinition
}
if s != KeywordSet {
return ErrKeywordRedefinition
}
}
if v, ok := obj[KeywordProtected]; ok {
var b bool
if err := json.Unmarshal(v, &b); err != nil {
return ErrKeywordRedefinition
}
}
} else {
// 5)
if isKeyword(term) {
return ErrKeywordRedefinition
}
if looksLikeKeyword(term) {
p.logger.Warn("keyword lookalike term encountered", slog.String("term", term))
return nil
}
}
// 6)
oldDef, oldDefOK := activeContext.defs[term]
delete(activeContext.defs, term)
if !oldDefOK {
// check for aliasses
for _, def := range activeContext.defs {
if def.IRI != "" && def.IRI == term {
oldDef = def
oldDefOK = true
delete(activeContext.defs, term)
break
}
}
}
simpleTerm := false
var valueObj map[string]json.RawMessage
// 7) 8)
if json.IsNull(value) || json.IsString(value) {
// 8)
if json.IsString(value) {
simpleTerm = true
}
// 7)
value = bytes.Join([][]byte{
[]byte(`{"@id":`),
value,
[]byte(`}`),
}, nil)
}
// 9)
if err := json.Unmarshal(value, &valueObj); err != nil {
return ErrInvalidTermDefinition
}
// 10)
termDef := Term{
Prefix: false,
Protected: opts.protected,
Reverse: false,
}
// 11)
if prot, ok := valueObj[KeywordProtected]; ok {
if p.modeLD10 {
return ErrInvalidTermDefinition
}
var b bool
if err := json.Unmarshal(prot, &b); err != nil {
return ErrInvalidProtectedValue
}
termDef.Protected = b
}
// at this point protected is finalised, so add the
// term to the protected set on activeContext
if termDef.Protected {
activeContext.protected[term] = struct{}{}
}
// 12)
if typ, ok := valueObj[KeywordType]; ok {
if json.IsNull(typ) {
return ErrInvalidTypeMapping
}
var s string
// 12.1)
if err := json.Unmarshal(typ, &s); err != nil {
return ErrInvalidTypeMapping
}
// 12.2)
u, err := p.expandIRI(activeContext, s, false, true, localContext, defined)
if err != nil {
return ErrInvalidTypeMapping
}
// 12.3
if p.modeLD10 {
if u == KeywordNone || u == KeywordJSON {
return ErrInvalidTypeMapping
}
}
// 12.4)
switch u {
case KeywordID, KeywordJSON, KeywordNone, KeywordVocab:
default:
if !url.IsIRI(u) {
return ErrInvalidTypeMapping
}
}
// 12.5)
termDef.Type = u
}
// prep for branch 14)
id, idOK := valueObj[KeywordID]
var idStr string
idErr := json.Unmarshal(id, &idStr)
// 13)
if rev, ok := valueObj[KeywordReverse]; ok {
_, hasID := valueObj[KeywordID]
_, hasNest := valueObj[KeywordNest]
// 13.1)
if hasID || hasNest {
return ErrInvalidReverseProperty
}
// 13.2)
if json.IsNull(rev) {
return ErrInvalidIRIMapping
}
var s string
if err := json.Unmarshal(rev, &s); err != nil {
return ErrInvalidIRIMapping
}
// 13.3)
if looksLikeKeyword(s) {
p.logger.Warn("keyword lookalike value encountered",
slog.String("value", s))
return nil
}
// 13.4)
u, err := p.expandIRI(activeContext, s, false, true, localContext, defined)
if err != nil {
return ErrInvalidIRIMapping
}
if !url.IsIRI(u) && u != BlankNode {
return ErrInvalidIRIMapping
}
termDef.IRI = u
// 13.5)
if v, ok := valueObj[KeywordContainer]; ok {
if json.IsNull(v) {
termDef.Container = nil
} else {
var c string
if err := json.Unmarshal(v, &c); err != nil {
return ErrInvalidReverseProperty
}
if c != KeywordSet && c != KeywordIndex {
return ErrInvalidReverseProperty
}
termDef.Container = []string{c}
}
}
// 13.6)
termDef.Reverse = true
// This whole step is missing in the spec but without
// it t0131 can't pass. So. YOLO.
if slices.Contains(termDef.Container, KeywordIndex) {
idxVal, idxOK := valueObj[KeywordIndex]
if idxOK && !json.IsNull(idxVal) {
var idx string
if err := json.Unmarshal(idxVal, &idx); err != nil {
return err
}
termDef.Index = idx
}
}
// 13.7
activeContext.defs[term] = termDef
b := true
defined[term] = &b
return nil
} else if idOK && term != idStr {
// 14.1) 14.2)
if idErr != nil {
return ErrInvalidIRIMapping
}
// 14.1)
if !json.IsNull(id) {
// 14.2)
if !isKeyword(idStr) && looksLikeKeyword(idStr) {
// 14.2.2)
p.logger.Warn("keyword lookalike value encountered",
slog.String("value", idStr))
return nil
}
// 14.2.3)
u, err := p.expandIRI(activeContext, idStr, false, true, localContext, defined)
if err != nil {
return err
}
if !isKeyword(u) && !url.IsIRI(u) && u != BlankNode {
return ErrInvalidIRIMapping
}
if u == KeywordContext {
return ErrInvalidKeywordAlias
}
termDef.IRI = u
// 14.2.4)
if strings.Contains(term, "/") || (!strings.HasPrefix(term, ":") && !strings.HasSuffix(term, ":") && strings.Contains(term, ":")) {
b := true
// 14.2.4.1)
defined[term] = &b
// 14.2.4.2)
tu, err := p.expandIRI(activeContext, term, false, true, localContext, defined)
if err != nil {
return ErrInvalidIRIMapping
}
if tu != u {
return ErrInvalidIRIMapping
}
} else {
// 14.2.5)
if simpleTerm && url.EndsInGenDelim(u) || u == BlankNode {
if v, ok := p.remapPrefixIRIs[u]; ok {
termDef.IRI = v
}
termDef.Prefix = true
}
}
}
} else if strings.Contains(term[1:], ":") {
// 15)
prefix, suffix, _ := strings.Cut(term, ":")
// 15.1)
if !strings.HasPrefix(suffix, "//") {
if _, ok := localContext[prefix]; ok {
err := p.createTerm(activeContext, localContext, prefix, defined, newCreateTermOptions())
if err != nil {
return err
}
}
}
// 15.2)
if def, ok := activeContext.defs[prefix]; ok {
termDef.IRI = def.IRI + suffix
} else {
// 15.3)
termDef.IRI = term
}
} else if strings.Contains(term, "/") {
// 16)
// 16.2)
u, err := p.expandIRI(activeContext, term, false, true, nil, nil)
if err != nil {
return ErrInvalidIRIMapping
}
if !url.IsIRI(u) {
return ErrInvalidIRIMapping
}
termDef.IRI = u
} else if term == KeywordType {
// 17)
termDef.IRI = KeywordType
} else if activeContext.vocabMapping != "" {
// 18)
termDef.IRI = activeContext.vocabMapping + term
} else {
return ErrInvalidIRIMapping
}
// 19)
if cnt, ok := valueObj[KeywordContainer]; ok {
if json.IsNull(cnt) {
return ErrInvalidContainerMapping
}
// 19.2)
// do this check early since we're going to rewrap
// into an array
if p.modeLD10 && !json.IsString(cnt) {
return ErrInvalidContainerMapping
}
cnt = json.MakeArray(cnt)
// 19.1)
var values []string
if err := json.Unmarshal(cnt, &values); err != nil {
return ErrInvalidContainerMapping
}
for _, vl := range values {
switch vl {
case KeywordGraph, KeywordID, KeywordIndex,
KeywordLanguage, KeywordList, KeywordSet,
KeywordType:
default:
return ErrInvalidContainerMapping
}
}
if slices.Contains(values, KeywordGraph) && (slices.Contains(values, KeywordID) || slices.Contains(values, KeywordIndex)) {
kws := map[string]struct{}{}
for _, vl := range values {
kws[vl] = struct{}{}
}
delete(kws, KeywordGraph)
delete(kws, KeywordIndex)
delete(kws, KeywordID)
if _, ok := kws[KeywordSet]; ok && len(kws) != 1 {
return ErrInvalidIRIMapping
}
} else if slices.Contains(values, KeywordSet) {
for _, vl := range values {
switch vl {
case KeywordGraph, KeywordID, KeywordIndex,
KeywordLanguage, KeywordType,
KeywordSet:
default:
return ErrInvalidContainerMapping
}
}
}
// 19.2)
if p.modeLD10 {
if len(values) > 1 {
return ErrInvalidContainerMapping
}
switch values[0] {
case KeywordID, KeywordGraph, KeywordType:
return ErrInvalidContainerMapping
}
}
// 19.3)
termDef.Container = values
// 19.4)
if slices.Contains(values, KeywordType) {
// 19.4.1)
if termDef.Type == "" {
termDef.Type = KeywordID
}
// 19.4.2)
switch termDef.Type {
case KeywordID, KeywordVocab, "":
default:
return ErrInvalidTypeMapping
}
}
}
// 20)
if idx, ok := valueObj[KeywordIndex]; ok {
// 20.1)
if p.modeLD10 {
return ErrInvalidTermDefinition
}
if !slices.Contains(termDef.Container, KeywordIndex) {
return ErrInvalidTermDefinition
}
// 20.2)
var s string
if err := json.Unmarshal(idx, &s); err != nil {
return ErrInvalidTermDefinition
}
u, err := p.expandIRI(activeContext, s, false, true, localContext, defined)
if err != nil {
return ErrInvalidTermDefinition
}
if !url.IsIRI(u) {
return ErrInvalidTermDefinition
}
// 20.3)
termDef.Index = s
}
// 21)
if ctx, ok := valueObj[KeywordContext]; ok {
// 21.1)
if p.modeLD10 {
return ErrInvalidTermDefinition
}
// 21.3)
resolvOpts := newCtxProcessingOpts()
resolvOpts.override = true
resolvOpts.remotes = slices.Clone(opts.remotes)
resolvOpts.validate = false
_, err := p.context(
activeContext,
ctx,
opts.baseURL,
resolvOpts,
)
if err != nil {
return ErrInvalidScopedContext
}
// 21.4
termDef.Context = ctx
termDef.BaseIRI = opts.baseURL
}
_, hasType := valueObj[KeywordType]
// 22)
if lang, ok := valueObj[KeywordLanguage]; ok && !hasType {
if json.IsNull(lang) {
termDef.Language = KeywordNull
} else {
var lm string
// 22.1)
if err := json.Unmarshal(lang, &lm); err != nil {
return ErrInvalidLanguageMapping
}
// 22.2)
termDef.Language = strings.ToLower(lm)
}
}
// 23)
if dir, ok := valueObj[KeywordDirection]; ok && !hasType {
if json.IsNull(dir) {
termDef.Direction = KeywordNull
} else {
var d string
// 23.1)
if err := json.Unmarshal(dir, &d); err != nil {
return ErrInvalidBaseDirection
}
switch d {
case DirectionLTR, DirectionRTL:
default:
return ErrInvalidBaseDirection
}
// 23.2)
termDef.Direction = d
}
}
// 24)
if nest, ok := valueObj[KeywordNest]; ok {
// 24.1)
if p.modeLD10 {
return ErrInvalidTermDefinition
}
if json.IsNull(nest) {
return ErrInvalidNestValue
}
// 24.2)
var n string
if err := json.Unmarshal(nest, &n); err != nil {
return ErrInvalidNestValue
}
if isKeyword(n) && n != KeywordNest {
return ErrInvalidNestValue
}
termDef.Nest = n
}
// 25)
if prefix, ok := valueObj[KeywordPrefix]; ok {
// 25.1)
if p.modeLD10 {
return ErrInvalidTermDefinition
}
// 25.2)
if json.IsNull(prefix) {
return ErrInvalidPrefixValue
}
if strings.Contains(term, ":") || strings.Contains(term, "/") {
return ErrInvalidTermDefinition
}
var p bool
if err := json.Unmarshal(prefix, &p); err != nil {
return ErrInvalidPrefixValue
}
// 25.3)
if p && isKeyword(termDef.IRI) {
return ErrInvalidTermDefinition
}
termDef.Prefix = p
}
// 26)
valKeys := map[string]struct{}{}
for k := range valueObj {
valKeys[k] = struct{}{}
}
for _, kw := range []string{KeywordID, KeywordReverse, KeywordContainer,
KeywordContext, KeywordDirection, KeywordIndex, KeywordLanguage,
KeywordNest, KeywordPrefix, KeywordProtected, KeywordType} {
delete(valKeys, kw)
}
if len(valKeys) > 0 {
return ErrInvalidTermDefinition
}
// 27)
if oldDefOK && oldDef.Protected && !opts.override {
// 27.1)
if !oldDef.equalWithoutProtected(&termDef) {
return ErrProtectedTermRedefinition
}
// 27.2)
termDef = oldDef
}
// 28)
activeContext.defs[term] = termDef
b := true
defined[term] = &b
return nil
}
func selectTerm(
activeContext *Context,
keyIriVar string,
containers []string,
typeLanguage string,
preferredValues []string,
) string {
// 1)
if activeContext.inverse == nil {
activeContext.inverse = workIt(activeContext)
}
// 2)
inverse := activeContext.inverse
// 3)
containerMap := inverse[keyIriVar]
for _, container := range containers {
// 4.1)
// 4.2)
typeLanguageMap, ok := containerMap[container]
if !ok {
continue
}
// 4.3)
var valMap map[string]string
switch typeLanguage {
case KeywordLanguage:
valMap = typeLanguageMap.Language
case KeywordType:
valMap = typeLanguageMap.Type
case KeywordAny:
valMap = typeLanguageMap.Any
}
// 4.4)
for _, pval := range preferredValues {
if v, ok := valMap[pval]; !ok {
// 4.4.1)
continue
} else {
// 4.4.2)
return v
}
}
}
// 5)
return ""
}