From 71c2b1d83d749ba6a5ee8e63af4f683154563c26 Mon Sep 17 00:00:00 2001 From: Christian Ortner Date: Tue, 30 Jun 2020 12:29:15 +0200 Subject: [PATCH 1/6] Extend schema to support variant information output --- example/responseWithVariants.json | 169 +++++++++++++++++++++ resources/schema.json | 236 ++++++++++++++++-------------- test/validate.js | 12 +- 3 files changed, 305 insertions(+), 112 deletions(-) create mode 100644 example/responseWithVariants.json diff --git a/example/responseWithVariants.json b/example/responseWithVariants.json new file mode 100644 index 0000000..67d9ace --- /dev/null +++ b/example/responseWithVariants.json @@ -0,0 +1,169 @@ +{ + "request": { + "query": "Blubergurken", + "first": 0, + "count": 24, + "serviceId": "F53ABAB42D7931BE13532AFCA1A95CCE", + "usergroup": "foo", + "userId": "cd7984ec-e0c5-4bfb-a925-d607668153cd", + "order": { + "field": "salesfrequency", + "relevanceBased": true, + "direction": "DESC" + } + }, + "result": { + "metadata": { + "effectiveQuery": "Blubbergurken", + "totalResults": 1337, + "requestId": "9cd42225-90d0-4858-bcb6-b05f33d8ec5e", + "searchConcept": "Seeds", + "currencySymbol": "€", + "landingpage": { + "name": "New arrivals", + "url": "https://example.org/new_stuff.html" + }, + "promotion": { + "name": "Blubbergurken Brand", + "url": "https://example.org/top_brands/Blubbergurken_International_Inc.html", + "imageUrl": "https://example.org/top_brands/blubbergurken_international.png" + } + }, + "variant": { + "name": "sdym", + "correctedQuery": "Blubbergurken" + }, + "items": [ + { + "id": "123ab", + "url": "https://example.org/product.html", + "imageUrl": "https://example.org/product.png", + "name": "Blubbergurken Seeds", + "highlightedName": "Blubbergurken Seeds", + "price": 13.37, + "ordernumbers": ["0012BLUB-42"], + "matchingOrdernumber": "34567", + "score": 4.667, + "summary": "These are some very nice seeds.", + "properties": { + "overriddenPrice": "15.00", + "taxRate": "20" + }, + "productPlacement": "Seeds spring 2020", + "pushRules": [ + "Seeds", + "Cucumbers" + ], + "attributes": { + "cat": [ + "Gardening" + ], + "vendor": [ + "Blubbergurken International Inc." + ] + }, + "children": [ + { + "id": "123ab-A", + "url": "https://example.org/product-a.html", + "imageUrl": "https://example.org/product-a.png", + "name": "Blubbergurken Seeds - Class A", + "price": 15.00, + "ordernumbers": ["0012BLUB-42-A"], + "matchingOrdernumber": "", + "score": 4.667, + "summary": "These are some very nice seeds.", + "properties": { + "overriddenPrice": "15.00", + "taxRate": "20" + }, + "attributes": { + "cat": [ + "Gardening" + ], + "vendor": [ + "Blubbergurken International Inc." + ] + } + } + ] + } + ], + "filters": { + "main": [ + { + "name": "cat", + "displayName": "Category", + "type": "select", + "selectMode": "single", + "values": [ + { + "displayName": "Spring", + "value": "Gardening_Spring", + "weight": 1.2, + "frequency": 13 + } + ], + "pinnedFilterValueCount": 6 + }, + { + "type": "range-slider", + "totalRange": { + "min": 2.37, + "max": 10106.09 + }, + "selectedRange": { + "min": 2.37, + "max": 10106.09 + }, + "stepSize": 0.1, + "unit": "€", + "name": "price", + "displayName": "Preis", + "selectMode": "single", + "values": [ + { + "value": { + "min": 2.37, + "max": 30.75 + }, + "weight": 0.3948, + "frequency": null + } + ] + }, + { + "name": "vendor", + "displayName": "Brand", + "type": "select", + "selectMode": "multiple", + "noAvailableFiltersText": "Sorry, no more filters for you!", + "values": [ + { + "value": "Blubbergurken International Inc.", + "weight": 0.8, + "frequency": 5, + "selected": true, + "frequencyType" : "additive" + } + ] + } + ], + "other": [ + { + "name": "color", + "displayName": "Color", + "type": "color", + "selectMode": "multiple", + "cssClass": "my-colors", + "values": [ + { + "value": "Green", + "color": "#00FF00" + } + ] + } + ] + } + } +} diff --git a/resources/schema.json b/resources/schema.json index 8ad0871..ef7f5ff 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -176,6 +176,133 @@ "displayName", "type" ] + }, + "baseItem": { + "description": "Properties shared by parent- and child items.", + "type": "object", + "properties": { + "id": { + "description": "Item ID, as exported.", + "type": "string", + "minLength": 1 + }, + "url": { + "description": "Detail page URL.", + "type": "string", + "pattern": "^https?://.*$" + }, + "imageUrl": { + "description": "Primary image URL. Additionally exported images can be accessed via the item's properties.", + "type": "string", + "pattern": "^https?://.*$" + }, + "name": { + "description": "Name of the item without any query-based highlighting.", + "type": "string", + "minLength": 0 + }, + "price": { + "description": "The item's price.", + "type": "number" + }, + "ordernumbers": { + "description": "The item's ordernumbers. Currently, only the first exported one is available.", + "type": "array", + "items": { + "type": "string" + } + }, + "matchingOrdernumber": { + "type": "string", + "minLength": 0 + }, + "score": { + "description": "Search score.", + "type": "number", + "min": 0 + }, + "summary": { + "description": "Exported short summary.", + "type": "string", + "minLength": 0 + }, + "properties": { + "description": "Non-searchable value exported as properties. Includes additional images, if applicable. The desired values have to be requested with the 'properties[]' parameter.", + "type": "object", + "properties": { + ".*": { + "type": "string" + } + } + }, + "attributes": { + "type": "object", + "description": "Attribute values that apply to an item, if the attribute was requested via outputAttrib.", + "patternProperties": { + "^.*$": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minLength": 0 + } + } + } + }, + "additionalProperties": true, + "required": [ + "id", + "score", + "url", + "name", + "ordernumbers", + "matchingOrdernumber", + "summary", + "price", + "properties", + "attributes", + "imageUrl" + ] + }, + "parentItem": { + "description": "Top level product. In case the service supports variants, this may contain children. Without variant support, this is just a regular item.", + "allOf": [ + {"$ref": "#/definitions/baseItem"}, + { + "properties": { + "highlightedName": { + "description": "Name of the item with portions of it highlighted if matching the query. The matching part is wrapped in a tag.", + "type": "string", + "minLength": 0 + }, + "productPlacement": { + "type": ["string", "null"], + "minLength": 1, + "description": "In case a Product Placement matches the product, this is its name." + }, + "pushRules": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "description": "In one or more Push Rules match a product, these are the names." + }, + "children": { + "type": "array", + "items": {"$ref": "#/definitions/childItem"} + } + }, + "required": ["highlightedName", "productPlacement", "pushRules"] + } + ] + }, + "childItem": { + "description": "Child item of a parent in case variants are supported by the service.", + "allOf": [ + {"$ref": "#/definitions/baseItem"} + ] } }, "type": "object", @@ -380,114 +507,7 @@ "description": "The matching items, constraint by the pagination parameters. The values being shown respect the specified usergroup, if applicable.", "type": "array", "minLength": 0, - "items": { - "type": "object", - "properties": { - "id": { - "description": "Item ID, as exported.", - "type": "string", - "minLength": 1 - }, - "url": { - "description": "Detail page URL.", - "type": "string", - "pattern": "^https?://.*$" - }, - "imageUrl": { - "description": "Primary image URL. Additionally exported images can be accessed via the item's properties.", - "type": "string", - "pattern": "^https?://.*$" - }, - "name": { - "description": "Name of the item without any query-based highlighting.", - "type": "string", - "minLength": 0 - }, - "highlightedName": { - "description": "Name of the item with portions of it highlighted if matching the query. The matching part is wrapped in a tag.", - "type": "string", - "minLength": 0 - }, - "price": { - "description": "The item's price.", - "type": "number" - }, - "ordernumbers": { - "description": "The item's ordernumbers. Currently, only the first exported one is available.", - "type": "array", - "items": { - "type": "string" - } - }, - "matchingOrdernumber": { - "type": "string", - "minLength": 0 - }, - "score": { - "description": "Search score.", - "type": "number", - "min": 0 - }, - "summary": { - "description": "Exported short summary.", - "type": "string", - "minLength": 0 - }, - "properties": { - "description": "Non-searchable value exported as properties. Includes additional images, if applicable. The desired values have to be requested with the 'properties[]' parameter.", - "type": "object", - "properties": { - ".*": { - "type": "string" - } - } - }, - "productPlacement": { - "type": ["string", "null"], - "minLength": 1, - "description": "In case a Product Placement matches the product, this is its name." - }, - "pushRules": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "description": "In one or more Push Rules match a product, these are the names." - }, - "attributes": { - "type": "object", - "description": "Attribute values that apply to an item, if the attribute was requested via outputAttrib.", - "patternProperties": { - "^.*$": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "minLength": 0 - } - } - } - }, - "additionalProperties": false, - "required": [ - "id", - "score", - "url", - "name", - "highlightedName", - "ordernumbers", - "matchingOrdernumber", - "summary", - "price", - "properties", - "productPlacement", - "pushRules", - "attributes", - "imageUrl" - ] - } + "items": {"$ref": "#/definitions/parentItem"} }, "filters": { "description": "Filters available based on the query, and as configured in the filter configuration. Does not include inactive filters.", diff --git a/test/validate.js b/test/validate.js index 3e29c72..6c7e76b 100644 --- a/test/validate.js +++ b/test/validate.js @@ -12,16 +12,16 @@ async function parseJsonWithErrorHandling(path) { } } -async function main() { +async function validateFile(fileName) { const validator = new Validator(); - const instance = await parseJsonWithErrorHandling(__dirname + '/../example/response.json'); + const instance = await parseJsonWithErrorHandling(__dirname + '/../example/' + fileName); const schema = await parseJsonWithErrorHandling(__dirname + '/../resources/schema.json'); const result = validator.validate(instance, schema); if (!result.valid) { - console.error('Schema or example are not valid. Errors:'); + console.error(fileName + ': Schema or example are not valid. Errors:'); result.errors.forEach((error) => { console.error(error.property + ' ' + error.message); @@ -29,8 +29,12 @@ async function main() { process.exit(1); } else { - console.log('Schema and example are valid.'); + console.log(fileName + ': Schema and example are valid.'); } } +async function main() { + ['response.json', 'responseWithVariants.json'].forEach(fileName => validateFile(fileName)); +} + main(); From bfe9a64b64b07aacc2f71430fd938f73fa6a8602 Mon Sep 17 00:00:00 2001 From: Christian Ortner Date: Wed, 1 Jul 2020 16:43:57 +0200 Subject: [PATCH 2/6] Adapt schema for more nullability in child items --- example/responseWithVariants.json | 13 +++++ resources/schema.json | 92 ++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/example/responseWithVariants.json b/example/responseWithVariants.json index 67d9ace..ede52ca 100644 --- a/example/responseWithVariants.json +++ b/example/responseWithVariants.json @@ -85,6 +85,19 @@ "Blubbergurken International Inc." ] } + }, + { + "id": "123ab-A", + "url": null, + "imageUrl": null, + "name": null, + "price": null, + "ordernumbers": ["0012BLUB-42-A"], + "matchingOrdernumber": "", + "score": 0, + "summary": null, + "properties": {}, + "attributes": {} } ] } diff --git a/resources/schema.json b/resources/schema.json index ef7f5ff..8451d30 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -177,6 +177,14 @@ "type" ] }, + "requiredUrl": { + "type": "string", + "pattern": "^https?://.*$" + }, + "nullableUrl": { + "type": ["string", "null"], + "pattern": "^https?://.*$" + }, "baseItem": { "description": "Properties shared by parent- and child items.", "type": "object", @@ -186,25 +194,6 @@ "type": "string", "minLength": 1 }, - "url": { - "description": "Detail page URL.", - "type": "string", - "pattern": "^https?://.*$" - }, - "imageUrl": { - "description": "Primary image URL. Additionally exported images can be accessed via the item's properties.", - "type": "string", - "pattern": "^https?://.*$" - }, - "name": { - "description": "Name of the item without any query-based highlighting.", - "type": "string", - "minLength": 0 - }, - "price": { - "description": "The item's price.", - "type": "number" - }, "ordernumbers": { "description": "The item's ordernumbers. Currently, only the first exported one is available.", "type": "array", @@ -221,11 +210,6 @@ "type": "number", "min": 0 }, - "summary": { - "description": "Exported short summary.", - "type": "string", - "minLength": 0 - }, "properties": { "description": "Non-searchable value exported as properties. Includes additional images, if applicable. The desired values have to be requested with the 'properties[]' parameter.", "type": "object", @@ -254,15 +238,10 @@ "required": [ "id", "score", - "url", - "name", "ordernumbers", "matchingOrdernumber", - "summary", - "price", "properties", - "attributes", - "imageUrl" + "attributes" ] }, "parentItem": { @@ -271,6 +250,28 @@ {"$ref": "#/definitions/baseItem"}, { "properties": { + "name": { + "description": "Name of the item without any query-based highlighting.", + "type": "string", + "minLength": 0 + }, + "price": { + "description": "The item's price.", + "type": "number" + }, + "summary": { + "description": "Exported short summary.", + "type": "string", + "minLength": 0 + }, + "url": { + "description": "Detail page URL.", + "$ref": "#/definitions/requiredUrl" + }, + "imageUrl": { + "description": "Primary image URL. Additionally exported images can be accessed via the item's properties.", + "$ref": "#/definitions/requiredUrl" + }, "highlightedName": { "description": "Name of the item with portions of it highlighted if matching the query. The matching part is wrapped in a tag.", "type": "string", @@ -294,14 +295,41 @@ "items": {"$ref": "#/definitions/childItem"} } }, - "required": ["highlightedName", "productPlacement", "pushRules"] + "required": ["name", "price", "summary", "url", "imageUrl", "highlightedName", "productPlacement", "pushRules"] } ] }, "childItem": { "description": "Child item of a parent in case variants are supported by the service.", "allOf": [ - {"$ref": "#/definitions/baseItem"} + {"$ref": "#/definitions/baseItem"}, + { + "properties": { + "name": { + "description": "Name of the item without any query-based highlighting.", + "type": ["string", "null"], + "minLength": 0 + }, + "price": { + "description": "The item's price.", + "type": ["number", "null"] + }, + "summary": { + "description": "Exported short summary.", + "type": ["string", "null"], + "minLength": 0 + }, + "url": { + "description": "Detail page URL.", + "$ref": "#/definitions/nullableUrl" + }, + "imageUrl": { + "description": "Primary image URL. Additionally exported images can be accessed via the item's properties.", + "$ref": "#/definitions/nullableUrl" + } + }, + "required": ["name", "price", "summary", "url", "imageUrl"] + } ] } }, From 48fac364209ccccbb50d7ee5c95c1bf9aa92c1ae Mon Sep 17 00:00:00 2001 From: Christian Ortner Date: Thu, 2 Jul 2020 10:45:50 +0200 Subject: [PATCH 3/6] Rename "children" to "variants" --- example/responseWithVariants.json | 2 +- resources/schema.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/responseWithVariants.json b/example/responseWithVariants.json index ede52ca..6a2b95d 100644 --- a/example/responseWithVariants.json +++ b/example/responseWithVariants.json @@ -62,7 +62,7 @@ "Blubbergurken International Inc." ] }, - "children": [ + "variants": [ { "id": "123ab-A", "url": "https://example.org/product-a.html", diff --git a/resources/schema.json b/resources/schema.json index 8451d30..29b2402 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -245,7 +245,7 @@ ] }, "parentItem": { - "description": "Top level product. In case the service supports variants, this may contain children. Without variant support, this is just a regular item.", + "description": "Top level item. In case the service supports variants, this may contain variants. Without variant support, this is just a regular item.", "allOf": [ {"$ref": "#/definitions/baseItem"}, { @@ -290,17 +290,17 @@ }, "description": "In one or more Push Rules match a product, these are the names." }, - "children": { + "variants": { "type": "array", - "items": {"$ref": "#/definitions/childItem"} + "items": {"$ref": "#/definitions/variantItem"} } }, "required": ["name", "price", "summary", "url", "imageUrl", "highlightedName", "productPlacement", "pushRules"] } ] }, - "childItem": { - "description": "Child item of a parent in case variants are supported by the service.", + "variantItem": { + "description": "Variant item of a parent in case variants are supported by the service.", "allOf": [ {"$ref": "#/definitions/baseItem"}, { From e7c342588d9c8697d11dcbe66d7c03fe6132ff18 Mon Sep 17 00:00:00 2001 From: Wladislaw Koschemako Date: Mon, 13 Jul 2020 10:43:06 +0200 Subject: [PATCH 4/6] test should validate everything from the example folder --- test/validate.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/validate.js b/test/validate.js index 6c7e76b..fa5d49c 100644 --- a/test/validate.js +++ b/test/validate.js @@ -1,8 +1,10 @@ const Validator = require('jsonschema').Validator; -const fs = require('fs').promises; +const fs = require('fs'); +const exampleFolderPath = __dirname + '/../example/'; +const schemaPath = __dirname + '/../resources/schema.json'; async function parseJsonWithErrorHandling(path) { - const rawBuffer = await fs.readFile(path); + const rawBuffer = await fs.promises.readFile(path); try { return JSON.parse(rawBuffer.toString('utf8')); @@ -15,8 +17,8 @@ async function parseJsonWithErrorHandling(path) { async function validateFile(fileName) { const validator = new Validator(); - const instance = await parseJsonWithErrorHandling(__dirname + '/../example/' + fileName); - const schema = await parseJsonWithErrorHandling(__dirname + '/../resources/schema.json'); + const instance = await parseJsonWithErrorHandling(exampleFolderPath + fileName); + const schema = await parseJsonWithErrorHandling(schemaPath); const result = validator.validate(instance, schema); @@ -33,8 +35,10 @@ async function validateFile(fileName) { } } -async function main() { - ['response.json', 'responseWithVariants.json'].forEach(fileName => validateFile(fileName)); +function main() { + fs.readdir(exampleFolderPath, (err, files) => { + files.forEach(fileName => validateFile(fileName)); + }); } main(); From b44a7d27160965f74c2b2c4e3b9b4d13d359458a Mon Sep 17 00:00:00 2001 From: Wladislaw Koschemako Date: Wed, 15 Jul 2020 12:00:30 +0200 Subject: [PATCH 5/6] fix errors in schema --- resources/schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/schema.json b/resources/schema.json index 29b2402..8ed8934 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -9,7 +9,7 @@ "properties": { "value": { "description": "The filter value, as selected or suitable for visualization.", - "type": ["string", "object"], + "type": ["string", "object", "number"], "minLength": 1 }, "min": { @@ -179,7 +179,7 @@ }, "requiredUrl": { "type": "string", - "pattern": "^https?://.*$" + "pattern": "^(https?:\/\/.*|)$" }, "nullableUrl": { "type": ["string", "null"], From 22258ddb96269df0c839c17b00fb3158d5f66525 Mon Sep 17 00:00:00 2001 From: Wladislaw Koschemako Date: Thu, 16 Jul 2020 15:45:19 +0200 Subject: [PATCH 6/6] make url pattern regex easier --- resources/schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/schema.json b/resources/schema.json index 8ed8934..332e80b 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -179,7 +179,7 @@ }, "requiredUrl": { "type": "string", - "pattern": "^(https?:\/\/.*|)$" + "pattern": "^(https?://.*)?$" }, "nullableUrl": { "type": ["string", "null"],