import Lens from './lens.js';
import { Parser, states, actions } from '../src-cjs/tag-parser.js';
const RAW_VALUE_MARK = '⦃…⦄';
const MISSING_VALUE = new Error();
const cacheSpaceAllocations = new Map([[null, 100]]);
const lensBuilderCache = new Map();
lensBuilderCache.allocated = cacheSpaceAllocations.get(null);
/**
* @class Sugar_CacheControl
* @hideconstructor
* @since 2.3.0
*
* @classdesc
* The only instance of this class is available as {@link module:natural-lenses/sugar#cache}.
*/
/**
* @module natural-lenses/sugar
* @since 2.3.0
* @summary String template tag for constructing a Lens with JSONPath-like syntax
* @returns {Lens} A lens constructed from the JSONPath (and intercalated values) given
*
* @description
* This module is (when `require`d) or exports as default (when `import`ed) a
* Function implementing a string template tag interpreting a subset of JSONPath
* to construct a {@link Lens}. The only supported JSONPath operators
* are the single dot (`.`) and the square brackets (`[...]`); all other
* operators would result in a non-Lens optic. Within the square brackets,
* only string literals (in single- or double-quotes), unsigned or negative
* integers, and intercalated values are allowed. Use of unquoted `@` (the
* JSONPath *current object/element*) in the expression is not allowed, and the
* `$` (the JSONPath *root object*) is only allowed — and required — as the
* first character of the expression.
*
* When an intercalated value is used within a subscript operator, the actual
* JavaScript value — not its string representation — is used as the step in
* the {@link Lens}; this allows for using [`lens.Step`]{@link Step} for
* custom stepping or arbitrary JavaScript values for keys into a Map or similar
* container.
*
* This template tag processes the raw strings used in the template (to avoid
* doubling of backslashes for escape sequences); though this means a
* backslash-backtick combination (since backtick by itself ends the template)
* is processed as two characters, the only valid context for this to occur —
* due to the JSONPath syntax — is inside a string literal within square
* brackets, in which case the backslash-backtick sequence will be interpreted
* as a single backtick anyway. If this causes confusion, the `\x60` escape
* sequence can be used instead.
*
* # Examples
*
* ```js
* const lens = require('natural-lenses'), A = require('natural-lenses/sugar');
*
* # Equivalent expressions
*
* const lensExplicit1 = lens('foo', 'bar'), lensSugar1 = A`$.foo.bar`;
*
* const lensExplicit2 = lens('street', 1), lensSugar2 = A`$.street[1]`;
*
* const marker = Symbol('marker');
* const lensExplicit3 = lens('item', marker), lensSugar3 = A`$.item[${marker}]`;
* ```
*/
export default function lensFromJSONPath(stringParts, ...values) {
const cacheKey = stringParts.raw.join('!');
let lensBuilder = lensBuilderCache.get(cacheKey) ||
lensBuilderFromTemplateStrings(stringParts.raw);
// Delete before setting to implement LRU caching
lensBuilderCache.delete(cacheKey);
lensBuilderCache.set(cacheKey, lensBuilder);
pruneLensBuilderCache();
return lensBuilder(values);
}
const INTERCALATED_VALUE_PLACEHOLDER = Symbol('intercalated value');
function lensBuilderFromTemplateStrings(stringParts) {
const stringsCursor = stringParts[Symbol.iterator]();
let {value: curString, done} = stringsCursor.next();
if (done) {
return () => new Lens();
}
const parser = new Parser();
const steps = [], ivIndexes = [];
let accum = '', charCursor = curString[Symbol.iterator](), curCharRecord;
let consumed = '', captureStart;
const actions = {
append(ch) {
if (!accum) {
captureStart = consumed.length;
}
accum += ch;
},
emit_value() {
steps.push(accum);
accum = '';
},
emit_literal() {
steps.push(eval(accum));
accum = '';
},
consume_intercalated_value() {
ivIndexes.push(steps.length);
steps.push(INTERCALATED_VALUE_PLACEHOLDER);
consumed += RAW_VALUE_MARK;
curString = getNext(stringsCursor, () => {
throw new Error("Too few template parts!");
});
charCursor = curString[Symbol.iterator]();
curCharRecord = charCursor.next();
parser.state = states.subscript_value_emitted;
},
scan() {
consumed += curCharRecord.value;
curCharRecord = charCursor.next();
},
};
curCharRecord = charCursor.next();
try {
while(!curCharRecord.done && parser.processChar(curCharRecord.value, actions)) {
if (curCharRecord.done) {
parser.inputEnds(actions);
}
}
} catch (e) {
if (e !== MISSING_VALUE) throw e;
}
if (!parser.state) {
const reducedInput = stringParts.join(RAW_VALUE_MARK),
asciiArt = `\n ${reducedInput}\n ${' '.repeat(consumed.length)}^\n`;
throw Object.assign(
new Error("JSONPath (subset) syntax error\n" + asciiArt),
{ consumed, from: reducedInput }
);
}
if (!parser.isFinal()) {
const reducedInput = stringParts.join(RAW_VALUE_MARK),
asciiArt = `\n\n ${reducedInput}\n ${' '.repeat(captureStart) + '^'.repeat(reducedInput.length - captureStart)}\n`;
const error = new Error("Path ended prematurely!" + (
accum ? asciiArt : ''
));
if (accum) {
error.accumulatedText = accum;
}
throw error;
}
if (!stringsCursor.next().done) {
throw new Error("Too many string parts!");
}
return (values) => {
if (values.length !== ivIndexes.length) {
throw new Error(`Expected ${ivIndexes.length} values, received ${values.length}`);
}
const lensSteps = [...steps];
ivIndexes.forEach((stepsIndex, i) => {
lensSteps[stepsIndex] = values[i];
});
return new Lens(...lensSteps);
};
}
function getNext(cursor, getDefault) {
const { value, done } = cursor.next();
if (done) {
return getDefault();
}
return value;
}
function pruneLensBuilderCache() {
for (const key of lensBuilderCache.keys()) {
if (lensBuilderCache.size <= lensBuilderCache.allocated) {
break;
}
lensBuilderCache.delete(key);
}
}
/**
* @member {Sugar_CacheControl} module:natural-lenses/sugar#cache
* @since 2.3.0
* @summary Parse cache control
*
* @description
* This object contains methods and properties to observe and control the parse
* cache for {@link module:natural-lenses/sugar}.
*/
export const cache = {
/**
* @callback Sugar_CacheControl~AllocationAdjuster
* @since 2.3.0
* @param {number} [newSize = 0] - The new size (>= 0) for this allocation
*
* @description
* Call this to adjust the size of the allocation (which returned this
* function); setting *newSize* to 0 (the default) cancels the allocation,
* though it can be reinstated by later calls to this function.
*/
/**
* @memberof Sugar_CacheControl
* @since 2.3.0
* @instance
* @summary Create an allocation of parser cache entries
* @param {Number} size - Number of cache slots to allocate
* @returns {Sugar_CacheControl~AllocationAdjuster} A function to cancel or adjust the allocation
*
* @description
* Cache allocations should be made by any package consuming this package
* and making significant use of sugar syntax. It can be used to either
* temporarily boost the cache size or to more permanently boost the cache
* size for ongoing operations.
*/
addCapacity(size) {
size = validateCacheAllocationSize(size);
const allocationKey = Symbol();
cacheSpaceAllocations.set(allocationKey, size);
recomputeCacheAllocation();
return function adjustTo(newSize = 0) {
newSize = validateCacheAllocationSize(newSize);
if (newSize === 0) {
cacheSpaceAllocations.delete(allocationKey);
} else {
cacheSpaceAllocations.set(allocationKey, newSize);
}
recomputeCacheAllocation();
};
},
/**
* @memberof Sugar_CacheControl
* @since 2.3.0
* @instance
* @summary Current total of allocated cache slots
* @type {number}
* @readonly
*/
get totalAllocated() {
return lensBuilderCache.allocated;
},
/**
* @memberof Sugar_CacheControl
* @since 2.3.0
* @instance
* @summary Current number of cache slots consumed
* @type {number}
* @readonly
*/
get used() {
return lensBuilderCache.size;
},
}
function recomputeCacheAllocation() {
lensBuilderCache.allocated = [...cacheSpaceAllocations.values()].reduce(
(r, v) => r + v,
0
);
if (lensBuilderCache.size > lensBuilderCache.allocated) {
pruneLensBuilderCache();
}
}
function validateCacheAllocationSize(size) {
if (isNaN(size) || (size = Number(size)) < 0) {
throw new Error("Cache allocation size must be a valid, non-negative number");
}
return size;
}