hyperupcall / config

A deno module that helps you load configuration
MIT License
5 stars 4 forks source link

Support for a configuration schema #7

Open Radiergummi opened 1 month ago

Radiergummi commented 1 month ago

To ensure application configuration conforms to a specific schema, the library should support providing a layout for the expected configuration, and validate the parsed configuration against that. To avoid redundant validation work, I suggest to use Zod; to avoid bc breaks, the Config.load function could accept a new, optional, schema property, that, if present, triggers schema validation. The new signature could look like so:

interface ILoadOptions<T> {
  readonly searchDir?: string;
  readonly file: string;
  readonly schema?: ZodType<T> 
}

Implementing this feature would allow inferring the type of the configuration returned (the T can be used for the return type), and also add support for override values derived from environment variables or command line arguments.

For a concrete example, this would allow for a load operation such as the following:

const config = await Config.load({
  file: 'my-app',
  schema: z.object({
    database_host: z.string().url().transform(host => new URL(host).hostname),
    database_port: z.number().default(5432).min(1).max(65534),
    database_name: z.string(),
    database_user: z.string("Database user is mandatory"),
    database_password: z.string("Database password is mandatory").
  }),
});

Even when ignoring the benefits of Zod's validation constraints, which allow specific error messages and transformations, we also gain a lot of type safety.
Assuming the type of load was extended with a new overload like this:

load<T>(ILoadOptions<T>): Promise<T>;

we would get back a configuration object with proper types. This would be amazing for DX.

Is this a direction you'd like to see for this library? I'd be open to work on this.

Radiergummi commented 1 month ago

Coming to think of it, it may be worthwhile to add another parameter to ILoadOptions called strict, that defaults to false. If strict mode is enabled, validation must pass, to ensure the configuration is valid; if disabled, values should be parsed individually, with all invalid values just dropped. This way, applications can decide to work with partial configuration, if necessary.