From aa827b936190ba5e8a96a78b233c9d4cb35d2d61 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Mon, 29 May 2017 09:50:59 -0400 Subject: [PATCH 01/34] Add tests for page and page_size --- src/utils/url-beautifier.ts | 23 ++++++++++++++++++++--- test/unit/utils/url-beautifier.ts | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index 891968ab..c5f2a439 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -8,6 +8,8 @@ export class UrlBeautifier { config: BeautifierConfig = { refinementMapping: [], extraRefinementsParam: 'refinements', + pageSizeParam: 'page_size', + pageParam: 'page', queryToken: 'q', suffix: '' }; @@ -65,7 +67,7 @@ export class UrlGenerator { const request = query.build(); const uri = { path: [], - query: '' + query: {} }; // let url = ''; const origRefinements = Array.of(...request.refinements); @@ -94,15 +96,28 @@ export class UrlGenerator { // add remaining refinements if (origRefinements.length) { - uri.query = origRefinements + uri.query[this.config.extraRefinementsParam] = origRefinements .sort((lhs, rhs) => lhs.navigationName.localeCompare(rhs.navigationName)) .map(this.stringifyRefinement) .join('~'); } + // add page size + if (query.raw.pageSize) { + uri.query[this.config.pageSizeParam] = query.raw.pageSize; + if (query.raw.skip) { + uri.query[this.config.pageParam] = Math.floor(query.raw.skip/query.raw.pageSize)+1; + } + } + let url = `/${uri.path.map((path) => encodeURIComponent(path)).join('/')}`; if (this.config.suffix) url += `/${this.config.suffix.replace(/^\/+/, '')}`; - if (uri.query) url += `?${this.config.extraRefinementsParam}=${encodeURIComponent(uri.query)}`; + + const queryString = Object.keys(uri.query).sort().map((key) => { + return `${key}=${encodeURIComponent(uri.query[key])}`; + }).join('&'); + + if (queryString) url += '?' + queryString; return url.replace(/\s|%20/g, '+'); } @@ -222,6 +237,8 @@ export class UrlParser { export interface BeautifierConfig { refinementMapping?: any[]; extraRefinementsParam?: string; + pageSizeParam?: string; + pageParam?: string; queryToken?: string; suffix?: string; } diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index 6a80a265..b5e135f0 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -98,6 +98,30 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq('/?refinements=colour%3Ddark+purple~price%3A100..220'); }); + it('should convert pageSize to a query parameter', () => { + query.withPageSize(24); + + expect(generator.build(query)).to.eq('/?page_size=24'); + }); + + it('should convert pageSize and unmapped refinements to a query parameter list', () => { + query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); + query.withPageSize(24); + + expect(generator.build(query)).to.eq('/?page_size=24&refinements=colour%3Ddark+purple~price%3A100..220'); + }); + + it('should convert skip and pageSize to a query parameter', () => { + const pageSize = 10; + const skip = 32; + const page = 4; + + query.withPageSize(pageSize); + query.skip(skip); + + expect(generator.build(query)).to.eq(`/?page=${page}&page_size=${pageSize}`); + }); + describe('canonical URLs', () => { const ref1 = refinement('colour', 'orange'); const ref2 = refinement('brand', 'DeWalt'); From 134988ccfa4ca596407b9b1fcafbc376da1ac44c Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Mon, 29 May 2017 10:31:22 -0400 Subject: [PATCH 02/34] Change ' ' to '-' instead of '+' --- src/utils/url-beautifier.ts | 4 ++-- test/unit/utils/url-beautifier.ts | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index c5f2a439..91a0d01b 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -119,7 +119,7 @@ export class UrlGenerator { if (queryString) url += '?' + queryString; - return url.replace(/\s|%20/g, '+'); + return url.replace(/\s|%20/g, '-'); } private generateRefinementMap(refinements: SelectedRefinement[]): { map: any, keys: string[] } { @@ -230,7 +230,7 @@ export class UrlParser { } private decode(value: string): string { - return decodeURIComponent(value.replace('+', ' ')); + return decodeURIComponent(value.replace('-', ' ')); } } diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index b5e135f0..06c970ec 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -20,7 +20,7 @@ describe('URL beautifier', () => { it('should convert a simple query to a URL', () => { query.withQuery('red apples'); - expect(generator.build(query)).to.eq('/red+apples/q'); + expect(generator.build(query)).to.eq('/red-apples/q'); }); it('should convert query with a slash to a URL', () => { @@ -74,7 +74,7 @@ describe('URL beautifier', () => { beautifier.config.refinementMapping.push({ b: 'brand' }, { h: 'height' }); query.withSelectedRefinements(refinement('brand', 'Farmer John'), refinement('height', '20in')); - expect(generator.build(query)).to.eq('/Farmer+John/20in/bh'); + expect(generator.build(query)).to.eq('/Farmer-John/20in/bh'); }); it('should convert query and refinements to a URL', () => { @@ -82,7 +82,7 @@ describe('URL beautifier', () => { query.withQuery('cool sneakers') .withSelectedRefinements(refinement('colour', 'green')); - expect(generator.build(query)).to.eq('/cool+sneakers/green/qc'); + expect(generator.build(query)).to.eq('/cool-sneakers/green/qc'); }); it('should not convert range refinements to a URL', () => { @@ -95,7 +95,7 @@ describe('URL beautifier', () => { it('should convert unmapped refinements to a query parameter', () => { query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); - expect(generator.build(query)).to.eq('/?refinements=colour%3Ddark+purple~price%3A100..220'); + expect(generator.build(query)).to.eq('/?refinements=colour%3Ddark-purple~price%3A100..220'); }); it('should convert pageSize to a query parameter', () => { @@ -108,7 +108,7 @@ describe('URL beautifier', () => { query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); query.withPageSize(24); - expect(generator.build(query)).to.eq('/?page_size=24&refinements=colour%3Ddark+purple~price%3A100..220'); + expect(generator.build(query)).to.eq('/?page_size=24&refinements=colour%3Ddark-purple~price%3A100..220'); }); it('should convert skip and pageSize to a query parameter', () => { @@ -163,7 +163,7 @@ describe('URL beautifier', () => { const url = generator.build(query); - expect(url).to.eq('/power+drill/DeWalt/Drills/sbc/index.php?refs=colour%3Dorange'); + expect(url).to.eq('/power-drill/DeWalt/Drills/sbc/index.php?refs=colour%3Dorange'); expect(url).to.eq(generator.build(otherQuery)); }); }); @@ -230,7 +230,7 @@ describe('URL beautifier', () => { beautifier.config.refinementMapping.push({ c: 'colour', b: 'brand' }); query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('brand', 'Wellingtons')); - expect(parser.parse('/dark+purple/Wellingtons/cb').build()).to.eql(query.build()); + expect(parser.parse('/dark-purple/Wellingtons/cb').build()).to.eql(query.build()); }); it('should extract a query and refinement from URL', () => { @@ -262,14 +262,14 @@ describe('URL beautifier', () => { beautifier.config.queryToken = 'n'; beautifier.config.suffix = 'index.html'; - const request = parser.parse('/power+drill/orange/Drills/nsc/index.html?nav=brand%3DDeWalt').build(); + const request = parser.parse('/power-drill/orange/Drills/nsc/index.html?nav=brand%3DDeWalt').build(); expect(request.query).to.eql('power drill'); expect(request.refinements).to.have.deep.members(refs); }); it('should extract deeply nested URL', () => { - const request = parser.parse('http://example.com/my/nested/path/power+drill/q').build(); + const request = parser.parse('http://example.com/my/nested/path/power-drill/q').build(); expect(request.query).to.eql('power drill'); }); @@ -287,7 +287,7 @@ describe('URL beautifier', () => { it('should error on invalid reference keys', () => { beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); - expect(() => parser.parse('/power+drill/orange/Drills/qccb').build()).to.throw('token reference is invalid'); + expect(() => parser.parse('/power-drill/orange/Drills/qccb').build()).to.throw('token reference is invalid'); }); it('should error on unrecognized key', () => { @@ -326,7 +326,7 @@ describe('URL beautifier', () => { }); it('should convert from URL to a query and back', () => { - const url = '/duvet+cover/Duvet+King/linen/kbf/index.html?refs=price%3A10..40'; + const url = '/duvet-cover/Duvet-King/linen/kbf/index.html?refs=price%3A10..40'; const convertedUrl = beautifier.build(beautifier.parse(url)); From c0607b763fdde13468174545276a03b7f2d2df80 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Mon, 29 May 2017 11:14:44 -0400 Subject: [PATCH 03/34] Don't generate /q when it's not necessary --- src/utils/url-beautifier.ts | 2 +- test/unit/utils/url-beautifier.ts | 35 ++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index 91a0d01b..e908893e 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -87,7 +87,7 @@ export class UrlGenerator { } // add reference key - if (keys.length || request.query) { + if (keys.length) { let referenceKey = ''; if (request.query) referenceKey += this.config.queryToken; keys.forEach((key) => referenceKey += key.repeat(countMap[key])); diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index 06c970ec..5a8f12a6 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -20,26 +20,19 @@ describe('URL beautifier', () => { it('should convert a simple query to a URL', () => { query.withQuery('red apples'); - expect(generator.build(query)).to.eq('/red-apples/q'); + expect(generator.build(query)).to.eq('/red-apples'); }); it('should convert query with a slash to a URL', () => { query.withQuery('red/apples'); - expect(generator.build(query)).to.eq('/red%2Fapples/q'); + expect(generator.build(query)).to.eq('/red%2Fapples'); }); it('should convert query with a plus to a URL', () => { query.withQuery('red+apples'); - expect(generator.build(query)).to.eq('/red%2Bapples/q'); - }); - - it('should convert a simple query to a URL with a custom token', () => { - beautifier.config.queryToken = 'a'; - query.withQuery('sneakers'); - - expect(generator.build(query)).to.eq('/sneakers/a'); + expect(generator.build(query)).to.eq('/red%2Bapples'); }); it('should convert a value refinement query to a URL', () => { @@ -85,6 +78,15 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq('/cool-sneakers/green/qc'); }); + it('should convert query and refinements to a URL with a custom token', () => { + beautifier.config.queryToken = 'a'; + beautifier.config.refinementMapping.push({ c: 'colour' }); + query.withQuery('cool sneakers') + .withSelectedRefinements(refinement('colour', 'green')); + + expect(generator.build(query)).to.eq('/cool-sneakers/green/ac'); + }); + it('should not convert range refinements to a URL', () => { beautifier.config.refinementMapping.push({ p: 'price' }); query.withSelectedRefinements(refinement('price', 20, 40)); @@ -122,6 +124,19 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq(`/?page=${page}&page_size=${pageSize}`); }); + it('should convert query with skip, page size and unmapped refinements to a URL with a query parameter list', () => { + const pageSize = 6; + const skip = 6; + const page = 2; + + query.withPageSize(pageSize); + query.skip(skip); + query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); + query.withQuery('red apples'); + + expect(generator.build(query)).to.eq(`/red-apples?page=${page}&page_size=${pageSize}&refinements=colour%3Ddark-purple~price%3A100..220`); + }); + describe('canonical URLs', () => { const ref1 = refinement('colour', 'orange'); const ref2 = refinement('brand', 'DeWalt'); From d67979617273eb7fdcdb38dc3f5ad2c25c085a62 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Mon, 29 May 2017 11:29:10 -0400 Subject: [PATCH 04/34] Allow page even when page_size is not specified --- src/utils/url-beautifier.ts | 10 +++++++--- test/unit/utils/url-beautifier.ts | 14 ++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index e908893e..66ace628 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -10,6 +10,7 @@ export class UrlBeautifier { extraRefinementsParam: 'refinements', pageSizeParam: 'page_size', pageParam: 'page', + defaultPageSize: 10, queryToken: 'q', suffix: '' }; @@ -105,9 +106,11 @@ export class UrlGenerator { // add page size if (query.raw.pageSize) { uri.query[this.config.pageSizeParam] = query.raw.pageSize; - if (query.raw.skip) { - uri.query[this.config.pageParam] = Math.floor(query.raw.skip/query.raw.pageSize)+1; - } + } + + // add page + if (query.raw.skip) { + uri.query[this.config.pageParam] = Math.floor(query.raw.skip/(query.raw.pageSize || this.config.defaultPageSize))+1; } let url = `/${uri.path.map((path) => encodeURIComponent(path)).join('/')}`; @@ -239,6 +242,7 @@ export interface BeautifierConfig { extraRefinementsParam?: string; pageSizeParam?: string; pageParam?: string; + defaultPageSize?: number; queryToken?: string; suffix?: string; } diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index 5a8f12a6..bd108cba 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -106,17 +106,19 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq('/?page_size=24'); }); - it('should convert pageSize and unmapped refinements to a query parameter list', () => { - query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); - query.withPageSize(24); + it('should convert skip to a query parameter', () => { + const skip = 32; + const page = 4; - expect(generator.build(query)).to.eq('/?page_size=24&refinements=colour%3Ddark-purple~price%3A100..220'); + query.skip(skip); + + expect(generator.build(query)).to.eq(`/?page=${page}`); }); it('should convert skip and pageSize to a query parameter', () => { - const pageSize = 10; + const pageSize = 30; const skip = 32; - const page = 4; + const page = 2; query.withPageSize(pageSize); query.skip(skip); From 94f2033ca270e0c9d54021c64b91c1ac6334056c Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Mon, 29 May 2017 13:49:49 -0400 Subject: [PATCH 05/34] Revert "Don't generate /q when it's not necessary" This reverts commit c0607b763fdde13468174545276a03b7f2d2df80. --- src/utils/url-beautifier.ts | 2 +- test/unit/utils/url-beautifier.ts | 35 +++++++++---------------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index 66ace628..2f51e893 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -88,7 +88,7 @@ export class UrlGenerator { } // add reference key - if (keys.length) { + if (keys.length || request.query) { let referenceKey = ''; if (request.query) referenceKey += this.config.queryToken; keys.forEach((key) => referenceKey += key.repeat(countMap[key])); diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index bd108cba..cda41039 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -20,19 +20,26 @@ describe('URL beautifier', () => { it('should convert a simple query to a URL', () => { query.withQuery('red apples'); - expect(generator.build(query)).to.eq('/red-apples'); + expect(generator.build(query)).to.eq('/red-apples/q'); }); it('should convert query with a slash to a URL', () => { query.withQuery('red/apples'); - expect(generator.build(query)).to.eq('/red%2Fapples'); + expect(generator.build(query)).to.eq('/red%2Fapples/q'); }); it('should convert query with a plus to a URL', () => { query.withQuery('red+apples'); - expect(generator.build(query)).to.eq('/red%2Bapples'); + expect(generator.build(query)).to.eq('/red%2Bapples/q'); + }); + + it('should convert a simple query to a URL with a custom token', () => { + beautifier.config.queryToken = 'a'; + query.withQuery('sneakers'); + + expect(generator.build(query)).to.eq('/sneakers/a'); }); it('should convert a value refinement query to a URL', () => { @@ -78,15 +85,6 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq('/cool-sneakers/green/qc'); }); - it('should convert query and refinements to a URL with a custom token', () => { - beautifier.config.queryToken = 'a'; - beautifier.config.refinementMapping.push({ c: 'colour' }); - query.withQuery('cool sneakers') - .withSelectedRefinements(refinement('colour', 'green')); - - expect(generator.build(query)).to.eq('/cool-sneakers/green/ac'); - }); - it('should not convert range refinements to a URL', () => { beautifier.config.refinementMapping.push({ p: 'price' }); query.withSelectedRefinements(refinement('price', 20, 40)); @@ -126,19 +124,6 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq(`/?page=${page}&page_size=${pageSize}`); }); - it('should convert query with skip, page size and unmapped refinements to a URL with a query parameter list', () => { - const pageSize = 6; - const skip = 6; - const page = 2; - - query.withPageSize(pageSize); - query.skip(skip); - query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); - query.withQuery('red apples'); - - expect(generator.build(query)).to.eq(`/red-apples?page=${page}&page_size=${pageSize}&refinements=colour%3Ddark-purple~price%3A100..220`); - }); - describe('canonical URLs', () => { const ref1 = refinement('colour', 'orange'); const ref2 = refinement('brand', 'DeWalt'); From e44fb84f4aa2d605ccffb6c5cdec4d120fd25de9 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Mon, 29 May 2017 17:05:21 -0400 Subject: [PATCH 06/34] Add option to disable reference keys --- src/utils/url-beautifier.ts | 65 +++++++++++++++++++++++-------- test/unit/utils/url-beautifier.ts | 37 ++++++++++++++++++ 2 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index 2f51e893..49e72495 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -12,7 +12,8 @@ export class UrlBeautifier { pageParam: 'page', defaultPageSize: 10, queryToken: 'q', - suffix: '' + suffix: '', + useReferenceKeys: true }; private generator: UrlGenerator = new UrlGenerator(this); private parser: UrlParser = new UrlParser(this); @@ -72,27 +73,48 @@ export class UrlGenerator { }; // let url = ''; const origRefinements = Array.of(...request.refinements); - const countMap = {}; - const { map, keys } = this.generateRefinementMap(origRefinements); // add query if (request.query) { uri.path.push(request.query); } - // add refinements - for (let key of keys) { - const refinements = map[key]; - countMap[key] = refinements.length; - uri.path.push(...refinements.map(this.convertRefinement).sort()); - } + if (this.config.useReferenceKeys) { + const countMap = {}; + const { map, keys } = this.generateRefinementMap(origRefinements); + + // add refinements + for (let key of keys) { + const refinements = map[key]; + countMap[key] = refinements.length; + refinements.map(this.convertToSelectedValueRefinement) + .sort(this.refinementsComparator) + .forEach((selectedValueRefinement) => { + uri.path.push(selectedValueRefinement.value); + }); + } - // add reference key - if (keys.length || request.query) { - let referenceKey = ''; - if (request.query) referenceKey += this.config.queryToken; - keys.forEach((key) => referenceKey += key.repeat(countMap[key])); - uri.path.push(referenceKey); + // add reference key + if (keys.length || request.query) { + let referenceKey = ''; + if (request.query) referenceKey += this.config.queryToken; + keys.forEach((key) => referenceKey += key.repeat(countMap[key])); + uri.path.push(referenceKey); + } + } else { + // add refinements + let valueRefinements = [], rangeRefinement = []; + for (let i = origRefinements.length - 1; i >= 0; --i) { + if (origRefinements[i].type === 'Value') { + valueRefinements.push(...origRefinements.splice(i, 1)); + } + } + + valueRefinements.map(this.convertToSelectedValueRefinement) + .sort(this.refinementsComparator) + .forEach((selectedValueRefinement) => { + uri.path.push(selectedValueRefinement.value, selectedValueRefinement.navigationName); + }); } // add remaining refinements @@ -140,9 +162,9 @@ export class UrlGenerator { return { map: refinementMap, keys: refinementKeys }; } - private convertRefinement(refinement: SelectedRefinement): string { + private convertToSelectedValueRefinement(refinement: SelectedRefinement): SelectedValueRefinement { if (refinement.type === 'Value') { - return (refinement).value; + return refinement; } else { throw new Error('cannot map range refinements'); } @@ -156,6 +178,14 @@ export class UrlGenerator { return `${name}:${(refinement).low}..${(refinement).high}`; } } + + private refinementsComparator(refinement1: SelectedValueRefinement, refinement2: SelectedValueRefinement): number { + let comparison = refinement1.navigationName.localeCompare(refinement2.navigationName); + if (comparison === 0) { + comparison = refinement1.value.localeCompare(refinement2.value); + } + return comparison; + } } export class UrlParser { @@ -245,4 +275,5 @@ export interface BeautifierConfig { defaultPageSize?: number; queryToken?: string; suffix?: string; + useReferenceKeys?: boolean; } diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index cda41039..8a527bba 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -23,6 +23,13 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq('/red-apples/q'); }); + it('should convert a simple query to a URL without reference keys', () => { + beautifier.config.useReferenceKeys = false; + query.withQuery('red apples'); + + expect(generator.build(query)).to.eq('/red-apples'); + }); + it('should convert query with a slash to a URL', () => { query.withQuery('red/apples'); @@ -56,6 +63,22 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq('/DeWalt/Henson/bb'); }); + it('should convert a multiple refinements on same field a URL without reference keys', () => { + beautifier.config.useReferenceKeys = false; + query.withQuery('tool') + .withSelectedRefinements(refinement('brand', 'DeWalt'), refinement('brand', 'Henson')); + + expect(generator.build(query)).to.eq('/tool/DeWalt/brand/Henson/brand'); + }); + + it('should convert a sorted refinements list on same field a URL without reference keys', () => { + beautifier.config.useReferenceKeys = false; + query.withQuery('shoe') + .withSelectedRefinements(refinement('colour', 'blue'), refinement('Brand', 'nike'), refinement('Brand', 'adidas'), refinement('colour', 'red')); + + expect(generator.build(query)).to.eq('/shoe/adidas/Brand/nike/Brand/blue/colour/red/colour'); + }); + it('should convert a refinement with a slash to a URL', () => { beautifier.config.refinementMapping.push({ b: 'brand' }); query.withSelectedRefinements(refinement('brand', 'De/Walt')); @@ -124,6 +147,20 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq(`/?page=${page}&page_size=${pageSize}`); }); + it('should convert query with skip, page size and unmapped refinements to a URL with a query parameter list without reference keys', () => { + const pageSize = 6; + const skip = 6; + const page = 2; + + beautifier.config.useReferenceKeys = false; + query.withPageSize(pageSize); + query.skip(skip); + query.withQuery('red apples') + .withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); + + expect(generator.build(query)).to.eq(`/red-apples/dark-purple/colour?page=${page}&page_size=${pageSize}&refinements=price%3A100..220`); + }); + describe('canonical URLs', () => { const ref1 = refinement('colour', 'orange'); const ref2 = refinement('brand', 'DeWalt'); From 24afb533fca002579c8a5c297e0c39bda2ce7818 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Tue, 30 May 2017 09:11:16 -0400 Subject: [PATCH 07/34] Use ':' to connect navigation name and navigation value of value refinements instead of using '=' --- src/utils/url-beautifier.ts | 2 +- test/unit/utils/url-beautifier.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index 49e72495..37e09bbf 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -173,7 +173,7 @@ export class UrlGenerator { private stringifyRefinement(refinement: SelectedRefinement): string { const name = refinement.navigationName; if (refinement.type === 'Value') { - return `${name}=${(refinement).value}`; + return `${name}:${(refinement).value}`; } else { return `${name}:${(refinement).low}..${(refinement).high}`; } diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index 8a527bba..73a7bdd9 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -118,7 +118,7 @@ describe('URL beautifier', () => { it('should convert unmapped refinements to a query parameter', () => { query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); - expect(generator.build(query)).to.eq('/?refinements=colour%3Ddark-purple~price%3A100..220'); + expect(generator.build(query)).to.eq('/?refinements=colour%3Adark-purple~price%3A100..220'); }); it('should convert pageSize to a query parameter', () => { @@ -161,6 +161,14 @@ describe('URL beautifier', () => { expect(generator.build(query)).to.eq(`/red-apples/dark-purple/colour?page=${page}&page_size=${pageSize}&refinements=price%3A100..220`); }); + it('should convert query with unmapped refinements to a URL with a query parameter list with reference keys', () => { + beautifier.config.refinementMapping.push({ c: 'category' }); + query.withQuery('long red dress') + .withSelectedRefinements(refinement('category', 'evening wear'), refinement('category', 'formal'), refinement('size', 'large'), refinement('shipping', 'true')); + + expect(generator.build(query)).to.eq(`/long-red-dress/evening-wear/formal/qcc?refinements=shipping%3Atrue~size%3Alarge`); + }); + describe('canonical URLs', () => { const ref1 = refinement('colour', 'orange'); const ref2 = refinement('brand', 'DeWalt'); @@ -202,7 +210,7 @@ describe('URL beautifier', () => { const url = generator.build(query); - expect(url).to.eq('/power-drill/DeWalt/Drills/sbc/index.php?refs=colour%3Dorange'); + expect(url).to.eq('/power-drill/DeWalt/Drills/sbc/index.php?refs=colour%3Aorange'); expect(url).to.eq(generator.build(otherQuery)); }); }); From 3d7d52a261f5c6ef7270ca4eaaa99c9d186a2717 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Tue, 30 May 2017 11:30:27 -0400 Subject: [PATCH 08/34] Parse query and value refinements from URL without reference keys --- src/utils/url-beautifier.ts | 39 +++++++++++++++++++++++-------- test/unit/utils/url-beautifier.ts | 22 +++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index 37e09bbf..1e402c3b 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -206,32 +206,51 @@ export class UrlParser { if (paths[paths.length - 1] === this.config.suffix) paths.pop(); - const keys = (paths.pop() || '').split(''); - const map = this.generateRefinementMapping(); - const query = new Query().withConfiguration(this.searchandiserConfig, CONFIGURATION_MASK); + const query = this.config.useReferenceKeys ? this.parsePathWithReferenceKeys(paths) : this.parsePathWithoutReferenceKeys(paths); + + const unmappedRefinements = queryString.parse(url.query)[this.config.extraRefinementsParam]; + if (unmappedRefinements) { + query.withSelectedRefinements(...this.extractUnmapped(unmappedRefinements)); + } + + return query; + } + private parsePathWithReferenceKeys(path: string[]): Query { + const query = new Query().withConfiguration(this.searchandiserConfig, CONFIGURATION_MASK); + const keys = (path.pop() || '').split(''); + const map = this.generateRefinementMapping(); for (let key of keys) { if (!(key in map || key === this.config.queryToken)) { throw new Error(`unexpected token '${key}' found in reference`); } } - if (paths.length < keys.length) throw new Error('token reference is invalid'); + if (path.length < keys.length) throw new Error('token reference is invalid'); // remove prefixed paths - paths.splice(0, paths.length - keys.length); + path.splice(0, path.length - keys.length); for (let i = 0; i < keys.length; i++) { if (keys[i] === this.config.queryToken) { - query.withQuery(this.decode(paths[i])); + query.withQuery(this.decode(path[i])); } else { - query.withSelectedRefinements(...this.extractRefinements(paths[i], map[keys[i]])); + query.withSelectedRefinements(...this.extractRefinements(path[i], map[keys[i]])); } } + return query; + } - const unmappedRefinements = queryString.parse(url.query)[this.config.extraRefinementsParam]; - if (unmappedRefinements) { - query.withSelectedRefinements(...this.extractUnmapped(unmappedRefinements)); + private parsePathWithoutReferenceKeys(path: string[]): Query { + const query = new Query().withConfiguration(this.searchandiserConfig, CONFIGURATION_MASK); + if (path.length % 2 === 1) { + query.withQuery(this.decode(path.shift())); + } + + while (path.length > 0) { + const value = this.decode(path.shift()); + const navigationName = path.shift(); + query.withSelectedRefinements({ navigationName, type: 'Value', value }); } return query; diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index 73a7bdd9..0adcb7fd 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -239,6 +239,13 @@ describe('URL beautifier', () => { expect(parser.parse('/red%2Bapples/q').build()).to.eql(query.build()); }); + it('should parse simple query URL with dash and without reference keys', () => { + beautifier.config.useReferenceKeys = false; + query.withQuery('red apples'); + + expect(parser.parse('/red-apples').build()).to.eql(query.build()); + }); + it('should parse simple query URL with custom token', () => { beautifier.config.queryToken = 'c'; @@ -288,6 +295,21 @@ describe('URL beautifier', () => { expect(parser.parse('/sneakers/green/qc').build()).to.eql(query.build()); }); + it('should extract query and value refinements from URL without reference keys', () => { + beautifier.config.useReferenceKeys = false; + query.withQuery('shoe') + .withSelectedRefinements(refinement('colour', 'blue'), refinement('colour', 'red'), refinement('Brand', 'adidas'), refinement('Brand', 'nike')); + + expect(parser.parse('/shoe/blue/colour/red/colour/adidas/Brand/nike/Brand').build()).to.eql(query.build()); + }); + + it('should extract value refinements from URL without reference keys', () => { + beautifier.config.useReferenceKeys = false; + query.withSelectedRefinements(refinement('colour', 'blue'), refinement('colour', 'red'), refinement('Brand', 'adidas'), refinement('Brand', 'nike')); + + expect(parser.parse('/blue/colour/red/colour/adidas/Brand/nike/Brand').build()).to.eql(query.build()); + }); + it('should extract unmapped query from URL parameters', () => { query.withSelectedRefinements(refinement('height', '20in'), refinement('price', 20, 30)); From 25723f93f21035c8cd7c336b3b004e8a3b1fab1d Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Tue, 30 May 2017 14:03:31 -0400 Subject: [PATCH 09/34] parse pageSize and page from URL --- src/utils/url-beautifier.ts | 21 +++++++++++++----- test/unit/utils/url-beautifier.ts | 37 +++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier.ts index 1e402c3b..50cb69c7 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier.ts @@ -138,11 +138,13 @@ export class UrlGenerator { let url = `/${uri.path.map((path) => encodeURIComponent(path)).join('/')}`; if (this.config.suffix) url += `/${this.config.suffix.replace(/^\/+/, '')}`; - const queryString = Object.keys(uri.query).sort().map((key) => { + const queryPart = Object.keys(uri.query).sort().map((key) => { return `${key}=${encodeURIComponent(uri.query[key])}`; }).join('&'); - if (queryString) url += '?' + queryString; + if (queryPart) { + url += '?' + queryPart; + } return url.replace(/\s|%20/g, '-'); } @@ -208,10 +210,19 @@ export class UrlParser { const query = this.config.useReferenceKeys ? this.parsePathWithReferenceKeys(paths) : this.parsePathWithoutReferenceKeys(paths); - const unmappedRefinements = queryString.parse(url.query)[this.config.extraRefinementsParam]; + const queryVariables = queryString.parse(url.query); + const unmappedRefinements = queryVariables[this.config.extraRefinementsParam]; + const pageSize = parseInt(queryVariables[this.config.pageSizeParam], 10); + const page = parseInt(queryVariables[this.config.pageParam], 10); if (unmappedRefinements) { query.withSelectedRefinements(...this.extractUnmapped(unmappedRefinements)); } + if (pageSize) { + query.withPageSize(pageSize); + } + if (page) { + query.skip((query.raw.pageSize || this.config.defaultPageSize) * (page - 1)); + } return query; } @@ -271,7 +282,7 @@ export class UrlParser { return refinementStrings .map(this.decode) .map((refinement) => { - const [navigationName, value] = refinement.split(/=|:/); + const [navigationName, value] = refinement.split(':'); if (value.indexOf('..') >= 0) { const [low, high] = value.split('..'); return { navigationName, low: Number(low), high: Number(high), type: 'Range' }; @@ -282,7 +293,7 @@ export class UrlParser { } private decode(value: string): string { - return decodeURIComponent(value.replace('-', ' ')); + return decodeURIComponent(value.replace(/-/g, ' ')); } } diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index 0adcb7fd..0987e3db 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -313,7 +313,40 @@ describe('URL beautifier', () => { it('should extract unmapped query from URL parameters', () => { query.withSelectedRefinements(refinement('height', '20in'), refinement('price', 20, 30)); - expect(parser.parse('/?refinements=height%3D20in~price%3A20..30').build()).to.eql(query.build()); + expect(parser.parse('/?refinements=height%3A20in~price%3A20..30').build()).to.eql(query.build()); + }); + + it('should extract query and range refinements from URL without reference key', () => { + beautifier.config.useReferenceKeys = false; + query.withQuery('long red dress') + .withSelectedRefinements(refinement('category', 'evening wear'), refinement('category', 'formal'), refinement('price', 50, 200)); + + expect(parser.parse('/long-red-dress/evening-wear/category/formal/category?refinements=price:50..200').build()).to.eql(query.build()); + }); + + it('should extract page size from URL', () => { + const pageSize = 5; + query.withPageSize(pageSize); + + expect(parser.parse(`/?page_size=${pageSize}`).build()).to.eql(query.build()); + }); + + it('should extract page from URL', () => { + const skip = 10; + const page = 2; + query.skip(skip); + + expect(parser.parse(`/?page=${page}`).build()).to.eql(query.build()); + }); + + it('should extract page and page size from URL', () => { + const page = 3; + const pageSize = 6; + const skip = (page - 1) * pageSize; + query.skip(skip) + .withPageSize(pageSize); + + expect(parser.parse(`/?page=${page}&page_size=${pageSize}`).build()).to.eql(query.build()); }); it('should ignore suffix', () => { @@ -331,7 +364,7 @@ describe('URL beautifier', () => { beautifier.config.queryToken = 'n'; beautifier.config.suffix = 'index.html'; - const request = parser.parse('/power-drill/orange/Drills/nsc/index.html?nav=brand%3DDeWalt').build(); + const request = parser.parse('/power-drill/orange/Drills/nsc/index.html?nav=brand%3ADeWalt').build(); expect(request.query).to.eql('power drill'); expect(request.refinements).to.have.deep.members(refs); From c8f41c5953fb3afe5a929861770941504a2feac3 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Tue, 30 May 2017 15:10:30 -0400 Subject: [PATCH 10/34] Add test for URL with suffix --- test/unit/utils/url-beautifier.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier.ts index 0987e3db..4bbc69ce 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier.ts @@ -370,6 +370,15 @@ describe('URL beautifier', () => { expect(request.refinements).to.have.deep.members(refs); }); + it('should extract mapped and unmapped refinements with query and suffix from URL without reference keys', () => { + beautifier.config.suffix = 'index.html'; + beautifier.config.useReferenceKeys = false; + query.withQuery('power drill') + .withSelectedRefinements(refinement('brand', 'DeWalt'), refinement('category', 'Drills'), refinement('colour', 'orange')); + + expect(parser.parse('/power-drill/DeWalt/brand/Drills/category/orange/colour/index.html').build()).to.eql(query.build()); + }); + it('should extract deeply nested URL', () => { const request = parser.parse('http://example.com/my/nested/path/power-drill/q').build(); From 87ef2161dd3ac7af06695e2f14b7196bea90312f Mon Sep 17 00:00:00 2001 From: Johann Tutor Date: Tue, 30 May 2017 16:02:38 -0400 Subject: [PATCH 11/34] Extract URL Beautifier interfaces --- .../index.ts} | 20 ++++---------- src/utils/url-beautifier/interfaces.ts | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+), 15 deletions(-) rename src/utils/{url-beautifier.ts => url-beautifier/index.ts} (96%) create mode 100644 src/utils/url-beautifier/interfaces.ts diff --git a/src/utils/url-beautifier.ts b/src/utils/url-beautifier/index.ts similarity index 96% rename from src/utils/url-beautifier.ts rename to src/utils/url-beautifier/index.ts index 50cb69c7..e7191606 100644 --- a/src/utils/url-beautifier.ts +++ b/src/utils/url-beautifier/index.ts @@ -1,9 +1,10 @@ -import { CONFIGURATION_MASK, SearchandiserConfig } from '../searchandiser'; +import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; +import { Beautifier, BeautifierConfig, Generator, Parser } from './interfaces'; import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement } from 'groupby-api'; import * as parseUri from 'parseUri'; import * as queryString from 'query-string'; -export class UrlBeautifier { +export class UrlBeautifier implements Beautifier { config: BeautifierConfig = { refinementMapping: [], @@ -57,7 +58,7 @@ export class UrlBeautifier { } } -export class UrlGenerator { +export class UrlGenerator implements Generator { config: BeautifierConfig; @@ -190,7 +191,7 @@ export class UrlGenerator { } } -export class UrlParser { +export class UrlParser implements Parser { searchandiserConfig: SearchandiserConfig; config: BeautifierConfig; @@ -296,14 +297,3 @@ export class UrlParser { return decodeURIComponent(value.replace(/-/g, ' ')); } } - -export interface BeautifierConfig { - refinementMapping?: any[]; - extraRefinementsParam?: string; - pageSizeParam?: string; - pageParam?: string; - defaultPageSize?: number; - queryToken?: string; - suffix?: string; - useReferenceKeys?: boolean; -} diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts new file mode 100644 index 00000000..614b7308 --- /dev/null +++ b/src/utils/url-beautifier/interfaces.ts @@ -0,0 +1,26 @@ +import { Query } from 'groupby-api'; + +export interface Beautifier { + config: BeautifierConfig; + parse(url: string): Query; + build(query: Query): string; +} + +export interface Generator { + build(query: Query): string; +} + +export interface Parser { + parse(url: string): Query; +} + +export interface BeautifierConfig { + refinementMapping?: any[]; + extraRefinementsParam?: string; + pageSizeParam?: string; + pageParam?: string; + defaultPageSize?: number; + queryToken?: string; + suffix?: string; + useReferenceKeys?: boolean; +} From 13691d0a24bf2e4ed03cc1d187ca59e40ca14613 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Wed, 31 May 2017 10:57:38 -0400 Subject: [PATCH 12/34] Separate generator and parser into separate modules --- src/utils/url-beautifier/index.ts | 302 +----------------- src/utils/url-beautifier/interfaces.ts | 2 + .../url-beautifier/queries-url-beautifier.ts | 245 ++++++++++++++ src/utils/url-beautifier/url-beautifier.ts | 58 ++++ .../query-url-beautifier.ts} | 18 +- 5 files changed, 318 insertions(+), 307 deletions(-) create mode 100644 src/utils/url-beautifier/queries-url-beautifier.ts create mode 100644 src/utils/url-beautifier/url-beautifier.ts rename test/unit/utils/{url-beautifier.ts => url-beautifier/query-url-beautifier.ts} (97%) diff --git a/src/utils/url-beautifier/index.ts b/src/utils/url-beautifier/index.ts index e7191606..3f297680 100644 --- a/src/utils/url-beautifier/index.ts +++ b/src/utils/url-beautifier/index.ts @@ -1,299 +1,5 @@ -import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; -import { Beautifier, BeautifierConfig, Generator, Parser } from './interfaces'; -import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement } from 'groupby-api'; -import * as parseUri from 'parseUri'; -import * as queryString from 'query-string'; +import { UrlBeautifier, UrlGenerator, UrlParser } from './url-beautifier'; +import { QueryUrlGenerator, QueryUrlParser } from './queries-url-beautifier'; -export class UrlBeautifier implements Beautifier { - - config: BeautifierConfig = { - refinementMapping: [], - extraRefinementsParam: 'refinements', - pageSizeParam: 'page_size', - pageParam: 'page', - defaultPageSize: 10, - queryToken: 'q', - suffix: '', - useReferenceKeys: true - }; - private generator: UrlGenerator = new UrlGenerator(this); - private parser: UrlParser = new UrlParser(this); - - constructor(public searchandiserConfig: SearchandiserConfig = {}) { - const urlConfig = searchandiserConfig.url || {}; - const config = typeof urlConfig.beautifier === 'object' ? urlConfig.beautifier : {}; - Object.assign(this.config, config); - - const keys = []; - for (let mapping of this.config.refinementMapping) { - const key = Object.keys(mapping)[0]; - if (key.length !== 1) { - throw new Error('refinement mapping token must be a single character'); - } - if (key.match(/[aeiouy]/)) { - throw new Error('refinement mapping token must not be a vowel'); - } - if (keys.indexOf(key) > -1) { - throw new Error('refinement mapping tokens must be unique'); - } - keys.push(key); - } - if (this.config.queryToken.length !== 1) { - throw new Error('query token must be a single character'); - } - if (this.config.queryToken.match(/[aeiouy]/)) { - throw new Error('query token must not be a vowel'); - } - if (keys.indexOf(this.config.queryToken) > -1) { - throw new Error('query token must be unique from refinement tokens'); - } - } - - parse(url: string) { - return this.parser.parse(url); - } - - build(query: Query) { - return this.generator.build(query); - } -} - -export class UrlGenerator implements Generator { - - config: BeautifierConfig; - - constructor({ config }: UrlBeautifier) { - this.config = config; - } - - build(query: Query): string { - const request = query.build(); - const uri = { - path: [], - query: {} - }; - // let url = ''; - const origRefinements = Array.of(...request.refinements); - - // add query - if (request.query) { - uri.path.push(request.query); - } - - if (this.config.useReferenceKeys) { - const countMap = {}; - const { map, keys } = this.generateRefinementMap(origRefinements); - - // add refinements - for (let key of keys) { - const refinements = map[key]; - countMap[key] = refinements.length; - refinements.map(this.convertToSelectedValueRefinement) - .sort(this.refinementsComparator) - .forEach((selectedValueRefinement) => { - uri.path.push(selectedValueRefinement.value); - }); - } - - // add reference key - if (keys.length || request.query) { - let referenceKey = ''; - if (request.query) referenceKey += this.config.queryToken; - keys.forEach((key) => referenceKey += key.repeat(countMap[key])); - uri.path.push(referenceKey); - } - } else { - // add refinements - let valueRefinements = [], rangeRefinement = []; - for (let i = origRefinements.length - 1; i >= 0; --i) { - if (origRefinements[i].type === 'Value') { - valueRefinements.push(...origRefinements.splice(i, 1)); - } - } - - valueRefinements.map(this.convertToSelectedValueRefinement) - .sort(this.refinementsComparator) - .forEach((selectedValueRefinement) => { - uri.path.push(selectedValueRefinement.value, selectedValueRefinement.navigationName); - }); - } - - // add remaining refinements - if (origRefinements.length) { - uri.query[this.config.extraRefinementsParam] = origRefinements - .sort((lhs, rhs) => lhs.navigationName.localeCompare(rhs.navigationName)) - .map(this.stringifyRefinement) - .join('~'); - } - - // add page size - if (query.raw.pageSize) { - uri.query[this.config.pageSizeParam] = query.raw.pageSize; - } - - // add page - if (query.raw.skip) { - uri.query[this.config.pageParam] = Math.floor(query.raw.skip/(query.raw.pageSize || this.config.defaultPageSize))+1; - } - - let url = `/${uri.path.map((path) => encodeURIComponent(path)).join('/')}`; - if (this.config.suffix) url += `/${this.config.suffix.replace(/^\/+/, '')}`; - - const queryPart = Object.keys(uri.query).sort().map((key) => { - return `${key}=${encodeURIComponent(uri.query[key])}`; - }).join('&'); - - if (queryPart) { - url += '?' + queryPart; - } - - return url.replace(/\s|%20/g, '-'); - } - - private generateRefinementMap(refinements: SelectedRefinement[]): { map: any, keys: string[] } { - const refinementMap = {}; - const refinementKeys = []; - for (let mapping of this.config.refinementMapping) { - const key = Object.keys(mapping)[0]; - const matchingRefinements = refinements.filter((refinement) => refinement.navigationName === mapping[key]); - if (matchingRefinements.length) { - refinementKeys.push(key); - refinementMap[key] = matchingRefinements; - matchingRefinements.forEach((ref) => refinements.splice(refinements.indexOf(ref), 1)); - } - } - return { map: refinementMap, keys: refinementKeys }; - } - - private convertToSelectedValueRefinement(refinement: SelectedRefinement): SelectedValueRefinement { - if (refinement.type === 'Value') { - return refinement; - } else { - throw new Error('cannot map range refinements'); - } - } - - private stringifyRefinement(refinement: SelectedRefinement): string { - const name = refinement.navigationName; - if (refinement.type === 'Value') { - return `${name}:${(refinement).value}`; - } else { - return `${name}:${(refinement).low}..${(refinement).high}`; - } - } - - private refinementsComparator(refinement1: SelectedValueRefinement, refinement2: SelectedValueRefinement): number { - let comparison = refinement1.navigationName.localeCompare(refinement2.navigationName); - if (comparison === 0) { - comparison = refinement1.value.localeCompare(refinement2.value); - } - return comparison; - } -} - -export class UrlParser implements Parser { - - searchandiserConfig: SearchandiserConfig; - config: BeautifierConfig; - suffixRegex: RegExp; - - constructor({ config, searchandiserConfig }: UrlBeautifier) { - this.config = config; - this.searchandiserConfig = searchandiserConfig; - this.suffixRegex = new RegExp(`^${this.config.suffix}`); - } - - parse(rawUrl: string): Query { - const url = parseUri(rawUrl); - const paths = url.path.split('/').filter((val) => val); - - if (paths[paths.length - 1] === this.config.suffix) paths.pop(); - - const query = this.config.useReferenceKeys ? this.parsePathWithReferenceKeys(paths) : this.parsePathWithoutReferenceKeys(paths); - - const queryVariables = queryString.parse(url.query); - const unmappedRefinements = queryVariables[this.config.extraRefinementsParam]; - const pageSize = parseInt(queryVariables[this.config.pageSizeParam], 10); - const page = parseInt(queryVariables[this.config.pageParam], 10); - if (unmappedRefinements) { - query.withSelectedRefinements(...this.extractUnmapped(unmappedRefinements)); - } - if (pageSize) { - query.withPageSize(pageSize); - } - if (page) { - query.skip((query.raw.pageSize || this.config.defaultPageSize) * (page - 1)); - } - - return query; - } - - private parsePathWithReferenceKeys(path: string[]): Query { - const query = new Query().withConfiguration(this.searchandiserConfig, CONFIGURATION_MASK); - const keys = (path.pop() || '').split(''); - const map = this.generateRefinementMapping(); - for (let key of keys) { - if (!(key in map || key === this.config.queryToken)) { - throw new Error(`unexpected token '${key}' found in reference`); - } - } - - if (path.length < keys.length) throw new Error('token reference is invalid'); - - // remove prefixed paths - path.splice(0, path.length - keys.length); - - for (let i = 0; i < keys.length; i++) { - if (keys[i] === this.config.queryToken) { - query.withQuery(this.decode(path[i])); - } else { - query.withSelectedRefinements(...this.extractRefinements(path[i], map[keys[i]])); - } - } - return query; - } - - private parsePathWithoutReferenceKeys(path: string[]): Query { - const query = new Query().withConfiguration(this.searchandiserConfig, CONFIGURATION_MASK); - if (path.length % 2 === 1) { - query.withQuery(this.decode(path.shift())); - } - - while (path.length > 0) { - const value = this.decode(path.shift()); - const navigationName = path.shift(); - query.withSelectedRefinements({ navigationName, type: 'Value', value }); - } - - return query; - } - - private generateRefinementMapping() { - return this.config.refinementMapping.reduce((map, mapping) => Object.assign(map, mapping), {}); - } - - private extractRefinements(refinementString: string, navigationName: string): SelectedValueRefinement[] { - const refinementStrings = refinementString.split('~'); - - return refinementStrings.map((value) => ({ navigationName, type: 'Value', value: this.decode(value) })); - } - - private extractUnmapped(refinementString: string): Array { - const refinementStrings = refinementString.split('~'); - return refinementStrings - .map(this.decode) - .map((refinement) => { - const [navigationName, value] = refinement.split(':'); - if (value.indexOf('..') >= 0) { - const [low, high] = value.split('..'); - return { navigationName, low: Number(low), high: Number(high), type: 'Range' }; - } else { - return { navigationName, value, type: 'Value' }; - } - }); - } - - private decode(value: string): string { - return decodeURIComponent(value.replace(/-/g, ' ')); - } -} +export { UrlBeautifier, UrlGenerator, UrlParser }; +export { QueryUrlGenerator, QueryUrlParser }; diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index 614b7308..0422e403 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -1,7 +1,9 @@ import { Query } from 'groupby-api'; +import { SearchandiserConfig } from '../../searchandiser'; export interface Beautifier { config: BeautifierConfig; + searchandiserConfig: SearchandiserConfig; parse(url: string): Query; build(query: Query): string; } diff --git a/src/utils/url-beautifier/queries-url-beautifier.ts b/src/utils/url-beautifier/queries-url-beautifier.ts new file mode 100644 index 00000000..8cae2f3e --- /dev/null +++ b/src/utils/url-beautifier/queries-url-beautifier.ts @@ -0,0 +1,245 @@ +import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement } from 'groupby-api'; +import { Beautifier, Generator, Parser, BeautifierConfig } from './interfaces'; +import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; +import * as parseUri from 'parseUri'; +import * as queryString from 'query-string'; + +export class QueryUrlGenerator implements Generator { + + config: BeautifierConfig; + + constructor({ config }: Beautifier) { + this.config = config; + } + + build(query: Query): string { + const request = query.build(); + const uri = { + path: [], + query: {} + }; + // let url = ''; + const origRefinements = Array.of(...request.refinements); + + // add query + if (request.query) { + uri.path.push(request.query); + } + + if (this.config.useReferenceKeys) { + const countMap = {}; + const { map, keys } = this.generateRefinementMap(origRefinements); + + // add refinements + for (let key of keys) { + const refinements = map[key]; + countMap[key] = refinements.length; + refinements.map(this.convertToSelectedValueRefinement) + .sort(this.refinementsComparator) + .forEach((selectedValueRefinement) => { + uri.path.push(selectedValueRefinement.value); + }); + } + + // add reference key + if (keys.length || request.query) { + let referenceKey = ''; + if (request.query) referenceKey += this.config.queryToken; + keys.forEach((key) => referenceKey += key.repeat(countMap[key])); + uri.path.push(referenceKey); + } + } else { + // add refinements + let valueRefinements = [], rangeRefinement = []; + for (let i = origRefinements.length - 1; i >= 0; --i) { + if (origRefinements[i].type === 'Value') { + valueRefinements.push(...origRefinements.splice(i, 1)); + } + } + + valueRefinements.map(this.convertToSelectedValueRefinement) + .sort(this.refinementsComparator) + .forEach((selectedValueRefinement) => { + uri.path.push(selectedValueRefinement.value, selectedValueRefinement.navigationName); + }); + } + + // add remaining refinements + if (origRefinements.length) { + uri.query[this.config.extraRefinementsParam] = origRefinements + .sort((lhs, rhs) => lhs.navigationName.localeCompare(rhs.navigationName)) + .map(this.stringifyRefinement) + .join('~'); + } + + // add page size + if (query.raw.pageSize) { + uri.query[this.config.pageSizeParam] = query.raw.pageSize; + } + + // add page + if (query.raw.skip) { + uri.query[this.config.pageParam] = Math.floor(query.raw.skip/(query.raw.pageSize || this.config.defaultPageSize))+1; + } + + let url = `/${uri.path.map((path) => encodeURIComponent(path)).join('/')}`; + if (this.config.suffix) url += `/${this.config.suffix.replace(/^\/+/, '')}`; + + const queryPart = Object.keys(uri.query).sort().map((key) => { + return `${key}=${encodeURIComponent(uri.query[key])}`; + }).join('&'); + + if (queryPart) { + url += '?' + queryPart; + } + + return url.replace(/\s|%20/g, '-'); + } + + private generateRefinementMap(refinements: SelectedRefinement[]): { map: any, keys: string[] } { + const refinementMap = {}; + const refinementKeys = []; + for (let mapping of this.config.refinementMapping) { + const key = Object.keys(mapping)[0]; + const matchingRefinements = refinements.filter((refinement) => refinement.navigationName === mapping[key]); + if (matchingRefinements.length) { + refinementKeys.push(key); + refinementMap[key] = matchingRefinements; + matchingRefinements.forEach((ref) => refinements.splice(refinements.indexOf(ref), 1)); + } + } + return { map: refinementMap, keys: refinementKeys }; + } + + private convertToSelectedValueRefinement(refinement: SelectedRefinement): SelectedValueRefinement { + if (refinement.type === 'Value') { + return refinement; + } else { + throw new Error('cannot map range refinements'); + } + } + + private stringifyRefinement(refinement: SelectedRefinement): string { + const name = refinement.navigationName; + if (refinement.type === 'Value') { + return `${name}:${(refinement).value}`; + } else { + return `${name}:${(refinement).low}..${(refinement).high}`; + } + } + + private refinementsComparator(refinement1: SelectedValueRefinement, refinement2: SelectedValueRefinement): number { + let comparison = refinement1.navigationName.localeCompare(refinement2.navigationName); + if (comparison === 0) { + comparison = refinement1.value.localeCompare(refinement2.value); + } + return comparison; + } +} + +export class QueryUrlParser implements Parser { + + searchandiserConfig: SearchandiserConfig; + config: BeautifierConfig; + suffixRegex: RegExp; + + constructor({ config, searchandiserConfig }: Beautifier) { + this.config = config; + this.searchandiserConfig = searchandiserConfig; + this.suffixRegex = new RegExp(`^${this.config.suffix}`); + } + + parse(rawUrl: string): Query { + const url = parseUri(rawUrl); + const paths = url.path.split('/').filter((val) => val); + + if (paths[paths.length - 1] === this.config.suffix) paths.pop(); + + const query = this.config.useReferenceKeys ? this.parsePathWithReferenceKeys(paths) : this.parsePathWithoutReferenceKeys(paths); + + const queryVariables = queryString.parse(url.query); + const unmappedRefinements = queryVariables[this.config.extraRefinementsParam]; + const pageSize = parseInt(queryVariables[this.config.pageSizeParam], 10); + const page = parseInt(queryVariables[this.config.pageParam], 10); + if (unmappedRefinements) { + query.withSelectedRefinements(...this.extractUnmapped(unmappedRefinements)); + } + if (pageSize) { + query.withPageSize(pageSize); + } + if (page) { + query.skip((query.raw.pageSize || this.config.defaultPageSize) * (page - 1)); + } + + return query; + } + + private parsePathWithReferenceKeys(path: string[]): Query { + const query = new Query().withConfiguration(this.searchandiserConfig, CONFIGURATION_MASK); + const keys = (path.pop() || '').split(''); + const map = this.generateRefinementMapping(); + for (let key of keys) { + if (!(key in map || key === this.config.queryToken)) { + throw new Error(`unexpected token '${key}' found in reference`); + } + } + + if (path.length < keys.length) throw new Error('token reference is invalid'); + + // remove prefixed paths + path.splice(0, path.length - keys.length); + + for (let i = 0; i < keys.length; i++) { + if (keys[i] === this.config.queryToken) { + query.withQuery(this.decode(path[i])); + } else { + query.withSelectedRefinements(...this.extractRefinements(path[i], map[keys[i]])); + } + } + return query; + } + + private parsePathWithoutReferenceKeys(path: string[]): Query { + const query = new Query().withConfiguration(this.searchandiserConfig, CONFIGURATION_MASK); + if (path.length % 2 === 1) { + query.withQuery(this.decode(path.shift())); + } + + while (path.length > 0) { + const value = this.decode(path.shift()); + const navigationName = path.shift(); + query.withSelectedRefinements({ navigationName, type: 'Value', value }); + } + + return query; + } + + private generateRefinementMapping() { + return this.config.refinementMapping.reduce((map, mapping) => Object.assign(map, mapping), {}); + } + + private extractRefinements(refinementString: string, navigationName: string): SelectedValueRefinement[] { + const refinementStrings = refinementString.split('~'); + + return refinementStrings.map((value) => ({ navigationName, type: 'Value', value: this.decode(value) })); + } + + private extractUnmapped(refinementString: string): Array { + const refinementStrings = refinementString.split('~'); + return refinementStrings + .map(this.decode) + .map((refinement) => { + const [navigationName, value] = refinement.split(':'); + if (value.indexOf('..') >= 0) { + const [low, high] = value.split('..'); + return { navigationName, low: Number(low), high: Number(high), type: 'Range' }; + } else { + return { navigationName, value, type: 'Value' }; + } + }); + } + + private decode(value: string): string { + return decodeURIComponent(value.replace(/-/g, ' ')); + } +} \ No newline at end of file diff --git a/src/utils/url-beautifier/url-beautifier.ts b/src/utils/url-beautifier/url-beautifier.ts new file mode 100644 index 00000000..a4f390eb --- /dev/null +++ b/src/utils/url-beautifier/url-beautifier.ts @@ -0,0 +1,58 @@ +import { Query } from 'groupby-api'; +import { Beautifier, BeautifierConfig, Generator, Parser } from './interfaces'; +import { QueryUrlGenerator, QueryUrlParser } from './queries-url-beautifier'; +import { SearchandiserConfig } from '../../searchandiser'; + +export class UrlBeautifier implements Beautifier { + + config: BeautifierConfig = { + refinementMapping: [], + extraRefinementsParam: 'refinements', + pageSizeParam: 'page_size', + pageParam: 'page', + defaultPageSize: 10, + queryToken: 'q', + suffix: '', + useReferenceKeys: true + }; + private generator: QueryUrlGenerator = new QueryUrlGenerator(this); + private parser: QueryUrlParser = new QueryUrlParser(this); + + constructor(public searchandiserConfig: SearchandiserConfig = {}) { + const urlConfig = searchandiserConfig.url || {}; + const config = typeof urlConfig.beautifier === 'object' ? urlConfig.beautifier : {}; + Object.assign(this.config, config); + + const keys = []; + for (let mapping of this.config.refinementMapping) { + const key = Object.keys(mapping)[0]; + if (key.length !== 1) { + throw new Error('refinement mapping token must be a single character'); + } + if (key.match(/[aeiouy]/)) { + throw new Error('refinement mapping token must not be a vowel'); + } + if (keys.indexOf(key) > -1) { + throw new Error('refinement mapping tokens must be unique'); + } + keys.push(key); + } + if (this.config.queryToken.length !== 1) { + throw new Error('query token must be a single character'); + } + if (this.config.queryToken.match(/[aeiouy]/)) { + throw new Error('query token must not be a vowel'); + } + if (keys.indexOf(this.config.queryToken) > -1) { + throw new Error('query token must be unique from refinement tokens'); + } + } + + parse(url: string) { + return this.parser.parse(url); + } + + build(query: Query) { + return this.generator.build(query); + } +} diff --git a/test/unit/utils/url-beautifier.ts b/test/unit/utils/url-beautifier/query-url-beautifier.ts similarity index 97% rename from test/unit/utils/url-beautifier.ts rename to test/unit/utils/url-beautifier/query-url-beautifier.ts index 4bbc69ce..56371c0f 100644 --- a/test/unit/utils/url-beautifier.ts +++ b/test/unit/utils/url-beautifier/query-url-beautifier.ts @@ -1,9 +1,9 @@ -import { UrlBeautifier, UrlGenerator, UrlParser } from '../../../src/utils/url-beautifier'; -import { refinement } from '../../utils/fixtures'; import { expect } from 'chai'; import { Query } from 'groupby-api'; +import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser } from '../../../../src/utils/url-beautifier'; +import { refinement } from '../../../utils/fixtures'; -describe('URL beautifier', () => { +describe('query URL beautifier', () => { let beautifier: UrlBeautifier; let query: Query; @@ -12,10 +12,10 @@ describe('URL beautifier', () => { query = new Query(); }); - describe('URL generator', () => { - let generator: UrlGenerator; + describe('query URL generator', () => { + let generator: QueryUrlGenerator; - beforeEach(() => generator = new UrlGenerator(beautifier)); + beforeEach(() => generator = new QueryUrlGenerator(beautifier)); it('should convert a simple query to a URL', () => { query.withQuery('red apples'); @@ -216,10 +216,10 @@ describe('URL beautifier', () => { }); }); - describe('URL parser', () => { - let parser: UrlParser; + describe('query URL parser', () => { + let parser: QueryUrlParser; - beforeEach(() => parser = new UrlParser(beautifier)); + beforeEach(() => parser = new QueryUrlParser(beautifier)); it('should parse simple query URL', () => { query.withQuery('apples'); From 9b68bb081ce2e267589e74eebed4a73f88d07da0 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Wed, 31 May 2017 14:23:27 -0400 Subject: [PATCH 13/34] Parse simple navigation url and return a query --- src/utils/url-beautifier/index.ts | 9 +-- src/utils/url-beautifier/interfaces.ts | 9 +-- .../navigation-url-beautifier.ts | 28 +++++++++ ...-beautifier.ts => query-url-beautifier.ts} | 6 +- src/utils/url-beautifier/url-beautifier.ts | 4 +- .../navigation-url-beautifier.ts | 59 +++++++++++++++++++ 6 files changed, 98 insertions(+), 17 deletions(-) create mode 100644 src/utils/url-beautifier/navigation-url-beautifier.ts rename src/utils/url-beautifier/{queries-url-beautifier.ts => query-url-beautifier.ts} (97%) create mode 100644 test/unit/utils/url-beautifier/navigation-url-beautifier.ts diff --git a/src/utils/url-beautifier/index.ts b/src/utils/url-beautifier/index.ts index 3f297680..77ee543a 100644 --- a/src/utils/url-beautifier/index.ts +++ b/src/utils/url-beautifier/index.ts @@ -1,5 +1,6 @@ -import { UrlBeautifier, UrlGenerator, UrlParser } from './url-beautifier'; -import { QueryUrlGenerator, QueryUrlParser } from './queries-url-beautifier'; +import { UrlBeautifier } from './url-beautifier'; +import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; +import { NavigationUrlGenerator, NavigationUrlParser } from './navigation-url-beautifier'; +import { BeautifierConfig } from './interfaces'; -export { UrlBeautifier, UrlGenerator, UrlParser }; -export { QueryUrlGenerator, QueryUrlParser }; +export { UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, BeautifierConfig }; diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index 0422e403..443b6033 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -8,14 +8,6 @@ export interface Beautifier { build(query: Query): string; } -export interface Generator { - build(query: Query): string; -} - -export interface Parser { - parse(url: string): Query; -} - export interface BeautifierConfig { refinementMapping?: any[]; extraRefinementsParam?: string; @@ -25,4 +17,5 @@ export interface BeautifierConfig { queryToken?: string; suffix?: string; useReferenceKeys?: boolean; + navigations?: Object; } diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts new file mode 100644 index 00000000..0551582a --- /dev/null +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -0,0 +1,28 @@ +import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement, Navigation } from 'groupby-api'; +import { Beautifier, BeautifierConfig } from './interfaces'; +import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; +import * as parseUri from 'parseUri'; +import * as queryString from 'query-string'; + +export class NavigationUrlGenerator { + config: BeautifierConfig; + + constructor({ config }: Beautifier) { + this.config = config; + } +}; + +export class NavigationUrlParser { + config: BeautifierConfig; + + constructor({ config }: Beautifier) { + this.config = config; + } + + parse(rawUrl: string): Query { + const paths = parseUri(rawUrl).path.split('/').filter((val) => val); + + return this.config.navigations[paths[0]]; + } + +} \ No newline at end of file diff --git a/src/utils/url-beautifier/queries-url-beautifier.ts b/src/utils/url-beautifier/query-url-beautifier.ts similarity index 97% rename from src/utils/url-beautifier/queries-url-beautifier.ts rename to src/utils/url-beautifier/query-url-beautifier.ts index 8cae2f3e..a555d125 100644 --- a/src/utils/url-beautifier/queries-url-beautifier.ts +++ b/src/utils/url-beautifier/query-url-beautifier.ts @@ -1,10 +1,10 @@ import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement } from 'groupby-api'; -import { Beautifier, Generator, Parser, BeautifierConfig } from './interfaces'; +import { Beautifier, BeautifierConfig } from './interfaces'; import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; import * as parseUri from 'parseUri'; import * as queryString from 'query-string'; -export class QueryUrlGenerator implements Generator { +export class QueryUrlGenerator { config: BeautifierConfig; @@ -137,7 +137,7 @@ export class QueryUrlGenerator implements Generator { } } -export class QueryUrlParser implements Parser { +export class QueryUrlParser { searchandiserConfig: SearchandiserConfig; config: BeautifierConfig; diff --git a/src/utils/url-beautifier/url-beautifier.ts b/src/utils/url-beautifier/url-beautifier.ts index a4f390eb..3ff46631 100644 --- a/src/utils/url-beautifier/url-beautifier.ts +++ b/src/utils/url-beautifier/url-beautifier.ts @@ -1,6 +1,6 @@ import { Query } from 'groupby-api'; -import { Beautifier, BeautifierConfig, Generator, Parser } from './interfaces'; -import { QueryUrlGenerator, QueryUrlParser } from './queries-url-beautifier'; +import { Beautifier, BeautifierConfig } from './interfaces'; +import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; import { SearchandiserConfig } from '../../searchandiser'; export class UrlBeautifier implements Beautifier { diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts new file mode 100644 index 00000000..6caf159a --- /dev/null +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import { Query } from 'groupby-api'; +import { UrlBeautifier, NavigationUrlGenerator, NavigationUrlParser } from '../../../../src/utils/url-beautifier'; +import { refinement } from '../../../utils/fixtures'; + +describe('navigation URL beautifier', () => { + let beautifier: UrlBeautifier; + let query: Query; + + beforeEach(() => { + beautifier = new UrlBeautifier(); + query = new Query(); + }); + + describe('navigation URL generator', () => { + let generator: NavigationUrlGenerator; + + beforeEach(() => generator = new NavigationUrlGenerator(beautifier)); + + it('should convert a simple navigation name to a URL'); + }); + + describe('query URL parser', () => { + let parser: NavigationUrlParser; + + beforeEach(() => parser = new NavigationUrlParser(beautifier)); + + it('should parse URL and return the associated query', () => { + beautifier.config.navigations = { + Apples: query + }; + + expect(parser.parse('/Apples')).to.be.eql(query); + }); + + describe('error states', () => { + + }); + }); + + describe('compatibility', () => { + + beforeEach(() => beautifier = new UrlBeautifier({ + url: { + beautifier: { + refinementMapping: [{ b: 'brand' }, { f: 'fabric' }], + queryToken: 'k', + extraRefinementsParam: 'refs', + suffix: 'index.html' + } + } + })); + + }); + + describe('configuration errors', () => { + + }); +}); From 333816f89b4cf1a770a943c766c0b67ca68c3d41 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Wed, 31 May 2017 14:39:12 -0400 Subject: [PATCH 14/34] handle case where no navigation mapping exits --- src/utils/url-beautifier/navigation-url-beautifier.ts | 5 ++++- .../utils/url-beautifier/navigation-url-beautifier.ts | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts index 0551582a..19d7b4b5 100644 --- a/src/utils/url-beautifier/navigation-url-beautifier.ts +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -21,7 +21,10 @@ export class NavigationUrlParser { parse(rawUrl: string): Query { const paths = parseUri(rawUrl).path.split('/').filter((val) => val); - + const name = paths[0]; + if (!(name in this.config.navigations)) { + throw new Error(`no navigation mapping found for ${name}`); + } return this.config.navigations[paths[0]]; } diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index 6caf159a..791ceb3f 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -23,18 +23,21 @@ describe('navigation URL beautifier', () => { describe('query URL parser', () => { let parser: NavigationUrlParser; - beforeEach(() => parser = new NavigationUrlParser(beautifier)); - - it('should parse URL and return the associated query', () => { + beforeEach(() => { + parser = new NavigationUrlParser(beautifier) beautifier.config.navigations = { Apples: query }; + }); + it('should parse URL and return the associated query', () => { expect(parser.parse('/Apples')).to.be.eql(query); }); describe('error states', () => { - + it('should parse URL and throw an error if associated query is not found', () => { + expect(() => parser.parse('/Orange')).to.throw('no navigation mapping found for Orange'); + }); }); }); From ce6137ca6e34869ee648fc529c9b997d72222b9b Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Wed, 31 May 2017 14:45:09 -0400 Subject: [PATCH 15/34] handle case where navigation url has more than one part --- src/utils/url-beautifier/navigation-url-beautifier.ts | 3 +++ test/unit/utils/url-beautifier/navigation-url-beautifier.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts index 19d7b4b5..27bafa12 100644 --- a/src/utils/url-beautifier/navigation-url-beautifier.ts +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -21,6 +21,9 @@ export class NavigationUrlParser { parse(rawUrl: string): Query { const paths = parseUri(rawUrl).path.split('/').filter((val) => val); + if (paths.length > 1) { + throw new Error('path contains more than one part'); + } const name = paths[0]; if (!(name in this.config.navigations)) { throw new Error(`no navigation mapping found for ${name}`); diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index 791ceb3f..a175a608 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -38,6 +38,10 @@ describe('navigation URL beautifier', () => { it('should parse URL and throw an error if associated query is not found', () => { expect(() => parser.parse('/Orange')).to.throw('no navigation mapping found for Orange'); }); + + it('should parse URL and throw an error if the path has more than one part', () => { + expect(() => parser.parse('/Apples/Orange')).to.throw('path contains more than one part'); + }) }); }); From ddcece83055e190d4bddd25edd9dd4074ae057cc Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Wed, 31 May 2017 14:58:09 -0400 Subject: [PATCH 16/34] Parse navigation url with encoded characters --- src/utils/url-beautifier/navigation-url-beautifier.ts | 4 ++-- test/unit/utils/url-beautifier/navigation-url-beautifier.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts index 27bafa12..6c358d4f 100644 --- a/src/utils/url-beautifier/navigation-url-beautifier.ts +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -24,11 +24,11 @@ export class NavigationUrlParser { if (paths.length > 1) { throw new Error('path contains more than one part'); } - const name = paths[0]; + const name = decodeURIComponent(paths[0]); if (!(name in this.config.navigations)) { throw new Error(`no navigation mapping found for ${name}`); } - return this.config.navigations[paths[0]]; + return this.config.navigations[name]; } } \ No newline at end of file diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index a175a608..5845cf91 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -34,6 +34,12 @@ describe('navigation URL beautifier', () => { expect(parser.parse('/Apples')).to.be.eql(query); }); + it('should parse URL with encoded characters', () => { + const navigationName = 'Red apples/cherries'; + beautifier.config.navigations[navigationName] = query; + expect(parser.parse('/' + encodeURIComponent(navigationName))).to.be.eql(query); + }); + describe('error states', () => { it('should parse URL and throw an error if associated query is not found', () => { expect(() => parser.parse('/Orange')).to.throw('no navigation mapping found for Orange'); From 7e91190eeb0da582a9d21f44561992f9d338c0d9 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Wed, 31 May 2017 15:51:18 -0400 Subject: [PATCH 17/34] Build navigation url with encoded characters --- .../url-beautifier/navigation-url-beautifier.ts | 4 ++++ .../url-beautifier/navigation-url-beautifier.ts | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts index 6c358d4f..4286aa0d 100644 --- a/src/utils/url-beautifier/navigation-url-beautifier.ts +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -10,6 +10,10 @@ export class NavigationUrlGenerator { constructor({ config }: Beautifier) { this.config = config; } + + build(name: string): string { + return '/' + encodeURIComponent(name.replace(/\s/g, '-')); + } }; export class NavigationUrlParser { diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index 5845cf91..e55d20c8 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -3,7 +3,7 @@ import { Query } from 'groupby-api'; import { UrlBeautifier, NavigationUrlGenerator, NavigationUrlParser } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; -describe('navigation URL beautifier', () => { +describe.only('navigation URL beautifier', () => { let beautifier: UrlBeautifier; let query: Query; @@ -17,7 +17,17 @@ describe('navigation URL beautifier', () => { beforeEach(() => generator = new NavigationUrlGenerator(beautifier)); - it('should convert a simple navigation name to a URL'); + it('should convert a simple navigation name to a URL', () => { + expect(generator.build('Apples')).to.be.eq('/Apples'); + }); + + it('should replace spaces in a navigation name with hyphen', () => { + expect(generator.build('red apples')).to.be.eq('/red-apples'); + }) + + it('should encode special characters in navigation name', () => { + expect(generator.build('red&green apples/grapes')).to.be.eq('/red%26green-apples%2Fgrapes'); + }); }); describe('query URL parser', () => { From 7ac3de7366310ea651c9b7c839c41ca2216f1c53 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Thu, 1 Jun 2017 10:03:11 -0400 Subject: [PATCH 18/34] Throw error when passing unmapped name to generator --- src/utils/url-beautifier/index.ts | 5 +-- .../navigation-url-beautifier.ts | 9 +++-- src/utils/url-beautifier/url-beautifier.ts | 3 +- .../navigation-url-beautifier.ts | 36 ++++++++----------- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/utils/url-beautifier/index.ts b/src/utils/url-beautifier/index.ts index 77ee543a..e77a9149 100644 --- a/src/utils/url-beautifier/index.ts +++ b/src/utils/url-beautifier/index.ts @@ -1,6 +1,7 @@ +import { BeautifierConfig } from './interfaces'; import { UrlBeautifier } from './url-beautifier'; import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; import { NavigationUrlGenerator, NavigationUrlParser } from './navigation-url-beautifier'; -import { BeautifierConfig } from './interfaces'; +import { DetailUrlGenerator, DetailUrlParser } from './detail-url-beautifier'; -export { UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, BeautifierConfig }; +export { UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, DetailUrlGenerator, DetailUrlParser, BeautifierConfig }; diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts index 4286aa0d..dfb9e69d 100644 --- a/src/utils/url-beautifier/navigation-url-beautifier.ts +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -12,9 +12,13 @@ export class NavigationUrlGenerator { } build(name: string): string { + if (!(name in this.config.navigations)) { + throw new Error(`no navigation mapping found for ${name}`); + } + return '/' + encodeURIComponent(name.replace(/\s/g, '-')); } -}; +} export class NavigationUrlParser { config: BeautifierConfig; @@ -28,11 +32,12 @@ export class NavigationUrlParser { if (paths.length > 1) { throw new Error('path contains more than one part'); } + const name = decodeURIComponent(paths[0]); if (!(name in this.config.navigations)) { throw new Error(`no navigation mapping found for ${name}`); } + return this.config.navigations[name]; } - } \ No newline at end of file diff --git a/src/utils/url-beautifier/url-beautifier.ts b/src/utils/url-beautifier/url-beautifier.ts index 3ff46631..96a87ff9 100644 --- a/src/utils/url-beautifier/url-beautifier.ts +++ b/src/utils/url-beautifier/url-beautifier.ts @@ -13,7 +13,8 @@ export class UrlBeautifier implements Beautifier { defaultPageSize: 10, queryToken: 'q', suffix: '', - useReferenceKeys: true + useReferenceKeys: true, + navigations: {} }; private generator: QueryUrlGenerator = new QueryUrlGenerator(this); private parser: QueryUrlParser = new QueryUrlParser(this); diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index e55d20c8..5280cfa9 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -3,7 +3,7 @@ import { Query } from 'groupby-api'; import { UrlBeautifier, NavigationUrlGenerator, NavigationUrlParser } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; -describe.only('navigation URL beautifier', () => { +describe('navigation URL beautifier', () => { let beautifier: UrlBeautifier; let query: Query; @@ -15,26 +15,37 @@ describe.only('navigation URL beautifier', () => { describe('navigation URL generator', () => { let generator: NavigationUrlGenerator; - beforeEach(() => generator = new NavigationUrlGenerator(beautifier)); + beforeEach(() => { + generator = new NavigationUrlGenerator(beautifier) + }); it('should convert a simple navigation name to a URL', () => { + beautifier.config.navigations['Apples'] = query; expect(generator.build('Apples')).to.be.eq('/Apples'); }); it('should replace spaces in a navigation name with hyphen', () => { + beautifier.config.navigations['red apples'] = query; expect(generator.build('red apples')).to.be.eq('/red-apples'); }) it('should encode special characters in navigation name', () => { + beautifier.config.navigations['red&green apples/grapes'] = query; expect(generator.build('red&green apples/grapes')).to.be.eq('/red%26green-apples%2Fgrapes'); }); + + describe('error states', () => { + it('should throw an error if the given name is not mapped', () => { + expect(() => generator.build('Apples')).to.throw('no navigation mapping found for Apples'); + }); + }); }); describe('query URL parser', () => { let parser: NavigationUrlParser; beforeEach(() => { - parser = new NavigationUrlParser(beautifier) + parser = new NavigationUrlParser(beautifier); beautifier.config.navigations = { Apples: query }; @@ -60,23 +71,4 @@ describe.only('navigation URL beautifier', () => { }) }); }); - - describe('compatibility', () => { - - beforeEach(() => beautifier = new UrlBeautifier({ - url: { - beautifier: { - refinementMapping: [{ b: 'brand' }, { f: 'fabric' }], - queryToken: 'k', - extraRefinementsParam: 'refs', - suffix: 'index.html' - } - } - })); - - }); - - describe('configuration errors', () => { - - }); }); From 3b7493f2936195ea2da52ee0db77d06560a20b13 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Thu, 1 Jun 2017 10:42:51 -0400 Subject: [PATCH 19/34] Generate URL for simple details page --- .../url-beautifier/detail-url-beautifier.ts | 27 ++++++++++++ src/utils/url-beautifier/index.ts | 4 +- src/utils/url-beautifier/interfaces.ts | 6 +++ .../url-beautifier/detail-url-beautifer.ts | 44 +++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/utils/url-beautifier/detail-url-beautifier.ts create mode 100644 test/unit/utils/url-beautifier/detail-url-beautifer.ts diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts new file mode 100644 index 00000000..d65f99fa --- /dev/null +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -0,0 +1,27 @@ +import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement, Navigation } from 'groupby-api'; +import { Beautifier, BeautifierConfig, Detail } from './interfaces'; +import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; +import * as parseUri from 'parseUri'; +import * as queryString from 'query-string'; + +export class DetailUrlGenerator { + config: BeautifierConfig; + + constructor({ config }: Beautifier) { + this.config = config; + } + + build(detail: Detail): string { + let paths = [detail.productTitle]; + paths.push(detail.productID); + return `/${paths.map((path) => encodeURIComponent(path.replace(/\s/g, '-'))).join('/')}`; + } +} + +export class DetailUrlParser { + config: BeautifierConfig; + + constructor({ config }: Beautifier) { + this.config = config; + } +} diff --git a/src/utils/url-beautifier/index.ts b/src/utils/url-beautifier/index.ts index e77a9149..4446de37 100644 --- a/src/utils/url-beautifier/index.ts +++ b/src/utils/url-beautifier/index.ts @@ -1,7 +1,7 @@ -import { BeautifierConfig } from './interfaces'; +import { BeautifierConfig, Detail } from './interfaces'; import { UrlBeautifier } from './url-beautifier'; import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; import { NavigationUrlGenerator, NavigationUrlParser } from './navigation-url-beautifier'; import { DetailUrlGenerator, DetailUrlParser } from './detail-url-beautifier'; -export { UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, DetailUrlGenerator, DetailUrlParser, BeautifierConfig }; +export { BeautifierConfig, Detail, UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, DetailUrlGenerator, DetailUrlParser }; diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index 443b6033..aebf66e4 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -19,3 +19,9 @@ export interface BeautifierConfig { useReferenceKeys?: boolean; navigations?: Object; } + +export interface Detail { + productTitle: string; + productID: string; + refinements?: any[]; +} \ No newline at end of file diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts new file mode 100644 index 00000000..946a317f --- /dev/null +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -0,0 +1,44 @@ +import { expect } from 'chai'; +import { Query } from 'groupby-api'; +import { UrlBeautifier, DetailUrlGenerator, DetailUrlParser, Detail } from '../../../../src/utils/url-beautifier'; +import { refinement } from '../../../utils/fixtures'; + +describe('detail URL beautifier', () => { + let beautifier: UrlBeautifier; + let query: Query; + + beforeEach(() => { + beautifier = new UrlBeautifier(); + query = new Query(); + }); + + describe('detail URL generator', () => { + let generator: DetailUrlGenerator; + + beforeEach(() => generator = new DetailUrlGenerator(beautifier)); + + it('should convert a simple detail to a URL', () => { + expect(generator.build({ productTitle: 'red and delicious apples', productID: '1923' })).to.be.eq('/red-and-delicious-apples/1923'); + }); + + it('should encode special characters + in detail', () => { + expect(generator.build({ productTitle: 'red+and+delicious+apples', productID: '1923' })).to.be.eq('/red%2Band%2Bdelicious%2Bapples/1923'); + }); + + it('should encode special characters / in detail', () => { + expect(generator.build({ productTitle: 'red/and/delicious/apples', productID: '1923' })).to.be.eq('/red%2Fand%2Fdelicious%2Fapples/1923'); + }); + }); + + describe('query URL parser', () => { + let parser: DetailUrlParser; + + beforeEach(() => { + parser = new DetailUrlParser(beautifier); + }); + + describe('error states', () => { + + }); + }); +}); From 82b3c347981f6feff62f678585b2a14c6e83e6d9 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Thu, 1 Jun 2017 11:23:39 -0400 Subject: [PATCH 20/34] Add refinements to the generated details URL --- src/utils/url-beautifier/detail-url-beautifier.ts | 8 ++++++++ test/unit/utils/url-beautifier/detail-url-beautifer.ts | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts index d65f99fa..dc2492be 100644 --- a/src/utils/url-beautifier/detail-url-beautifier.ts +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -13,6 +13,14 @@ export class DetailUrlGenerator { build(detail: Detail): string { let paths = [detail.productTitle]; + + if (detail.refinements) { + detail.refinements.forEach((ref) => { + paths.push(ref.value); + paths.push(ref.navigationName); + }); + } + paths.push(detail.productID); return `/${paths.map((path) => encodeURIComponent(path.replace(/\s/g, '-'))).join('/')}`; } diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index 946a317f..0d0a9014 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -28,6 +28,16 @@ describe('detail URL beautifier', () => { it('should encode special characters / in detail', () => { expect(generator.build({ productTitle: 'red/and/delicious/apples', productID: '1923' })).to.be.eq('/red%2Fand%2Fdelicious%2Fapples/1923'); }); + + it('should convert a detail with refinements to a URL without reference keys', () => { + beautifier.config.useReferenceKeys = false; + expect(generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red') ] })).to.be.eq('/satin-shiny-party-dress/red/colour/293014'); + }); + + it('should convert a detail with refinements to a URL and encode special characters without reference keys', () => { + beautifier.config.useReferenceKeys = false; + expect(generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red+green/blue') ] })).to.be.eq('/satin-shiny-party-dress/red%2Bgreen%2Fblue/colour/293014'); + }); }); describe('query URL parser', () => { From 4160d2158c6d43e21919963051c786787da6dab6 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Thu, 1 Jun 2017 15:23:14 -0400 Subject: [PATCH 21/34] Replace '-' with ' ' when parsing navigation url --- src/utils/url-beautifier/navigation-url-beautifier.ts | 7 +++---- .../utils/url-beautifier/navigation-url-beautifier.ts | 10 ++++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts index dfb9e69d..4255aa45 100644 --- a/src/utils/url-beautifier/navigation-url-beautifier.ts +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -1,6 +1,5 @@ -import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement, Navigation } from 'groupby-api'; +import { Query } from 'groupby-api'; import { Beautifier, BeautifierConfig } from './interfaces'; -import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; import * as parseUri from 'parseUri'; import * as queryString from 'query-string'; @@ -33,11 +32,11 @@ export class NavigationUrlParser { throw new Error('path contains more than one part'); } - const name = decodeURIComponent(paths[0]); + const name = decodeURIComponent(paths[0]).replace(/-/g, ' '); if (!(name in this.config.navigations)) { throw new Error(`no navigation mapping found for ${name}`); } return this.config.navigations[name]; } -} \ No newline at end of file +} diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index 5280cfa9..c4b3c437 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -41,7 +41,7 @@ describe('navigation URL beautifier', () => { }); }); - describe('query URL parser', () => { + describe('navigation URL parser', () => { let parser: NavigationUrlParser; beforeEach(() => { @@ -58,9 +58,15 @@ describe('navigation URL beautifier', () => { it('should parse URL with encoded characters', () => { const navigationName = 'Red apples/cherries'; beautifier.config.navigations[navigationName] = query; - expect(parser.parse('/' + encodeURIComponent(navigationName))).to.be.eql(query); + expect(parser.parse('/Red-apples%2Fcherries')).to.be.eql(query); }); + it('should parse URL with hyphen', () => { + const navigationName = 'Red apples'; + beautifier.config.navigations[navigationName] = query; + expect(parser.parse('/' + encodeURIComponent(navigationName))).to.be.eql(query); + }) + describe('error states', () => { it('should parse URL and throw an error if associated query is not found', () => { expect(() => parser.parse('/Orange')).to.throw('no navigation mapping found for Orange'); From e98914157df28266bfdfcee3c6c06aa028a13708 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Thu, 1 Jun 2017 15:27:59 -0400 Subject: [PATCH 22/34] Finish detail url generator --- .../url-beautifier/detail-url-beautifier.ts | 37 ++++++++++++++++--- src/utils/url-beautifier/interfaces.ts | 6 +-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts index dc2492be..1ad1b90d 100644 --- a/src/utils/url-beautifier/detail-url-beautifier.ts +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -1,6 +1,5 @@ -import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement, Navigation } from 'groupby-api'; +import { SelectedValueRefinement } from 'groupby-api'; import { Beautifier, BeautifierConfig, Detail } from './interfaces'; -import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; import * as parseUri from 'parseUri'; import * as queryString from 'query-string'; @@ -15,15 +14,41 @@ export class DetailUrlGenerator { let paths = [detail.productTitle]; if (detail.refinements) { - detail.refinements.forEach((ref) => { - paths.push(ref.value); - paths.push(ref.navigationName); - }); + if (this.config.useReferenceKeys) { + let referenceKeys = ''; + let refinementsToKeys = this.config.refinementMapping.reduce((map, mapping) => { + const key = Object.keys(mapping)[0]; + map[mapping[key]] = key; + return map; + }, {}); + + detail.refinements.sort(this.refinementsComparator).forEach((refinement) => { + if (!(refinement.navigationName in refinementsToKeys)) + throw new Error(`no mapping found for navigation "${refinement.navigationName}"`); + paths.push(refinement.value); + referenceKeys += (refinementsToKeys[refinement.navigationName]); + }); + + paths.push(referenceKeys); + } else { + detail.refinements.forEach((ref) => { + paths.push(ref.value); + paths.push(ref.navigationName); + }); + } } paths.push(detail.productID); return `/${paths.map((path) => encodeURIComponent(path.replace(/\s/g, '-'))).join('/')}`; } + + private refinementsComparator(refinement1: SelectedValueRefinement, refinement2: SelectedValueRefinement): number { + let comparison = refinement1.navigationName.localeCompare(refinement2.navigationName); + if (comparison === 0) { + comparison = refinement1.value.localeCompare(refinement2.value); + } + return comparison; + } } export class DetailUrlParser { diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index aebf66e4..56ce967c 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -1,4 +1,4 @@ -import { Query } from 'groupby-api'; +import { SelectedValueRefinement, Query } from 'groupby-api'; import { SearchandiserConfig } from '../../searchandiser'; export interface Beautifier { @@ -23,5 +23,5 @@ export interface BeautifierConfig { export interface Detail { productTitle: string; productID: string; - refinements?: any[]; -} \ No newline at end of file + refinements?: SelectedValueRefinement[]; +} From 5cc4d2cab305fe8e0999b68ebebdfe1cc687c611 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Thu, 1 Jun 2017 15:29:11 -0400 Subject: [PATCH 23/34] Parse detail page url without reference keys --- .../url-beautifier/detail-url-beautifier.ts | 34 ++++++++++ .../url-beautifier/detail-url-beautifer.ts | 67 +++++++++++++++++-- 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts index 1ad1b90d..65402c93 100644 --- a/src/utils/url-beautifier/detail-url-beautifier.ts +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -57,4 +57,38 @@ export class DetailUrlParser { constructor({ config }: Beautifier) { this.config = config; } + + parse(rawUrl: string): Detail { + let paths = parseUri(rawUrl).path.split('/').filter((val) => val); + + if (paths.length < 2) { + throw new Error('path has less than two parts'); + } + + const name = decodeURIComponent(paths.shift()).replace(/-/g, ' '); + const id = paths.pop(); + const result = { + productTitle: name, + productID: id + }; + + let refinements = []; + + if (paths.length) { + if (!this.config.useReferenceKeys) { + if (paths.length % 2 !== 0) { + throw new Error('path has an odd number of parts'); + } + + while (paths.length) { + const value = decodeURIComponent(paths.shift()).replace(/-/g, ' '); + const navigationName = decodeURIComponent(paths.shift()).replace(/-/g, ' '); + refinements.push({ navigationName, value, type: 'Value' }); + } + result['refinements'] = refinements; + } + } + + return result; + } } diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index 0d0a9014..b26df4fa 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -18,37 +18,92 @@ describe('detail URL beautifier', () => { beforeEach(() => generator = new DetailUrlGenerator(beautifier)); it('should convert a simple detail to a URL', () => { - expect(generator.build({ productTitle: 'red and delicious apples', productID: '1923' })).to.be.eq('/red-and-delicious-apples/1923'); + expect(generator.build({ productTitle: 'red and delicious apples', productID: '1923' })).to.eq('/red-and-delicious-apples/1923'); }); it('should encode special characters + in detail', () => { - expect(generator.build({ productTitle: 'red+and+delicious+apples', productID: '1923' })).to.be.eq('/red%2Band%2Bdelicious%2Bapples/1923'); + expect(generator.build({ productTitle: 'red+and+delicious+apples', productID: '1923' })).to.eq('/red%2Band%2Bdelicious%2Bapples/1923'); }); it('should encode special characters / in detail', () => { - expect(generator.build({ productTitle: 'red/and/delicious/apples', productID: '1923' })).to.be.eq('/red%2Fand%2Fdelicious%2Fapples/1923'); + expect(generator.build({ productTitle: 'red/and/delicious/apples', productID: '1923' })).to.eq('/red%2Fand%2Fdelicious%2Fapples/1923'); }); it('should convert a detail with refinements to a URL without reference keys', () => { beautifier.config.useReferenceKeys = false; - expect(generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red') ] })).to.be.eq('/satin-shiny-party-dress/red/colour/293014'); + const url = generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + expect(url).to.eq('/satin-shiny-party-dress/red/colour/293014'); }); it('should convert a detail with refinements to a URL and encode special characters without reference keys', () => { beautifier.config.useReferenceKeys = false; - expect(generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red+green/blue') ] })).to.be.eq('/satin-shiny-party-dress/red%2Bgreen%2Fblue/colour/293014'); + const url = generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red+green/blue') ] }); + expect(url).to.eq('/satin-shiny-party-dress/red%2Bgreen%2Fblue/colour/293014'); + }); + + it('should convert a detail with a single refinement to a URL with a reference key', () => { + beautifier.config.refinementMapping.push({ c: 'colour' }); + const url = generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + expect(url).to.eq('/dress/red/c/293014'); + }); + + it('should convert a detail with multiple refinements to a URL with reference keys', () => { + beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + const url = generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red'), refinement('brand', 'h&m') ] }); + expect(url).to.eq('/dress/h%26m/red/bc/293014'); + }); + + describe('error states', () => { + it('should throw an error if no reference key found for refinement navigation name', () => { + const build = () => generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + expect(build).to.throw('no mapping found for navigation "colour"'); + }); }); }); - describe('query URL parser', () => { + describe('detail URL parser', () => { let parser: DetailUrlParser; beforeEach(() => { parser = new DetailUrlParser(beautifier); }); + it('should parse a simple URL and return a detail object', () => { + const expectedDetail = { productTitle: 'apples', productID: '1923' }; + expect(parser.parse('/apples/1923')).to.eql(expectedDetail); + }); + + it('should parse a simple URL, replace \'-\' with \' \' and return a detail object', () => { + const expectedDetail = { productTitle: 'red and delicious apples', productID: '1923' }; + expect(parser.parse('/red-and-delicious-apples/1923')).to.eql(expectedDetail); + }); + + it('should parse a simple URL, decode special characters and return a detail object', () => { + const expectedDetail = { productTitle: 'red+and+delicious+apples', productID: '1923' }; + expect(parser.parse('/red%2Band%2Bdelicious%2Bapples/1923')).to.eql(expectedDetail); + }); + + it('should parse a URL with a pair of navigation name and value and return a detail object without reference keys', () => { + beautifier.config.useReferenceKeys = false; + const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'blue') ] }; + expect(parser.parse('/satin-shiny-party-dress/blue/colour/293014')).to.eql(expectedDetail); + }); + + it('should decode special characters in navigation name and values', () => { + beautifier.config.useReferenceKeys = false; + const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red') ] }; + expect(parser.parse('/satin-shiny-party-dress/h%26m/brand/blue/colour/red/colour/293014')).to.eql(expectedDetail); + }); + describe('error states', () => { + it('should throw an error if the path has less than two parts', () => { + expect(() => parser.parse('/dress')).to.throw('path has less than two parts'); + }); + it('should throw an error if the path without reference keys has an odd number of parts', () => { + beautifier.config.useReferenceKeys = false; + expect(() => parser.parse('/dress/blue/colour/red/293014')).to.throw('path has an odd number of parts'); + }); }); }); }); From 5d9a16155f7c2090a051f4a50329d3991c0db7a2 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Thu, 1 Jun 2017 16:44:36 -0400 Subject: [PATCH 24/34] Finish detail url parser --- .../url-beautifier/detail-url-beautifier.ts | 31 +++++++++++++++---- .../url-beautifier/detail-url-beautifer.ts | 16 +++++++++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts index 65402c93..3c01f859 100644 --- a/src/utils/url-beautifier/detail-url-beautifier.ts +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -16,7 +16,7 @@ export class DetailUrlGenerator { if (detail.refinements) { if (this.config.useReferenceKeys) { let referenceKeys = ''; - let refinementsToKeys = this.config.refinementMapping.reduce((map, mapping) => { + const refinementsToKeys = this.config.refinementMapping.reduce((map, mapping) => { const key = Object.keys(mapping)[0]; map[mapping[key]] = key; return map; @@ -59,13 +59,13 @@ export class DetailUrlParser { } parse(rawUrl: string): Detail { - let paths = parseUri(rawUrl).path.split('/').filter((val) => val); + let paths = parseUri(rawUrl).path.split('/').filter((val) => val).map((val) => decodeURIComponent(val).replace(/-/g, ' ')); if (paths.length < 2) { throw new Error('path has less than two parts'); } - const name = decodeURIComponent(paths.shift()).replace(/-/g, ' '); + const name = paths.shift(); const id = paths.pop(); const result = { productTitle: name, @@ -81,12 +81,31 @@ export class DetailUrlParser { } while (paths.length) { - const value = decodeURIComponent(paths.shift()).replace(/-/g, ' '); - const navigationName = decodeURIComponent(paths.shift()).replace(/-/g, ' '); + const value = paths.shift(); + const navigationName = paths.shift(); refinements.push({ navigationName, value, type: 'Value' }); } - result['refinements'] = refinements; + } else { + if (paths.length < 2) { + throw new Error('path has wrong number of parts'); + } + + const referenceKeys = paths.pop().split(''); + const keysToRefinements = this.config.refinementMapping.reduce((map, mapping) => { + const key = Object.keys(mapping)[0]; + map[key] = mapping[key]; + return map; + }, {}); + + if (paths.length !== referenceKeys.length) { + throw new Error('token reference is invalid'); + } + + while (paths.length) { + refinements.push({ navigationName: keysToRefinements[referenceKeys.shift()], value: paths.shift(), type: 'Value' }); + } } + result['refinements'] = refinements; } return result; diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index b26df4fa..aa6bb2be 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -83,7 +83,7 @@ describe('detail URL beautifier', () => { expect(parser.parse('/red%2Band%2Bdelicious%2Bapples/1923')).to.eql(expectedDetail); }); - it('should parse a URL with a pair of navigation name and value and return a detail object without reference keys', () => { + it('should parse a URL with navigation names and values and return a detail object without reference keys', () => { beautifier.config.useReferenceKeys = false; const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'blue') ] }; expect(parser.parse('/satin-shiny-party-dress/blue/colour/293014')).to.eql(expectedDetail); @@ -95,6 +95,12 @@ describe('detail URL beautifier', () => { expect(parser.parse('/satin-shiny-party-dress/h%26m/brand/blue/colour/red/colour/293014')).to.eql(expectedDetail); }); + it('should parse a URL with reference keys', () => { + beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }) + const expectedDetail = { productTitle: 'dress', productID: '293014', refinements: [ refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red') ] }; + expect(parser.parse('/dress/h%26m/blue/red/bcc/293014')).to.eql(expectedDetail); + }); + describe('error states', () => { it('should throw an error if the path has less than two parts', () => { expect(() => parser.parse('/dress')).to.throw('path has less than two parts'); @@ -104,6 +110,14 @@ describe('detail URL beautifier', () => { beautifier.config.useReferenceKeys = false; expect(() => parser.parse('/dress/blue/colour/red/293014')).to.throw('path has an odd number of parts'); }); + + it('should throw an error if the path has wrong number of parts', () => { + expect(() => parser.parse('/shoe/blue/colour')).to.throw('path has wrong number of parts'); + }); + + it('should throw an error if token reference is invalid', () => { + expect(() => parser.parse('/apples/green/cs/2931')).to.throw('token reference is invalid'); + }) }); }); }); From 751ff448c48399c10bc3c65e8581c52083af0dff Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Thu, 1 Jun 2017 16:58:58 -0400 Subject: [PATCH 25/34] test compatibility for detail url beautifier --- .../url-beautifier/detail-url-beautifer.ts | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index aa6bb2be..569bc3c5 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -6,6 +6,8 @@ import { refinement } from '../../../utils/fixtures'; describe('detail URL beautifier', () => { let beautifier: UrlBeautifier; let query: Query; + let generator: DetailUrlGenerator; + let parser: DetailUrlParser; beforeEach(() => { beautifier = new UrlBeautifier(); @@ -13,7 +15,6 @@ describe('detail URL beautifier', () => { }); describe('detail URL generator', () => { - let generator: DetailUrlGenerator; beforeEach(() => generator = new DetailUrlGenerator(beautifier)); @@ -62,8 +63,6 @@ describe('detail URL beautifier', () => { }); describe('detail URL parser', () => { - let parser: DetailUrlParser; - beforeEach(() => { parser = new DetailUrlParser(beautifier); }); @@ -120,4 +119,47 @@ describe('detail URL beautifier', () => { }) }); }); + + describe('compatibility', () => { + const obj = { + productTitle: 'dress', + productID: '293014', + refinements: [ + refinement('brand', 'h&m'), + refinement('colour', 'blue'), + refinement('colour', 'red') + ] + }; + + beforeEach(() => { + generator = new DetailUrlGenerator(beautifier); + parser = new DetailUrlParser(beautifier); + }); + + it('should convert from detail object to a URL and back with reference keys', () => { + const url = '/dress/h%26m/blue/red/bcc/293014'; + beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + expect(parser.parse(generator.build(obj))).to.eql(obj); + }); + + it('should convert from URL to a query and back with reference keys', () => { + const url = '/dress/h%26m/blue/red/bcc/293014'; + beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + expect(generator.build(parser.parse(url))).to.eq(url); + }); + + it('should convert from detail object to a URL and back without reference keys', () => { + const url = '/dress/h%26m/brand/blue/colour/red/colour/293014'; + beautifier.config.useReferenceKeys = false; + beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + expect(parser.parse(generator.build(obj))).to.eql(obj); + }); + + it('should convert from URL to a query and back without reference keys', () => { + const url = '/dress/h%26m/brand/blue/colour/red/colour/293014'; + beautifier.config.useReferenceKeys = false; + beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + expect(generator.build(parser.parse(url))).to.eq(url); + }); + }); }); From 75b87eec30a82c3fcf0048a21a264d032fdd20ec Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Fri, 2 Jun 2017 10:22:23 -0400 Subject: [PATCH 26/34] Clean up url-beautifier tests --- .../url-beautifier/detail-url-beautifer.ts | 22 +++++++++++++++---- .../navigation-url-beautifier.ts | 5 +++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index 569bc3c5..a31b0734 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -33,30 +33,35 @@ describe('detail URL beautifier', () => { it('should convert a detail with refinements to a URL without reference keys', () => { beautifier.config.useReferenceKeys = false; const url = generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + expect(url).to.eq('/satin-shiny-party-dress/red/colour/293014'); }); it('should convert a detail with refinements to a URL and encode special characters without reference keys', () => { beautifier.config.useReferenceKeys = false; const url = generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red+green/blue') ] }); + expect(url).to.eq('/satin-shiny-party-dress/red%2Bgreen%2Fblue/colour/293014'); }); it('should convert a detail with a single refinement to a URL with a reference key', () => { beautifier.config.refinementMapping.push({ c: 'colour' }); const url = generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + expect(url).to.eq('/dress/red/c/293014'); }); it('should convert a detail with multiple refinements to a URL with reference keys', () => { beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); const url = generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red'), refinement('brand', 'h&m') ] }); + expect(url).to.eq('/dress/h%26m/red/bc/293014'); }); describe('error states', () => { it('should throw an error if no reference key found for refinement navigation name', () => { const build = () => generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + expect(build).to.throw('no mapping found for navigation "colour"'); }); }); @@ -69,34 +74,40 @@ describe('detail URL beautifier', () => { it('should parse a simple URL and return a detail object', () => { const expectedDetail = { productTitle: 'apples', productID: '1923' }; + expect(parser.parse('/apples/1923')).to.eql(expectedDetail); }); it('should parse a simple URL, replace \'-\' with \' \' and return a detail object', () => { const expectedDetail = { productTitle: 'red and delicious apples', productID: '1923' }; + expect(parser.parse('/red-and-delicious-apples/1923')).to.eql(expectedDetail); }); it('should parse a simple URL, decode special characters and return a detail object', () => { const expectedDetail = { productTitle: 'red+and+delicious+apples', productID: '1923' }; + expect(parser.parse('/red%2Band%2Bdelicious%2Bapples/1923')).to.eql(expectedDetail); }); it('should parse a URL with navigation names and values and return a detail object without reference keys', () => { beautifier.config.useReferenceKeys = false; const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'blue') ] }; + expect(parser.parse('/satin-shiny-party-dress/blue/colour/293014')).to.eql(expectedDetail); }); it('should decode special characters in navigation name and values', () => { beautifier.config.useReferenceKeys = false; const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red') ] }; + expect(parser.parse('/satin-shiny-party-dress/h%26m/brand/blue/colour/red/colour/293014')).to.eql(expectedDetail); }); it('should parse a URL with reference keys', () => { beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }) const expectedDetail = { productTitle: 'dress', productID: '293014', refinements: [ refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red') ] }; + expect(parser.parse('/dress/h%26m/blue/red/bcc/293014')).to.eql(expectedDetail); }); @@ -107,6 +118,7 @@ describe('detail URL beautifier', () => { it('should throw an error if the path without reference keys has an odd number of parts', () => { beautifier.config.useReferenceKeys = false; + expect(() => parser.parse('/dress/blue/colour/red/293014')).to.throw('path has an odd number of parts'); }); @@ -139,26 +151,28 @@ describe('detail URL beautifier', () => { it('should convert from detail object to a URL and back with reference keys', () => { const url = '/dress/h%26m/blue/red/bcc/293014'; beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + expect(parser.parse(generator.build(obj))).to.eql(obj); }); - it('should convert from URL to a query and back with reference keys', () => { + it('should convert from URL to a detail and back with reference keys', () => { const url = '/dress/h%26m/blue/red/bcc/293014'; beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + expect(generator.build(parser.parse(url))).to.eq(url); }); it('should convert from detail object to a URL and back without reference keys', () => { const url = '/dress/h%26m/brand/blue/colour/red/colour/293014'; beautifier.config.useReferenceKeys = false; - beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + expect(parser.parse(generator.build(obj))).to.eql(obj); }); - it('should convert from URL to a query and back without reference keys', () => { + it('should convert from URL to a detail and back without reference keys', () => { const url = '/dress/h%26m/brand/blue/colour/red/colour/293014'; beautifier.config.useReferenceKeys = false; - beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + expect(generator.build(parser.parse(url))).to.eq(url); }); }); diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index c4b3c437..b753897c 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -21,16 +21,19 @@ describe('navigation URL beautifier', () => { it('should convert a simple navigation name to a URL', () => { beautifier.config.navigations['Apples'] = query; + expect(generator.build('Apples')).to.be.eq('/Apples'); }); it('should replace spaces in a navigation name with hyphen', () => { beautifier.config.navigations['red apples'] = query; + expect(generator.build('red apples')).to.be.eq('/red-apples'); }) it('should encode special characters in navigation name', () => { beautifier.config.navigations['red&green apples/grapes'] = query; + expect(generator.build('red&green apples/grapes')).to.be.eq('/red%26green-apples%2Fgrapes'); }); @@ -58,12 +61,14 @@ describe('navigation URL beautifier', () => { it('should parse URL with encoded characters', () => { const navigationName = 'Red apples/cherries'; beautifier.config.navigations[navigationName] = query; + expect(parser.parse('/Red-apples%2Fcherries')).to.be.eql(query); }); it('should parse URL with hyphen', () => { const navigationName = 'Red apples'; beautifier.config.navigations[navigationName] = query; + expect(parser.parse('/' + encodeURIComponent(navigationName))).to.be.eql(query); }) From 933fa08d40ba6fd671d97a7c6fc6cdbc22ad3049 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Fri, 2 Jun 2017 11:27:13 -0400 Subject: [PATCH 27/34] Incomplete --- src/utils/url-beautifier/url-beautifier.ts | 12 ++++--- .../utils/url-beautifier/url-beautifier.ts | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 test/unit/utils/url-beautifier/url-beautifier.ts diff --git a/src/utils/url-beautifier/url-beautifier.ts b/src/utils/url-beautifier/url-beautifier.ts index 96a87ff9..01eaf755 100644 --- a/src/utils/url-beautifier/url-beautifier.ts +++ b/src/utils/url-beautifier/url-beautifier.ts @@ -16,8 +16,8 @@ export class UrlBeautifier implements Beautifier { useReferenceKeys: true, navigations: {} }; - private generator: QueryUrlGenerator = new QueryUrlGenerator(this); - private parser: QueryUrlParser = new QueryUrlParser(this); + private queryGenerator: QueryUrlGenerator = new QueryUrlGenerator(this); + private queryParser: QueryUrlParser = new QueryUrlParser(this); constructor(public searchandiserConfig: SearchandiserConfig = {}) { const urlConfig = searchandiserConfig.url || {}; @@ -50,10 +50,14 @@ export class UrlBeautifier implements Beautifier { } parse(url: string) { - return this.parser.parse(url); + return this.queryParser.parse(url); + } + + buildQueryUrl(query: Query) { + return this.queryGenerator.build(query); } build(query: Query) { - return this.generator.build(query); + return this.queryGenerator.build(query); } } diff --git a/test/unit/utils/url-beautifier/url-beautifier.ts b/test/unit/utils/url-beautifier/url-beautifier.ts new file mode 100644 index 00000000..6979a6e7 --- /dev/null +++ b/test/unit/utils/url-beautifier/url-beautifier.ts @@ -0,0 +1,36 @@ +import * as sinonChai from 'sinon-chai'; +import { sandbox } from 'sinon'; +import { expect, use } from 'chai'; +import { Query } from 'groupby-api'; +import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser } from '../../../../src/utils/url-beautifier'; +import { refinement } from '../../../utils/fixtures'; + +use(sinonChai); + +describe.only('URL beautifier', () => { + let beautifier: UrlBeautifier; + let sandbox: Sinon.SinonSandbox; + let stub; + + beforeEach(() => { + beautifier = new UrlBeautifier(); + sandbox = sinon.sandbox.create(); + stub = sandbox.stub; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('generator', () => { + + it('should call query url generator', () => { + const query: Query = new Query(); + const build = sandbox.stub(QueryUrlGenerator.prototype, 'build'); + + beautifier.buildQueryUrl(query); + + expect(build).to.have.been.calledWith(query); + }); + }); +}); From 806012aaa7c5929207a855822caa8bccf76bb71d Mon Sep 17 00:00:00 2001 From: Johann Tutor Date: Fri, 2 Jun 2017 12:02:19 -0400 Subject: [PATCH 28/34] Wrapper function --- test/unit/utils/url-beautifier/url-beautifier.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/utils/url-beautifier/url-beautifier.ts b/test/unit/utils/url-beautifier/url-beautifier.ts index 6979a6e7..8dfeeee8 100644 --- a/test/unit/utils/url-beautifier/url-beautifier.ts +++ b/test/unit/utils/url-beautifier/url-beautifier.ts @@ -1,5 +1,5 @@ import * as sinonChai from 'sinon-chai'; -import { sandbox } from 'sinon'; +import { sandbox as sinonSandbox } from 'sinon'; import { expect, use } from 'chai'; import { Query } from 'groupby-api'; import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser } from '../../../../src/utils/url-beautifier'; @@ -9,13 +9,13 @@ use(sinonChai); describe.only('URL beautifier', () => { let beautifier: UrlBeautifier; - let sandbox: Sinon.SinonSandbox; + let sandbox; let stub; beforeEach(() => { beautifier = new UrlBeautifier(); - sandbox = sinon.sandbox.create(); - stub = sandbox.stub; + sandbox = sinonSandbox.create(); + stub = (...args) => sandbox.stub(..args); }); afterEach(() => { @@ -26,7 +26,7 @@ describe.only('URL beautifier', () => { it('should call query url generator', () => { const query: Query = new Query(); - const build = sandbox.stub(QueryUrlGenerator.prototype, 'build'); + const build = stub(QueryUrlGenerator, 'build'); beautifier.buildQueryUrl(query); From 2f6b854d71cc51c280d57d49b0d61f2ab9f8856a Mon Sep 17 00:00:00 2001 From: Johann Tutor Date: Fri, 2 Jun 2017 14:00:49 -0400 Subject: [PATCH 29/34] Make sandbox work --- test/unit/utils/url-beautifier/url-beautifier.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/unit/utils/url-beautifier/url-beautifier.ts b/test/unit/utils/url-beautifier/url-beautifier.ts index 8dfeeee8..9c803f17 100644 --- a/test/unit/utils/url-beautifier/url-beautifier.ts +++ b/test/unit/utils/url-beautifier/url-beautifier.ts @@ -1,5 +1,4 @@ import * as sinonChai from 'sinon-chai'; -import { sandbox as sinonSandbox } from 'sinon'; import { expect, use } from 'chai'; import { Query } from 'groupby-api'; import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser } from '../../../../src/utils/url-beautifier'; @@ -14,8 +13,8 @@ describe.only('URL beautifier', () => { beforeEach(() => { beautifier = new UrlBeautifier(); - sandbox = sinonSandbox.create(); - stub = (...args) => sandbox.stub(..args); + sandbox = sinon.sandbox.create(); + stub = (...args) => sandbox.stub(...args); }); afterEach(() => { From 3d92713010e56c94436b1e2964341d772de22e74 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Tue, 6 Jun 2017 09:11:51 -0400 Subject: [PATCH 30/34] Add compatibility tests --- src/utils/url-beautifier/interfaces.ts | 5 +- src/utils/url-beautifier/url-beautifier.ts | 37 +++++++++-- .../url-beautifier/detail-url-beautifer.ts | 3 - .../url-beautifier/query-url-beautifier.ts | 56 ++++++++--------- .../utils/url-beautifier/url-beautifier.ts | 62 ++++++++++++++++--- 5 files changed, 117 insertions(+), 46 deletions(-) diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index 56ce967c..847ba9df 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -4,8 +4,8 @@ import { SearchandiserConfig } from '../../searchandiser'; export interface Beautifier { config: BeautifierConfig; searchandiserConfig: SearchandiserConfig; - parse(url: string): Query; - build(query: Query): string; + parse(url: string): any; + build(query: any): string; } export interface BeautifierConfig { @@ -18,6 +18,7 @@ export interface BeautifierConfig { suffix?: string; useReferenceKeys?: boolean; navigations?: Object; + prefix: Object; } export interface Detail { diff --git a/src/utils/url-beautifier/url-beautifier.ts b/src/utils/url-beautifier/url-beautifier.ts index 01eaf755..b913d729 100644 --- a/src/utils/url-beautifier/url-beautifier.ts +++ b/src/utils/url-beautifier/url-beautifier.ts @@ -1,7 +1,10 @@ import { Query } from 'groupby-api'; -import { Beautifier, BeautifierConfig } from './interfaces'; +import { Beautifier, BeautifierConfig, Detail } from './interfaces'; import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; +import { NavigationUrlGenerator, NavigationUrlParser } from './navigation-url-beautifier'; +import { DetailUrlGenerator, DetailUrlParser } from './detail-url-beautifier'; import { SearchandiserConfig } from '../../searchandiser'; +import * as parseUri from 'parseUri'; export class UrlBeautifier implements Beautifier { @@ -14,11 +17,22 @@ export class UrlBeautifier implements Beautifier { queryToken: 'q', suffix: '', useReferenceKeys: true, - navigations: {} + navigations: {}, + prefix: { + query: '/query', + detail: '/detail', + navigation: '/navigation' + } }; private queryGenerator: QueryUrlGenerator = new QueryUrlGenerator(this); private queryParser: QueryUrlParser = new QueryUrlParser(this); + private navigationGenerator: NavigationUrlGenerator = new NavigationUrlGenerator(this); + private navigationParser: NavigationUrlParser = new NavigationUrlParser(this); + + private detailGenerator: DetailUrlGenerator = new DetailUrlGenerator(this); + private detailParser: DetailUrlParser = new DetailUrlParser(this); + constructor(public searchandiserConfig: SearchandiserConfig = {}) { const urlConfig = searchandiserConfig.url || {}; const config = typeof urlConfig.beautifier === 'object' ? urlConfig.beautifier : {}; @@ -49,14 +63,29 @@ export class UrlBeautifier implements Beautifier { } } - parse(url: string) { - return this.queryParser.parse(url); + parse(url: string): any { + const path = parseUri(url).path; + if (path.indexOf(this.config.prefix.query) === 0) { + return this.queryParser.parse(path.substr(this.config.prefix.query.length)); + } else if (path.indexOf(this.config.prefix.detail) === 0) { + return this.detailParser.parse(path.substr(this.config.prefix.detail.length)); + } else if (path.indexOf(this.config.prefix.navigation) === 0) { + return this.navigationParser.parse(path.substr(this.config.prefix.navigation.length)); + } } buildQueryUrl(query: Query) { return this.queryGenerator.build(query); } + buildNavigationUrl(name: string) { + return this.navigationGenerator.build(name); + } + + buildDetailUrl(detail: Detail) { + return this.detailGenerator.build(detail); + } + build(query: Query) { return this.queryGenerator.build(query); } diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index a31b0734..f3ef9e8e 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -1,17 +1,14 @@ import { expect } from 'chai'; -import { Query } from 'groupby-api'; import { UrlBeautifier, DetailUrlGenerator, DetailUrlParser, Detail } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; describe('detail URL beautifier', () => { let beautifier: UrlBeautifier; - let query: Query; let generator: DetailUrlGenerator; let parser: DetailUrlParser; beforeEach(() => { beautifier = new UrlBeautifier(); - query = new Query(); }); describe('detail URL generator', () => { diff --git a/test/unit/utils/url-beautifier/query-url-beautifier.ts b/test/unit/utils/url-beautifier/query-url-beautifier.ts index 56371c0f..774cba30 100644 --- a/test/unit/utils/url-beautifier/query-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/query-url-beautifier.ts @@ -6,6 +6,8 @@ import { refinement } from '../../../utils/fixtures'; describe('query URL beautifier', () => { let beautifier: UrlBeautifier; let query: Query; + let generator: QueryUrlGenerator; + let parser: QueryUrlParser; beforeEach(() => { beautifier = new UrlBeautifier(); @@ -13,8 +15,6 @@ describe('query URL beautifier', () => { }); describe('query URL generator', () => { - let generator: QueryUrlGenerator; - beforeEach(() => generator = new QueryUrlGenerator(beautifier)); it('should convert a simple query to a URL', () => { @@ -217,8 +217,6 @@ describe('query URL beautifier', () => { }); describe('query URL parser', () => { - let parser: QueryUrlParser; - beforeEach(() => parser = new QueryUrlParser(beautifier)); it('should parse simple query URL', () => { @@ -411,37 +409,39 @@ describe('query URL beautifier', () => { describe('compatibility', () => { - beforeEach(() => beautifier = new UrlBeautifier({ - url: { - beautifier: { - refinementMapping: [{ b: 'brand' }, { f: 'fabric' }], - queryToken: 'k', - extraRefinementsParam: 'refs', - suffix: 'index.html' - } - } - })); + beforeEach(() => { + generator = new QueryUrlGenerator(beautifier); + parser = new QueryUrlParser(beautifier); + query = new Query(); + query.withQuery('dress') + .withSelectedRefinements(refinement('brand', 'h&m')); + }); - it('should convert from query to a URL and back', () => { - query.withQuery('duvet cover') - .withSelectedRefinements( - refinement('brand', 'Duvet King'), - refinement('fabric', 'linen'), - refinement('price', 10, 40)); + it('should convert from query object to a URL and back with reference keys', () => { + const url = '/dress/h%26m/qb'; + beautifier.config.refinementMapping.push({ b: 'brand' }); + expect(parser.parse(generator.build(query))).to.eql(query); + }); - const origRequest = query.build(); - const convertedRequest = beautifier.parse(beautifier.build(query)).build(); + it('should convert from URL to a query and back with reference keys', () => { + const url = '/dress/h%26m/qb'; + beautifier.config.refinementMapping.push({ b: 'brand' }); - expect(convertedRequest.query).to.eql(origRequest.query); - expect(convertedRequest.refinements).to.have.deep.members(origRequest.refinements); + expect(generator.build(parser.parse(url))).to.eq(url); }); - it('should convert from URL to a query and back', () => { - const url = '/duvet-cover/Duvet-King/linen/kbf/index.html?refs=price%3A10..40'; + it('should convert from query object to a URL and back without reference keys', () => { + const url = '/dress/h%26m/brand'; + beautifier.config.useReferenceKeys = false; - const convertedUrl = beautifier.build(beautifier.parse(url)); + expect(parser.parse(generator.build(query))).to.eql(query); + }); + + it('should convert from URL to a query and back without reference keys', () => { + const url = '/dress/h%26m/brand'; + beautifier.config.useReferenceKeys = false; - expect(convertedUrl).to.eq(url); + expect(generator.build(parser.parse(url))).to.eq(url); }); }); diff --git a/test/unit/utils/url-beautifier/url-beautifier.ts b/test/unit/utils/url-beautifier/url-beautifier.ts index 9c803f17..ee69301f 100644 --- a/test/unit/utils/url-beautifier/url-beautifier.ts +++ b/test/unit/utils/url-beautifier/url-beautifier.ts @@ -1,20 +1,17 @@ -import * as sinonChai from 'sinon-chai'; -import { expect, use } from 'chai'; +import { expect } from 'chai'; import { Query } from 'groupby-api'; -import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser } from '../../../../src/utils/url-beautifier'; +import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, DetailUrlGenerator, DetailUrlParser, Detail } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; -use(sinonChai); - -describe.only('URL beautifier', () => { +describe('URL beautifier', () => { let beautifier: UrlBeautifier; - let sandbox; + let sandbox: Sinon.SinonSandbox; let stub; beforeEach(() => { beautifier = new UrlBeautifier(); sandbox = sinon.sandbox.create(); - stub = (...args) => sandbox.stub(...args); + stub = (...args: any[]) => (sandbox.stub)(...args); }); afterEach(() => { @@ -25,11 +22,58 @@ describe.only('URL beautifier', () => { it('should call query url generator', () => { const query: Query = new Query(); - const build = stub(QueryUrlGenerator, 'build'); + const build = stub(QueryUrlGenerator.prototype, 'build'); beautifier.buildQueryUrl(query); expect(build).to.have.been.calledWith(query); }); + + it('should call navigation url generator', () => { + const name = 'Apples'; + const build = stub(NavigationUrlGenerator.prototype, 'build'); + + beautifier.buildNavigationUrl(name); + + expect(build).to.have.been.calledWith(name); + }); + + it('should call detail url generator', () => { + const detail = { + productTitle: 'Apples', + productID: '12345' + }; + const build = stub(DetailUrlGenerator.prototype, 'build'); + + beautifier.buildDetailUrl(detail); + + expect(build).to.have.been.calledWith(detail); + }); + }); + + describe('parser', () => { + it('should call query url parser', () => { + const parse = stub(QueryUrlParser.prototype, 'parse'); + + beautifier.parse('http://example.com/query/apples/green/qc'); + + expect(parse).to.have.been.calledWith('/apples/green/qc'); + }); + + it('should call detail url parser', () => { + const parse = stub(DetailUrlParser.prototype, 'parse'); + + beautifier.parse('http://example.com/detail/apples/green/qc/1045'); + + expect(parse).to.have.been.calledWith('/apples/green/qc/1045'); + }); + + it('should call navigation url parser', () => { + const parse = stub(NavigationUrlParser.prototype, 'parse'); + + beautifier.parse('http://example.com/navigation/Apples'); + + expect(parse).to.have.been.calledWith('/Apples'); + }); }); }); From 1177facf6abc96578f5e1b36a766d6d6826a16de Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Tue, 6 Jun 2017 10:38:19 -0400 Subject: [PATCH 31/34] Make parser accept uri object to remove prefix without reconstructing and reparsing url --- .../url-beautifier/detail-url-beautifier.ts | 5 +- src/utils/url-beautifier/interfaces.ts | 8 ++- .../navigation-url-beautifier.ts | 5 +- .../url-beautifier/query-url-beautifier.ts | 3 +- src/utils/url-beautifier/url-beautifier.ts | 17 ++++-- .../url-beautifier/detail-url-beautifer.ts | 29 ++++----- .../navigation-url-beautifier.ts | 11 ++-- .../url-beautifier/query-url-beautifier.ts | 59 ++++++++++--------- .../utils/url-beautifier/url-beautifier.ts | 29 ++++++++- 9 files changed, 100 insertions(+), 66 deletions(-) diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts index 3c01f859..63994c91 100644 --- a/src/utils/url-beautifier/detail-url-beautifier.ts +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -1,7 +1,6 @@ import { SelectedValueRefinement } from 'groupby-api'; import { Beautifier, BeautifierConfig, Detail } from './interfaces'; import * as parseUri from 'parseUri'; -import * as queryString from 'query-string'; export class DetailUrlGenerator { config: BeautifierConfig; @@ -58,8 +57,8 @@ export class DetailUrlParser { this.config = config; } - parse(rawUrl: string): Detail { - let paths = parseUri(rawUrl).path.split('/').filter((val) => val).map((val) => decodeURIComponent(val).replace(/-/g, ' ')); + parse(url: { path: string, query: string}): Detail { + let paths = url.path.split('/').filter((val) => val).map((val) => decodeURIComponent(val).replace(/-/g, ' ')); if (paths.length < 2) { throw new Error('path has less than two parts'); diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index 847ba9df..a82ceea3 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -4,7 +4,7 @@ import { SearchandiserConfig } from '../../searchandiser'; export interface Beautifier { config: BeautifierConfig; searchandiserConfig: SearchandiserConfig; - parse(url: string): any; + parse(rawUrl: string): any; build(query: any): string; } @@ -18,7 +18,11 @@ export interface BeautifierConfig { suffix?: string; useReferenceKeys?: boolean; navigations?: Object; - prefix: Object; + prefix: { + query: string, + detail: string, + navigation: string + }; } export interface Detail { diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts index 4255aa45..80d918c7 100644 --- a/src/utils/url-beautifier/navigation-url-beautifier.ts +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -1,7 +1,6 @@ import { Query } from 'groupby-api'; import { Beautifier, BeautifierConfig } from './interfaces'; import * as parseUri from 'parseUri'; -import * as queryString from 'query-string'; export class NavigationUrlGenerator { config: BeautifierConfig; @@ -26,8 +25,8 @@ export class NavigationUrlParser { this.config = config; } - parse(rawUrl: string): Query { - const paths = parseUri(rawUrl).path.split('/').filter((val) => val); + parse(url: { path: string, query: string} ): Query { + const paths = url.path.split('/').filter((val) => val); if (paths.length > 1) { throw new Error('path contains more than one part'); } diff --git a/src/utils/url-beautifier/query-url-beautifier.ts b/src/utils/url-beautifier/query-url-beautifier.ts index a555d125..3b610c64 100644 --- a/src/utils/url-beautifier/query-url-beautifier.ts +++ b/src/utils/url-beautifier/query-url-beautifier.ts @@ -149,8 +149,7 @@ export class QueryUrlParser { this.suffixRegex = new RegExp(`^${this.config.suffix}`); } - parse(rawUrl: string): Query { - const url = parseUri(rawUrl); + parse(url: { path: string, query: string}): Query { const paths = url.path.split('/').filter((val) => val); if (paths[paths.length - 1] === this.config.suffix) paths.pop(); diff --git a/src/utils/url-beautifier/url-beautifier.ts b/src/utils/url-beautifier/url-beautifier.ts index b913d729..e0c2c09a 100644 --- a/src/utils/url-beautifier/url-beautifier.ts +++ b/src/utils/url-beautifier/url-beautifier.ts @@ -63,17 +63,24 @@ export class UrlBeautifier implements Beautifier { } } - parse(url: string): any { - const path = parseUri(url).path; + parse(rawUrl: string): any { + const uri = parseUri(rawUrl); + const path = uri.path; if (path.indexOf(this.config.prefix.query) === 0) { - return this.queryParser.parse(path.substr(this.config.prefix.query.length)); + return this.queryParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.prefix.query)); } else if (path.indexOf(this.config.prefix.detail) === 0) { - return this.detailParser.parse(path.substr(this.config.prefix.detail.length)); + return this.detailParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.prefix.detail)); } else if (path.indexOf(this.config.prefix.navigation) === 0) { - return this.navigationParser.parse(path.substr(this.config.prefix.navigation.length)); + return this.navigationParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.prefix.navigation)); + } else { + throw new Error('invalid prefix'); } } + private extractUnprefixedPathAndQuery(uri: Object, prefix: string): { path: string, query: string } { + return { path: uri.path.substr(prefix.length), query: uri.query }; + } + buildQueryUrl(query: Query) { return this.queryGenerator.build(query); } diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index f3ef9e8e..d6c68948 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { UrlBeautifier, DetailUrlGenerator, DetailUrlParser, Detail } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; +import * as parseUri from 'parseUri'; describe('detail URL beautifier', () => { let beautifier: UrlBeautifier; @@ -72,59 +73,59 @@ describe('detail URL beautifier', () => { it('should parse a simple URL and return a detail object', () => { const expectedDetail = { productTitle: 'apples', productID: '1923' }; - expect(parser.parse('/apples/1923')).to.eql(expectedDetail); + expect(parser.parse(parseUri('/apples/1923'))).to.eql(expectedDetail); }); it('should parse a simple URL, replace \'-\' with \' \' and return a detail object', () => { const expectedDetail = { productTitle: 'red and delicious apples', productID: '1923' }; - expect(parser.parse('/red-and-delicious-apples/1923')).to.eql(expectedDetail); + expect(parser.parse(parseUri('/red-and-delicious-apples/1923'))).to.eql(expectedDetail); }); it('should parse a simple URL, decode special characters and return a detail object', () => { const expectedDetail = { productTitle: 'red+and+delicious+apples', productID: '1923' }; - expect(parser.parse('/red%2Band%2Bdelicious%2Bapples/1923')).to.eql(expectedDetail); + expect(parser.parse(parseUri('/red%2Band%2Bdelicious%2Bapples/1923'))).to.eql(expectedDetail); }); it('should parse a URL with navigation names and values and return a detail object without reference keys', () => { beautifier.config.useReferenceKeys = false; const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'blue') ] }; - expect(parser.parse('/satin-shiny-party-dress/blue/colour/293014')).to.eql(expectedDetail); + expect(parser.parse(parseUri('/satin-shiny-party-dress/blue/colour/293014'))).to.eql(expectedDetail); }); it('should decode special characters in navigation name and values', () => { beautifier.config.useReferenceKeys = false; const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red') ] }; - expect(parser.parse('/satin-shiny-party-dress/h%26m/brand/blue/colour/red/colour/293014')).to.eql(expectedDetail); + expect(parser.parse(parseUri('/satin-shiny-party-dress/h%26m/brand/blue/colour/red/colour/293014'))).to.eql(expectedDetail); }); it('should parse a URL with reference keys', () => { beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }) const expectedDetail = { productTitle: 'dress', productID: '293014', refinements: [ refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red') ] }; - expect(parser.parse('/dress/h%26m/blue/red/bcc/293014')).to.eql(expectedDetail); + expect(parser.parse(parseUri('/dress/h%26m/blue/red/bcc/293014'))).to.eql(expectedDetail); }); describe('error states', () => { it('should throw an error if the path has less than two parts', () => { - expect(() => parser.parse('/dress')).to.throw('path has less than two parts'); + expect(() => parser.parse(parseUri('/dress'))).to.throw('path has less than two parts'); }); it('should throw an error if the path without reference keys has an odd number of parts', () => { beautifier.config.useReferenceKeys = false; - expect(() => parser.parse('/dress/blue/colour/red/293014')).to.throw('path has an odd number of parts'); + expect(() => parser.parse(parseUri('/dress/blue/colour/red/293014'))).to.throw('path has an odd number of parts'); }); it('should throw an error if the path has wrong number of parts', () => { - expect(() => parser.parse('/shoe/blue/colour')).to.throw('path has wrong number of parts'); + expect(() => parser.parse(parseUri('/shoe/blue/colour'))).to.throw('path has wrong number of parts'); }); it('should throw an error if token reference is invalid', () => { - expect(() => parser.parse('/apples/green/cs/2931')).to.throw('token reference is invalid'); + expect(() => parser.parse(parseUri('/apples/green/cs/2931'))).to.throw('token reference is invalid'); }) }); }); @@ -149,28 +150,28 @@ describe('detail URL beautifier', () => { const url = '/dress/h%26m/blue/red/bcc/293014'; beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); - expect(parser.parse(generator.build(obj))).to.eql(obj); + expect(parser.parse(parseUri(generator.build(obj)))).to.eql(obj); }); it('should convert from URL to a detail and back with reference keys', () => { const url = '/dress/h%26m/blue/red/bcc/293014'; beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); - expect(generator.build(parser.parse(url))).to.eq(url); + expect(generator.build(parser.parse(parseUri(url)))).to.eq(url); }); it('should convert from detail object to a URL and back without reference keys', () => { const url = '/dress/h%26m/brand/blue/colour/red/colour/293014'; beautifier.config.useReferenceKeys = false; - expect(parser.parse(generator.build(obj))).to.eql(obj); + expect(parser.parse(parseUri(generator.build(obj)))).to.eql(obj); }); it('should convert from URL to a detail and back without reference keys', () => { const url = '/dress/h%26m/brand/blue/colour/red/colour/293014'; beautifier.config.useReferenceKeys = false; - expect(generator.build(parser.parse(url))).to.eq(url); + expect(generator.build(parser.parse(parseUri(url)))).to.eq(url); }); }); }); diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index b753897c..ab038dfe 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { Query } from 'groupby-api'; import { UrlBeautifier, NavigationUrlGenerator, NavigationUrlParser } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; +import * as parseUri from 'parseUri'; describe('navigation URL beautifier', () => { let beautifier: UrlBeautifier; @@ -55,30 +56,30 @@ describe('navigation URL beautifier', () => { }); it('should parse URL and return the associated query', () => { - expect(parser.parse('/Apples')).to.be.eql(query); + expect(parser.parse(parseUri('/Apples'))).to.be.eql(query); }); it('should parse URL with encoded characters', () => { const navigationName = 'Red apples/cherries'; beautifier.config.navigations[navigationName] = query; - expect(parser.parse('/Red-apples%2Fcherries')).to.be.eql(query); + expect(parser.parse(parseUri('/Red-apples%2Fcherries'))).to.be.eql(query); }); it('should parse URL with hyphen', () => { const navigationName = 'Red apples'; beautifier.config.navigations[navigationName] = query; - expect(parser.parse('/' + encodeURIComponent(navigationName))).to.be.eql(query); + expect(parser.parse(parseUri('/' + encodeURIComponent(navigationName)))).to.be.eql(query); }) describe('error states', () => { it('should parse URL and throw an error if associated query is not found', () => { - expect(() => parser.parse('/Orange')).to.throw('no navigation mapping found for Orange'); + expect(() => parser.parse(parseUri('/Orange'))).to.throw('no navigation mapping found for Orange'); }); it('should parse URL and throw an error if the path has more than one part', () => { - expect(() => parser.parse('/Apples/Orange')).to.throw('path contains more than one part'); + expect(() => parser.parse(parseUri('/Apples/Orange'))).to.throw('path contains more than one part'); }) }); }); diff --git a/test/unit/utils/url-beautifier/query-url-beautifier.ts b/test/unit/utils/url-beautifier/query-url-beautifier.ts index 774cba30..19c5b3aa 100644 --- a/test/unit/utils/url-beautifier/query-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/query-url-beautifier.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { Query } from 'groupby-api'; import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; +import * as parseUri from 'parseUri'; describe('query URL beautifier', () => { let beautifier: UrlBeautifier; @@ -222,67 +223,67 @@ describe('query URL beautifier', () => { it('should parse simple query URL', () => { query.withQuery('apples'); - expect(parser.parse('/apples/q').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/apples/q')).build()).to.eql(query.build()); }); it('should parse URL with a slash in the query', () => { query.withQuery('red/apples'); - expect(parser.parse('/red%2Fapples/q').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/red%2Fapples/q')).build()).to.eql(query.build()); }); it('should parse URL with a plus in the query', () => { query.withQuery('red+apples'); - expect(parser.parse('/red%2Bapples/q').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/red%2Bapples/q')).build()).to.eql(query.build()); }); it('should parse simple query URL with dash and without reference keys', () => { beautifier.config.useReferenceKeys = false; query.withQuery('red apples'); - expect(parser.parse('/red-apples').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/red-apples')).build()).to.eql(query.build()); }); it('should parse simple query URL with custom token', () => { beautifier.config.queryToken = 'c'; - expect(parser.parse('/sneakers/c').build()).to.eql(new Query('sneakers').build()); + expect(parser.parse(parseUri('/sneakers/c')).build()).to.eql(new Query('sneakers').build()); }); it('should extract a value refinement from URL', () => { beautifier.config.refinementMapping.push({ c: 'colour' }); query.withSelectedRefinements(refinement('colour', 'green')); - expect(parser.parse('/green/c').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/green/c')).build()).to.eql(query.build()); }); it('should extract a multiple value refinements for field from URL', () => { beautifier.config.refinementMapping.push({ c: 'colour' }); query.withSelectedRefinements(refinement('colour', 'green'), refinement('colour', 'blue')); - expect(parser.parse('/green/blue/cc').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/green/blue/cc')).build()).to.eql(query.build()); }); it('should extract a value refinement with a slash from URL', () => { beautifier.config.refinementMapping.push({ b: 'brand' }); query.withSelectedRefinements(refinement('brand', 'De/Walt')); - expect(parser.parse('/De%2FWalt/b').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/De%2FWalt/b')).build()).to.eql(query.build()); }); it('should extract a value refinement with a plus from URL', () => { beautifier.config.refinementMapping.push({ b: 'brand' }); query.withSelectedRefinements(refinement('brand', 'De+Walt')); - expect(parser.parse('/De%2BWalt/b').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/De%2BWalt/b')).build()).to.eql(query.build()); }); it('should extract multiple refinements from URL', () => { beautifier.config.refinementMapping.push({ c: 'colour', b: 'brand' }); query.withSelectedRefinements(refinement('colour', 'dark purple'), refinement('brand', 'Wellingtons')); - expect(parser.parse('/dark-purple/Wellingtons/cb').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/dark-purple/Wellingtons/cb')).build()).to.eql(query.build()); }); it('should extract a query and refinement from URL', () => { @@ -290,7 +291,7 @@ describe('query URL beautifier', () => { query.withQuery('sneakers') .withSelectedRefinements(refinement('colour', 'green')); - expect(parser.parse('/sneakers/green/qc').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/sneakers/green/qc')).build()).to.eql(query.build()); }); it('should extract query and value refinements from URL without reference keys', () => { @@ -298,20 +299,20 @@ describe('query URL beautifier', () => { query.withQuery('shoe') .withSelectedRefinements(refinement('colour', 'blue'), refinement('colour', 'red'), refinement('Brand', 'adidas'), refinement('Brand', 'nike')); - expect(parser.parse('/shoe/blue/colour/red/colour/adidas/Brand/nike/Brand').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/shoe/blue/colour/red/colour/adidas/Brand/nike/Brand')).build()).to.eql(query.build()); }); it('should extract value refinements from URL without reference keys', () => { beautifier.config.useReferenceKeys = false; query.withSelectedRefinements(refinement('colour', 'blue'), refinement('colour', 'red'), refinement('Brand', 'adidas'), refinement('Brand', 'nike')); - expect(parser.parse('/blue/colour/red/colour/adidas/Brand/nike/Brand').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/blue/colour/red/colour/adidas/Brand/nike/Brand')).build()).to.eql(query.build()); }); it('should extract unmapped query from URL parameters', () => { query.withSelectedRefinements(refinement('height', '20in'), refinement('price', 20, 30)); - expect(parser.parse('/?refinements=height%3A20in~price%3A20..30').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/?refinements=height%3A20in~price%3A20..30')).build()).to.eql(query.build()); }); it('should extract query and range refinements from URL without reference key', () => { @@ -319,14 +320,14 @@ describe('query URL beautifier', () => { query.withQuery('long red dress') .withSelectedRefinements(refinement('category', 'evening wear'), refinement('category', 'formal'), refinement('price', 50, 200)); - expect(parser.parse('/long-red-dress/evening-wear/category/formal/category?refinements=price:50..200').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/long-red-dress/evening-wear/category/formal/category?refinements=price:50..200')).build()).to.eql(query.build()); }); it('should extract page size from URL', () => { const pageSize = 5; query.withPageSize(pageSize); - expect(parser.parse(`/?page_size=${pageSize}`).build()).to.eql(query.build()); + expect(parser.parse(parseUri(`/?page_size=${pageSize}`)).build()).to.eql(query.build()); }); it('should extract page from URL', () => { @@ -334,7 +335,7 @@ describe('query URL beautifier', () => { const page = 2; query.skip(skip); - expect(parser.parse(`/?page=${page}`).build()).to.eql(query.build()); + expect(parser.parse(parseUri(`/?page=${page}`)).build()).to.eql(query.build()); }); it('should extract page and page size from URL', () => { @@ -344,7 +345,7 @@ describe('query URL beautifier', () => { query.skip(skip) .withPageSize(pageSize); - expect(parser.parse(`/?page=${page}&page_size=${pageSize}`).build()).to.eql(query.build()); + expect(parser.parse(parseUri(`/?page=${page}&page_size=${pageSize}`)).build()).to.eql(query.build()); }); it('should ignore suffix', () => { @@ -352,7 +353,7 @@ describe('query URL beautifier', () => { beautifier.config.suffix = 'index.html'; query.withSelectedRefinements(refinement('height', '20in'), refinement('price', 20, 30)); - expect(parser.parse('/20in/h/index.html?refinements=price%3A20..30').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/20in/h/index.html?refinements=price%3A20..30')).build()).to.eql(query.build()); }); it('should extract mapped and unmapped refinements with query and suffix', () => { @@ -362,7 +363,7 @@ describe('query URL beautifier', () => { beautifier.config.queryToken = 'n'; beautifier.config.suffix = 'index.html'; - const request = parser.parse('/power-drill/orange/Drills/nsc/index.html?nav=brand%3ADeWalt').build(); + const request = parser.parse(parseUri('/power-drill/orange/Drills/nsc/index.html?nav=brand%3ADeWalt')).build(); expect(request.query).to.eql('power drill'); expect(request.refinements).to.have.deep.members(refs); @@ -374,11 +375,11 @@ describe('query URL beautifier', () => { query.withQuery('power drill') .withSelectedRefinements(refinement('brand', 'DeWalt'), refinement('category', 'Drills'), refinement('colour', 'orange')); - expect(parser.parse('/power-drill/DeWalt/brand/Drills/category/orange/colour/index.html').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/power-drill/DeWalt/brand/Drills/category/orange/colour/index.html')).build()).to.eql(query.build()); }); it('should extract deeply nested URL', () => { - const request = parser.parse('http://example.com/my/nested/path/power-drill/q').build(); + const request = parser.parse(parseUri('http://example.com/my/nested/path/power-drill/q')).build(); expect(request.query).to.eql('power drill'); }); @@ -389,20 +390,20 @@ describe('query URL beautifier', () => { Object.assign(beautifier.searchandiserConfig, { area, collection }); query.withQuery('drills').withConfiguration({ area, collection }); - expect(parser.parse('/drills/q').build()).to.eql(query.build()); + expect(parser.parse(parseUri('/drills/q')).build()).to.eql(query.build()); }); describe('error states', () => { it('should error on invalid reference keys', () => { beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); - expect(() => parser.parse('/power-drill/orange/Drills/qccb').build()).to.throw('token reference is invalid'); + expect(() => parser.parse(parseUri('/power-drill/orange/Drills/qccb')).build()).to.throw('token reference is invalid'); }); it('should error on unrecognized key', () => { beautifier.config.refinementMapping.push({ c: 'colour' }); - expect(() => parser.parse('/Drills/b').build()).to.throw('unexpected token \'b\' found in reference'); + expect(() => parser.parse(parseUri('/Drills/b')).build()).to.throw('unexpected token \'b\' found in reference'); }); }); }); @@ -420,28 +421,28 @@ describe('query URL beautifier', () => { it('should convert from query object to a URL and back with reference keys', () => { const url = '/dress/h%26m/qb'; beautifier.config.refinementMapping.push({ b: 'brand' }); - expect(parser.parse(generator.build(query))).to.eql(query); + expect(parser.parse(parseUri(generator.build(query)))).to.eql(query); }); it('should convert from URL to a query and back with reference keys', () => { const url = '/dress/h%26m/qb'; beautifier.config.refinementMapping.push({ b: 'brand' }); - expect(generator.build(parser.parse(url))).to.eq(url); + expect(generator.build(parser.parse(parseUri(url)))).to.eq(url); }); it('should convert from query object to a URL and back without reference keys', () => { const url = '/dress/h%26m/brand'; beautifier.config.useReferenceKeys = false; - expect(parser.parse(generator.build(query))).to.eql(query); + expect(parser.parse(parseUri(generator.build(query)))).to.eql(query); }); it('should convert from URL to a query and back without reference keys', () => { const url = '/dress/h%26m/brand'; beautifier.config.useReferenceKeys = false; - expect(generator.build(parser.parse(url))).to.eq(url); + expect(generator.build(parser.parse(parseUri(url)))).to.eq(url); }); }); diff --git a/test/unit/utils/url-beautifier/url-beautifier.ts b/test/unit/utils/url-beautifier/url-beautifier.ts index ee69301f..71adbd51 100644 --- a/test/unit/utils/url-beautifier/url-beautifier.ts +++ b/test/unit/utils/url-beautifier/url-beautifier.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { Query } from 'groupby-api'; import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, DetailUrlGenerator, DetailUrlParser, Detail } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; +import * as parseUri from 'parseUri'; describe('URL beautifier', () => { let beautifier: UrlBeautifier; @@ -54,26 +55,48 @@ describe('URL beautifier', () => { describe('parser', () => { it('should call query url parser', () => { const parse = stub(QueryUrlParser.prototype, 'parse'); + const uri = parseUri('http://example.com/apples/green/qc'); beautifier.parse('http://example.com/query/apples/green/qc'); - expect(parse).to.have.been.calledWith('/apples/green/qc'); + expect(parse).to.have.been.calledWith({ path: uri.path, query: uri.query }); }); it('should call detail url parser', () => { const parse = stub(DetailUrlParser.prototype, 'parse'); + const uri = parseUri('http://example.com/apples/green/qc/1045'); beautifier.parse('http://example.com/detail/apples/green/qc/1045'); - expect(parse).to.have.been.calledWith('/apples/green/qc/1045'); + expect(parse).to.have.been.calledWith({ path: uri.path, query: uri.query }); }); it('should call navigation url parser', () => { const parse = stub(NavigationUrlParser.prototype, 'parse'); + const uri = parseUri('http://example.com/Apples'); beautifier.parse('http://example.com/navigation/Apples'); - expect(parse).to.have.been.calledWith('/Apples'); + expect(parse).to.have.been.calledWith({ path: uri.path, query: uri.query }); + }); + + it('should extract mapped and unmapped refinements with query and suffix', () => { + const refs = [refinement('category', 'Drills'), refinement('brand', 'DeWalt'), refinement('colour', 'orange')]; + beautifier.config.refinementMapping.push({ s: 'colour' }, { c: 'category' }); + beautifier.config.extraRefinementsParam = 'nav'; + beautifier.config.queryToken = 'n'; + beautifier.config.suffix = 'index.html'; + + const request = beautifier.parse('http://example.com/query/power-drill/orange/Drills/nsc/index.html?nav=brand%3ADeWalt').build(); + + expect(request.query).to.eql('power drill'); + expect(request.refinements).to.have.deep.members(refs); + }); + + describe('error state', () => { + it('should throw an error if prefix is none of query, detail or navigation', () => { + expect(() => beautifier.parse('http://example.com/my/nested/path/power-drill/q')).to.throw('invalid prefix'); + }); }); }); }); From 7b0f697a25917758e75e264e798fc1693edd6f28 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Wed, 7 Jun 2017 09:23:16 -0400 Subject: [PATCH 32/34] Improve code formating --- .../url-beautifier/detail-url-beautifier.ts | 9 ++- src/utils/url-beautifier/index.ts | 14 +++- src/utils/url-beautifier/interfaces.ts | 2 +- .../navigation-url-beautifier.ts | 3 +- .../url-beautifier/query-url-beautifier.ts | 13 ++-- src/utils/url-beautifier/url-beautifier.ts | 16 ++-- .../url-beautifier/detail-url-beautifer.ts | 75 ++++++++++++++----- .../navigation-url-beautifier.ts | 11 ++- .../url-beautifier/query-url-beautifier.ts | 68 ++++++++++++----- .../utils/url-beautifier/url-beautifier.ts | 11 ++- 10 files changed, 151 insertions(+), 71 deletions(-) diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts index 63994c91..dd2de8aa 100644 --- a/src/utils/url-beautifier/detail-url-beautifier.ts +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -1,6 +1,5 @@ -import { SelectedValueRefinement } from 'groupby-api'; import { Beautifier, BeautifierConfig, Detail } from './interfaces'; -import * as parseUri from 'parseUri'; +import { SelectedValueRefinement } from 'groupby-api'; export class DetailUrlGenerator { config: BeautifierConfig; @@ -101,7 +100,11 @@ export class DetailUrlParser { } while (paths.length) { - refinements.push({ navigationName: keysToRefinements[referenceKeys.shift()], value: paths.shift(), type: 'Value' }); + refinements.push({ + navigationName: keysToRefinements[referenceKeys.shift()], + value: paths.shift(), + type: 'Value' + }); } } result['refinements'] = refinements; diff --git a/src/utils/url-beautifier/index.ts b/src/utils/url-beautifier/index.ts index 4446de37..16ecf2de 100644 --- a/src/utils/url-beautifier/index.ts +++ b/src/utils/url-beautifier/index.ts @@ -1,7 +1,13 @@ +import { DetailUrlGenerator, DetailUrlParser } from './detail-url-beautifier'; import { BeautifierConfig, Detail } from './interfaces'; -import { UrlBeautifier } from './url-beautifier'; -import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; import { NavigationUrlGenerator, NavigationUrlParser } from './navigation-url-beautifier'; -import { DetailUrlGenerator, DetailUrlParser } from './detail-url-beautifier'; +import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; +import { UrlBeautifier } from './url-beautifier'; -export { BeautifierConfig, Detail, UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, DetailUrlGenerator, DetailUrlParser }; +export { + BeautifierConfig, Detail, + DetailUrlGenerator, DetailUrlParser, + NavigationUrlGenerator, NavigationUrlParser, + QueryUrlGenerator, QueryUrlParser, + UrlBeautifier +}; diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index a82ceea3..08836e49 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -1,5 +1,5 @@ -import { SelectedValueRefinement, Query } from 'groupby-api'; import { SearchandiserConfig } from '../../searchandiser'; +import { SelectedValueRefinement } from 'groupby-api'; export interface Beautifier { config: BeautifierConfig; diff --git a/src/utils/url-beautifier/navigation-url-beautifier.ts b/src/utils/url-beautifier/navigation-url-beautifier.ts index 80d918c7..652a13df 100644 --- a/src/utils/url-beautifier/navigation-url-beautifier.ts +++ b/src/utils/url-beautifier/navigation-url-beautifier.ts @@ -1,6 +1,5 @@ -import { Query } from 'groupby-api'; import { Beautifier, BeautifierConfig } from './interfaces'; -import * as parseUri from 'parseUri'; +import { Query } from 'groupby-api'; export class NavigationUrlGenerator { config: BeautifierConfig; diff --git a/src/utils/url-beautifier/query-url-beautifier.ts b/src/utils/url-beautifier/query-url-beautifier.ts index 3b610c64..b2baeec3 100644 --- a/src/utils/url-beautifier/query-url-beautifier.ts +++ b/src/utils/url-beautifier/query-url-beautifier.ts @@ -1,7 +1,6 @@ -import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement } from 'groupby-api'; -import { Beautifier, BeautifierConfig } from './interfaces'; import { CONFIGURATION_MASK, SearchandiserConfig } from '../../searchandiser'; -import * as parseUri from 'parseUri'; +import { Beautifier, BeautifierConfig } from './interfaces'; +import { Query, SelectedRangeRefinement, SelectedRefinement, SelectedValueRefinement } from 'groupby-api'; import * as queryString from 'query-string'; export class QueryUrlGenerator { @@ -50,7 +49,7 @@ export class QueryUrlGenerator { } } else { // add refinements - let valueRefinements = [], rangeRefinement = []; + let valueRefinements = []; for (let i = origRefinements.length - 1; i >= 0; --i) { if (origRefinements[i].type === 'Value') { valueRefinements.push(...origRefinements.splice(i, 1)); @@ -79,7 +78,8 @@ export class QueryUrlGenerator { // add page if (query.raw.skip) { - uri.query[this.config.pageParam] = Math.floor(query.raw.skip/(query.raw.pageSize || this.config.defaultPageSize))+1; + uri.query[this.config.pageParam] = + Math.floor(query.raw.skip / (query.raw.pageSize || this.config.defaultPageSize)) + 1; } let url = `/${uri.path.map((path) => encodeURIComponent(path)).join('/')}`; @@ -154,7 +154,8 @@ export class QueryUrlParser { if (paths[paths.length - 1] === this.config.suffix) paths.pop(); - const query = this.config.useReferenceKeys ? this.parsePathWithReferenceKeys(paths) : this.parsePathWithoutReferenceKeys(paths); + const query = this.config.useReferenceKeys ? + this.parsePathWithReferenceKeys(paths) : this.parsePathWithoutReferenceKeys(paths); const queryVariables = queryString.parse(url.query); const unmappedRefinements = queryVariables[this.config.extraRefinementsParam]; diff --git a/src/utils/url-beautifier/url-beautifier.ts b/src/utils/url-beautifier/url-beautifier.ts index e0c2c09a..0776f433 100644 --- a/src/utils/url-beautifier/url-beautifier.ts +++ b/src/utils/url-beautifier/url-beautifier.ts @@ -1,9 +1,9 @@ -import { Query } from 'groupby-api'; +import { SearchandiserConfig } from '../../searchandiser'; +import { DetailUrlGenerator, DetailUrlParser } from './detail-url-beautifier'; import { Beautifier, BeautifierConfig, Detail } from './interfaces'; -import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; import { NavigationUrlGenerator, NavigationUrlParser } from './navigation-url-beautifier'; -import { DetailUrlGenerator, DetailUrlParser } from './detail-url-beautifier'; -import { SearchandiserConfig } from '../../searchandiser'; +import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; +import { Query } from 'groupby-api'; import * as parseUri from 'parseUri'; export class UrlBeautifier implements Beautifier { @@ -77,10 +77,6 @@ export class UrlBeautifier implements Beautifier { } } - private extractUnprefixedPathAndQuery(uri: Object, prefix: string): { path: string, query: string } { - return { path: uri.path.substr(prefix.length), query: uri.query }; - } - buildQueryUrl(query: Query) { return this.queryGenerator.build(query); } @@ -96,4 +92,8 @@ export class UrlBeautifier implements Beautifier { build(query: Query) { return this.queryGenerator.build(query); } + + private extractUnprefixedPathAndQuery(uri: parseUri.UriStructure, prefix: string): { path: string, query: string } { + return { path: uri.path.substr(prefix.length), query: uri.query }; + } } diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index d6c68948..839ecfb6 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -1,6 +1,6 @@ -import { expect } from 'chai'; -import { UrlBeautifier, DetailUrlGenerator, DetailUrlParser, Detail } from '../../../../src/utils/url-beautifier'; +import { DetailUrlGenerator, DetailUrlParser, UrlBeautifier } from '../../../../src/utils/url-beautifier'; import { refinement } from '../../../utils/fixtures'; +import { expect } from 'chai'; import * as parseUri from 'parseUri'; describe('detail URL beautifier', () => { @@ -17,48 +17,71 @@ describe('detail URL beautifier', () => { beforeEach(() => generator = new DetailUrlGenerator(beautifier)); it('should convert a simple detail to a URL', () => { - expect(generator.build({ productTitle: 'red and delicious apples', productID: '1923' })).to.eq('/red-and-delicious-apples/1923'); + expect(generator.build({ productTitle: 'red and delicious apples', productID: '1923' })) + .to.eq('/red-and-delicious-apples/1923'); }); it('should encode special characters + in detail', () => { - expect(generator.build({ productTitle: 'red+and+delicious+apples', productID: '1923' })).to.eq('/red%2Band%2Bdelicious%2Bapples/1923'); + expect(generator.build({ productTitle: 'red+and+delicious+apples', productID: '1923' })) + .to.eq('/red%2Band%2Bdelicious%2Bapples/1923'); }); it('should encode special characters / in detail', () => { - expect(generator.build({ productTitle: 'red/and/delicious/apples', productID: '1923' })).to.eq('/red%2Fand%2Fdelicious%2Fapples/1923'); + expect(generator.build({ productTitle: 'red/and/delicious/apples', productID: '1923' })) + .to.eq('/red%2Fand%2Fdelicious%2Fapples/1923'); }); it('should convert a detail with refinements to a URL without reference keys', () => { beautifier.config.useReferenceKeys = false; - const url = generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + const url = generator.build({ + productTitle: 'satin shiny party dress', + productID: '293014', + refinements: [ refinement('colour', 'red') ] + }); expect(url).to.eq('/satin-shiny-party-dress/red/colour/293014'); }); - it('should convert a detail with refinements to a URL and encode special characters without reference keys', () => { + it('should convert detail with refinements to a URL and encode special characters without reference keys', () => { beautifier.config.useReferenceKeys = false; - const url = generator.build({ productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'red+green/blue') ] }); + const url = generator.build({ + productTitle: 'satin shiny party dress', + productID: '293014', + refinements: [refinement('colour', 'red+green/blue')] + }); expect(url).to.eq('/satin-shiny-party-dress/red%2Bgreen%2Fblue/colour/293014'); }); it('should convert a detail with a single refinement to a URL with a reference key', () => { beautifier.config.refinementMapping.push({ c: 'colour' }); - const url = generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + const url = generator.build({ + productTitle: 'dress', + productID: '293014', + refinements: [refinement('colour', 'red')] + }); expect(url).to.eq('/dress/red/c/293014'); }); it('should convert a detail with multiple refinements to a URL with reference keys', () => { beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); - const url = generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red'), refinement('brand', 'h&m') ] }); + const url = generator.build({ + productTitle: 'dress', + productID: '293014', + refinements: [refinement('colour', 'red'), refinement('brand', 'h&m') ] + }); expect(url).to.eq('/dress/h%26m/red/bc/293014'); }); describe('error states', () => { it('should throw an error if no reference key found for refinement navigation name', () => { - const build = () => generator.build({ productTitle: 'dress', productID: '293014', refinements: [ refinement('colour', 'red') ] }); + const build = () => generator.build({ + productTitle: 'dress', + productID: '293014', + refinements: [refinement('colour', 'red')] + }); expect(build).to.throw('no mapping found for navigation "colour"'); }); @@ -90,21 +113,34 @@ describe('detail URL beautifier', () => { it('should parse a URL with navigation names and values and return a detail object without reference keys', () => { beautifier.config.useReferenceKeys = false; - const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('colour', 'blue') ] }; + const expectedDetail = { + productTitle: 'satin shiny party dress', + productID: '293014', + refinements: [refinement('colour', 'blue')] + }; expect(parser.parse(parseUri('/satin-shiny-party-dress/blue/colour/293014'))).to.eql(expectedDetail); }); it('should decode special characters in navigation name and values', () => { beautifier.config.useReferenceKeys = false; - const expectedDetail = { productTitle: 'satin shiny party dress', productID: '293014', refinements: [ refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red') ] }; + const url = '/satin-shiny-party-dress/h%26m/brand/blue/colour/red/colour/293014'; + const expectedDetail = { + productTitle: 'satin shiny party dress', + productID: '293014', + refinements: [refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red')] + }; - expect(parser.parse(parseUri('/satin-shiny-party-dress/h%26m/brand/blue/colour/red/colour/293014'))).to.eql(expectedDetail); + expect(parser.parse(parseUri(url))).to.eql(expectedDetail); }); it('should parse a URL with reference keys', () => { - beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }) - const expectedDetail = { productTitle: 'dress', productID: '293014', refinements: [ refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red') ] }; + beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); + const expectedDetail = { + productTitle: 'dress', + productID: '293014', + refinements: [refinement('brand', 'h&m'), refinement('colour', 'blue'), refinement('colour', 'red')] + }; expect(parser.parse(parseUri('/dress/h%26m/blue/red/bcc/293014'))).to.eql(expectedDetail); }); @@ -117,7 +153,8 @@ describe('detail URL beautifier', () => { it('should throw an error if the path without reference keys has an odd number of parts', () => { beautifier.config.useReferenceKeys = false; - expect(() => parser.parse(parseUri('/dress/blue/colour/red/293014'))).to.throw('path has an odd number of parts'); + expect(() => parser.parse(parseUri('/dress/blue/colour/red/293014'))) + .to.throw('path has an odd number of parts'); }); it('should throw an error if the path has wrong number of parts', () => { @@ -126,7 +163,7 @@ describe('detail URL beautifier', () => { it('should throw an error if token reference is invalid', () => { expect(() => parser.parse(parseUri('/apples/green/cs/2931'))).to.throw('token reference is invalid'); - }) + }); }); }); @@ -147,7 +184,6 @@ describe('detail URL beautifier', () => { }); it('should convert from detail object to a URL and back with reference keys', () => { - const url = '/dress/h%26m/blue/red/bcc/293014'; beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); expect(parser.parse(parseUri(generator.build(obj)))).to.eql(obj); @@ -161,7 +197,6 @@ describe('detail URL beautifier', () => { }); it('should convert from detail object to a URL and back without reference keys', () => { - const url = '/dress/h%26m/brand/blue/colour/red/colour/293014'; beautifier.config.useReferenceKeys = false; expect(parser.parse(parseUri(generator.build(obj)))).to.eql(obj); diff --git a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts index ab038dfe..a004a1ca 100644 --- a/test/unit/utils/url-beautifier/navigation-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/navigation-url-beautifier.ts @@ -1,7 +1,6 @@ +import { NavigationUrlGenerator, NavigationUrlParser, UrlBeautifier } from '../../../../src/utils/url-beautifier'; import { expect } from 'chai'; import { Query } from 'groupby-api'; -import { UrlBeautifier, NavigationUrlGenerator, NavigationUrlParser } from '../../../../src/utils/url-beautifier'; -import { refinement } from '../../../utils/fixtures'; import * as parseUri from 'parseUri'; describe('navigation URL beautifier', () => { @@ -17,7 +16,7 @@ describe('navigation URL beautifier', () => { let generator: NavigationUrlGenerator; beforeEach(() => { - generator = new NavigationUrlGenerator(beautifier) + generator = new NavigationUrlGenerator(beautifier); }); it('should convert a simple navigation name to a URL', () => { @@ -30,7 +29,7 @@ describe('navigation URL beautifier', () => { beautifier.config.navigations['red apples'] = query; expect(generator.build('red apples')).to.be.eq('/red-apples'); - }) + }); it('should encode special characters in navigation name', () => { beautifier.config.navigations['red&green apples/grapes'] = query; @@ -71,7 +70,7 @@ describe('navigation URL beautifier', () => { beautifier.config.navigations[navigationName] = query; expect(parser.parse(parseUri('/' + encodeURIComponent(navigationName)))).to.be.eql(query); - }) + }); describe('error states', () => { it('should parse URL and throw an error if associated query is not found', () => { @@ -80,7 +79,7 @@ describe('navigation URL beautifier', () => { it('should parse URL and throw an error if the path has more than one part', () => { expect(() => parser.parse(parseUri('/Apples/Orange'))).to.throw('path contains more than one part'); - }) + }); }); }); }); diff --git a/test/unit/utils/url-beautifier/query-url-beautifier.ts b/test/unit/utils/url-beautifier/query-url-beautifier.ts index 19c5b3aa..bc52ee86 100644 --- a/test/unit/utils/url-beautifier/query-url-beautifier.ts +++ b/test/unit/utils/url-beautifier/query-url-beautifier.ts @@ -1,7 +1,7 @@ +import { QueryUrlGenerator, QueryUrlParser, UrlBeautifier } from '../../../../src/utils/url-beautifier'; +import { refinement } from '../../../utils/fixtures'; import { expect } from 'chai'; import { Query } from 'groupby-api'; -import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser } from '../../../../src/utils/url-beautifier'; -import { refinement } from '../../../utils/fixtures'; import * as parseUri from 'parseUri'; describe('query URL beautifier', () => { @@ -75,7 +75,12 @@ describe('query URL beautifier', () => { it('should convert a sorted refinements list on same field a URL without reference keys', () => { beautifier.config.useReferenceKeys = false; query.withQuery('shoe') - .withSelectedRefinements(refinement('colour', 'blue'), refinement('Brand', 'nike'), refinement('Brand', 'adidas'), refinement('colour', 'red')); + .withSelectedRefinements( + refinement('colour', 'blue'), + refinement('Brand', 'nike'), + refinement('Brand', 'adidas'), + refinement('colour', 'red') + ); expect(generator.build(query)).to.eq('/shoe/adidas/Brand/nike/Brand/blue/colour/red/colour'); }); @@ -148,7 +153,7 @@ describe('query URL beautifier', () => { expect(generator.build(query)).to.eq(`/?page=${page}&page_size=${pageSize}`); }); - it('should convert query with skip, page size and unmapped refinements to a URL with a query parameter list without reference keys', () => { + it('should convert query with skip, page size and unmapped refinements to a URL without reference keys', () => { const pageSize = 6; const skip = 6; const page = 2; @@ -159,15 +164,22 @@ describe('query URL beautifier', () => { query.withQuery('red apples') .withSelectedRefinements(refinement('colour', 'dark purple'), refinement('price', 100, 220)); - expect(generator.build(query)).to.eq(`/red-apples/dark-purple/colour?page=${page}&page_size=${pageSize}&refinements=price%3A100..220`); + expect(generator.build(query)) + .to.eq(`/red-apples/dark-purple/colour?page=${page}&page_size=${pageSize}&refinements=price%3A100..220`); }); - it('should convert query with unmapped refinements to a URL with a query parameter list with reference keys', () => { + it('should convert query with unmapped refinements to a URL with reference keys', () => { beautifier.config.refinementMapping.push({ c: 'category' }); query.withQuery('long red dress') - .withSelectedRefinements(refinement('category', 'evening wear'), refinement('category', 'formal'), refinement('size', 'large'), refinement('shipping', 'true')); + .withSelectedRefinements( + refinement('category', 'evening wear'), + refinement('category', 'formal'), + refinement('size', 'large'), + refinement('shipping', 'true') + ); - expect(generator.build(query)).to.eq(`/long-red-dress/evening-wear/formal/qcc?refinements=shipping%3Atrue~size%3Alarge`); + expect(generator.build(query)) + .to.eq(`/long-red-dress/evening-wear/formal/qcc?refinements=shipping%3Atrue~size%3Alarge`); }); describe('canonical URLs', () => { @@ -297,14 +309,25 @@ describe('query URL beautifier', () => { it('should extract query and value refinements from URL without reference keys', () => { beautifier.config.useReferenceKeys = false; query.withQuery('shoe') - .withSelectedRefinements(refinement('colour', 'blue'), refinement('colour', 'red'), refinement('Brand', 'adidas'), refinement('Brand', 'nike')); + .withSelectedRefinements( + refinement('colour', 'blue'), + refinement('colour', 'red'), + refinement('Brand', 'adidas'), + refinement('Brand', 'nike') + ); - expect(parser.parse(parseUri('/shoe/blue/colour/red/colour/adidas/Brand/nike/Brand')).build()).to.eql(query.build()); + expect(parser.parse(parseUri('/shoe/blue/colour/red/colour/adidas/Brand/nike/Brand')).build()) + .to.eql(query.build()); }); it('should extract value refinements from URL without reference keys', () => { beautifier.config.useReferenceKeys = false; - query.withSelectedRefinements(refinement('colour', 'blue'), refinement('colour', 'red'), refinement('Brand', 'adidas'), refinement('Brand', 'nike')); + query.withSelectedRefinements( + refinement('colour', 'blue'), + refinement('colour', 'red'), + refinement('Brand', 'adidas'), + refinement('Brand', 'nike') + ); expect(parser.parse(parseUri('/blue/colour/red/colour/adidas/Brand/nike/Brand')).build()).to.eql(query.build()); }); @@ -316,11 +339,16 @@ describe('query URL beautifier', () => { }); it('should extract query and range refinements from URL without reference key', () => { + const url = '/long-red-dress/evening-wear/category/formal/category?refinements=price:50..200'; beautifier.config.useReferenceKeys = false; query.withQuery('long red dress') - .withSelectedRefinements(refinement('category', 'evening wear'), refinement('category', 'formal'), refinement('price', 50, 200)); + .withSelectedRefinements( + refinement('category', 'evening wear'), + refinement('category', 'formal'), + refinement('price', 50, 200) + ); - expect(parser.parse(parseUri('/long-red-dress/evening-wear/category/formal/category?refinements=price:50..200')).build()).to.eql(query.build()); + expect(parser.parse(parseUri(url)).build()).to.eql(query.build()); }); it('should extract page size from URL', () => { @@ -370,12 +398,17 @@ describe('query URL beautifier', () => { }); it('should extract mapped and unmapped refinements with query and suffix from URL without reference keys', () => { + const url = '/power-drill/DeWalt/brand/Drills/category/orange/colour/index.html'; beautifier.config.suffix = 'index.html'; beautifier.config.useReferenceKeys = false; query.withQuery('power drill') - .withSelectedRefinements(refinement('brand', 'DeWalt'), refinement('category', 'Drills'), refinement('colour', 'orange')); + .withSelectedRefinements( + refinement('brand', 'DeWalt'), + refinement('category', 'Drills'), + refinement('colour', 'orange') + ); - expect(parser.parse(parseUri('/power-drill/DeWalt/brand/Drills/category/orange/colour/index.html')).build()).to.eql(query.build()); + expect(parser.parse(parseUri(url)).build()).to.eql(query.build()); }); it('should extract deeply nested URL', () => { @@ -397,7 +430,8 @@ describe('query URL beautifier', () => { it('should error on invalid reference keys', () => { beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); - expect(() => parser.parse(parseUri('/power-drill/orange/Drills/qccb')).build()).to.throw('token reference is invalid'); + expect(() => parser.parse(parseUri('/power-drill/orange/Drills/qccb')).build()) + .to.throw('token reference is invalid'); }); it('should error on unrecognized key', () => { @@ -419,7 +453,6 @@ describe('query URL beautifier', () => { }); it('should convert from query object to a URL and back with reference keys', () => { - const url = '/dress/h%26m/qb'; beautifier.config.refinementMapping.push({ b: 'brand' }); expect(parser.parse(parseUri(generator.build(query)))).to.eql(query); }); @@ -432,7 +465,6 @@ describe('query URL beautifier', () => { }); it('should convert from query object to a URL and back without reference keys', () => { - const url = '/dress/h%26m/brand'; beautifier.config.useReferenceKeys = false; expect(parser.parse(parseUri(generator.build(query)))).to.eql(query); diff --git a/test/unit/utils/url-beautifier/url-beautifier.ts b/test/unit/utils/url-beautifier/url-beautifier.ts index 71adbd51..9bd0e7e6 100644 --- a/test/unit/utils/url-beautifier/url-beautifier.ts +++ b/test/unit/utils/url-beautifier/url-beautifier.ts @@ -1,7 +1,11 @@ +import { + DetailUrlGenerator, DetailUrlParser, + NavigationUrlGenerator, NavigationUrlParser, + QueryUrlGenerator, QueryUrlParser, UrlBeautifier +} from '../../../../src/utils/url-beautifier'; +import { refinement } from '../../../utils/fixtures'; import { expect } from 'chai'; import { Query } from 'groupby-api'; -import { UrlBeautifier, QueryUrlGenerator, QueryUrlParser, NavigationUrlGenerator, NavigationUrlParser, DetailUrlGenerator, DetailUrlParser, Detail } from '../../../../src/utils/url-beautifier'; -import { refinement } from '../../../utils/fixtures'; import * as parseUri from 'parseUri'; describe('URL beautifier', () => { @@ -82,12 +86,13 @@ describe('URL beautifier', () => { it('should extract mapped and unmapped refinements with query and suffix', () => { const refs = [refinement('category', 'Drills'), refinement('brand', 'DeWalt'), refinement('colour', 'orange')]; + const url = 'http://example.com/query/power-drill/orange/Drills/nsc/index.html?nav=brand%3ADeWalt'; beautifier.config.refinementMapping.push({ s: 'colour' }, { c: 'category' }); beautifier.config.extraRefinementsParam = 'nav'; beautifier.config.queryToken = 'n'; beautifier.config.suffix = 'index.html'; - const request = beautifier.parse('http://example.com/query/power-drill/orange/Drills/nsc/index.html?nav=brand%3ADeWalt').build(); + const request = beautifier.parse(url).build(); expect(request.query).to.eql('power drill'); expect(request.refinements).to.have.deep.members(refs); From c9f09de2d70909bb8ba3b448f80a8cf7992adf45 Mon Sep 17 00:00:00 2001 From: Haowei-Guo Date: Wed, 7 Jun 2017 12:02:44 -0400 Subject: [PATCH 33/34] Change detail.refinements to non-nullable --- .../url-beautifier/detail-url-beautifier.ts | 13 ++++--- src/utils/url-beautifier/interfaces.ts | 2 +- .../url-beautifier/detail-url-beautifer.ts | 36 +++++++++++++++---- .../utils/url-beautifier/url-beautifier.ts | 3 +- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts index dd2de8aa..794cc9f7 100644 --- a/src/utils/url-beautifier/detail-url-beautifier.ts +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -11,7 +11,7 @@ export class DetailUrlGenerator { build(detail: Detail): string { let paths = [detail.productTitle]; - if (detail.refinements) { + if (detail.refinements.length !== 0) { if (this.config.useReferenceKeys) { let referenceKeys = ''; const refinementsToKeys = this.config.refinementMapping.reduce((map, mapping) => { @@ -65,10 +65,6 @@ export class DetailUrlParser { const name = paths.shift(); const id = paths.pop(); - const result = { - productTitle: name, - productID: id - }; let refinements = []; @@ -107,9 +103,12 @@ export class DetailUrlParser { }); } } - result['refinements'] = refinements; } - return result; + return { + productTitle: name, + productID: id, + refinements: refinements + }; } } diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index 08836e49..2275caf9 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -28,5 +28,5 @@ export interface BeautifierConfig { export interface Detail { productTitle: string; productID: string; - refinements?: SelectedValueRefinement[]; + refinements: SelectedValueRefinement[]; } diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index 839ecfb6..651d010a 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -17,17 +17,29 @@ describe('detail URL beautifier', () => { beforeEach(() => generator = new DetailUrlGenerator(beautifier)); it('should convert a simple detail to a URL', () => { - expect(generator.build({ productTitle: 'red and delicious apples', productID: '1923' })) + expect(generator.build({ + productTitle: 'red and delicious apples', + productID: '1923', + refinements: [] + })) .to.eq('/red-and-delicious-apples/1923'); }); it('should encode special characters + in detail', () => { - expect(generator.build({ productTitle: 'red+and+delicious+apples', productID: '1923' })) + expect(generator.build({ + productTitle: 'red+and+delicious+apples', + productID: '1923', + refinements: [] + })) .to.eq('/red%2Band%2Bdelicious%2Bapples/1923'); }); it('should encode special characters / in detail', () => { - expect(generator.build({ productTitle: 'red/and/delicious/apples', productID: '1923' })) + expect(generator.build({ + productTitle: 'red/and/delicious/apples', + productID: '1923', + refinements: [] + })) .to.eq('/red%2Fand%2Fdelicious%2Fapples/1923'); }); @@ -94,19 +106,31 @@ describe('detail URL beautifier', () => { }); it('should parse a simple URL and return a detail object', () => { - const expectedDetail = { productTitle: 'apples', productID: '1923' }; + const expectedDetail = { + productTitle: 'apples', + productID: '1923', + refinements: [] + }; expect(parser.parse(parseUri('/apples/1923'))).to.eql(expectedDetail); }); it('should parse a simple URL, replace \'-\' with \' \' and return a detail object', () => { - const expectedDetail = { productTitle: 'red and delicious apples', productID: '1923' }; + const expectedDetail = { + productTitle: 'red and delicious apples', + productID: '1923', + refinements: [] + }; expect(parser.parse(parseUri('/red-and-delicious-apples/1923'))).to.eql(expectedDetail); }); it('should parse a simple URL, decode special characters and return a detail object', () => { - const expectedDetail = { productTitle: 'red+and+delicious+apples', productID: '1923' }; + const expectedDetail = { + productTitle: 'red+and+delicious+apples', + productID: '1923', + refinements: [] + }; expect(parser.parse(parseUri('/red%2Band%2Bdelicious%2Bapples/1923'))).to.eql(expectedDetail); }); diff --git a/test/unit/utils/url-beautifier/url-beautifier.ts b/test/unit/utils/url-beautifier/url-beautifier.ts index 9bd0e7e6..a7fb8bf6 100644 --- a/test/unit/utils/url-beautifier/url-beautifier.ts +++ b/test/unit/utils/url-beautifier/url-beautifier.ts @@ -46,7 +46,8 @@ describe('URL beautifier', () => { it('should call detail url generator', () => { const detail = { productTitle: 'Apples', - productID: '12345' + productID: '12345', + refinements: [] }; const build = stub(DetailUrlGenerator.prototype, 'build'); From 664a448c36923c603c4496601bae603b9d5e5cfb Mon Sep 17 00:00:00 2001 From: Ben Teichman Date: Thu, 8 Jun 2017 15:57:57 -0400 Subject: [PATCH 34/34] Apply review changes (#200) --- .../url-beautifier/detail-url-beautifier.ts | 48 +++++++-------- src/utils/url-beautifier/index.ts | 12 ++-- src/utils/url-beautifier/interfaces.ts | 16 ++--- .../url-beautifier/query-url-beautifier.ts | 61 +++++++++---------- src/utils/url-beautifier/url-beautifier.ts | 59 ++++++++---------- .../url-beautifier/detail-url-beautifer.ts | 16 ++--- 6 files changed, 104 insertions(+), 108 deletions(-) diff --git a/src/utils/url-beautifier/detail-url-beautifier.ts b/src/utils/url-beautifier/detail-url-beautifier.ts index 794cc9f7..6bc38c2d 100644 --- a/src/utils/url-beautifier/detail-url-beautifier.ts +++ b/src/utils/url-beautifier/detail-url-beautifier.ts @@ -20,27 +20,27 @@ export class DetailUrlGenerator { return map; }, {}); - detail.refinements.sort(this.refinementsComparator).forEach((refinement) => { - if (!(refinement.navigationName in refinementsToKeys)) - throw new Error(`no mapping found for navigation "${refinement.navigationName}"`); - paths.push(refinement.value); - referenceKeys += (refinementsToKeys[refinement.navigationName]); - }); + detail.refinements.sort(DetailUrlGenerator.refinementsComparator) + .forEach((refinement) => { + if (!(refinement.navigationName in refinementsToKeys)) { + throw new Error(`no mapping found for navigation '${refinement.navigationName}'`); + } + + paths.push(refinement.value); + referenceKeys += refinementsToKeys[refinement.navigationName]; + }); paths.push(referenceKeys); } else { - detail.refinements.forEach((ref) => { - paths.push(ref.value); - paths.push(ref.navigationName); - }); + detail.refinements.forEach(({ value, navigationName }) => paths.push(value, navigationName)); } } - paths.push(detail.productID); + paths.push(detail.productId); return `/${paths.map((path) => encodeURIComponent(path.replace(/\s/g, '-'))).join('/')}`; } - private refinementsComparator(refinement1: SelectedValueRefinement, refinement2: SelectedValueRefinement): number { + static refinementsComparator(refinement1: SelectedValueRefinement, refinement2: SelectedValueRefinement): number { let comparison = refinement1.navigationName.localeCompare(refinement2.navigationName); if (comparison === 0) { comparison = refinement1.value.localeCompare(refinement2.value); @@ -57,7 +57,7 @@ export class DetailUrlParser { } parse(url: { path: string, query: string}): Detail { - let paths = url.path.split('/').filter((val) => val).map((val) => decodeURIComponent(val).replace(/-/g, ' ')); + const paths = url.path.split('/').filter((val) => val).map((val) => decodeURIComponent(val).replace(/-/g, ' ')); if (paths.length < 2) { throw new Error('path has less than two parts'); @@ -66,15 +66,15 @@ export class DetailUrlParser { const name = paths.shift(); const id = paths.pop(); - let refinements = []; + const refinements = []; - if (paths.length) { + if (paths.length !== 0) { if (!this.config.useReferenceKeys) { if (paths.length % 2 !== 0) { throw new Error('path has an odd number of parts'); } - while (paths.length) { + while (paths.length !== 0) { const value = paths.shift(); const navigationName = paths.shift(); refinements.push({ navigationName, value, type: 'Value' }); @@ -95,20 +95,18 @@ export class DetailUrlParser { throw new Error('token reference is invalid'); } - while (paths.length) { - refinements.push({ - navigationName: keysToRefinements[referenceKeys.shift()], - value: paths.shift(), - type: 'Value' - }); - } + paths.forEach((value) => refinements.push({ + value, + navigationName: keysToRefinements[referenceKeys.shift()], + type: 'Value' + })); } } return { productTitle: name, - productID: id, - refinements: refinements + productId: id, + refinements }; } } diff --git a/src/utils/url-beautifier/index.ts b/src/utils/url-beautifier/index.ts index 16ecf2de..73ee0eef 100644 --- a/src/utils/url-beautifier/index.ts +++ b/src/utils/url-beautifier/index.ts @@ -5,9 +5,13 @@ import { QueryUrlGenerator, QueryUrlParser } from './query-url-beautifier'; import { UrlBeautifier } from './url-beautifier'; export { - BeautifierConfig, Detail, - DetailUrlGenerator, DetailUrlParser, - NavigationUrlGenerator, NavigationUrlParser, - QueryUrlGenerator, QueryUrlParser, + BeautifierConfig, + Detail, + DetailUrlGenerator, + DetailUrlParser, + NavigationUrlGenerator, + NavigationUrlParser, + QueryUrlGenerator, + QueryUrlParser, UrlBeautifier }; diff --git a/src/utils/url-beautifier/interfaces.ts b/src/utils/url-beautifier/interfaces.ts index 2275caf9..db067362 100644 --- a/src/utils/url-beautifier/interfaces.ts +++ b/src/utils/url-beautifier/interfaces.ts @@ -10,15 +10,17 @@ export interface Beautifier { export interface BeautifierConfig { refinementMapping?: any[]; - extraRefinementsParam?: string; - pageSizeParam?: string; - pageParam?: string; - defaultPageSize?: number; + params?: { + page?: string; + pageSize?: string; + refinements?: string; + sort?: string; + }; queryToken?: string; suffix?: string; useReferenceKeys?: boolean; - navigations?: Object; - prefix: { + navigations?: any; + routes: { query: string, detail: string, navigation: string @@ -27,6 +29,6 @@ export interface BeautifierConfig { export interface Detail { productTitle: string; - productID: string; + productId: string; refinements: SelectedValueRefinement[]; } diff --git a/src/utils/url-beautifier/query-url-beautifier.ts b/src/utils/url-beautifier/query-url-beautifier.ts index b2baeec3..73ecdc98 100644 --- a/src/utils/url-beautifier/query-url-beautifier.ts +++ b/src/utils/url-beautifier/query-url-beautifier.ts @@ -17,8 +17,7 @@ export class QueryUrlGenerator { path: [], query: {} }; - // let url = ''; - const origRefinements = Array.of(...request.refinements); + const origRefinements = [...request.refinements]; // add query if (request.query) { @@ -30,18 +29,16 @@ export class QueryUrlGenerator { const { map, keys } = this.generateRefinementMap(origRefinements); // add refinements - for (let key of keys) { + keys.forEach((key) => { const refinements = map[key]; countMap[key] = refinements.length; refinements.map(this.convertToSelectedValueRefinement) .sort(this.refinementsComparator) - .forEach((selectedValueRefinement) => { - uri.path.push(selectedValueRefinement.value); - }); - } + .forEach((selectedValueRefinement) => uri.path.push(selectedValueRefinement.value)); + }); // add reference key - if (keys.length || request.query) { + if (keys.length !== 0 || request.query) { let referenceKey = ''; if (request.query) referenceKey += this.config.queryToken; keys.forEach((key) => referenceKey += key.repeat(countMap[key])); @@ -49,37 +46,36 @@ export class QueryUrlGenerator { } } else { // add refinements - let valueRefinements = []; + const valueRefinements = []; for (let i = origRefinements.length - 1; i >= 0; --i) { if (origRefinements[i].type === 'Value') { valueRefinements.push(...origRefinements.splice(i, 1)); } } - valueRefinements.map(this.convertToSelectedValueRefinement) - .sort(this.refinementsComparator) + valueRefinements.map(QueryUrlGenerator.convertToSelectedValueRefinement) + .sort(QueryUrlGenerator.refinementsComparator) .forEach((selectedValueRefinement) => { uri.path.push(selectedValueRefinement.value, selectedValueRefinement.navigationName); }); } // add remaining refinements - if (origRefinements.length) { - uri.query[this.config.extraRefinementsParam] = origRefinements + if (origRefinements.length !== 0) { + uri.query[this.config.params.refinements] = origRefinements .sort((lhs, rhs) => lhs.navigationName.localeCompare(rhs.navigationName)) - .map(this.stringifyRefinement) + .map(QueryUrlGenerator.stringifyRefinement) .join('~'); } // add page size if (query.raw.pageSize) { - uri.query[this.config.pageSizeParam] = query.raw.pageSize; + uri.query[this.config.params.pageSize] = query.raw.pageSize; } // add page if (query.raw.skip) { - uri.query[this.config.pageParam] = - Math.floor(query.raw.skip / (query.raw.pageSize || this.config.defaultPageSize)) + 1; + uri.query[this.config.params.page] = Math.floor(query.raw.skip / query.raw.pageSize) + 1; } let url = `/${uri.path.map((path) => encodeURIComponent(path)).join('/')}`; @@ -102,7 +98,7 @@ export class QueryUrlGenerator { for (let mapping of this.config.refinementMapping) { const key = Object.keys(mapping)[0]; const matchingRefinements = refinements.filter((refinement) => refinement.navigationName === mapping[key]); - if (matchingRefinements.length) { + if (matchingRefinements.length !== 0) { refinementKeys.push(key); refinementMap[key] = matchingRefinements; matchingRefinements.forEach((ref) => refinements.splice(refinements.indexOf(ref), 1)); @@ -111,7 +107,7 @@ export class QueryUrlGenerator { return { map: refinementMap, keys: refinementKeys }; } - private convertToSelectedValueRefinement(refinement: SelectedRefinement): SelectedValueRefinement { + static convertToSelectedValueRefinement(refinement: SelectedRefinement): SelectedValueRefinement { if (refinement.type === 'Value') { return refinement; } else { @@ -119,7 +115,7 @@ export class QueryUrlGenerator { } } - private stringifyRefinement(refinement: SelectedRefinement): string { + static stringifyRefinement(refinement: SelectedRefinement): string { const name = refinement.navigationName; if (refinement.type === 'Value') { return `${name}:${(refinement).value}`; @@ -128,7 +124,7 @@ export class QueryUrlGenerator { } } - private refinementsComparator(refinement1: SelectedValueRefinement, refinement2: SelectedValueRefinement): number { + static refinementsComparator(refinement1: SelectedValueRefinement, refinement2: SelectedValueRefinement): number { let comparison = refinement1.navigationName.localeCompare(refinement2.navigationName); if (comparison === 0) { comparison = refinement1.value.localeCompare(refinement2.value); @@ -152,15 +148,17 @@ export class QueryUrlParser { parse(url: { path: string, query: string}): Query { const paths = url.path.split('/').filter((val) => val); - if (paths[paths.length - 1] === this.config.suffix) paths.pop(); + if (paths[paths.length - 1] === this.config.suffix) { + paths.pop(); + } const query = this.config.useReferenceKeys ? this.parsePathWithReferenceKeys(paths) : this.parsePathWithoutReferenceKeys(paths); const queryVariables = queryString.parse(url.query); - const unmappedRefinements = queryVariables[this.config.extraRefinementsParam]; - const pageSize = parseInt(queryVariables[this.config.pageSizeParam], 10); - const page = parseInt(queryVariables[this.config.pageParam], 10); + const unmappedRefinements = queryVariables[this.config.params.refinements]; + const pageSize = parseInt(queryVariables[this.config.params.pageSize], 10); + const page = parseInt(queryVariables[this.config.params.page], 10); if (unmappedRefinements) { query.withSelectedRefinements(...this.extractUnmapped(unmappedRefinements)); } @@ -168,7 +166,7 @@ export class QueryUrlParser { query.withPageSize(pageSize); } if (page) { - query.skip((query.raw.pageSize || this.config.defaultPageSize) * (page - 1)); + query.skip(query.raw.pageSize * (page - 1)); } return query; @@ -177,14 +175,15 @@ export class QueryUrlParser { private parsePathWithReferenceKeys(path: string[]): Query { const query = new Query().withConfiguration(this.searchandiserConfig, CONFIGURATION_MASK); const keys = (path.pop() || '').split(''); + if (path.length < keys.length) { + throw new Error('token reference is invalid'); + } const map = this.generateRefinementMapping(); - for (let key of keys) { + keys.forEach((key) => { if (!(key in map || key === this.config.queryToken)) { throw new Error(`unexpected token '${key}' found in reference`); } - } - - if (path.length < keys.length) throw new Error('token reference is invalid'); + }); // remove prefixed paths path.splice(0, path.length - keys.length); @@ -242,4 +241,4 @@ export class QueryUrlParser { private decode(value: string): string { return decodeURIComponent(value.replace(/-/g, ' ')); } -} \ No newline at end of file +} diff --git a/src/utils/url-beautifier/url-beautifier.ts b/src/utils/url-beautifier/url-beautifier.ts index 0776f433..ff35f70e 100644 --- a/src/utils/url-beautifier/url-beautifier.ts +++ b/src/utils/url-beautifier/url-beautifier.ts @@ -10,15 +10,16 @@ export class UrlBeautifier implements Beautifier { config: BeautifierConfig = { refinementMapping: [], - extraRefinementsParam: 'refinements', - pageSizeParam: 'page_size', - pageParam: 'page', - defaultPageSize: 10, + params: { + refinements: 'refinements', + page: 'page', + pageSize: 'page_size' + }, queryToken: 'q', suffix: '', useReferenceKeys: true, navigations: {}, - prefix: { + routes: { query: '/query', detail: '/detail', navigation: '/navigation' @@ -41,42 +42,38 @@ export class UrlBeautifier implements Beautifier { const keys = []; for (let mapping of this.config.refinementMapping) { const key = Object.keys(mapping)[0]; - if (key.length !== 1) { - throw new Error('refinement mapping token must be a single character'); - } - if (key.match(/[aeiouy]/)) { - throw new Error('refinement mapping token must not be a vowel'); - } - if (keys.indexOf(key) > -1) { - throw new Error('refinement mapping tokens must be unique'); - } + this.validateToken(key, keys); keys.push(key); } - if (this.config.queryToken.length !== 1) { - throw new Error('query token must be a single character'); - } - if (this.config.queryToken.match(/[aeiouy]/)) { - throw new Error('query token must not be a vowel'); - } - if (keys.indexOf(this.config.queryToken) > -1) { - throw new Error('query token must be unique from refinement tokens'); - } + this.validateToken(this.config.queryToken, keys); } parse(rawUrl: string): any { const uri = parseUri(rawUrl); const path = uri.path; - if (path.indexOf(this.config.prefix.query) === 0) { - return this.queryParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.prefix.query)); - } else if (path.indexOf(this.config.prefix.detail) === 0) { - return this.detailParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.prefix.detail)); - } else if (path.indexOf(this.config.prefix.navigation) === 0) { - return this.navigationParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.prefix.navigation)); + if (path.indexOf(this.config.routes.query) === 0) { + return this.queryParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.routes.query)); + } else if (path.indexOf(this.config.routes.detail) === 0) { + return this.detailParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.routes.detail)); + } else if (path.indexOf(this.config.routes.navigation) === 0) { + return this.navigationParser.parse(this.extractUnprefixedPathAndQuery(uri, this.config.routes.navigation)); } else { throw new Error('invalid prefix'); } } + validateToken(token: string, keys: string[]) { + if (token.length !== 1) { + throw new Error('token must be a single character'); + } + if (token.match(/[aeiouy]/)) { + throw new Error('token must not be a vowel'); + } + if (keys.indexOf(token) > -1) { + throw new Error('tokens must be unique'); + } + } + buildQueryUrl(query: Query) { return this.queryGenerator.build(query); } @@ -89,10 +86,6 @@ export class UrlBeautifier implements Beautifier { return this.detailGenerator.build(detail); } - build(query: Query) { - return this.queryGenerator.build(query); - } - private extractUnprefixedPathAndQuery(uri: parseUri.UriStructure, prefix: string): { path: string, query: string } { return { path: uri.path.substr(prefix.length), query: uri.query }; } diff --git a/test/unit/utils/url-beautifier/detail-url-beautifer.ts b/test/unit/utils/url-beautifier/detail-url-beautifer.ts index 651d010a..1c747558 100644 --- a/test/unit/utils/url-beautifier/detail-url-beautifer.ts +++ b/test/unit/utils/url-beautifier/detail-url-beautifer.ts @@ -19,7 +19,7 @@ describe('detail URL beautifier', () => { it('should convert a simple detail to a URL', () => { expect(generator.build({ productTitle: 'red and delicious apples', - productID: '1923', + productId: '1923', refinements: [] })) .to.eq('/red-and-delicious-apples/1923'); @@ -28,7 +28,7 @@ describe('detail URL beautifier', () => { it('should encode special characters + in detail', () => { expect(generator.build({ productTitle: 'red+and+delicious+apples', - productID: '1923', + productId: '1923', refinements: [] })) .to.eq('/red%2Band%2Bdelicious%2Bapples/1923'); @@ -37,7 +37,7 @@ describe('detail URL beautifier', () => { it('should encode special characters / in detail', () => { expect(generator.build({ productTitle: 'red/and/delicious/apples', - productID: '1923', + productId: '1923', refinements: [] })) .to.eq('/red%2Fand%2Fdelicious%2Fapples/1923'); @@ -47,7 +47,7 @@ describe('detail URL beautifier', () => { beautifier.config.useReferenceKeys = false; const url = generator.build({ productTitle: 'satin shiny party dress', - productID: '293014', + productId: '293014', refinements: [ refinement('colour', 'red') ] }); @@ -58,7 +58,7 @@ describe('detail URL beautifier', () => { beautifier.config.useReferenceKeys = false; const url = generator.build({ productTitle: 'satin shiny party dress', - productID: '293014', + productId: '293014', refinements: [refinement('colour', 'red+green/blue')] }); @@ -69,7 +69,7 @@ describe('detail URL beautifier', () => { beautifier.config.refinementMapping.push({ c: 'colour' }); const url = generator.build({ productTitle: 'dress', - productID: '293014', + productId: '293014', refinements: [refinement('colour', 'red')] }); @@ -80,7 +80,7 @@ describe('detail URL beautifier', () => { beautifier.config.refinementMapping.push({ c: 'colour' }, { b: 'brand' }); const url = generator.build({ productTitle: 'dress', - productID: '293014', + productId: '293014', refinements: [refinement('colour', 'red'), refinement('brand', 'h&m') ] }); @@ -91,7 +91,7 @@ describe('detail URL beautifier', () => { it('should throw an error if no reference key found for refinement navigation name', () => { const build = () => generator.build({ productTitle: 'dress', - productID: '293014', + productId: '293014', refinements: [refinement('colour', 'red')] });