Repository navigation
feat: allow config file to have config globs #129
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { minimatch } from 'minimatch'; | ||
| import z from 'zod'; | ||
|
|
||
| import { ACTOR_FULL_NAME_REGEX, CONFIG_FILE_STRATEGY, defineStrategy, type ResolvedActorConfig } from './base.js'; | ||
|
|
||
| const SETTING_KEYS = ['tokenEnvVar', 'overrideActorContext'] as const; | ||
|
|
||
| const schema = z.object({ | ||
| actors: z | ||
| .array( | ||
| z.object({ | ||
| folder: z.string(), | ||
| actorFullName: z.string().regex(ACTOR_FULL_NAME_REGEX), | ||
| tokenEnvVar: z.string().optional(), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. THEY CAN HAVE STUFF HERE? Also, nice for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no precedence, if the configs try to set something that it is already there, it errors. It's why there is the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, saw it in the implementation. I think its good. |
||
| overrideActorContext: z.array(z.string()).optional(), | ||
| }), | ||
| ) | ||
| .min(1), | ||
| configs: z | ||
| .array( | ||
| z.object({ | ||
| match: z | ||
| .object({ | ||
| folderGlob: z.string().optional(), | ||
| actorFullNameGlob: z.string().optional(), | ||
| }) | ||
| .refine((data) => Object.keys(data).length > 0, 'Invalid input: At least one glob is required.'), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i hate that this is the zod standard for this kind of thing. |
||
|
|
||
| set: z | ||
| .object({ | ||
| tokenEnvVar: z.string().optional(), | ||
| overrideActorContext: z.array(z.string()).optional(), | ||
| }) | ||
| .refine((data) => Object.keys(data).length > 0, 'Invalid input: At least one setting is required.'), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should do a non-empty object refining function as a zod utility. |
||
| }), | ||
| ) | ||
| .min(1), | ||
| }); | ||
|
|
||
| type GlobsConfig = z.infer<typeof schema>; | ||
| type Actor = GlobsConfig['actors'][number]; | ||
| type Match = GlobsConfig['configs'][number]['match']; | ||
|
|
||
| // folders are matched as written (no normalization) and dotfolders are skipped | ||
| const matches = (actor: Actor, { folderGlob, actorFullNameGlob }: Match) => | ||
| (folderGlob === undefined || minimatch(actor.folder, folderGlob)) && | ||
| (actorFullNameGlob === undefined || minimatch(actor.actorFullName, actorFullNameGlob)); | ||
|
|
||
| export const GLOBS_PARSER = defineStrategy(CONFIG_FILE_STRATEGY.GLOBS, schema, (body: GlobsConfig) => { | ||
| const errors: string[] = []; | ||
|
|
||
| const resolved = body.actors.flatMap((actor): ResolvedActorConfig[] => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. holy function nesting batman! |
||
| const result = { ...actor }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| // keep tracks of who set the key, the actor definition or the index of the config | ||
| const owners = new Map<(typeof SETTING_KEYS)[number], 'actor' | number>(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. smort for debuggability |
||
| for (const key of SETTING_KEYS) { | ||
| if (actor[key] !== undefined) owners.set(key, 'actor'); | ||
| } | ||
|
|
||
| for (const [index, config] of body.configs.entries()) { | ||
| if (!matches(actor, config.match)) continue; | ||
|
|
||
| for (const key of SETTING_KEYS) { | ||
| if (config.set[key] === undefined) continue; | ||
|
|
||
| const owner = owners.get(key); | ||
| if (owner === undefined) { | ||
| owners.set(key, index); | ||
| Object.assign(result, { [key]: config.set[key] }); | ||
| } else if (owner === 'actor') { | ||
| errors.push( | ||
| `Actor "${actor.actorFullName}": "${key}" is set on the actor and by configs[${index}].`, | ||
| ); | ||
| } else { | ||
| errors.push( | ||
| `Actor "${actor.actorFullName}": "${key}" is set by both configs[${owner}] and configs[${index}].`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. having configs be an array of rules make these errors very painful to write 😭. Maybe a shortened "selector" view could be nice. |
||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const { tokenEnvVar } = result; | ||
| if (tokenEnvVar === undefined) { | ||
| errors.push( | ||
| `Actor "${actor.actorFullName}" has no tokenEnvVar: set it on the actor or through a matching config.`, | ||
| ); | ||
| return []; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some Either return could be beneficial here, with the added bonus of no mutation of |
||
| } | ||
| return [{ ...result, tokenEnvVar }]; | ||
| }); | ||
|
|
||
| if (errors.length > 0) { | ||
| throw new Error(errors.join('\n')); | ||
| } | ||
|
|
||
| return resolved; | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import z from 'zod'; | ||
|
|
||
| import { ACTOR_FULL_NAME_REGEX, CONFIG_FILE_STRATEGY, defineStrategy } from './base.js'; | ||
|
|
||
| const schema = z.object({ | ||
| groups: z | ||
| .record( | ||
| z.string(), | ||
| z.object({ | ||
| actors: z | ||
| .array( | ||
| z.object({ | ||
| folder: z.string(), | ||
| actorFullName: z.string().regex(ACTOR_FULL_NAME_REGEX), | ||
| // these allow overrides from stuff set at the group level | ||
| tokenEnvVar: z.string().optional(), | ||
| overrideActorContext: z.array(z.string()).optional(), | ||
| }), | ||
| ) | ||
| .min(1), | ||
| tokenEnvVar: z.string(), | ||
| overrideActorContext: z.array(z.string()).optional(), | ||
| }), | ||
| ) | ||
| .refine((data) => Object.keys(data).length > 0, 'Invalid input: At least one configuration group is required.'), | ||
| }); | ||
|
|
||
| type GroupedConfig = z.infer<typeof schema>; | ||
|
|
||
| export const GROUPED_PARSER = defineStrategy(CONFIG_FILE_STRATEGY.GROUPED, schema, (body: GroupedConfig) => { | ||
| const entries = Object.entries(body.groups); | ||
| return entries.flatMap(([_folder, entry]) => | ||
| entry.actors.map((actor) => ({ | ||
| tokenEnvVar: entry.tokenEnvVar, | ||
| overrideActorContext: entry.overrideActorContext, | ||
| // last so actor can override stuff from the group | ||
| ...actor, | ||
| })), | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tracking useless globs (they are not owner for no key) might be beneficial to steer people out of dumb configurations