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 }