UNPKG

@jbrowse/core

Version:

JBrowse 2 core libraries used by plugins

231 lines (230 loc) 9.22 kB
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); }