diff --git a/src/schema.ts b/src/schema.ts index 533eca2..0a94227 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -6,7 +6,7 @@ import { regexpType } from './type/Regexp'; import { streamType } from './type/Stream'; export interface SchemaOptions { - include: IncludeOptions; + include: Readonly; } /** @@ -14,8 +14,8 @@ export interface SchemaOptions { * * @public */ -export function createSchema(options: SchemaOptions) { - const includeType = createInclude(options.include); +export function createSchema(options: Readonly) { + const {includeType, setSchema} = createInclude(options.include); const schema = DEFAULT_SCHEMA.extend([ envType, includeType, @@ -23,7 +23,7 @@ export function createSchema(options: SchemaOptions) { streamType, ]); - options.include.schema = schema; + setSchema(schema); return schema; } diff --git a/src/type/Include.ts b/src/type/Include.ts index 33fb21b..34e9379 100644 --- a/src/type/Include.ts +++ b/src/type/Include.ts @@ -1,5 +1,5 @@ -import { InvalidArgumentError, NotFoundError } from '@apextoaster/js-utils'; -import { load, Schema, Type as YamlType } from 'js-yaml'; +import { InvalidArgumentError, mustCoalesce, NotFoundError, Optional } from '@apextoaster/js-utils'; +import { DEFAULT_SCHEMA, load, Schema, Type as YamlType } from 'js-yaml'; export type ReaderEncoding = 'ascii' | 'utf-8'; export interface ReaderOptions { @@ -14,15 +14,17 @@ export interface IncludeOptions { join: (...path: Array) => string; read: IncludeReader; resolve: (path: string) => string; - schema: Schema; + schema: Optional; } /** * Instantiate an includer with closure over the provided options. * @public */ -export function createInclude(options: IncludeOptions) { - return new YamlType('!include', { +export function createInclude(options: Readonly) { + const optionsCopy = {...options}; + + const includeType = new YamlType('!include', { kind: 'scalar', resolve(path: string) { try { @@ -43,11 +45,21 @@ export function createInclude(options: IncludeOptions) { return load(options.read(abs, { encoding: 'utf-8', }), { - schema: options.schema, + schema: mustCoalesce(optionsCopy.schema, DEFAULT_SCHEMA), }); } catch (err) { throw new InvalidArgumentError('error including file', err); } }, }); + + // callback to avoid circular dependency (type must be created before schema) + function setSchema(schema: Schema) { + optionsCopy.schema = schema; + }; + + return { + includeType, + setSchema, + }; } diff --git a/test/type/TestInclude.ts b/test/type/TestInclude.ts index 33478df..83dbd0a 100644 --- a/test/type/TestInclude.ts +++ b/test/type/TestInclude.ts @@ -17,12 +17,12 @@ const TEST_OPTIONS: IncludeOptions = { describe('include config type', async () => { it('should resolve existing files', async () => { - const includeType = createInclude(TEST_OPTIONS); + const {includeType} = createInclude(TEST_OPTIONS); expect(includeType.resolve(join(TEST_ROOT, 'include.yml'))).to.equal(true); }); it('should throw when resolving missing files', async () => { - const includeType = createInclude({ + const {includeType} = createInclude({ ...TEST_OPTIONS, resolve: () => { throw new NotFoundError(); @@ -35,12 +35,12 @@ describe('include config type', async () => { }); it('should construct data from file', async () => { - const includeType = createInclude(TEST_OPTIONS); + const {includeType} = createInclude(TEST_OPTIONS); expect(includeType.construct(join(TEST_ROOT, 'include.yml'))).to.equal('test'); }); it('should throw when constructing missing files', async () => { - const includeType = createInclude({ + const {includeType} = createInclude({ ...TEST_OPTIONS, read: () => { throw new InvalidArgumentError();