From b7c6d465cc6a688febdc1bb4ac38a2accbc33163 Mon Sep 17 00:00:00 2001 From: Julien Deniau Date: Thu, 29 Aug 2024 07:52:44 +0000 Subject: [PATCH] add levenstein test to help users if they use a wrong key --- __tests__/RestClientSdk.test.js | 26 ++++++++++++++-- src/RestClientSdk.ts | 9 +++++- src/utils/levenshtein.ts | 53 +++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 src/utils/levenshtein.ts diff --git a/__tests__/RestClientSdk.test.js b/__tests__/RestClientSdk.test.js index a477122..aacc8c4 100644 --- a/__tests__/RestClientSdk.test.js +++ b/__tests__/RestClientSdk.test.js @@ -9,8 +9,17 @@ import tokenStorage from '../__mocks__/tokenStorage'; const mapping = new Mapping('/v2'); const testMetadata = new ClassMetadata('test'); +const cartMetadata = new ClassMetadata('cart'); +const cartItemMetadata = new ClassMetadata('cartItem'); +const orderMetadata = new ClassMetadata('order'); + testMetadata.setAttributeList([new Attribute('@id', '@id', 'string', true)]); -mapping.setMapping([testMetadata]); +mapping.setMapping([ + testMetadata, + cartMetadata, + cartItemMetadata, + orderMetadata, +]); describe('Mapado Sdk tests', () => { test('Test wrong SDK configuration', () => { @@ -75,7 +84,20 @@ describe('Mapado Sdk tests', () => { ); expect(() => sdk.getRepository('toast')).toThrowError( - 'Unable to get metadata for repository toast' + 'Unable to get metadata for repository "toast". Did you mean "test"?' + ); + + expect(() => sdk.getRepository('Cart')).toThrowError( + 'Unable to get metadata for repository "Cart". Did you mean "cart"?' + ); + + expect(() => sdk.getRepository('cart_items')).toThrowError( + 'Unable to get metadata for repository "cart_items". Did you mean "cartItem"?' + ); + + // word is too far from any other words + expect(() => sdk.getRepository('zargiblou')).toThrowError( + 'Unable to get metadata for repository "zargiblou".' ); }); }); diff --git a/src/RestClientSdk.ts b/src/RestClientSdk.ts index 4939375..4ef96c7 100644 --- a/src/RestClientSdk.ts +++ b/src/RestClientSdk.ts @@ -9,6 +9,7 @@ import UnitOfWork from './UnitOfWork'; import AbstractClient from './client/AbstractClient'; import JsSerializer from './serializer/JsSerializer'; import SerializerInterface from './serializer/SerializerInterface'; +import { findClosestWord } from './utils/levenstein'; import { Logger } from './utils/logging'; import { generateRepository } from './utils/repositoryGenerator'; @@ -80,7 +81,13 @@ class RestClientSdk const metadata = this.mapping.getClassMetadataByKey(key); if (!metadata) { - throw new Error(`Unable to get metadata for repository ${key}`); + const closeKey = findClosestWord(key, this.mapping.getMappingKeys(), 5); + + const suggestions = closeKey ? ` Did you mean "${closeKey}"?` : ''; + + throw new Error( + `Unable to get metadata for repository "${key}".${suggestions}` + ); } // eslint-disable-next-line new-cap diff --git a/src/utils/levenshtein.ts b/src/utils/levenshtein.ts new file mode 100644 index 0000000..8a9330b --- /dev/null +++ b/src/utils/levenshtein.ts @@ -0,0 +1,53 @@ +function levenshteinDistance(a: string, b: string): number { + const matrix: number[][] = []; + + // Increment along the first column of each row + for (let i = 0; i <= b.length; i += 1) { + matrix[i] = [i]; + } + + // Increment each column in the first row + for (let j = 0; j <= a.length; j += 1) { + matrix[0][j] = j; + } + + // Fill in the rest of the matrix + for (let i = 1; i <= b.length; i += 1) { + for (let j = 1; j <= a.length; j += 1) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1 // deletion + ); + } + } + } + + return matrix[b.length][a.length]; +} + +// eslint-disable-next-line import/prefer-default-export +export function findClosestWord( + word: string, + list: string[], + maxDistance = Infinity +): string | null { + let closestWord: string | null = null; + let minDistance = Infinity; + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < list.length; i += 1) { + const candidate = list[i]; + const distance = levenshteinDistance(word, candidate); + + if (distance < minDistance && distance <= maxDistance) { + minDistance = distance; + closestWord = candidate; + } + } + + return closestWord; +}