Skip to content

perf: no deep cloning #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 38 additions & 33 deletions lib/ContextParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,14 @@ export class ContextParser {
*/
public idifyReverseTerms(context: IJsonLdContextNormalizedRaw): IJsonLdContextNormalizedRaw {
for (const key of Object.keys(context)) {
const value: IPrefixValue = context[key];
let value = context[key];
if (value && typeof value === 'object') {
if (value['@reverse'] && !value['@id']) {
if (typeof value['@reverse'] !== 'string' || Util.isValidKeyword(value['@reverse'])) {
throw new ErrorCoded(`Invalid @reverse value, must be absolute IRI or blank node: '${value['@reverse']}'`,
ERROR_CODES.INVALID_IRI_MAPPING);
}
value = context[key] = {...value, '@id': value['@reverse']};
value['@id'] = <string> value['@reverse'];
if (Util.isPotentialKeyword(value['@reverse'])) {
delete value['@reverse'];
Expand Down Expand Up @@ -162,27 +163,30 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
if ('@id' in value) {
// Use @id value for expansion
if (id !== undefined && id !== null && typeof id === 'string') {
contextRaw[key]['@id'] = context.expandTerm(id, true);
contextRaw[key] = { ...contextRaw[key], '@id': context.expandTerm(id, true) };
changed = changed || id !== contextRaw[key]['@id'];
}
} else if (!Util.isPotentialKeyword(key) && canAddIdEntry) {
// Add an explicit @id value based on the expanded key value
const newId = context.expandTerm(key, true);
if (newId !== key) {
// Don't set @id if expansion failed
contextRaw[key]['@id'] = newId;
contextRaw[key] = { ...contextRaw[key], '@id': newId };
changed = true;
}
}
if (type && typeof type === 'string' && type !== '@vocab'
&& (!value['@container'] || !(<any> value['@container'])['@type'])
&& canAddIdEntry) {
// First check @vocab, then fallback to @base
contextRaw[key]['@type'] = context.expandTerm(type, true);
if (expandContentTypeToBase && type === contextRaw[key]['@type']) {
contextRaw[key]['@type'] = context.expandTerm(type, false);
let expandedType = context.expandTerm(type, true);
if (expandContentTypeToBase && type === expandedType) {
expandedType = context.expandTerm(type, false);
}
if (expandedType !== type) {
changed = true;
contextRaw[key] = { ...contextRaw[key], '@type': expandedType };
}
changed = changed || type !== contextRaw[key]['@type'];
}
}
if (!changed) {
Expand All @@ -209,7 +213,10 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
const value = context[key];
if (value && typeof value === 'object') {
if (typeof value['@language'] === 'string') {
value['@language'] = value['@language'].toLowerCase();
const lowercase = value['@language'].toLowerCase();
if (lowercase !== value['@language']) {
context[key] = {...value, '@language': lowercase};
}
}
}
}
Expand All @@ -226,16 +233,17 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
const value = context[key];
if (value && typeof value === 'object') {
if (typeof value['@container'] === 'string') {
value['@container'] = { [value['@container']]: true };
context[key] = { ...value, '@container': { [value['@container']]: true } };
} else if (Array.isArray(value['@container'])) {
const newValue: {[key: string]: boolean} = {};
for (const containerValue of value['@container']) {
newValue[containerValue] = true;
}
value['@container'] = newValue;
context[key] = { ...value, '@container': newValue };
}
}
}
return context;
}

/**
Expand All @@ -256,7 +264,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
if (value && typeof value === 'object') {
if (!('@protected' in context[key])) {
// Mark terms with object values as protected if they don't have an @protected: false annotation
context[key]['@protected'] = true;
context[key] = {...context[key], '@protected': true};
}
} else {
// Convert string-based term values to object-based values with @protected: true
Expand All @@ -265,7 +273,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
'@protected': true,
};
if (Util.isSimpleTermDefinitionPrefix(value, expandOptions)) {
context[key]['@prefix'] = true
context[key] = {...context[key], '@prefix': true};
}
}
}
Expand Down Expand Up @@ -298,7 +306,7 @@ Tried mapping ${key} to ${JSON.stringify(keyValue)}`, ERROR_CODES.INVALID_KEYWOR
// We modify this deliberately,
// as we need it for the value comparison (they must be identical modulo '@protected')),
// and for the fact that this new value will override the first one.
contextAfter[key]['@protected'] = true;
contextAfter[key] = {...contextAfter[key], '@protected': true};
const valueAfter = canonicalizeJson(contextAfter[key]);

// Error if they are not identical
Expand Down Expand Up @@ -607,19 +615,17 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
// https://w3c.github.io/json-ld-api/#h-note-10
if (this.validateContext) {
try {
const parentContext = {...context};
parentContext[key] = {...parentContext[key]};
const parentContext = {...context, [key]: {...context[key]}};
delete parentContext[key]['@context'];
await this.parse(value['@context'],
{ ...options, external: false, parentContext, ignoreProtection: true, ignoreRemoteScopedContexts: true, ignoreScopedContexts: true });
} catch (e) {
throw new ErrorCoded(e.message, ERROR_CODES.INVALID_SCOPED_CONTEXT);
}
}

value['@context'] = (await this.parse(value['@context'],
{ ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context }))
.getContextRaw();
context[key] = {...value, '@context': (await this.parse(value['@context'],
{ ...options, external: false, minimalProcessing: true, ignoreRemoteScopedContexts: true, parentContext: context }))
.getContextRaw()}
}
}
}
Expand Down Expand Up @@ -718,9 +724,9 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
}

// Make a deep clone of the given context, to avoid modifying it.
context = <IJsonLdContextNormalizedRaw> JSON.parse(JSON.stringify(context)); // No better way in JS at the moment.
context = <IJsonLdContextNormalizedRaw> {...context};
if (parentContext && !minimalProcessing) {
parentContext = <IJsonLdContextNormalizedRaw> JSON.parse(JSON.stringify(parentContext));
parentContext = <IJsonLdContextNormalizedRaw> {...parentContext};
}

// According to the JSON-LD spec, @base must be ignored from external contexts.
Expand All @@ -733,7 +739,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP

// Hashify container entries
// Do this before protected term validation as that influences term format
this.containersToHash(context);
context = this.containersToHash(context);

// Don't perform any other modifications if only minimal processing is needed.
if (minimalProcessing) {
Expand Down Expand Up @@ -767,21 +773,20 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
newContext = { ...parentContext, ...newContext };
}

// Parse inner contexts with minimal processing
newContext = await this.parseInnerContexts(newContext, options);

const newContextWrapped = new JsonLdContextNormalized(newContext);

// Parse inner contexts with minimal processing
await this.parseInnerContexts(newContext, options);

// In JSON-LD 1.1, @vocab can be relative to @vocab in the parent context, or a compact IRI.
if ((newContext && newContext['@version'] || ContextParser.DEFAULT_PROCESSING_MODE) >= 1.1
&& ((context['@vocab'] && typeof context['@vocab'] === 'string') || context['@vocab'] === '')) {
if (parentContext && '@vocab' in parentContext && context['@vocab'].indexOf(':') < 0) {
newContext['@vocab'] = parentContext['@vocab'] + context['@vocab'];
} else {
if (Util.isCompactIri(context['@vocab']) || context['@vocab'] in newContextWrapped.getContextRaw()) {
} else if (Util.isCompactIri(context['@vocab']) || context['@vocab'] in newContext) {
// @vocab is a compact IRI or refers exactly to a prefix
newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true);
}
newContext['@vocab'] = newContextWrapped.expandTerm(context['@vocab'], true);
}
}

Expand Down Expand Up @@ -816,7 +821,7 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
// First try to retrieve the context from cache
const cached = this.documentCache[url];
if (cached) {
return typeof cached === 'string' ? cached : Array.isArray(cached) ? cached.slice() : {... cached};
return cached;
}

// If not in cache, load it
Expand Down Expand Up @@ -863,8 +868,8 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
* @param importContextIri The full URI of an @import value.
*/
public async loadImportContext(importContextIri: string): Promise<IJsonLdContextNormalizedRaw> {
// Load the context
const importContext = await this.load(importContextIri);
// Load the context - and do a deep clone since we are about to mutate it
let importContext = await this.load(importContextIri);

// Require the context to be a non-array object
if (typeof importContext !== 'object' || Array.isArray(importContext)) {
Expand All @@ -877,11 +882,11 @@ must be one of ${Util.CONTAINERS.join(', ')}`, ERROR_CODES.INVALID_CONTAINER_MAP
throw new ErrorCoded('An imported context can not import another context: ' + importContextIri,
ERROR_CODES.INVALID_CONTEXT_ENTRY);
}
importContext = {...importContext};

// Containers have to be converted into hash values the same way as for the importing context
// Otherwise context validation will fail for container values
this.containersToHash(importContext);
return importContext;
return this.containersToHash(importContext);
}

}
Expand Down