diff --git a/docs/gbfs-validator.yaml b/docs/gbfs-validator.yaml index e2363f7..015ad4a 100644 --- a/docs/gbfs-validator.yaml +++ b/docs/gbfs-validator.yaml @@ -8,12 +8,12 @@ info: email: api@mobilitydata.org license: name: MobilityData License - url: https://www.apache.org/licenses/LICENSE-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 servers: - url: https://gbfs-validator.netlify.app/.netlify/functions - description: Production release environment + description: Production release environment - url: http://localhost:8888/.netlify/functions - description: Local development environment + description: Local development environment paths: /validator: post: @@ -41,7 +41,7 @@ paths: /feed: post: summary: Get feed content - description: Get content of all GBFS's files. Usefull to avoid CORS errors. + description: Get content of all GBFS's files. Useful to avoid CORS errors. requestBody: content: application/json: @@ -61,6 +61,29 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /validator-summary: + post: + summary: Get a summary of the validation results + description: Returns a summary from the validator's response, including grouped error details. + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ValidatorRequest' + required: true + responses: + '200': + description: Validation summary + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationSummary' + '500': + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' components: schemas: Error: @@ -162,7 +185,7 @@ components: tokenUrl: type: string required: - - url + - url FeedRequest: required: - url @@ -183,3 +206,42 @@ components: type: array items: type: object + ValidationSummary: + type: object + properties: + summary: + type: object + filesSummary: + type: array + items: + type: object + properties: + required: + type: boolean + exists: + type: boolean + file: + type: string + hasErrors: + type: boolean + errorsCount: + type: number + groupedErrors: + type: array + items: + type: object + properties: + keyword: + type: string + message: + type: string + schemaPath: + type: string + count: + type: number + required: + - required + - exists + - file + - hasErrors + - errorsCount diff --git a/functions/validator-summary.js b/functions/validator-summary.js new file mode 100644 index 0000000..58649ba --- /dev/null +++ b/functions/validator-summary.js @@ -0,0 +1,114 @@ +const GBFS = require('gbfs-validator') + +/** @typedef {{ + * summary: { + * validatorVersion: string, + * hasErrors: boolean, + * errorsCount: number, + * version: { + * detected: string, + * validated: string + * } + * filesSummary: [ + * { + * required: boolean, + * exists: boolean, + * hasErrors: boolean, + * file: string, + * errorsCount: number, + * groupedErrors: [ + * { + * keyword: string, + * message: string, + * schemaPath: string, + * count: number + * } + * ] + * } + * ] + * } + * }} Summary + */ + +/** + * This function returns a summary from the validator's response, stripping out the notices and grouping errors by message, keyword, and schemaPath. + * + * @param validationResult from the GBFS validator class + * @returns { Summary } + */ +const getSummary = (validationResult) => ( + { + ...validationResult, + files: undefined, + filesSummary: (validationResult.files || []).map(item => ({ + required: item.required, + exists: item.exists, + file: item.file, + hasErrors: item.hasErrors, + errorsCount: item.errorsCount, + groupedErrors: item.exists && item.languages && item.languages[0] && item.languages[0].errors + ? groupErrors(item.languages[0].errors) + : [] + })) + } +) + +/** + * Groups errors by keyword, message, and schemaPath, adding a count for each group. + * + * @param errors array of error objects + * @returns {Array} grouped errors with count + */ +const groupErrors = (errors) => { + const errorMap = {}; + + errors.forEach(error => { + const key = `${error.keyword}-${error.message}-${error.schemaPath}`; + if (errorMap[key]) { + errorMap[key].count += 1; + } else { + errorMap[key] = { + keyword: error.keyword, + message: error.message, + schemaPath: error.schemaPath, + count: 1 + }; + } + }); + + return Object.values(errorMap); +}; + + + /** + * call the callback function with {@link Summary} + */ + exports.handler = function (event, context, callback) { + let body + + try { + body = JSON.parse(event.body) + } catch (err) { + callback(err, { + statusCode: 500, + body: JSON.stringify(err) + }) + } + + const gbfs = new GBFS(body.url, body.options) + + gbfs + .validation() + .then(result => { + callback(null, { + statusCode: 200, + body: JSON.stringify(getSummary(result)) + }) + }) + .catch(err => { + callback(null, { + statusCode: 500, + body: JSON.stringify(err.message) + }) + }) + }