Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bundleListing, customLookups, and customOutputs settings to control code generation #504

Merged
merged 15 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
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