Validate value types returned by API against expectations

This commit is contained in:
Michael B. Gale
2026-03-09 12:46:16 +00:00
parent 9c75a5f60c
commit 58991590bd
3 changed files with 98 additions and 36 deletions
+3 -3
View File
@@ -38,7 +38,7 @@ test.serial(
);
test.serial(
"loadPropertiesFromApi throws if response data contains unexpected objects",
"loadPropertiesFromApi throws if response data contains objects without `property_name`",
async (t) => {
sinon.stub(api, "getRepositoryProperties").resolves({
headers: {},
@@ -197,7 +197,7 @@ test.serial(
);
test.serial(
"loadPropertiesFromApi throws if property value is not a string",
"loadPropertiesFromApi throws if known property value is not a string",
async (t) => {
sinon.stub(api, "getRepositoryProperties").resolves({
headers: {},
@@ -217,7 +217,7 @@ test.serial(
),
{
message:
/Expected repository property 'github-codeql-extra-queries' to have a string value/,
/Unexpected value for repository property 'github-codeql-extra-queries', got: 123/,
},
);
},
+67 -19
View File
@@ -20,16 +20,56 @@ type AllRepositoryProperties = {
/** Parsed repository properties. */
export type RepositoryProperties = Partial<AllRepositoryProperties>;
/** Maps known repository properties to the type we expect to get from the API. */
type RepositoryPropertyApiType = {
[RepositoryPropertyName.DISABLE_OVERLAY]: string;
[RepositoryPropertyName.EXTRA_QUERIES]: string;
};
/** The type of functions which take the `value` from the API and try to convert it to the type we want. */
export type PropertyParser<K extends RepositoryPropertyName> = (
name: K,
value: RepositoryPropertyApiType[K],
logger: Logger,
) => AllRepositoryProperties[K];
/** Possible types of `value`s we get from the API. */
export type RepositoryPropertyValue = string | string[];
/** The type of repository property configurations. */
export type PropertyInfo<K extends RepositoryPropertyName> = {
/** A validator which checks that the value received from the API is what we expect. */
validate: (
value: RepositoryPropertyValue,
) => value is RepositoryPropertyApiType[K];
/** A `PropertyParser` for the property. */
parse: PropertyParser<K>;
};
/** Determines whether a value from the API is a string or not. */
function isString(value: RepositoryPropertyValue): value is string {
return typeof value === "string";
}
/** A repository property that we expect to contain a string value. */
const stringProperty = {
validate: isString,
parse: parseStringRepositoryProperty,
};
/** A repository property that we expect to contain a boolean value. */
const booleanProperty = {
// The value from the API should come as a string, which we then parse into a boolean.
validate: isString,
parse: parseBooleanRepositoryProperty,
};
/** Parsers that transform repository properties from the API response into typed values. */
const repositoryPropertyParsers: {
[K in RepositoryPropertyName]: (
name: K,
value: string,
logger: Logger,
) => AllRepositoryProperties[K];
[K in RepositoryPropertyName]: PropertyInfo<K>;
} = {
[RepositoryPropertyName.DISABLE_OVERLAY]: parseBooleanRepositoryProperty,
[RepositoryPropertyName.EXTRA_QUERIES]: parseStringRepositoryProperty,
[RepositoryPropertyName.DISABLE_OVERLAY]: booleanProperty,
[RepositoryPropertyName.EXTRA_QUERIES]: stringProperty,
};
/**
@@ -37,7 +77,7 @@ const repositoryPropertyParsers: {
*/
export interface GitHubRepositoryProperty {
property_name: string;
value: string | string[];
value: RepositoryPropertyValue;
}
/**
@@ -86,14 +126,6 @@ export async function loadPropertiesFromApi(
}
if (isKnownPropertyName(property.property_name)) {
// Only validate the type of `value` if this is a property we care about, to avoid throwing
// on unrelated properties that may use representations we do not support.
if (typeof property.value !== "string") {
throw new Error(
`Expected repository property '${property.property_name}' to have a string value, but got: ${JSON.stringify(property)}`,
);
}
setProperty(properties, property.property_name, property.value, logger);
}
}
@@ -119,14 +151,30 @@ export async function loadPropertiesFromApi(
}
}
/** Update the partial set of repository properties with the parsed value of the specified property. */
/**
* Validate that `value` has the correct type for `K` and, if so, update the partial set of repository
* properties with the parsed value of the specified property.
*/
function setProperty<K extends RepositoryPropertyName>(
properties: RepositoryProperties,
name: K,
value: string,
value: RepositoryPropertyValue,
logger: Logger,
): void {
properties[name] = repositoryPropertyParsers[name](name, value, logger);
const propertyOptions = repositoryPropertyParsers[name];
// We perform the validation here for two reasons:
// 1. This function is only called if `name` is a property we care about, to avoid throwing
// on unrelated properties that may use representations we do not support.
// 2. The `propertyOptions.validate` function checks that the type of `value` we received from
// the API is what expect and narrows the type accordingly, allowing us to call `parse`.
if (propertyOptions.validate(value)) {
properties[name] = propertyOptions.parse(name, value, logger);
} else {
throw new Error(
`Unexpected value for repository property '${name}', got: ${JSON.stringify(value)}`,
);
}
}
/** Parse a boolean repository property. */