Improved schemas validation and added new function assertValidity

This commit is contained in:
Alexander Cerutti
2021-10-08 22:33:32 +02:00
parent 3d22cef805
commit bbecdeed79
3 changed files with 87 additions and 147 deletions

View File

@@ -87,18 +87,14 @@ export default class PKPass extends Bundle {
buffers = await getModelFolderContents(source.model); buffers = await getModelFolderContents(source.model);
certificates = source.certificates; certificates = source.certificates;
props = source.props ?? {}; props = Schemas.validate(Schemas.OverridablePassProps, props);
} }
if (additionalProps && Object.keys(additionalProps).length) { if (additionalProps && Object.keys(additionalProps).length) {
const validation = Schemas.getValidated( Object.assign(
additionalProps, props,
Schemas.OverridablePassProps, Schemas.validate(Schemas.OverridablePassProps, additionalProps),
); );
if (validation) {
Object.assign(props, validation);
}
} }
return new PKPass(buffers, certificates, props); return new PKPass(buffers, certificates, props);
@@ -161,9 +157,9 @@ export default class PKPass extends Bundle {
} }
/** Overrides validation and pushing in props */ /** Overrides validation and pushing in props */
const overridesValidation = Schemas.getValidated( const overridesValidation = Schemas.validate(
props,
Schemas.OverridablePassProps, Schemas.OverridablePassProps,
props,
); );
Object.assign(this[propsSymbol], overridesValidation); Object.assign(this[propsSymbol], overridesValidation);
@@ -180,10 +176,7 @@ export default class PKPass extends Bundle {
*/ */
public set certificates(certs: Schemas.CertificatesSchema) { public set certificates(certs: Schemas.CertificatesSchema) {
if (!Schemas.isValid(certs, Schemas.CertificatesSchema)) { Schemas.assertValidity(Schemas.CertificatesSchema, certs);
throw new TypeError("Cannot set certificates: invalid");
}
this[certificatesSymbol] = certs; this[certificatesSymbol] = certs;
} }
@@ -211,17 +204,7 @@ export default class PKPass extends Bundle {
); );
} }
/** Schemas.assertValidity(Schemas.TransitType, value);
* @TODO Make getValidated more explicit in case of error.
* @TODO maybe make an automated error.
*/
if (!Schemas.getValidated(value, Schemas.TransitType)) {
throw new TypeError(
`Cannot set transitType to '${value}': invalid type. Expected one of PKTransitTypeAir, PKTransitTypeBoat, PKTransitTypeBus, PKTransitTypeGeneric, PKTransitTypeTrain.`,
);
}
this[propsSymbol]["boardingPass"].transitType = value; this[propsSymbol]["boardingPass"].transitType = value;
} }
@@ -304,11 +287,7 @@ export default class PKPass extends Bundle {
*/ */
public set type(type: Schemas.PassTypesProps) { public set type(type: Schemas.PassTypesProps) {
if (!Schemas.isValid(type, Schemas.PassType)) { Schemas.assertValidity(Schemas.PassType, type);
throw new TypeError(
`Invalid type. Expected one of 'boardingPass' | 'coupon' | 'storeCard' | 'eventTicket' | 'generic' but received '${type}'`,
);
}
if (this.type) { if (this.type) {
/** /**
@@ -398,12 +377,10 @@ export default class PKPass extends Bundle {
const prsJSON = JSON.parse( const prsJSON = JSON.parse(
buffer.toString(), buffer.toString(),
) as Schemas.Personalization; ) as Schemas.Personalization;
const personalizationValidation = Schemas.getValidated(
prsJSON,
Schemas.Personalization,
);
if (!personalizationValidation) { try {
Schemas.assertValidity(Schemas.Personalization, prsJSON);
} catch (err) {
console.warn( console.warn(
"Personalization.json file has been omitted as invalid.", "Personalization.json file has been omitted as invalid.",
); );
@@ -458,28 +435,14 @@ export default class PKPass extends Bundle {
...otherPassData ...otherPassData
} = data; } = data;
/** if (Object.keys(this[propsSymbol]).length) {
* Validating the rest of the data and console.warn(
* importing all the props. They are going "The imported pass.json's properties will be joined with the current setted props. You might lose some data.",
* to overwrite props setted by user but );
* we can't do much about.
*/
const validation = Schemas.getValidated(
otherPassData,
Schemas.PassProps,
);
if (validation) {
if (Object.keys(this[propsSymbol]).length) {
console.warn(
"The imported pass.json's properties will be joined with the current setted props. You might lose some data.",
);
}
Object.assign(this[propsSymbol], validation);
} }
Object.assign(this[propsSymbol], otherPassData);
if (!type) { if (!type) {
if (!this[passTypeSymbol]) { if (!this[passTypeSymbol]) {
console.warn( console.warn(
@@ -748,8 +711,8 @@ export default class PKPass extends Bundle {
} }
this[propsSymbol]["beacons"] = Schemas.filterValid( this[propsSymbol]["beacons"] = Schemas.filterValid(
beacons,
Schemas.Beacon, Schemas.Beacon,
beacons,
); );
} }
@@ -782,8 +745,8 @@ export default class PKPass extends Bundle {
} }
this[propsSymbol]["locations"] = Schemas.filterValid( this[propsSymbol]["locations"] = Schemas.filterValid(
locations,
Schemas.Location, Schemas.Location,
locations,
); );
} }
@@ -848,15 +811,15 @@ export default class PKPass extends Bundle {
]; ];
finalBarcodes = supportedFormats.map((format) => finalBarcodes = supportedFormats.map((format) =>
Schemas.getValidated( Schemas.validate(Schemas.Barcode, {
{ format, message: barcodes[0] } as Schemas.Barcode, format,
Schemas.Barcode, message: barcodes[0],
), } as Schemas.Barcode),
); );
} else { } else {
finalBarcodes = Schemas.filterValid( finalBarcodes = Schemas.filterValid(
barcodes as Schemas.Barcode[],
Schemas.Barcode, Schemas.Barcode,
barcodes as Schemas.Barcode[],
); );
if (!finalBarcodes.length) { if (!finalBarcodes.length) {
@@ -887,7 +850,7 @@ export default class PKPass extends Bundle {
} }
this[propsSymbol]["nfc"] = this[propsSymbol]["nfc"] =
Schemas.getValidated(nfc, Schemas.NFC) ?? undefined; Schemas.validate(Schemas.NFC, nfc) ?? undefined;
} }
} }
@@ -917,28 +880,15 @@ function freezeRecusive(object: Object) {
} }
function readPassMetadata(buffer: Buffer) { function readPassMetadata(buffer: Buffer) {
let contentAsJSON: Schemas.PassProps;
try { try {
const contentAsJSON = JSON.parse( contentAsJSON = JSON.parse(
buffer.toString("utf8"), buffer.toString("utf8"),
) as Schemas.PassProps; ) as Schemas.PassProps;
const validation = Schemas.getValidated(
contentAsJSON,
Schemas.PassProps,
);
/**
* @TODO validation.error?
*/
if (!validation) {
throw new Error(
"Cannot validate pass.json file. Not conformant to",
);
}
return validation;
} catch (err) { } catch (err) {
console.error(err); throw new TypeError("Cannot validat Pass.json: invalid JSON");
} }
return Schemas.validate(Schemas.PassProps, contentAsJSON);
} }

View File

@@ -26,23 +26,22 @@ export default class FieldsArray extends Array {
push(...fieldsData: Schemas.Field[]): number { push(...fieldsData: Schemas.Field[]): number {
const validFields = fieldsData.reduce( const validFields = fieldsData.reduce(
(acc: Schemas.Field[], current: Schemas.Field) => { (acc: Schemas.Field[], current: Schemas.Field) => {
if ( try {
!(typeof current === "object") || Schemas.assertValidity(Schemas.Field, current);
!Schemas.isValid(current, Schemas.Field) } catch (err) {
) { console.warn(`Cannot add field: ${err}`);
return acc; return acc;
} }
if (this[poolSymbol].has(current.key)) { if (this[poolSymbol].has(current.key)) {
fieldsDebug( console.warn(
`Field with key "${current.key}" discarded: fields must be unique in pass scope.`, `Cannot add field with key '${current.key}': another field already owns this key. Ignored.`,
); );
} else { return acc;
this[poolSymbol].add(current.key);
acc.push(current);
} }
return acc; this[poolSymbol].add(current.key);
return [...acc, current];
}, },
[], [],
); );

View File

@@ -9,7 +9,6 @@ export * from "./Personalize";
export * from "./Certificates"; export * from "./Certificates";
import Joi from "joi"; import Joi from "joi";
import debug from "debug";
import { Barcode } from "./Barcodes"; import { Barcode } from "./Barcodes";
import { Location } from "./Location"; import { Location } from "./Location";
@@ -17,12 +16,9 @@ import { Beacon } from "./Beacons";
import { NFC } from "./NFC"; import { NFC } from "./NFC";
import { Field } from "./PassFieldContent"; import { Field } from "./PassFieldContent";
import { PassFields, TransitType } from "./PassFields"; import { PassFields, TransitType } from "./PassFields";
import { Personalization } from "./Personalize";
import { Semantics } from "./SemanticTags"; import { Semantics } from "./SemanticTags";
import { CertificatesSchema } from "./Certificates"; import { CertificatesSchema } from "./Certificates";
const schemaDebug = debug("Schema");
export interface FileBuffers { export interface FileBuffers {
[key: string]: Buffer; [key: string]: Buffer;
} }
@@ -168,19 +164,6 @@ export const Template = Joi.object<Template>({
// --------- UTILITIES ---------- // // --------- UTILITIES ---------- //
type AvailableSchemas =
| typeof Barcode
| typeof Location
| typeof Beacon
| typeof NFC
| typeof Field
| typeof PassFields
| typeof Personalization
| typeof TransitType
| typeof Template
| typeof CertificatesSchema
| typeof OverridablePassProps;
/** /**
* Checks if the passed options are compliant with the indicated schema * Checks if the passed options are compliant with the indicated schema
* @param {any} opts - options to be checks * @param {any} opts - options to be checks
@@ -188,69 +171,77 @@ type AvailableSchemas =
* @returns {boolean} - result of the check * @returns {boolean} - result of the check
*/ */
export function isValid(opts: any, schema: AvailableSchemas): boolean { export function isValid<T extends Object>(
if (!schema) { opts: T,
schemaDebug( schema: Joi.ObjectSchema<T>,
`validation failed due to missing or mispelled schema name`, ): boolean {
);
return false;
}
const validation = schema.validate(opts); const validation = schema.validate(opts);
if (validation.error) { if (validation.error) {
schemaDebug( throw new TypeError(validation.error.message);
`validation failed due to error: ${validation.error.message}`,
);
} }
return !validation.error; return !validation.error;
} }
/** /**
* Executes the validation in verbose mode, exposing the value or an empty object * Performs validation of a schema on an object.
* @param {object} opts - to be validated * If it fails, will throw an error.
* @param {*} schemaName - selected schema *
* @returns {object} the filtered value or empty object * @param schema
* @param data
*/ */
export function getValidated<T extends Object>( export function assertValidity<T>(
opts: T, schema: Joi.ObjectSchema<T> | Joi.StringSchema,
schema: AvailableSchemas, data: T,
): T | null { ): void {
if (!schema) { const validation = schema.validate(data);
schemaDebug(`validation failed due to missing schema`);
return null;
}
const validation = schema.validate(opts, { stripUnknown: true });
if (validation.error) { if (validation.error) {
schemaDebug( throw new TypeError(validation.error.message);
`Validation failed in getValidated due to error: ${validation.error.message}`, }
); }
return null;
/**
* Performs validation and throws the error if there's one.
* Otherwise returns a (possibly patched) version of the specified
* options (it depends on the schema)
*
* @param schema
* @param options
* @returns
*/
export function validate<T extends Object>(
schema: Joi.ObjectSchema<T> | Joi.StringSchema,
options: T,
): T {
const validationResult = schema.validate(options, {
stripUnknown: true,
abortEarly: true,
});
if (validationResult.error) {
throw validationResult.error;
} }
return validation.value; return validationResult.value;
} }
export function filterValid<T extends Object>( export function filterValid<T extends Object>(
schema: Joi.ObjectSchema<T>,
source: T[], source: T[],
schema: AvailableSchemas,
): T[] { ): T[] {
if (!source) { if (!source) {
return []; return [];
} }
return source.reduce((acc, current) => { return source.reduce((acc, current) => {
const validation = getValidated(current, schema); try {
return [...acc, validate(schema, current)];
if (!validation) { } catch {
return acc; return [...acc];
} }
return [...acc, validation];
}, []); }, []);
} }