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,27 +435,13 @@ export default class PKPass extends Bundle {
...otherPassData ...otherPassData
} = data; } = data;
/**
* Validating the rest of the data and
* importing all the props. They are going
* 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) { if (Object.keys(this[propsSymbol]).length) {
console.warn( console.warn(
"The imported pass.json's properties will be joined with the current setted props. You might lose some data.", "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]) {
@@ -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];
}, []); }, []);
} }