From 39339c95eafab24ff6b6a15072d0e20784dcc488 Mon Sep 17 00:00:00 2001 From: sdowell Date: Wed, 12 Jan 2022 17:21:00 -0800 Subject: [PATCH] feat: add sortResults function (#484) This API is intended to provide a consistent functionality for sorting function results. The sorting behavior is based on the functionality of the Sort implementation in the golang function sdk. --- ts/kpt-functions/src/types.ts | 60 +++++++++++++ ts/kpt-functions/src/types_test.ts | 140 ++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/ts/kpt-functions/src/types.ts b/ts/kpt-functions/src/types.ts index 7c76ae045..2494a4686 100644 --- a/ts/kpt-functions/src/types.ts +++ b/ts/kpt-functions/src/types.ts @@ -479,3 +479,63 @@ interface ConfigMap extends KubernetesObject { function isConfigMap(o: any): o is ConfigMap { return o && o.apiVersion === 'v1' && o.kind === 'ConfigMap'; } + +function fileLess(a: Result, b: Result): number { + const pathA = a.file?.path ?? ''; + const pathB = b.file?.path ?? ''; + const indexA = a.file?.index ?? 0; + const indexB = b.file?.index ?? 0; + + if (pathA < pathB) { + return -1; + } else if (pathA > pathB) { + return 1; + } + return indexA - indexB; +} + +function severityLess(a: Result, b: Result): number { + const severityToNumber = { + error: 0, + warn: 1, + info: 2, + }; + const severityLevelA = severityToNumber[a.severity]; + const severityLevelB = severityToNumber[b.severity]; + return severityLevelA - severityLevelB; +} + +function stringLess(a: Result, b: Result): number { + const stringify = (r: Result): string => { + return `resource-ref:${JSON.stringify( + r.resourceRef + )},field:${JSON.stringify(r.field)},message:${r.message}`; + }; + const s1 = stringify(a); + const s2 = stringify(b); + if (s1 < s2) { + return -1; + } else if (s1 > s2) { + return 1; + } + return 0; +} + +/** + * Perform an in place sort of Results + * @param results Array of Result objects + */ +export function sortResults(results: Result[]): Result[] { + results.sort((a: Result, b: Result): number => { + const file = fileLess(a, b); + if (file !== 0) { + return file; + } + const severity = severityLess(a, b); + if (severity !== 0) { + return severity; + } + return stringLess(a, b); + }); + return results; +} diff --git a/ts/kpt-functions/src/types_test.ts b/ts/kpt-functions/src/types_test.ts index 4cf951e6c..95f2d1c4c 100644 --- a/ts/kpt-functions/src/types_test.ts +++ b/ts/kpt-functions/src/types_test.ts @@ -16,7 +16,7 @@ import _ from 'lodash'; import { ObjectMeta } from './gen/io.k8s.apimachinery.pkg.apis.meta.v1'; -import { Configs, KubernetesObject } from './types'; +import { Configs, KubernetesObject, sortResults } from './types'; import { generalResult } from './result'; class Role implements KubernetesObject { @@ -333,3 +333,141 @@ describe('results', () => { expect(results).toEqual([generalResult('a'), generalResult('b')]); }); }); + +describe('sortResults', () => { + it('sort based on severity', () => { + const configs = new Configs(); + configs.addResults({ message: 'Error message 1', severity: 'info' }); + configs.addResults({ message: 'Error message 2', severity: 'error' }); + const results = sortResults(configs.getResults()); + + expect(results).toEqual([ + { message: 'Error message 2', severity: 'error' }, + { message: 'Error message 1', severity: 'info' }, + ]); + }); + + it('sort based on file', () => { + const configs = new Configs(); + configs.addResults({ + message: 'Error message', + severity: 'error', + file: { path: 'resource.yaml', index: 1 }, + }); + configs.addResults({ + message: 'Error message', + severity: 'info', + file: { path: 'resource.yaml', index: 0 }, + }); + configs.addResults({ + message: 'Error message', + severity: 'info', + file: { path: 'other-resource.yaml', index: 0 }, + }); + configs.addResults({ + message: 'Error message', + severity: 'warn', + file: { path: 'resource.yaml', index: 2 }, + }); + configs.addResults({ message: 'Error message', severity: 'warn' }); + const results = sortResults(configs.getResults()); + + expect(results).toEqual([ + { message: 'Error message', severity: 'warn' }, + { + message: 'Error message', + severity: 'info', + file: { path: 'other-resource.yaml', index: 0 }, + }, + { + message: 'Error message', + severity: 'info', + file: { path: 'resource.yaml', index: 0 }, + }, + { + message: 'Error message', + severity: 'error', + file: { path: 'resource.yaml', index: 1 }, + }, + { + message: 'Error message', + severity: 'warn', + file: { path: 'resource.yaml', index: 2 }, + }, + ]); + }); + + it('sort based on other fields', () => { + const configs = new Configs(); + configs.addResults({ + message: 'Error message', + severity: 'error', + resourceRef: { + apiVersion: 'v1', + kind: 'Pod', + name: 'bar', + namespace: 'foo-ns', + }, + field: { path: 'spec' }, + }); + configs.addResults({ + message: 'Error message', + severity: 'error', + resourceRef: { + apiVersion: 'v1', + kind: 'Pod', + name: 'bar', + namespace: 'foo-ns', + }, + field: { path: 'metadata.name' }, + }); + configs.addResults({ + message: 'Another error message', + severity: 'error', + resourceRef: { + apiVersion: 'v1', + kind: 'ConfigMap', + name: 'bar', + namespace: 'foo-ns', + }, + field: { path: 'metadata.name' }, + }); + const results = sortResults(configs.getResults()); + + expect(results).toEqual([ + { + message: 'Another error message', + severity: 'error', + resourceRef: { + apiVersion: 'v1', + kind: 'ConfigMap', + name: 'bar', + namespace: 'foo-ns', + }, + field: { path: 'metadata.name' }, + }, + { + message: 'Error message', + severity: 'error', + resourceRef: { + apiVersion: 'v1', + kind: 'Pod', + name: 'bar', + namespace: 'foo-ns', + }, + field: { path: 'metadata.name' }, + }, + { + message: 'Error message', + severity: 'error', + resourceRef: { + apiVersion: 'v1', + kind: 'Pod', + name: 'bar', + namespace: 'foo-ns', + }, + field: { path: 'spec' }, + }, + ]); + }); +});