Skip to content
Merged
Show file tree
Hide file tree
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
24 changes: 21 additions & 3 deletions core/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,22 @@ export abstract class Field<T = any>
workspace.getMarker(MarkerManager.LOCAL_MARKER)!.draw();
}
}

/**
* Subclasses should reimplement this method to construct their Field
* subclass from a JSON arg object.
*
* It is an error to attempt to register a field subclass in the
* FieldRegistry if that subclass has not overridden this method.
*
* @param _options JSON configuration object with properties needed
* to configure a specific field.
*/
static fromJson(_options: FieldConfig): Field {
Comment thread
maribethb marked this conversation as resolved.
throw new Error(
`Attempted to instantiate a field from the registry that hasn't defined a 'fromJson' method.`,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line length.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are bikeshedding this in the team chat if you want to join.

);
}
}

/**
Expand All @@ -1429,12 +1445,14 @@ export interface FieldConfig {
}

/**
* For use by Field and descendants of Field. Constructors can change
* Represents an object that has all the prototype properties of the `Field`
* class. This is necessary because constructors can change
* in descendants, though they should contain all of Field's prototype methods.
*
* @internal
* This type should only be used in places where we directly access the prototype
* of a Field class or subclass.
*/
export type FieldProto = Pick<typeof Field, 'prototype'>;
type FieldProto = Pick<typeof Field, 'prototype'>;

/**
* Represents an error where the field is trying to access its block or
Expand Down
2 changes: 1 addition & 1 deletion core/field_angle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export class FieldAngle extends FieldInput<number> {
* @nocollapse
* @internal
*/
static fromJson(options: FieldAngleFromJsonConfig): FieldAngle {
static override fromJson(options: FieldAngleFromJsonConfig): FieldAngle {
// `this` might be a subclass of FieldAngle if that class doesn't override
// the static fromJson method.
return new this(options.angle, undefined, options);
Expand Down
4 changes: 3 additions & 1 deletion core/field_checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ export class FieldCheckbox extends Field<CheckboxBool> {
* @nocollapse
* @internal
*/
static fromJson(options: FieldCheckboxFromJsonConfig): FieldCheckbox {
static override fromJson(
options: FieldCheckboxFromJsonConfig,
): FieldCheckbox {
// `this` might be a subclass of FieldCheckbox if that class doesn't
// 'override' the static fromJson method.
return new this(options.checked, undefined, options);
Expand Down
2 changes: 1 addition & 1 deletion core/field_colour.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ export class FieldColour extends Field<string> {
* @nocollapse
* @internal
*/
static fromJson(options: FieldColourFromJsonConfig): FieldColour {
static override fromJson(options: FieldColourFromJsonConfig): FieldColour {
// `this` might be a subclass of FieldColour if that class doesn't override
// the static fromJson method.
return new this(options.colour, undefined, options);
Expand Down
4 changes: 3 additions & 1 deletion core/field_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,9 @@ export class FieldDropdown extends Field<string> {
* @nocollapse
* @internal
*/
static fromJson(options: FieldDropdownFromJsonConfig): FieldDropdown {
static override fromJson(
options: FieldDropdownFromJsonConfig,
): FieldDropdown {
if (!options.options) {
throw new Error(
'options are required for the dropdown field. The ' +
Expand Down
2 changes: 1 addition & 1 deletion core/field_image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export class FieldImage extends Field<string> {
* @nocollapse
* @internal
*/
static fromJson(options: FieldImageFromJsonConfig): FieldImage {
static override fromJson(options: FieldImageFromJsonConfig): FieldImage {
if (!options.src || !options.width || !options.height) {
throw new Error(
'src, width, and height values for an image field are' +
Expand Down
2 changes: 1 addition & 1 deletion core/field_label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export class FieldLabel extends Field<string> {
* @nocollapse
* @internal
*/
static fromJson(options: FieldLabelFromJsonConfig): FieldLabel {
static override fromJson(options: FieldLabelFromJsonConfig): FieldLabel {
const text = parsing.replaceMessageReferences(options.text);
// `this` might be a subclass of FieldLabel if that class doesn't override
// the static fromJson method.
Expand Down
2 changes: 1 addition & 1 deletion core/field_number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export class FieldNumber extends FieldInput<number> {
* @nocollapse
* @internal
*/
static fromJson(options: FieldNumberFromJsonConfig): FieldNumber {
static override fromJson(options: FieldNumberFromJsonConfig): FieldNumber {
// `this` might be a subclass of FieldNumber if that class doesn't override
// the static fromJson method.
return new this(
Expand Down
51 changes: 42 additions & 9 deletions core/field_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,46 @@

// Former goog.module ID: Blockly.fieldRegistry

import type {Field, FieldProto} from './field.js';
import type {Field, FieldConfig} from './field.js';
import * as registry from './registry.js';

interface RegistryOptions {
/**
* When constructing a field from JSON using the registry, the
* `fromJson` method in this file is called with an options parameter
* object consisting of the "type" which is the name of the field, and
* other options that are part of the field's config object.
*
* These options are then passed to the field's static `fromJson`
* method. That method accepts an options parameter with a type that usually
* extends from FieldConfig, and may or may not have a "type" attribute (in
* fact, it shouldn't, because we'd overwrite it as described above!)
*
* Unfortunately the registry has no way of knowing the actual Field subclass
* that will be returned from passing in the name of the field. Therefore it
* also has no way of knowing that the options object not only implements
* `FieldConfig`, but it also should satisfy the Config that belongs to that
* specific class's `fromJson` method.
*
* Because of this uncertainty, we just give up on type checking the properties
* passed to the `fromJson` method, and allow arbitrary string keys with
* unknown types.
*/
type RegistryOptions = FieldConfig & {
// The name of the field, e.g. field_dropdown
type: string;
[key: string]: unknown;
};

/**
* Represents the static methods that must be defined on any
* field that is registered, i.e. the constructor and fromJson methods.
*
* Because we don't know which Field subclass will be registered, we
* are unable to typecheck the parameters of the constructor.
*/
export interface RegistrableField {
new (...args: any[]): Field;
Comment thread
cpcallen marked this conversation as resolved.
fromJson(options: FieldConfig): Field;
}

/**
Expand All @@ -25,7 +59,7 @@ interface RegistryOptions {
* @throws {Error} if the type name is empty, the field is already registered,
* or the fieldClass is not an object containing a fromJson function.
*/
export function register(type: string, fieldClass: FieldProto) {
export function register(type: string, fieldClass: RegistrableField) {
registry.register(registry.Type.FIELD, type, fieldClass);
}

Expand Down Expand Up @@ -59,7 +93,10 @@ export function fromJson<T>(options: RegistryOptions): Field<T> | null {
* @param options
*/
function fromJsonInternal<T>(options: RegistryOptions): Field<T> | null {
const fieldObject = registry.getObject(registry.Type.FIELD, options.type);
const fieldObject = registry.getObject(
registry.Type.FIELD,
options.type,
) as unknown as RegistrableField;
if (!fieldObject) {
console.warn(
'Blockly could not create a field of type ' +
Expand All @@ -69,12 +106,8 @@ function fromJsonInternal<T>(options: RegistryOptions): Field<T> | null {
' #1584), or the registration is not being reached.',
);
return null;
} else if (typeof (fieldObject as any).fromJson !== 'function') {
throw new TypeError('returned Field was not a IRegistrableField');
Comment thread
cpcallen marked this conversation as resolved.
} else {
type fromJson = (options: {}) => Field<T>;
return (fieldObject as unknown as {fromJson: fromJson}).fromJson(options);
}
return fieldObject.fromJson(options);
}

export const TEST_ONLY = {
Expand Down
4 changes: 3 additions & 1 deletion core/field_textinput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export class FieldTextInput extends FieldInput<string> {
* @nocollapse
* @internal
*/
static fromJson(options: FieldTextInputFromJsonConfig): FieldTextInput {
static override fromJson(
options: FieldTextInputFromJsonConfig,
): FieldTextInput {
const text = parsing.replaceMessageReferences(options.text);
// `this` might be a subclass of FieldTextInput if that class doesn't
// override the static fromJson method.
Expand Down
22 changes: 18 additions & 4 deletions tests/mocha/field_registry_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,10 @@ suite('Field Registry', function () {
}, 'Invalid name');
});
test('No fromJson', function () {
const fromJson = CustomFieldType.fromJson;
delete CustomFieldType.fromJson;
class IncorrectField {}
chai.assert.throws(function () {
Blockly.fieldRegistry.register('field_custom_test', CustomFieldType);
Blockly.fieldRegistry.register('field_custom_test', IncorrectField);
}, 'must have a fromJson function');
CustomFieldType.fromJson = fromJson;
});
test('fromJson not a function', function () {
const fromJson = CustomFieldType.fromJson;
Expand Down Expand Up @@ -97,5 +95,21 @@ suite('Field Registry', function () {
chai.assert.isNotNull(field);
chai.assert.equal(field.getValue(), 'ok');
});
test('Did not override fromJson', function () {
// This class will have a fromJson method, so it can be registered
// but it doesn't override the abstract class's method so it throws
class IncorrectField extends Blockly.Field {}

Blockly.fieldRegistry.register('field_custom_test', IncorrectField);

const json = {
type: 'field_custom_test',
value: 'ok',
};

chai.assert.throws(function () {
Blockly.fieldRegistry.fromJson(json);
}, 'Attempted to instantiate a field from the registry');
});
});
});