Skip to content

Commit

Permalink
🪚 OmniGraph™ Reusable config loading (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista authored Dec 14, 2023
1 parent f202724 commit 4d32e7b
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 44 deletions.
1 change: 1 addition & 0 deletions packages/io-utils/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './loading'
49 changes: 49 additions & 0 deletions packages/io-utils/src/config/loading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { importDefault, isFile, isReadable } from '@/filesystem/filesystem'
import { createModuleLogger } from '@/stdio/logger'
import { printZodErrors } from '@/stdio/printer'
import { z } from 'zod'

export const createConfigLoader =
<TConfig>(schema: z.ZodSchema<TConfig>, logger = createModuleLogger('config loader')) =>
async (path: string): Promise<TConfig> => {
// First we check that the config file is indeed there and we can read it
logger.verbose(`Checking config file '${path}' for existence & readability`)
const isConfigReadable = isFile(path) && isReadable(path)
if (!isConfigReadable) {
throw new Error(
`Unable to read config file '${path}'. Check that the file exists and is readable to your terminal user`
)
}

// Keep talking to the user
logger.verbose(`Config file '${path}' exists & is readable`)

// Now let's see if we can load the config file
let rawConfig: unknown
try {
logger.verbose(`Loading config file '${path}'`)

rawConfig = await importDefault(path)
} catch (error) {
throw new Error(`Unable to read config file '${path}': ${error}`)
}

logger.verbose(`Loaded config file '${path}'`)

// It's time to make sure that the config is not malformed
//
// At this stage we are only interested in the shape of the data,
// we are not checking whether the information makes sense (e.g.
// whether there are no missing nodes etc)
logger.verbose(`Validating the structure of config file '${path}'`)
const configParseResult = schema.safeParse(rawConfig)
if (configParseResult.success === false) {
const userFriendlyErrors = printZodErrors(configParseResult.error)

throw new Error(
`Config from file '${path}' is malformed. Please fix the following errors:\n\n${userFriendlyErrors}`
)
}

return configParseResult.data
}
1 change: 1 addition & 0 deletions packages/io-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './config'
export * from './filesystem'
export * from './language'
export * from './stdio'
19 changes: 19 additions & 0 deletions packages/io-utils/test/config/__snapshots__/loading.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`config/loading createConfigLoader should reject if the file cannot be imported 1`] = `[Error: Unable to read config file './myconfig.ts': No way]`;

exports[`config/loading createConfigLoader should reject if the file contents do not match the schema 1`] = `
[Error: Config from file './myconfig.ts' is malformed. Please fix the following errors:
Property 'good': Required]
`;

exports[`config/loading createConfigLoader should reject if the path is not a file 1`] = `[Error: Unable to read config file './myconfig.ts'. Check that the file exists and is readable to your terminal user]`;

exports[`config/loading createConfigLoader should reject if the path is not readable 1`] = `[Error: Unable to read config file './myconfig.ts'. Check that the file exists and is readable to your terminal user]`;

exports[`config/loading createConfigLoader should resolve if the file contents match the schema 1`] = `
{
"good": "config",
}
`;
72 changes: 72 additions & 0 deletions packages/io-utils/test/config/loading.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { createConfigLoader } from '@/config'
import { importDefault, isFile, isReadable } from '@/filesystem/filesystem'
import { z } from 'zod'

jest.mock('@/filesystem/filesystem', () => ({
isFile: jest.fn().mockReturnValue(false),
isReadable: jest.fn().mockReturnValue(false),
importDefault: jest.fn().mockRejectedValue('Not mocked'),
}))

const isFileMock = isFile as jest.Mock
const importDefaultMock = importDefault as jest.Mock
const isReadableMock = isReadable as jest.Mock

describe('config/loading', () => {
beforeEach(() => {
isFileMock.mockReset()
importDefaultMock.mockReset()
isReadableMock.mockReset()
})

describe('createConfigLoader', () => {
it('should reject if the path is not a file', async () => {
isFileMock.mockReturnValue(false)

const configLoader = createConfigLoader(z.unknown())

await expect(configLoader('./myconfig.ts')).rejects.toMatchSnapshot()
})

it('should reject if the path is not readable', async () => {
isFileMock.mockReturnValue(true)
isReadableMock.mockReturnValue(false)

const configLoader = createConfigLoader(z.unknown())

await expect(configLoader('./myconfig.ts')).rejects.toMatchSnapshot()
})

it('should reject if the file cannot be imported', async () => {
isFileMock.mockReturnValue(true)
isReadableMock.mockReturnValue(true)
importDefaultMock.mockRejectedValue('No way')

const configLoader = createConfigLoader(z.unknown())

await expect(configLoader('./myconfig.ts')).rejects.toMatchSnapshot()
})

it('should reject if the file contents do not match the schema', async () => {
isFileMock.mockReturnValue(true)
isReadableMock.mockReturnValue(true)
importDefaultMock.mockResolvedValue({ bad: 'config' })

const schema = z.object({ good: z.string() })
const configLoader = createConfigLoader(schema)

await expect(configLoader('./myconfig.ts')).rejects.toMatchSnapshot()
})

it('should resolve if the file contents match the schema', async () => {
isFileMock.mockReturnValue(true)
isReadableMock.mockReturnValue(true)
importDefaultMock.mockResolvedValue({ good: 'config' })

const schema = z.object({ good: z.string() })
const configLoader = createConfigLoader(schema)

await expect(configLoader('./myconfig.ts')).resolves.toMatchSnapshot()
})
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`task/oapp/wire with invalid configs should fail if the config file does not exist 1`] = `[Error: Unable to read config file './does-not-exist.js'. Check that the file exists and is readable to your terminal user]`;
exports[`task/oapp/wire with invalid configs should fail if the config file does not exist 1`] = `[Error: Unable to read config file '/app/packages/ua-utils-evm-hardhat-test/does-not-exist.js'. Check that the file exists and is readable to your terminal user]`;

exports[`task/oapp/wire with invalid configs should fail if the config file is not a file 1`] = `[Error: Unable to read config file '/app/packages/ua-utils-evm-hardhat-test/test/task/oapp'. Check that the file exists and is readable to your terminal user]`;

Expand Down
48 changes: 5 additions & 43 deletions packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import { task, types } from 'hardhat/config'
import type { ActionType } from 'hardhat/types'
import { TASK_LZ_WIRE_OAPP } from '@/constants/tasks'
import {
isFile,
isReadable,
createLogger,
createConfigLoader,
setDefaultLogLevel,
promptToContinue,
printJson,
printZodErrors,
pluralizeNoun,
importDefault,
} from '@layerzerolabs/io-utils'
import { OAppOmniGraphHardhat, OAppOmniGraphHardhatSchema } from '@/oapp'
import { OAppOmniGraph, configureOApp } from '@layerzerolabs/ua-utils'
Expand All @@ -29,49 +26,14 @@ const action: ActionType<TaskArgs> = async ({ oappConfig: oappConfigPath, logLev
// We'll set the global logging level to get as much info as needed
setDefaultLogLevel(logLevel)

// And we'll create a logger for ourselves
const logger = createLogger()

// First we check that the config file is indeed there and we can read it
logger.verbose(`Checking config file '${oappConfigPath}' for existence & readability`)
const isConfigReadable = isFile(oappConfigPath) && isReadable(oappConfigPath)
if (!isConfigReadable) {
throw new Error(
`Unable to read config file '${oappConfigPath}'. Check that the file exists and is readable to your terminal user`
)
}

// Keep talking to the user
logger.verbose(`Config file '${oappConfigPath}' exists & is readable`)

// Now let's see if we can load the config file
let rawConfig: unknown
try {
logger.verbose(`Loading config file '${oappConfigPath}'`)

rawConfig = await importDefault(resolve(oappConfigPath))
} catch (error) {
throw new Error(`Unable to read config file '${oappConfigPath}': ${error}`)
}

logger.verbose(`Loaded config file '${oappConfigPath}'`)

// It's time to make sure that the config is not malformed
//
// At this stage we are only interested in the shape of the data,
// we are not checking whether the information makes sense (e.g.
// whether there are no missing nodes etc)
logger.verbose(`Validating the structure of config file '${oappConfigPath}'`)
const configParseResult = OAppOmniGraphHardhatSchema.safeParse(rawConfig)
if (configParseResult.success === false) {
const userFriendlyErrors = printZodErrors(configParseResult.error)

throw new Error(
`Config from file '${oappConfigPath}' is malformed. Please fix the following errors:\n\n${userFriendlyErrors}`
)
}
// Now we create our config loader
const configLoader = createConfigLoader<OAppOmniGraphHardhat>(OAppOmniGraphHardhatSchema)

// At this point we have a correctly typed config in the hardhat format
const hardhatGraph: OAppOmniGraphHardhat = configParseResult.data
const hardhatGraph: OAppOmniGraphHardhat = await configLoader(resolve(oappConfigPath))

// We'll also print out the whole config for verbose loggers
logger.verbose(`Config file '${oappConfigPath}' has correct structure`)
Expand Down

0 comments on commit 4d32e7b

Please sign in to comment.