Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
ezzatron committed Mar 26, 2024
1 parent 533e665 commit bee8bb5
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 15 deletions.
11 changes: 9 additions & 2 deletions src/declaration/kubernetes-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
defaultFromOptions,
type ExactOptions,
} from "../declaration.js";
import { registerVariable } from "../environment.js";
import { registerComposite, registerVariable } from "../environment.js";
import { SpecError, normalize } from "../error.js";
import { resolveExamples, type Example } from "../example.js";
import { Maybe, map, resolve } from "../maybe.js";
Expand Down Expand Up @@ -51,7 +51,8 @@ export function kubernetesAddress<O extends Options>(
const portVar = registerPort(name, portExamples, isSensitive, def, portName);
const pName = portVar.spec.name;

return {
const composite = registerComposite({
variables: [hostVar, portVar],
value() {
const host = resolve(hostVar.nativeValue());
const port = resolve(portVar.nativeValue());
Expand All @@ -63,6 +64,12 @@ export function kubernetesAddress<O extends Options>(

return undefined as Value<KubernetesAddress, O>;
},
});

return {
value() {
return composite.value();
},
};
}

Expand Down
13 changes: 12 additions & 1 deletion src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Variable,
VariableSpec,
create as createVariable,
type VariableComposite,
} from "./variable.js";

let state: State = createInitialState();
Expand All @@ -22,7 +23,7 @@ export function initialize(options: InitializeOptions = {}): void {
process.exit(0);
} else {
const { onInvalid = defaultOnInvalid } = options;
const [isValid, results] = validate(variablesByName());
const [isValid, results] = validate(variablesByName(), state.composites);

if (!isValid) {
onInvalid({
Expand All @@ -42,6 +43,14 @@ export function registerVariable<T>(spec: VariableSpec<T>): Variable<T> {
return variable;
}

export function registerComposite<T>(
composite: VariableComposite<T>,
): VariableComposite<T> {
state.composites.push(composite);

return composite;
}

export function readVariable<T>(spec: VariableSpec<T>): string {
return process.env[spec.name] ?? "";
}
Expand All @@ -54,11 +63,13 @@ type State = {
// TODO: WTF TypeScript? Why can't I use unknown here?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly variables: Record<string, Variable<any>>;
readonly composites: VariableComposite<unknown>[];
};

function createInitialState(): State {
return {
variables: {},
composites: [],
};
}

Expand Down
9 changes: 9 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ export class ValueError extends Error {
}
}

export class CompositeError extends Error {
constructor(
public readonly name: string,
public readonly cause: Error,
) {
super(`${name} is invalid: ${cause.message}`);
}
}

export class NotSetError extends Error {
constructor(public readonly name: string) {
super(`${name} is not set and does not have a default value`);
Expand Down
11 changes: 8 additions & 3 deletions src/summary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValueError } from "./error.js";
import { CompositeError, ValueError } from "./error.js";
import { Visitor } from "./schema.js";
import { quote } from "./shell.js";
import { create as createTable } from "./table.js";
Expand Down Expand Up @@ -84,9 +84,14 @@ function renderResult(
}

function describeError(isSensitive: boolean, error: Error) {
if (!(error instanceof ValueError)) return "not set";
if (error instanceof ValueError) {
return (
`set to ${quoteAndSuppress(isSensitive, error.value)}, ` +
`${error.cause.message}`
);
}

return `set to ${quoteAndSuppress(isSensitive, error.value)}, ${error.cause.message}`;
return error instanceof CompositeError ? error.message : "not set";
}

function quoteAndSuppress(isSensitive: boolean, value: string) {
Expand Down
45 changes: 36 additions & 9 deletions src/validation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,50 @@
import { normalize } from "./error.js";
import { CompositeError, normalize } from "./error.js";
import { Maybe } from "./maybe.js";
import { Value, Variable } from "./variable.js";
import { Value, Variable, type VariableComposite } from "./variable.js";

export function validate(variables: Variable<unknown>[]): [boolean, Results] {
const results: Results = [];
export function validate(
variables: Variable<unknown>[],
composites: VariableComposite<unknown>[],
): [boolean, Results] {
const resultMap = new Map<Variable<unknown>, Result>();
let isValid = true;

for (const variable of variables) {
try {
results.push({
variable,
result: { maybe: variable.value() },
});
resultMap.set(variable, { maybe: variable.value() });
} catch (error) {
isValid = false;
results.push({ variable, result: { error: normalize(error) } });
resultMap.set(variable, { error: normalize(error) });
}
}

for (const composite of composites) {
const shouldSkip = composite.variables.some(
(variable) => resultMap.get(variable)?.error,
);

if (shouldSkip) continue;

try {
composite.value();
} catch (error) {
isValid = false;

for (const variable of composite.variables) {
resultMap.set(variable, {
error: new CompositeError(variable.spec.name, normalize(error)),
});
}
}
}

const results: Results = [];

for (const variable of variables) {
const result = resultMap.get(variable);
if (result) results.push({ variable, result });
}

return [isValid, results];
}

Expand Down
6 changes: 6 additions & 0 deletions src/variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export type Variable<T> = {
readonly unmarshal: (value: string) => T;
};

export type VariableComposite<T> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly variables: Variable<any>[];
readonly value: () => T;
};

export type Value<T> = {
readonly verbatim: string;
readonly canonical: string;
Expand Down
4 changes: 4 additions & 0 deletions test/fixture/summary/invalid-composite.ansi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Environment Variables:

❯ AUSTENITE_SVC_SERVICE_HOST kubernetes `austenite-svc` service host [ <hostname> ] ✗ AUSTENITE_SVC_SERVICE_HOST is invalid: AUSTENITE_SVC_SERVICE_HOST is defined but AUSTENITE_SVC_SERVICE_PORT is not, define both or neither
❯ AUSTENITE_SVC_SERVICE_PORT kubernetes `austenite-svc` service port [ <port number> ] ✗ AUSTENITE_SVC_SERVICE_PORT is invalid: AUSTENITE_SVC_SERVICE_HOST is defined but AUSTENITE_SVC_SERVICE_PORT is not, define both or neither
15 changes: 15 additions & 0 deletions test/suite/summary.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,21 @@ describe("Validation summary", () => {
expect(exitCode).toBeGreaterThan(0);
});

it("summarizes invalid composites", async () => {
Object.assign(process.env, {
AUSTENITE_SVC_SERVICE_HOST: "host.example.org",
});

kubernetesAddress("austenite-svc", { default: undefined });

initialize();

await expect(mockConsole.readStderr()).toMatchFileSnapshot(
fixturePath("invalid-composite"),
);
expect(exitCode).toBeGreaterThan(0);
});

it("summarizes variables that violate constraints", async () => {
Object.assign(process.env, {
AUSTENITE_STRING: "hello, world!",
Expand Down

0 comments on commit bee8bb5

Please sign in to comment.