twink/vendor/sourcery.dny.nu/longdistance/context.go
2025-12-05 12:20:05 +01:00

677 lines
14 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package longdistance
import (
"context"
"fmt"
"iter"
"maps"
"slices"
"strings"
"sourcery.dny.nu/longdistance/internal/json"
"sourcery.dny.nu/longdistance/internal/url"
)
// RemoteContextLimit is the recursion limit for resolving remote contexts.
const RemoteContextLimit = 10
// Context represents a processed JSON-LD context.
type Context struct {
defs map[string]Term
protected map[string]struct{}
currentBaseIRI string
originalBaseIRI string
vocabMapping string
defaultLang string
defaultDirection string
previousContext *Context
inverse inverseContext
}
// newContext initialises a new context with the specified documentURL set as
// the current and original base IRI.
func newContext(documentURL string) *Context {
return &Context{
defs: make(map[string]Term),
protected: make(map[string]struct{}),
defaultLang: "",
defaultDirection: "",
previousContext: nil,
inverse: nil,
currentBaseIRI: documentURL,
originalBaseIRI: documentURL,
}
}
// Terms returns an iterator over context term definitions.
func (c *Context) Terms() iter.Seq2[string, Term] {
return func(yield func(string, Term) bool) {
for k, v := range c.defs {
if !yield(k, v) {
return
}
}
}
}
func (c *Context) initInverse() {
if c.inverse == nil {
c.inverse = workIt(c)
}
}
func (c *Context) clone() *Context {
return &Context{
defs: maps.Clone(c.defs),
protected: maps.Clone(c.protected),
currentBaseIRI: c.currentBaseIRI,
originalBaseIRI: c.originalBaseIRI,
vocabMapping: c.vocabMapping,
defaultLang: c.defaultLang,
defaultDirection: c.defaultDirection,
previousContext: c.previousContext,
inverse: nil,
}
}
// Context takes in JSON and parses it into a [Context].
func (p *Processor) Context(localContext json.RawMessage, baseURL string) (*Context, error) {
if len(localContext) == 0 {
return nil, nil
}
if json.IsNull(localContext) {
return nil, nil
}
return p.context(nil, localContext, baseURL, newCtxProcessingOpts())
}
type ctxProcessingOpts struct {
remotes []string
override bool
propagate bool
validate bool
}
func newCtxProcessingOpts() ctxProcessingOpts {
return ctxProcessingOpts{
propagate: true,
validate: true,
}
}
func (p *Processor) context(
activeContext *Context,
localContext json.RawMessage,
baseURL string,
opts ctxProcessingOpts,
) (*Context, error) {
if activeContext == nil {
activeContext = newContext(baseURL)
}
if p.baseIRI != "" {
activeContext.currentBaseIRI = p.baseIRI
}
// 1)
result := activeContext.clone()
// 2)
if json.IsMap(localContext) {
var propcheck struct {
Propagate *bool `json:"@propagate,omitempty"`
}
if err := json.Unmarshal(localContext, &propcheck); err != nil {
return nil, ErrInvalidPropagateValue
}
if propcheck.Propagate != nil {
opts.propagate = *propcheck.Propagate
}
}
// 3)
if !opts.propagate {
if result.previousContext == nil {
result.previousContext = activeContext.clone()
}
}
// 4)
localContext = json.MakeArray(localContext)
var contexts []json.RawMessage
if err := json.Unmarshal(localContext, &contexts); err != nil {
return nil, fmt.Errorf("invalid context document")
}
if len(contexts) == 0 {
return nil, nil
}
// 5)
for _, context := range contexts {
// 5.1)
switch context[0] {
case '[':
return nil, ErrInvalidLocalContext
case '{':
// goes on after the switch
default:
// 5.1)
if json.IsNull(context) {
// 5.1.1)
if !opts.override && len(result.protected) != 0 {
return nil, ErrInvalidContextNullificaton
}
// 5.1.2)
previous := result.clone()
result = newContext(activeContext.originalBaseIRI)
if !opts.propagate {
result.previousContext = previous
}
// 5.1.3)
continue
}
var s string
if err := json.Unmarshal(context, &s); err != nil {
return nil, ErrInvalidLocalContext
}
// 5.2)
// 5.2.1)
if !url.IsIRI(baseURL) && !url.IsIRI(s) {
return nil, ErrLoadingDocument
}
iri, err := url.Resolve(baseURL, s)
if err != nil {
return nil, ErrLoadingDocument
}
// 5.2.2)
if !opts.validate && slices.Contains(opts.remotes, iri) {
return nil, nil
}
// 5.2.3)
if len(opts.remotes) > RemoteContextLimit {
if p.modeLD10 {
return nil, ErrRecursiveContextInclusion
}
return nil, ErrContextOverflow
}
opts.remotes = append(opts.remotes, iri)
// 5.2.4) 5.2.5)
doc, err := p.retrieveRemoteContext(iri)
if err != nil {
return nil, err
}
// 5.2.6)
newOpts := newCtxProcessingOpts()
newOpts.remotes = slices.Clone(opts.remotes)
newOpts.validate = opts.validate
res, err := p.context(
result,
doc.Context,
doc.URL,
newOpts,
)
if err != nil {
return nil, err
}
result = res
continue
}
// 5.3)
var ctxObj map[string]json.RawMessage
if err := json.Unmarshal(context, &ctxObj); err != nil {
return nil, fmt.Errorf("failed to unmarshal context: %s %w", err, ErrInvalidLocalContext)
}
// 5.5)
if version, ok := ctxObj[KeywordVersion]; ok {
if err := p.handleVersion(version); err != nil {
return nil, err
}
}
// 5.6)
if imp, ok := ctxObj[KeywordImport]; ok {
res, err := p.handleImport(baseURL, imp, ctxObj)
if err != nil {
return nil, err
}
ctxObj = res
}
// 5.7)
if base, ok := ctxObj[KeywordBase]; ok && len(opts.remotes) == 0 {
if err := p.handleBase(result, base); err != nil {
return nil, err
}
}
// 5.8)
if vocab, ok := ctxObj[KeywordVocab]; ok {
if err := p.handleVocab(result, vocab); err != nil {
return nil, err
}
}
// 5.9)
if lang, ok := ctxObj[KeywordLanguage]; ok {
if err := p.handleLanguage(result, lang); err != nil {
return nil, err
}
}
// 5.10)
if dir, ok := ctxObj[KeywordDirection]; ok {
if err := p.handleDirection(result, dir); err != nil {
return nil, err
}
}
// 5.11)
if prop, ok := ctxObj[KeywordPropagate]; ok {
if err := p.handlePropagate(prop); err != nil {
return nil, err
}
}
protected := false
if prot, ok := ctxObj[KeywordProtected]; ok && !json.IsNull(prot) {
if err := json.Unmarshal(prot, &protected); err != nil {
return nil, ErrInvalidProtectedValue
}
}
// 5.12)
defined := map[string]*bool{}
// 5.13)
for k := range ctxObj {
switch k {
case KeywordBase, KeywordDirection, KeywordImport,
KeywordLanguage, KeywordPropagate, KeywordProtected,
KeywordVersion, KeywordVocab:
default:
newOpts := newCreateTermOptions()
newOpts.baseURL = baseURL
newOpts.protected = protected
newOpts.override = opts.override
newOpts.remotes = slices.Clone(opts.remotes)
if err := p.createTerm(
result,
ctxObj,
k,
defined,
newOpts,
); err != nil {
return nil, err
}
}
}
}
return result, nil
}
func (p *Processor) handlePropagate(prop json.RawMessage) error {
if p.modeLD10 {
return ErrInvalidContextEntry
}
if json.IsNull(prop) {
return ErrInvalidPropagateValue
}
var b bool
if err := json.Unmarshal(prop, &b); err != nil {
return ErrInvalidPropagateValue
}
return nil
}
func (p *Processor) handleDirection(result *Context, dir json.RawMessage) error {
if p.modeLD10 {
return ErrInvalidContextEntry
}
if json.IsNull(dir) {
result.defaultDirection = ""
return nil
}
var d string
if err := json.Unmarshal(dir, &d); err != nil {
return ErrInvalidBaseDirection
}
switch d {
case DirectionLTR, DirectionRTL:
default:
return ErrInvalidBaseDirection
}
result.defaultDirection = d
return nil
}
func (p *Processor) handleLanguage(result *Context, lang json.RawMessage) error {
if json.IsNull(lang) {
result.defaultLang = ""
return nil
}
var l string
if err := json.Unmarshal(lang, &l); err != nil {
return ErrInvalidDefaultLanguage
}
result.defaultLang = strings.ToLower(l)
return nil
}
func (p *Processor) handleVocab(result *Context, vocab json.RawMessage) error {
// 5.8.2)
if json.IsNull(vocab) {
result.vocabMapping = ""
return nil
}
var s string
if err := json.Unmarshal(vocab, &s); err != nil {
return ErrInvalidVocabMapping
}
// 5.8.3)
if !(url.IsIRI(s) || url.IsRelative(s) || s == BlankNode) {
return ErrInvalidVocabMapping
}
u, err := p.expandIRI(result, s, true, true, nil, nil)
if err != nil {
return err
}
result.vocabMapping = u
return nil
}
func (p *Processor) handleBase(result *Context, base json.RawMessage) error {
// 5.7.2)
if json.IsNull(base) {
result.currentBaseIRI = ""
return nil
}
var iri string
if err := json.Unmarshal(base, &iri); err != nil {
return ErrInvalidBaseIRI
}
// 5.7.3)
if url.IsIRI(iri) {
result.currentBaseIRI = iri
return nil
}
// 5.7.4)
if url.IsRelative(iri) {
u, err := url.Resolve(result.currentBaseIRI, iri)
if err != nil {
return ErrInvalidBaseIRI
}
result.currentBaseIRI = u
return nil
}
// 5.7.5)
return ErrInvalidBaseIRI
}
func (p *Processor) handleImport(baseURL string, data json.RawMessage, context map[string]json.RawMessage) (map[string]json.RawMessage, error) {
// 5.6.1)
if p.modeLD10 {
return nil, ErrInvalidContextEntry
}
// 5.6.2)
var val string
if err := json.Unmarshal(data, &val); err != nil {
return nil, ErrInvalidImportValue
}
// 5.6.3)
iri, err := url.Resolve(baseURL, val)
if err != nil {
return nil, ErrInvalidRemoteContext
}
// 5.6.4) 5.6.5)
res, err := p.retrieveRemoteContext(iri)
if err != nil {
return nil, err
}
// 5.6.6)
var ctxObj map[string]json.RawMessage
if err := json.Unmarshal(res.Context, &ctxObj); err != nil {
return nil, ErrInvalidRemoteContext
}
// 5.6.7)
if _, ok := ctxObj[KeywordImport]; ok {
return nil, ErrInvalidContextEntry
}
maps.Copy(ctxObj, context)
return ctxObj, nil
}
func (p *Processor) handleVersion(data json.RawMessage) error {
var ver float64
if err := json.Unmarshal(data, &ver); err != nil {
return ErrInvalidVersionValue
}
if ver != 1.1 {
return ErrInvalidVersionValue
}
if p.modeLD10 {
return ErrProcessingMode
}
return nil
}
func (p *Processor) retrieveRemoteContext(
iri string,
) (Document, error) {
// 5.2.4) 5.2.5) the document loader is expected to do the caching
if p.loader == nil {
return Document{}, fmt.Errorf("no loader %w", ErrLoadingRemoteContext)
}
doc, err := p.loader(context.TODO(), iri)
if err != nil {
return Document{}, err
}
return doc, nil
}
type inverseContext map[string]map[string]mapping
type mapping struct {
Language map[string]string
Type map[string]string
Any map[string]string
}
// workIt flips a context and reverses it
//
// ti esrever dna ti pilf ,nwod gniht ym tuP
func workIt(activeContext *Context) inverseContext {
// 1)
result := inverseContext{}
// 2)
defaultLang := KeywordNone
if activeContext.defaultLang != "" {
defaultLang = strings.ToLower(activeContext.defaultLang)
}
// 3)
terms := slices.Collect(maps.Keys(activeContext.defs))
slices.SortFunc(terms, sortedLeast)
for _, key := range terms {
def := activeContext.defs[key]
// 3.1)
if def.IsZero() {
continue
}
// 3.2)
container := KeywordNone
if def.Container != nil {
dc := slices.Clone(def.Container)
slices.Sort(dc)
container = strings.Join(dc, "")
}
// 3.3)
vvar := def.IRI
// 3.4)
if _, ok := result[vvar]; !ok {
result[vvar] = map[string]mapping{}
}
// 3.5)
containerMap := result[vvar]
// 3.6)
if _, ok := containerMap[container]; !ok {
containerMap[container] = mapping{
Language: map[string]string{},
Type: map[string]string{},
Any: map[string]string{
KeywordNone: key,
},
}
}
// 3.7)
typeLanguage := containerMap[container]
// 3.8)
typeMap := typeLanguage.Type
// 3.9)
langMap := typeLanguage.Language
if def.Reverse {
// 3.10)
if _, ok := typeMap[KeywordReverse]; !ok {
typeMap[KeywordReverse] = key
}
} else if def.Type != "" {
if def.Type == KeywordNone {
// 3.11)
if _, ok := langMap[KeywordAny]; !ok {
// 3.11.1)
langMap[KeywordAny] = key
}
if _, ok := typeMap[KeywordAny]; !ok {
// 3.11.2)
typeMap[KeywordAny] = key
}
} else {
// 3.12)
if _, ok := typeMap[def.Type]; !ok {
// 3.12.1
typeMap[def.Type] = key
}
}
} else if def.Language != "" || def.Direction != "" {
if def.Language != "" && def.Direction != "" {
// 3.13)
// 3.13.1) + 3.13.5)
langDir := KeywordNone
if def.Language != KeywordNull && def.Direction != KeywordNull {
// 3.13.2)
langDir = strings.ToLower(def.Language) + "_" + def.Direction
} else if def.Language != KeywordNull {
// 3.13.3)
langDir = strings.ToLower(def.Language)
} else if def.Direction != KeywordNull {
// 3.13.4)
langDir = "_" + def.Direction
}
// 3.13.6)
if _, ok := langMap[langDir]; !ok {
langMap[langDir] = key
}
} else if def.Language != "" {
// 3.14)
lang := KeywordNull
if def.Language != KeywordNull {
lang = strings.ToLower(def.Language)
}
if _, ok := langMap[lang]; !ok {
langMap[lang] = key
}
} else if def.Direction != "" {
// 3.15)
dir := KeywordNone
if def.Direction != KeywordNull {
dir = "_" + def.Direction
}
if _, ok := langMap[dir]; !ok {
langMap[dir] = key
}
}
} else if activeContext.defaultDirection != "" {
// 3.16)
langDir := strings.ToLower(defaultLang) + "_" + activeContext.defaultDirection
if _, ok := langMap[langDir]; !ok {
langMap[langDir] = key
}
if _, ok := langMap[KeywordNone]; !ok {
langMap[KeywordNone] = key
}
if _, ok := typeMap[KeywordNone]; !ok {
typeMap[KeywordNone] = key
}
} else {
// 3.17)
// 3.17.1)
if _, ok := langMap[defaultLang]; !ok {
langMap[defaultLang] = key
}
// 3.17.2)
if _, ok := langMap[KeywordNone]; !ok {
langMap[KeywordNone] = key
}
// 3.17.3)
if _, ok := typeMap[KeywordNone]; !ok {
typeMap[KeywordNone] = key
}
}
}
return result
}