Skip to content

Commit

Permalink
Merge pull request #93 from flatironinstitute/type-guards
Browse files Browse the repository at this point in the history
type guards for SPAnalysisDataModel
  • Loading branch information
WardBrian authored Jun 28, 2024
2 parents 7c55ce0 + 6d6536d commit acdfe1d
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 16 deletions.
1 change: 1 addition & 0 deletions gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const SPAnalysisContextProvider: FunctionComponent<PropsWithChildren<SPAnalysisC
const savedState = localStorage.getItem('stan-playground-saved-state')
if (!savedState) return
const parsedData = deserializeAnalysisFromLocalStorage(savedState)
if (!parsedData) return // unsuccessful parse or type cast
update({ type: 'loadInitialData', state: parsedData })
}
// once we have loaded some data, we don't need the localStorage again
Expand Down
41 changes: 40 additions & 1 deletion gui/src/app/SPAnalysis/SPAnalysisDataModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SamplingOpts, defaultSamplingOpts } from "../StanSampler/StanSampler"
import { SamplingOpts, defaultSamplingOpts, isSamplingOpts } from "../StanSampler/StanSampler"

export enum SPAnalysisKnownFiles {
STANFILE = 'stanFileContent',
Expand All @@ -9,28 +9,67 @@ type SPAnalysisFiles = {
[filetype in SPAnalysisKnownFiles]: string
}

const isSPAnalysisFiles = (x: any): x is SPAnalysisFiles => {
if (!x) return false
if (typeof x !== 'object') return false
for (const k of Object.values(SPAnalysisKnownFiles)) {
if (typeof x[k] !== 'string') return false
}
return true
}

type SPAnalysisBase = SPAnalysisFiles &
{
samplingOpts: SamplingOpts
}

const isSPAnalysisBase = (x: any): x is SPAnalysisBase => {
if (!x) return false
if (typeof x !== 'object') return false
if (!isSamplingOpts(x.samplingOpts)) return false
if (!isSPAnalysisFiles(x)) return false
return true
}

type SPAnalysisMetadata = {
title: string
}

export const isSPAnalysisMetaData = (x: any): x is SPAnalysisMetadata => {
if (!x) return false
if (typeof x !== 'object') return false
if (typeof x.title !== 'string') return false
return true
}

type SPAnalysisEphemeralData = SPAnalysisFiles & {
// possible future things to track include the compilation status
// of the current stan src file(s)
// not implemented in this PR, but we need some content for the type
server?: string
}

const isSPAnalysisEphemeralData = (x: any): x is SPAnalysisEphemeralData => {
if (!isSPAnalysisFiles(x)) return false
return true
}

export type SPAnalysisDataModel = SPAnalysisBase &
{
meta: SPAnalysisMetadata,
ephemera: SPAnalysisEphemeralData
}

export const isSPAnalysisDataModel = (x: any): x is SPAnalysisDataModel => {
if (!x) return false
if (typeof x !== 'object') return false
if (!isSPAnalysisMetaData(x.meta)) return false
if (!isSPAnalysisEphemeralData(x.ephemera)) return false
if (!isSPAnalysisBase(x)) return false
return true
}


export type SPAnalysisPersistentDataModel = Omit<SPAnalysisDataModel, "ephemera">

export const initialDataModel: SPAnalysisDataModel = {
Expand Down
9 changes: 7 additions & 2 deletions gui/src/app/SPAnalysis/SPAnalysisQueryLoading.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SPAnalysisDataModel, initialDataModel, persistStateToEphemera } from "./SPAnalysisDataModel";
import { isSamplingOpts } from "../StanSampler/StanSampler";


enum QueryParamKeys {
Expand Down Expand Up @@ -85,8 +86,12 @@ export const fetchRemoteAnalysis = async (query: QueryParams) => {
if (sampling_opts) {
try {
const parsed = JSON.parse(sampling_opts)
// TODO validate the parsed object
data.samplingOpts = parsed
if (isSamplingOpts(parsed)) {
data.samplingOpts = parsed
}
else {
console.error('Invalid sampling_opts in fetchRemoteAnalysis', parsed)
}
} catch (err) {
console.error('Failed to parse sampling_opts', err)
}
Expand Down
21 changes: 16 additions & 5 deletions gui/src/app/SPAnalysis/SPAnalysisReducer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Reducer } from "react"
import { Stanie } from "../exampleStanies/exampleStanies"
import { defaultSamplingOpts, SamplingOpts } from '../StanSampler/StanSampler'
import { defaultSamplingOpts, isSamplingOpts, SamplingOpts } from '../StanSampler/StanSampler'
import { FieldsContentsMap } from "./FileMapping"
import { initialDataModel, persistStateToEphemera, SPAnalysisDataModel, SPAnalysisKnownFiles } from "./SPAnalysisDataModel"
import { initialDataModel, isSPAnalysisMetaData, persistStateToEphemera, SPAnalysisDataModel, SPAnalysisKnownFiles } from "./SPAnalysisDataModel"


export type SPAnalysisReducerType = Reducer<SPAnalysisDataModel, SPAnalysisReducerAction>
Expand Down Expand Up @@ -52,7 +52,14 @@ export const SPAnalysisReducer = (s: SPAnalysisDataModel, a: SPAnalysisReducerAc
}
}
case "loadFiles": {
return loadFromProjectFiles(s, a.files, a.clearExisting)
try {
return loadFromProjectFiles(s, a.files, a.clearExisting)
}
catch (e) {
// probably sampling opts or meta files were not valid
console.error('Error loading files', e)
return s
}
}
case "retitle": {
return {
Expand Down Expand Up @@ -84,14 +91,18 @@ export const SPAnalysisReducer = (s: SPAnalysisDataModel, a: SPAnalysisReducerAc

const loadMetaFromString = (data: SPAnalysisDataModel, json: string, clearExisting: boolean = false): SPAnalysisDataModel => {
const newMeta = JSON.parse(json)
// TODO: properly check type of deserialized meta
if (!isSPAnalysisMetaData(newMeta)) {
throw Error('Deserialized meta is not valid')
}
const newMetaMember = clearExisting ? { ...newMeta } : { ...data.meta, ...newMeta }
return { ...data, meta: newMetaMember }
}

const loadSamplingOptsFromString = (data: SPAnalysisDataModel, json: string, clearExisting: boolean = false): SPAnalysisDataModel => {
const newSampling = JSON.parse(json)
// TODO: properly check type/fields of deserialized sampling opts
if (!isSamplingOpts(newSampling)) {
throw Error('Deserialized sampling opts are not valid')
}
const newSamplingOptsMember = clearExisting ? { ...newSampling } : { ...data.samplingOpts, ...newSampling }
return { ...data, samplingOpts: newSamplingOptsMember }
}
Expand Down
26 changes: 18 additions & 8 deletions gui/src/app/SPAnalysis/SPAnalysisSerialization.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import JSZip from "jszip"
import { replaceSpacesWithUnderscores } from "../util/replaceSpaces"
import { FileNames, FileRegistry, mapFileContentsToModel, mapModelToFileManifest, SPAnalysisFileMap } from "./FileMapping"
import { getStringKnownFileKeys, SPAnalysisDataModel } from "./SPAnalysisDataModel"
import { getStringKnownFileKeys, isSPAnalysisDataModel, SPAnalysisDataModel } from "./SPAnalysisDataModel"

export const serializeAnalysisToLocalStorage = (data: SPAnalysisDataModel): string => {
const intermediary = {
...data, ephemera: undefined }
return JSON.stringify(intermediary)
}

export const deserializeAnalysisFromLocalStorage = (serialized: string): SPAnalysisDataModel => {
const intermediary = JSON.parse(serialized)
// Not sure if this is strictly necessary
intermediary.ephemera = {}
const stringFileKeys = getStringKnownFileKeys()
stringFileKeys.forEach((k) => intermediary.ephemera[k] = intermediary[k]);
return intermediary as SPAnalysisDataModel
export const deserializeAnalysisFromLocalStorage = (serialized: string): SPAnalysisDataModel | undefined => {
try {
const intermediary = JSON.parse(serialized)
// Not sure if this is strictly necessary
intermediary.ephemera = {}
const stringFileKeys = getStringKnownFileKeys()
stringFileKeys.forEach((k) => intermediary.ephemera[k] = intermediary[k]);
if (!isSPAnalysisDataModel(intermediary)) {
console.warn(intermediary)
throw Error('Deserialized data is not a valid SPAnalysisDataModel')
}
return intermediary
}
catch (e) {
console.error('Error deserializing data from local storage', e)
return undefined
}
}

export const serializeAsZip = async (data: SPAnalysisDataModel): Promise<[Blob, string]> => {
Expand Down
11 changes: 11 additions & 0 deletions gui/src/app/StanSampler/StanSampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ export type SamplingOpts = {
seed: number | undefined
}

export const isSamplingOpts = (x: any): x is SamplingOpts => {
if (!x) return false
if (typeof x !== 'object') return false
if (typeof x.num_chains !== 'number') return false
if (typeof x.num_warmup !== 'number') return false
if (typeof x.num_samples !== 'number') return false
if (typeof x.init_radius !== 'number') return false
if (x.seed !== undefined && typeof x.seed !== 'number') return false
return true
}

export const defaultSamplingOpts: SamplingOpts = {
num_chains: 4,
num_warmup: 1000,
Expand Down

0 comments on commit acdfe1d

Please sign in to comment.