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

678 lines
14 KiB
Go
Raw Normal View History

2025-12-05 12:20:05 +01:00
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
}