diff --git a/core/serialization.ts b/core/serialization.ts index 991b022ad..1b9739df0 100644 --- a/core/serialization.ts +++ b/core/serialization.ts @@ -13,6 +13,7 @@ goog.declareModuleId('Blockly.serialization'); import * as blocks from './serialization/blocks.js'; import * as exceptions from './serialization/exceptions.js'; import * as priorities from './serialization/priorities.js'; +import * as procedures from './serialization/procedures.js'; import * as registry from './serialization/registry.js'; import * as variables from './serialization/variables.js'; import * as workspaces from './serialization/workspaces.js'; @@ -22,6 +23,7 @@ export { blocks, exceptions, priorities, + procedures, registry, variables, workspaces, diff --git a/core/serialization/priorities.ts b/core/serialization/priorities.ts index b8c8f2613..b49c7222d 100644 --- a/core/serialization/priorities.ts +++ b/core/serialization/priorities.ts @@ -21,6 +21,12 @@ goog.declareModuleId('Blockly.serialization.priorities'); * @alias Blockly.serialization.priorities.VARIABLES */ export const VARIABLES = 100; + +/** + * The priority for deserializing variable data. + */ +export const PROCEDURES = 75; + /** * The priority for deserializing blocks. * diff --git a/core/serialization/procedures.ts b/core/serialization/procedures.ts new file mode 100644 index 000000000..606104fb4 --- /dev/null +++ b/core/serialization/procedures.ts @@ -0,0 +1,152 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {IParameterModel} from '../interfaces/i_parameter_model.js'; +import {IProcedureModel} from '../interfaces/i_procedure_model.js'; +import type {ISerializer} from '../interfaces/i_serializer.js'; +import {ObservableProcedureModel} from '../procedures/observable_procedure_model.js'; +import {ObservableParameterModel} from '../procedures/observable_parameter_model.js'; +import * as priorities from './priorities.js'; +import * as serializationRegistry from './registry.js'; +import type {Workspace} from '../workspace.js'; + + +/** + * Representation of a procedure data model. + */ +export interface State { + id: string, name: string, returnTypes: string[]|null, + parameters?: ParameterState[], +} + +/** + * Representation of a parameter data model. + */ +export interface ParameterState { + id: string, name: string, types?: string[], +} + +/** + * A newable signature for an IProcedureModel. + * + * Refer to + * https://www.typescriptlang.org/docs/handbook/2/generics.html#using-class-types-in-generics + * for what is going on with this. + */ +type ProcedureModelConstructor = + new (workspace: Workspace, name: string, id: string) => ProcedureModel; + +/** + * A newable signature for an IParameterModel. + * + * Refer to + * https://www.typescriptlang.org/docs/handbook/2/generics.html#using-class-types-in-generics + * for what is going on with this. + */ +type ParameterModelConstructor = + new (workspace: Workspace, name: string, id: string) => ParameterModel; + + +/** Serializes the given IProcedureModel to JSON. */ +function saveProcedure(proc: IProcedureModel): State { + const state: State = { + id: proc.getId(), + name: proc.getName(), + returnTypes: proc.getReturnTypes(), + }; + if (!proc.getParameters().length) return state; + state.parameters = proc.getParameters().map((param) => saveParameter(param)); + return state; +} + +/** Serializes the given IParameterModel to JSON. */ +function saveParameter(param: IParameterModel): ParameterState { + const state: ParameterState = { + id: param.getId(), + name: param.getName(), + }; + if (!param.getTypes().length) return state; + state.types = param.getTypes(); + return state; +} + +/** Deserializes the given procedure model State from JSON. */ +function +loadProcedure( + procedureModelClass: ProcedureModelConstructor, + parameterModelClass: ParameterModelConstructor, + state: State, workspace: Workspace): ProcedureModel { + const proc = new procedureModelClass(workspace, state.name, state.id) + .setReturnTypes(state.returnTypes); + if (!state.parameters) return proc; + for (const [index, param] of state.parameters.entries()) { + proc.insertParameter( + loadParameter(parameterModelClass, param, workspace), index); + } + return proc; +} + +/** Deserializes the given ParameterState from JSON. */ +function loadParameter( + parameterModelClass: ParameterModelConstructor, + state: ParameterState, workspace: Workspace): ParameterModel { + return new parameterModelClass(workspace, state.name, state.id) + .setTypes(state.types || []); +} + +/** Serializer for saving and loading procedure state. */ +export class ProcedureSerializer implements ISerializer { + public priority = priorities.PROCEDURES; + + /** + * Constructs the procedure serializer. + * + * Example usage: + * new ProcedureSerializer(MyProcedureModelClass, MyParameterModelClass) + * + * @param procedureModelClass The class (implementing IProcedureModel) that + * you want this serializer to deserialize. + * @param parameterModelClass The class (implementing IParameterModel) that + * you want this serializer to deserialize. + */ + constructor( + private readonly procedureModelClass: + ProcedureModelConstructor, + private readonly parameterModelClass: + ParameterModelConstructor) {} + + /** Serializes the procedure models of the given workspace. */ + save(workspace: Workspace): State[]|null { + return workspace.getProcedureMap().getProcedures().map( + (proc) => saveProcedure(proc)); + } + + /** + * Deserializes the procedures models defined by the given state into the + * workspace. + */ + load(state: State[], workspace: Workspace) { + const map = workspace.getProcedureMap(); + for (const procState of state) { + map.add(loadProcedure( + this.procedureModelClass, this.parameterModelClass, procState, + workspace)); + } + } + + /** Disposes of any procedure models that exist on the workspace. */ + clear(workspace: Workspace) { + workspace.getProcedureMap().clear(); + } +} + +serializationRegistry.register( + 'procedures', + new ProcedureSerializer( + ObservableProcedureModel, ObservableParameterModel)); diff --git a/tests/mocha/jso_deserialization_test.js b/tests/mocha/jso_deserialization_test.js index 3fa342ed0..889e93b7b 100644 --- a/tests/mocha/jso_deserialization_test.js +++ b/tests/mocha/jso_deserialization_test.js @@ -714,12 +714,11 @@ suite('JSO Deserialization', function() { }); }); - // TODO(#6522): Unskip tests. - suite.skip('Procedures', function() { + suite('Procedures', function() { class MockProcedureModel { - constructor(id) { + constructor(workspace, name, id) { this.id = id ?? Blockly.utils.idGenerator.genUid(); - this.name = ''; + this.name = name; this.parameters = []; this.returnTypes = null; this.enabled = true; @@ -776,7 +775,7 @@ suite('JSO Deserialization', function() { } class MockParameterModel { - constructor(name, id) { + constructor(workspace, name, id) { this.id = id ?? Blockly.utils.idGenerator.genUid(); this.name = name; this.types = []; @@ -808,7 +807,7 @@ suite('JSO Deserialization', function() { setup(function() { this.procedureSerializer = new Blockly.serialization.procedures.ProcedureSerializer( - MockProcedureModel, MockParameterModel); + MockProcedureModel, MockParameterModel); this.procedureMap = this.workspace.getProcedureMap(); }); diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index 1fd218134..55c014840 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -795,8 +795,7 @@ suite('JSO Serialization', function() { }); }); - // TODO(#6522): Unskip serialization tests. - suite.skip('Procedures', function() { + suite('Procedures', function() { class MockProcedureModel { constructor() { this.id = Blockly.utils.idGenerator.genUid(); @@ -968,7 +967,7 @@ suite('JSO Serialization', function() { new MockProcedureModel().insertParameter(parameterModel, 0)); const jso = Blockly.serialization.workspaces.save(this.workspace); const parameter = jso['procedures'][0]['parameters'][0]; - assertProperty(parameter, 'id', 'testparam'); + assertProperty(parameter, 'name', 'testparam'); }); });