@jbrowse/core
Version:
JBrowse 2 core libraries used by plugins
231 lines (230 loc) • 9.22 kB
JavaScript
import { getEnv, getRoot, getSnapshot, isLateType, isStateTreeNode, isType, resolveIdentifier, types, } from '@jbrowse/mobx-state-tree';
import ConfigSlot from "./configurationSlot.js";
import { isConfigurationSchemaType } from "./util.js";
import { getContainingTrack, getSession } from "../util/index.js";
import { ElementId } from "../util/types/mst.js";
function isEmptyObject(thing) {
return (typeof thing === 'object' &&
!Array.isArray(thing) &&
thing !== null &&
Object.keys(thing).length === 0);
}
function isEmptyArray(thing) {
return Array.isArray(thing) && thing.length === 0;
}
function preprocessConfigurationSchemaArguments(modelName, inputSchemaDefinition, inputOptions = {}) {
if (typeof modelName !== 'string') {
throw new Error('first arg must be string name of the model that this config schema goes with');
}
let schemaDefinition = inputSchemaDefinition;
let options = inputOptions;
if (inputOptions.baseConfiguration?.jbrowseSchemaDefinition) {
schemaDefinition = {
...inputOptions.baseConfiguration.jbrowseSchemaDefinition,
...schemaDefinition,
};
options = {
...inputOptions.baseConfiguration.jbrowseSchemaOptions,
...inputOptions,
baseConfiguration: undefined,
};
}
return { schemaDefinition, options };
}
function makeConfigurationSchemaModel(modelName, schemaDefinition, options) {
const modelDefinition = {};
let identifier;
if (options.explicitlyTyped) {
modelDefinition.type = types.optional(types.literal(modelName), modelName);
}
if (options.explicitIdentifier && options.implicitIdentifier) {
throw new Error(`Cannot have both explicit and implicit identifiers in ${modelName}`);
}
if (options.explicitIdentifier) {
if (typeof options.explicitIdentifier === 'string') {
modelDefinition[options.explicitIdentifier] = types.identifier;
identifier = options.explicitIdentifier;
}
else {
modelDefinition.id = types.identifier;
identifier = 'id';
}
}
else if (options.implicitIdentifier) {
if (typeof options.implicitIdentifier === 'string') {
modelDefinition[options.implicitIdentifier] = ElementId;
identifier = options.implicitIdentifier;
}
else {
modelDefinition.id = ElementId;
identifier = 'id';
}
}
const volatileConstants = {
isJBrowseConfigurationSchema: true,
jbrowseSchema: {
modelName,
definition: schemaDefinition,
options,
},
};
for (const [slotName, slotDefinition] of Object.entries(schemaDefinition)) {
if ((isType(slotDefinition) && isLateType(slotDefinition)) ||
isConfigurationSchemaType(slotDefinition)) {
modelDefinition[slotName] = slotDefinition;
}
else if (typeof slotDefinition === 'string' ||
typeof slotDefinition === 'number') {
volatileConstants[slotName] = slotDefinition;
}
else if (typeof slotDefinition === 'object') {
if (!slotDefinition.type) {
throw new Error(`no type set for config slot ${modelName}.${slotName}`);
}
try {
modelDefinition[slotName] = ConfigSlot(slotName, slotDefinition);
}
catch (e) {
throw new Error(`invalid config slot definition for ${modelName}.${slotName}: ${e}`);
}
}
else {
throw new Error(`invalid configuration schema definition, "${slotName}" must be either a valid configuration slot definition, a constant, or a nested configuration schema`);
}
}
let completeModel = types
.model(`${modelName}ConfigurationSchema`, modelDefinition)
.actions(self => ({
setSubschema(slotName, data) {
if (!isConfigurationSchemaType(modelDefinition[slotName])) {
throw new Error(`${slotName} is not a subschema, cannot replace`);
}
const newSchema = isStateTreeNode(data)
? data
: modelDefinition[slotName].create(data);
self[slotName] = newSchema;
return newSchema;
},
}));
if (Object.keys(volatileConstants).length) {
completeModel = completeModel.volatile(() => volatileConstants);
}
if (options.actions) {
completeModel = completeModel.actions(options.actions);
}
if (options.views) {
completeModel = completeModel.views(options.views);
}
if (options.extend) {
completeModel = completeModel.extend(options.extend);
}
const identifierDefault = identifier ? { [identifier]: 'placeholderId' } : {};
const modelDefault = options.explicitlyTyped
? { type: modelName, ...identifierDefault }
: identifierDefault;
const defaultSnap = getSnapshot(completeModel.create(modelDefault));
completeModel = completeModel.postProcessSnapshot(snap => {
const newSnap = {};
let matchesDefault = true;
for (const [key, value] of Object.entries(snap)) {
if (matchesDefault) {
if (typeof defaultSnap[key] === 'object' && typeof value === 'object') {
if (JSON.stringify(defaultSnap[key]) !== JSON.stringify(value)) {
matchesDefault = false;
}
}
else if (defaultSnap[key] !== value) {
matchesDefault = false;
}
}
if (value !== undefined &&
volatileConstants[key] === undefined &&
!isEmptyObject(value) &&
!isEmptyArray(value)) {
newSnap[key] = value;
}
}
if (matchesDefault) {
return {};
}
return newSnap;
});
if (options.preProcessSnapshot) {
completeModel = completeModel.preProcessSnapshot(options.preProcessSnapshot);
}
return types.optional(completeModel, modelDefault);
}
export function ConfigurationSchema(modelName, inputSchemaDefinition, inputOptions) {
const { schemaDefinition, options } = preprocessConfigurationSchemaArguments(modelName, inputSchemaDefinition, inputOptions);
const schemaType = makeConfigurationSchemaModel(modelName, schemaDefinition, options);
schemaType.isJBrowseConfigurationSchema = true;
schemaType.jbrowseSchemaDefinition = schemaDefinition;
schemaType.jbrowseSchemaOptions = options;
return schemaType;
}
export function TrackConfigurationReference(schemaType) {
const trackRef = types.reference(schemaType, {
get(id, parent) {
const session = getSession(parent);
let ret = session.getTracksById()[id];
if (!ret) {
ret = resolveIdentifier(schemaType, getRoot(parent), id);
}
if (!ret) {
throw new Error(`Could not resolve identifier "${id}"`);
}
return isStateTreeNode(ret) ? ret : schemaType.create(ret, getEnv(parent));
},
set(value) {
return value.trackId;
},
});
return types.snapshotProcessor(types.union({
dispatcher: snapshot => typeof snapshot === 'string' ? trackRef : schemaType,
}, trackRef, schemaType), {
postProcessor(snapshot) {
if (typeof snapshot === 'object' &&
snapshot !== null &&
'trackId' in snapshot) {
return snapshot.trackId;
}
return snapshot;
},
});
}
export function DisplayConfigurationReference(schemaType) {
const displayRef = types.reference(schemaType, {
get(id, parent) {
const track = getContainingTrack(parent);
const displays = track.configuration.displays || [];
let ret = displays.find((d) => d.displayId === id);
if (!ret) {
const displayType = `${id}`.split('-').slice(1).join('-');
if (displayType) {
ret = { displayId: `${id}`, type: displayType };
}
}
if (!ret) {
throw new Error(`Display configuration "${id}" not found`);
}
return isStateTreeNode(ret) ? ret : schemaType.create(ret, getEnv(parent));
},
set(value) {
return value.displayId;
},
});
return types.union({
dispatcher: snapshot => typeof snapshot === 'string' ? displayRef : schemaType,
}, displayRef, schemaType);
}
export function ConfigurationReference(schemaType) {
const name = schemaType.name;
if (name.includes('TrackConfigurationSchema') &&
!name.includes('DisplayConfigurationSchema')) {
return TrackConfigurationReference(schemaType);
}
if (name.includes('DisplayConfigurationSchema')) {
return DisplayConfigurationReference(schemaType);
}
return types.union(types.reference(schemaType), schemaType);
}