Skip to content

Commit

Permalink
feat: add bundleListing, customLookups, and customOutputs settings to…
Browse files Browse the repository at this point in the history
… control code generation (#504)

Fixes #503
  • Loading branch information
chrispcampbell authored Aug 16, 2024
1 parent 5690055 commit fcea642
Show file tree
Hide file tree
Showing 33 changed files with 921 additions and 157 deletions.
4 changes: 2 additions & 2 deletions examples/sir/config/model.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
graph default min time,graph default max time,model dat files
0,100,
graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs
0,100,,false,false,false
4 changes: 2 additions & 2 deletions examples/template-default/config/model.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
graph default min time,graph default max time,model dat files
0,100,
graph default min time,graph default max time,model dat files,bundle listing,custom lookups,custom outputs
0,100,,false,false,false
53 changes: 53 additions & 0 deletions packages/build/docs/interfaces/ModelSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,59 @@ model.

___

### bundleListing

`Optional` **bundleListing**: `boolean`

Whether to bundle a model listing with the generated model.

If undefined, defaults to false.

When this is true, a model listing will be bundled with the generated
model to allow the `runtime` package to resolve variables that are
referenced by name or identifier. This listing will increase the size
of the generated model, so it is recommended to set this to true only
if it is needed.

___

### customLookups

`Optional` **customLookups**: `boolean` \| `string`[]

Whether to allow lookups to be overridden at runtime using `setLookup`.

If undefined or false, the generated model will implement `setLookup`
as a no-op, meaning that lookups cannot be overridden at runtime.

If true, all lookups in the generated model will be available to be
overridden.

If an array is provided, only those variable names in the array will
be available to be overridden.

___

### customOutputs

`Optional` **customOutputs**: `boolean` \| `string`[]

Whether to allow for capturing the data for arbitrary variables at
runtime (including variables that are not configured in the `outputs`
array).

If undefined or false, the generated model will implement `storeOutput`
as a no-op, meaning that the data for arbitrary variables cannot be
captured at runtime.

If true, all variables in the generated model will be available to be
captured at runtime.

If an array is provided, only those variable names in the array will
be available to be captured at runtime.

___

### options

`Optional` **options**: `Object`
Expand Down
51 changes: 51 additions & 0 deletions packages/build/docs/interfaces/ResolvedModelSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,57 @@ model.

___

### bundleListing

**bundleListing**: `boolean`

Whether to bundle a model listing with the generated model.

When this is true, a model listing will be bundled with the generated
model to allow the `runtime` package to resolve variables that are
referenced by name or identifier. This listing will increase the size
of the generated model, so it is recommended to set this to true only
if it is needed.

___

### customLookups

**customLookups**: `boolean` \| `string`[]

Whether to allow lookups to be overridden at runtime using `setLookup`.

If false, the generated model will contain a `setLookup` function that
throws an error, meaning that lookups cannot be overridden at runtime.

If true, all lookups in the generated model will be available to be
overridden.

If an array is provided, only those variable names in the array will
be available to be overridden.

___

### customOutputs

**customOutputs**: `boolean` \| `string`[]

Whether to allow for capturing the data for arbitrary variables at
runtime (including variables that are not configured in the `outputs`
array).

If false, the generated model will contain a `storeOutput` function
that throws an error, meaning that the data for arbitrary variables
cannot be captured at runtime.

If true, all variables in the generated model will be available to be
captured at runtime.

If an array is provided, only those variable names in the array will
be available to be captured at runtime.

___

### options

`Optional` **options**: `Object`
Expand Down
86 changes: 86 additions & 0 deletions packages/build/src/_shared/model-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,50 @@ export interface ModelSpec {
*/
datFiles?: string[]

/**
* Whether to bundle a model listing with the generated model.
*
* If undefined, defaults to false.
*
* When this is true, a model listing will be bundled with the generated
* model to allow the `runtime` package to resolve variables that are
* referenced by name or identifier. This listing will increase the size
* of the generated model, so it is recommended to set this to true only
* if it is needed.
*/
bundleListing?: boolean

/**
* Whether to allow lookups to be overridden at runtime using `setLookup`.
*
* If undefined or false, the generated model will implement `setLookup`
* as a no-op, meaning that lookups cannot be overridden at runtime.
*
* If true, all lookups in the generated model will be available to be
* overridden.
*
* If an array is provided, only those variable names in the array will
* be available to be overridden.
*/
customLookups?: boolean | VarName[]

/**
* Whether to allow for capturing the data for arbitrary variables at
* runtime (including variables that are not configured in the `outputs`
* array).
*
* If undefined or false, the generated model will implement `storeOutput`
* as a no-op, meaning that the data for arbitrary variables cannot be
* captured at runtime.
*
* If true, all variables in the generated model will be available to be
* captured at runtime.
*
* If an array is provided, only those variable names in the array will
* be available to be captured at runtime.
*/
customOutputs?: boolean | VarName[]

/** Additional options included with the SDE `spec.json` file. */
// TODO: Remove references to `spec.json`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -111,6 +155,48 @@ export interface ResolvedModelSpec {
*/
datFiles: string[]

/**
* Whether to bundle a model listing with the generated model.
*
* When this is true, a model listing will be bundled with the generated
* model to allow the `runtime` package to resolve variables that are
* referenced by name or identifier. This listing will increase the size
* of the generated model, so it is recommended to set this to true only
* if it is needed.
*/
bundleListing: boolean

/**
* Whether to allow lookups to be overridden at runtime using `setLookup`.
*
* If false, the generated model will contain a `setLookup` function that
* throws an error, meaning that lookups cannot be overridden at runtime.
*
* If true, all lookups in the generated model will be available to be
* overridden.
*
* If an array is provided, only those variable names in the array will
* be available to be overridden.
*/
customLookups: boolean | VarName[]

/**
* Whether to allow for capturing the data for arbitrary variables at
* runtime (including variables that are not configured in the `outputs`
* array).
*
* If false, the generated model will contain a `storeOutput` function
* that throws an error, meaning that the data for arbitrary variables
* cannot be captured at runtime.
*
* If true, all variables in the generated model will be available to be
* captured at runtime.
*
* If an array is provided, only those variable names in the array will
* be available to be captured at runtime.
*/
customOutputs: boolean | VarName[]

/** Additional options included with the SDE `spec.json` file. */
// TODO: Remove references to `spec.json`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
20 changes: 20 additions & 0 deletions packages/build/src/build/impl/build-once.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export async function buildOnce(
inputVarNames: modelSpec.inputVarNames,
outputVarNames: modelSpec.outputVarNames,
externalDatfiles: modelSpec.datFiles,
bundleListing: modelSpec.bundleListing,
customLookups: modelSpec.customLookups,
customOutputs: modelSpec.customOutputs,
...modelSpec.options
}
const specPath = joinPath(config.prepDir, 'spec.json')
Expand Down Expand Up @@ -217,12 +220,29 @@ function resolveModelSpec(modelSpec: ModelSpec): ResolvedModelSpec {
outputSpecs = []
}

let customLookups: boolean | VarName[]
if (modelSpec.customLookups !== undefined) {
customLookups = modelSpec.customLookups
} else {
customLookups = false
}

let customOutputs: boolean | VarName[]
if (modelSpec.customOutputs !== undefined) {
customOutputs = modelSpec.customOutputs
} else {
customOutputs = false
}

return {
inputVarNames,
inputs: inputSpecs,
outputVarNames,
outputs: outputSpecs,
datFiles: modelSpec.datFiles || [],
bundleListing: modelSpec.bundleListing === true,
customLookups,
customOutputs,
options: modelSpec.options
}
}
57 changes: 57 additions & 0 deletions packages/build/src/config/config-loader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2024 Climate Interactive / New Venture Fund

import { describe, expect, it } from 'vitest'

import { loadConfig } from './config-loader'
import { type UserConfig } from './user-config'

function config(genFormat: string | undefined): UserConfig {
return {
...(genFormat ? { genFormat: genFormat as 'js' | 'c' } : {}),
modelFiles: [],
modelSpec: async () => {
return {
inputs: [],
outputs: []
}
}
}
}

describe('config loader', () => {
it('should resolve genFormat when left undefined', async () => {
const userConfig = config(undefined)
const result = await loadConfig('production', userConfig, '', '')
if (result.isErr()) {
throw new Error('Expected ok result but got: ' + result.error.message)
}
expect(result.value.resolvedConfig.genFormat).toBe('js')
})

it('should resolve genFormat when set to js', async () => {
const userConfig = config('js')
const result = await loadConfig('production', userConfig, '', '')
if (result.isErr()) {
throw new Error('Expected ok result but got: ' + result.error.message)
}
expect(result.value.resolvedConfig.genFormat).toBe('js')
})

it('should resolve genFormat when set to c', async () => {
const userConfig = config('c')
const result = await loadConfig('production', userConfig, '', '')
if (result.isErr()) {
throw new Error('Expected ok result but got: ' + result.error.message)
}
expect(result.value.resolvedConfig.genFormat).toBe('c')
})

it('should fail if genFormat is invalid', async () => {
const userConfig = config('JS')
const result = await loadConfig('production', userConfig, '', '')
if (result.isOk()) {
throw new Error('Expected err result but got: ' + result.value)
}
expect(result.error.message).toBe(`The configured genFormat value is invalid; must be either 'js' or 'c'`)
})
})
49 changes: 48 additions & 1 deletion packages/build/tests/build-prod/build-prod.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ describe('build in production mode', () => {
expect(resolvedModelSpec!.outputVarNames).toEqual(['Z'])
expect(resolvedModelSpec!.outputs).toEqual([{ varName: 'Z' }])
expect(resolvedModelSpec!.datFiles).toEqual([])
expect(resolvedModelSpec!.bundleListing).toBe(false)
expect(resolvedModelSpec!.customLookups).toBe(false)
expect(resolvedModelSpec!.customOutputs).toBe(false)
})

it('should resolve model spec (when input/output var names are provided)', async () => {
Expand All @@ -113,7 +116,10 @@ describe('build in production mode', () => {
// Note that we return only variable names here
return {
inputs: ['Y'],
outputs: ['Z']
outputs: ['Z'],
bundleListing: true,
customLookups: ['lookup1'],
customOutputs: ['output1']
}
},
plugins: [
Expand All @@ -137,6 +143,47 @@ describe('build in production mode', () => {
expect(resolvedModelSpec!.outputVarNames).toEqual(['Z'])
expect(resolvedModelSpec!.outputs).toEqual([{ varName: 'Z' }])
expect(resolvedModelSpec!.datFiles).toEqual([])
expect(resolvedModelSpec!.bundleListing).toBe(true)
expect(resolvedModelSpec!.customLookups).toEqual(['lookup1'])
expect(resolvedModelSpec!.customOutputs).toEqual(['output1'])
})

it('should resolve model spec (when boolean is provided for customLookups and customOutputs)', async () => {
let resolvedModelSpec: ResolvedModelSpec
const userConfig: UserConfig = {
genFormat: 'c',
rootDir: resolvePath(__dirname, '..'),
prepDir: resolvePath(__dirname, 'sde-prep'),
modelFiles: [resolvePath(__dirname, '..', '_shared', 'sample.mdl')],
modelSpec: async () => {
// Note that we return only variable names here
return {
inputs: ['Y'],
outputs: ['Z'],
bundleListing: true,
customLookups: true,
customOutputs: true
}
},
plugins: [
{
preGenerate: async (_context, modelSpec) => {
resolvedModelSpec = modelSpec
}
}
]
}

const result = await build('production', buildOptions(userConfig))
if (result.isErr()) {
throw new Error('Expected ok result but got: ' + result.error.message)
}

expect(result.value.exitCode).toBe(0)
expect(resolvedModelSpec!).toBeDefined()
expect(resolvedModelSpec!.bundleListing).toBe(true)
expect(resolvedModelSpec!.customLookups).toEqual(true)
expect(resolvedModelSpec!.customOutputs).toEqual(true)
})

it('should write listing.json file (when absolute path is provided)', async () => {
Expand Down
Loading

0 comments on commit fcea642

Please sign in to comment.