Add JSO hooks for blocks and fields. (#5052)

* Add JSON serialiation hooks for fields

* Add checking for JSON hooks

* Fix other checks and move checks to function

* Remove error for both serialization hooks being defined

* Fixup comments and errors

* Add tests

* Add json hooks to block properties

* Cleanup

* Rip out fragile backwards compatibility
This commit is contained in:
Beka Westberg
2021-07-20 13:28:30 -07:00
committed by GitHub
parent 9a9988ff3b
commit 9631efb3eb
4 changed files with 177 additions and 44 deletions
+18 -2
View File
@@ -328,18 +328,34 @@ Blockly.Block.prototype.onchange;
/**
* An optional serialization method for defining how to serialize the
* mutation state. This must be coupled with defining `domToMutation`.
* mutation state to XML. This must be coupled with defining `domToMutation`.
* @type {?function(...):!Element}
*/
Blockly.Block.prototype.mutationToDom;
/**
* An optional deserialization method for defining how to deserialize the
* mutation state. This must be coupled with defining `mutationToDom`.
* mutation state from XML. This must be coupled with defining `mutationToDom`.
* @type {?function(!Element)}
*/
Blockly.Block.prototype.domToMutation;
/**
* An optional serialization method for defining how to serialize the block's
* extra state (eg mutation state) to something JSON compatible. This must be
* coupled with defining `loadExtraState`.
* @type {?function(): *}
*/
Blockly.Block.prototype.saveExtraState;
/**
* An optional serialization method for defining how to deserialize the block's
* extra state (eg mutation state) from something JSON compatible. This must be
* coupled with defining `saveExtraState`.
* @type {?function(*)}
*/
Blockly.Block.prototype.loadExtraState;
/**
* An optional property for suppressing adding STATEMENT_PREFIX and
* STATEMENT_SUFFIX to generated code.
+88 -42
View File
@@ -86,17 +86,11 @@ Blockly.Extensions.registerMutator = function(name, mixinObj, opt_helperFn,
opt_blockList) {
var errorPrefix = 'Error when registering mutator "' + name + '": ';
// Sanity check the mixin object before registering it.
Blockly.Extensions.checkHasFunction_(
errorPrefix, mixinObj.domToMutation, 'domToMutation');
Blockly.Extensions.checkHasFunction_(
errorPrefix, mixinObj.mutationToDom, 'mutationToDom');
Blockly.Extensions.checkHasMutatorProperties_(errorPrefix, mixinObj);
var hasMutatorDialog =
Blockly.Extensions.checkMutatorDialog_(mixinObj, errorPrefix);
if (opt_helperFn && (typeof opt_helperFn != 'function')) {
throw Error('Extension "' + name + '" is not a function');
throw Error(errorPrefix + 'Extension "' + name + '" is not a function');
}
// Sanity checks passed.
@@ -154,7 +148,7 @@ Blockly.Extensions.apply = function(name, block, isMutator) {
if (isMutator) {
var errorPrefix = 'Error after applying mutator "' + name + '": ';
Blockly.Extensions.checkBlockHasMutatorProperties_(errorPrefix, block);
Blockly.Extensions.checkHasMutatorProperties_(errorPrefix, block);
} else {
if (!Blockly.Extensions.mutatorPropertiesMatch_(
/** @type {!Array<Object>} */ (mutatorProperties), block)) {
@@ -203,54 +197,100 @@ Blockly.Extensions.checkNoMutatorProperties_ = function(mutationName, block) {
};
/**
* Check that the given object has both or neither of the functions required
* to have a mutator dialog.
* These functions are 'compose' and 'decompose'. If a block has one, it must
* have both.
* Checks if the given object has both the 'mutationToDom' and 'domToMutation'
* functions.
* @param {!Object} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @private
*/
Blockly.Extensions.checkMutatorDialog_ = function(object, errorPrefix) {
var hasCompose = object.compose !== undefined;
var hasDecompose = object.decompose !== undefined;
if (hasCompose && hasDecompose) {
if (typeof object.compose != 'function') {
throw Error(errorPrefix + 'compose must be a function.');
} else if (typeof object.decompose != 'function') {
throw Error(errorPrefix + 'decompose must be a function.');
}
return true;
} else if (!hasCompose && !hasDecompose) {
return false;
}
throw Error(errorPrefix +
'Must have both or neither of "compose" and "decompose"');
Blockly.Extensions.checkXmlHooks_ = function(object, errorPrefix) {
return Blockly.Extensions.checkHasFunctionPair_(
object, 'mutationToDom', 'domToMutation', errorPrefix);
};
/**
* Check that a block has required mutator properties. This should be called
* after applying a mutation extension.
* Checks if the given object has both the 'saveExtraState' and 'loadExtraState'
* functions.
* @param {!Object} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @param {!Blockly.Block} block The block to inspect.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @private
*/
Blockly.Extensions.checkBlockHasMutatorProperties_ = function(errorPrefix,
block) {
if (typeof block.domToMutation != 'function') {
throw Error(errorPrefix + 'Applying a mutator didn\'t add "domToMutation"');
}
if (typeof block.mutationToDom != 'function') {
throw Error(errorPrefix + 'Applying a mutator didn\'t add "mutationToDom"');
}
Blockly.Extensions.checkJsonHooks_ = function(object, errorPrefix) {
return Blockly.Extensions.checkHasFunctionPair_(
object, 'saveExtraState', 'loadExtraState', errorPrefix);
};
/**
* Checks if the given object has both the 'compose' and 'decompose' functions.
* @param {!Object} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @private
*/
Blockly.Extensions.checkMutatorDialog_ = function(object, errorPrefix) {
return Blockly.Extensions
.checkHasFunctionPair_(object, 'compose', 'decompose', errorPrefix);
};
/**
* Checks that the given object has both or neither of the given functions, and
* that they are indeed functions.
* @param {!Object} object The object to check.
* @param {string} name1 The name of the first function in the pair.
* @param {string} name2 The name of the second function in the pair.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} If the object has only one of the functions, or either is
* not actually a function.
* @private
*/
Blockly.Extensions.checkHasFunctionPair_ =
function(object, name1, name2, errorPrefix) {
var has1 = object[name1] !== undefined;
var has2 = object[name2] !== undefined;
if (has1 && has2) {
if (typeof object[name1] != 'function') {
throw Error(errorPrefix + name1 + ' must be a function.');
} else if (typeof object[name2] != 'function') {
throw Error(errorPrefix + name2 + ' must be a function.');
}
return true;
} else if (!has1 && !has2) {
return false;
}
throw Error(errorPrefix +
'Must have both or neither of "' + name1 + '" and "' + name2 + '"');
};
/**
* Checks that the given object required mutator properties.
* @param {string} errorPrefix The string to prepend to any error message.
* @param {!Object} object The object to inspect.
* @private
*/
Blockly.Extensions.checkHasMutatorProperties_ = function(errorPrefix, object) {
var hasXmlHooks = Blockly.Extensions.checkXmlHooks_(object, errorPrefix);
var hasJsonHooks = Blockly.Extensions.checkJsonHooks_(object, errorPrefix);
if (!hasXmlHooks && !hasJsonHooks) {
throw Error(errorPrefix +
'Mutations must contain either XML hooks, or JSON hooks, or both');
}
// A block with a mutator isn't required to have a mutation dialog, but
// it should still have both or neither of compose and decompose.
Blockly.Extensions.checkMutatorDialog_(block, errorPrefix);
Blockly.Extensions.checkMutatorDialog_(object, errorPrefix);
};
/**
@@ -270,6 +310,12 @@ Blockly.Extensions.getMutatorProperties_ = function(block) {
if (block.mutationToDom !== undefined) {
result.push(block.mutationToDom);
}
if (block.saveExtraState !== undefined) {
result.push(block.saveExtraState);
}
if (block.loadExtraState !== undefined) {
result.push(block.loadExtraState);
}
if (block.compose !== undefined) {
result.push(block.compose);
}
+20
View File
@@ -415,6 +415,26 @@ Blockly.Field.prototype.toXml = function(fieldElement) {
return fieldElement;
};
/**
* Saves this fields value as something which can be serialized to JSON. Should
* only be called by the serialization system.
* @return {*} JSON serializable state.
* @package
*/
Blockly.Field.prototype.saveState = function() {
return this.getValue();
};
/**
* Sets the field's state based on the given state value. Should only be called
* by the serialization system.
* @param {*} state The state we want to apply to the field.
* @package
*/
Blockly.Field.prototype.loadState = function(state) {
this.setValue(state);
};
/**
* Dispose of all DOM objects and events belonging to this editable field.
* @package
+51
View File
@@ -448,6 +448,57 @@ suite('Extensions', function() {
}, /mutationToDom/);
});
test('No saveExtraState', function() {
this.extensionsCleanup_.push('mutator_test');
chai.assert.throws(function() {
Blockly.Extensions.registerMutator('mutator_test',
{
loadExtraState: function() {
return 'loadExtraState';
},
compose: function() {
return 'composeFn';
},
decompose: function() {
return 'decomposeFn';
}
});
}, /saveExtraState/);
});
test('No loadExtraState', function() {
this.extensionsCleanup_.push('mutator_test');
chai.assert.throws(function() {
Blockly.Extensions.registerMutator('mutator_test',
{
saveExtraState: function() {
return 'saveExtraState';
},
compose: function() {
return 'composeFn';
},
decompose: function() {
return 'decomposeFn';
}
});
}, /loadExtraState/);
});
test('No serialization hooks', function() {
this.extensionsCleanup_.push('mutator_test');
chai.assert.throws(function() {
Blockly.Extensions.registerMutator('mutator_test',
{
compose: function() {
return 'composeFn';
},
decompose: function() {
return 'decomposeFn';
}
});
}, 'Mutations must contain either XML hooks, or JSON hooks, or both');
});
test('Has decompose but no compose', function() {
this.extensionsCleanup_.push('mutator_test');
chai.assert.throws(function() {