src/variable-stack.js
class VariableStackLayer {
constructor(value = {}, isRoot = false, name = null){
this.value = value;
this.isRoot = isRoot;
this.name = name;
}
isRoot = false;
value = {};
name = null;
}
/**
* The VariableStack is a special kind of stack.
* It allows corrode to do black magic like loops in loops and other crazy stuff.
*
* To enable this we define a stack as an object, containing other objects.
* Seen this way it looks more like a TreeStructure which layers you can
* push and pop as you like.
*
* The VariableStack starts as an "empty" object.
* "empty" meaning, that each layer is an object consisting of two/three values:
* * `isRoot` telling the user whether this layer is the uppermost one.
* * `value` holding the value for this layer.
* * `[name]` name of this layer (root-layer won't have one)
*
* Pushing a new layer means adding a new object to the `value`-object of
* the current layer and setting the current layer to our newly created one.
*
* Actually, the object getting added to the `value`-object is not just any object.
* It itself is an object like `{ isRoot: false, value: {} }`.
*
* The root-layer (layer-object where `isRoot === true`) is the lowest layer.
* The current layer is the topmost one.
*
* @example
* +--------------------------+ +-----------------+
* | VariableStack#value: | | | current layer
* | { | | {} |
* | value_1: { foo: 'bar' }| | | isRoot: false
* | } | +-----------------+
* +------------+-------------+ | |
* | | { |
* + | foo: 'bar', | VariableStack
* .push('value_1') | value_2: {} | #peek(1)
* + +----------> | } |
* | | | isRoot: false
* v +----------> +-----------------+
* +-----------+----------+ | |
* |#value: { foo: 'bar' }| | { |
* +-----------+----------+ | value_1: { | VariableStack
* | | foo: 'bar', | #peek(2)
* + | value_2: {} |
* .push('value_2') | } | isRoot: true
* + | } |
* | | |
* v +-----------------+
* +----+-----+
* |#value: {}|
* +----------+
*
*/
export default class VariableStack {
constructor(){
this.top = this.stack[0];
}
/**
* internal storage for the stack
* @access public
* @type {Array<Object>}
*/
stack = [new VariableStackLayer({}, true)];
/**
* retrieve the top-layer
* @return {Object} the current layer
*/
top = null;
/**
* retrieve the value of the top.layer
* @return {Object|*} the current value
*/
get value(){
return this.top.value;
}
/**
* set the current value
* this also updates the value in the parent-layer
* @param {Object|*} val the new value
*/
set value(val){
if(!this.top.isRoot){
this.peek()[this.top.name] = val;
}
this.top.value = val;
}
/**
* get a layer below the current one
* @param {number} layerCount how many layers deeper relative from the current
* @return {Object} layer-object
*/
peekLayer(layerCount = 1){
if(layerCount > this.stack.length - 1){
throw new ReferenceError(`can't retrieve layer ${layerCount}, stack is ${this.stack.length - 1} layers`);
}
return this.stack[this.stack.length - 1 - layerCount];
}
/**
* get the value of a layer below the current one
* @param {number} layerCount how many layers deeper relative from the current
* @return {Object|*} value
*/
peek(layerCount = 1){
return this.peekLayer(layerCount).value;
}
/**
* push a value onto the stack
*
* The value doesn't have to be a object, but only objects will properly support child-layers.
* When pushing the new layer the current one will receive a reference to the pushed
* object as a value at the given name.
*
* Note, that if you're pushing a non-object value this reference will not work,
* as only arrays & objects are passed by reference. Instead the value in the
* layer above will be updated, when the current layer's value will be set.
*
* If the value you want to push already exists at the current layer
* VariableStack ignores your value and just re-uses the old one, so no
* layer will be replaced.
*
* @example
* varStack.push('foo');
* varStack.value.bar = 'baz';
* // varStack.value => { bar: 'baz' }
* // varStack.peek() => { foo: { bar: 'baz' } }
*
* @param {string} name name of the new layer
* @param {Object|*} [value={}] value-object of the new layer
*/
push(name, value = {}){
if(typeof this.value[name] === 'undefined'){
// only push new value if there's no old one
this.value[name] = value;
} else {
// otherwise re-push the current one
value = this.value[name];
}
const index = this.stack.push(new VariableStackLayer(value, false, name));
this.top = this.stack[index - 1];
}
/**
* pop the current layer
* @throws {ReferenceError} thrown if the layer to be popped is the root-layer
*/
pop(){
const popLayer = this.top;
if(popLayer.isRoot){
throw new ReferenceError('can\'t pop root layer');
}
this.stack.pop();
this.top = this.stack[this.stack.length - 1];
// reassure that the value in the layer above is right
// (in case of non-object values)
this.value[popLayer.name] = popLayer.value;
}
/**
* pop all layers until the root-layer is reached
*/
popAll(){
while(!this.top.isRoot){
this.pop();
}
}
}