@gsriram24/structured-data-validator 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.eslint-header.txt +9 -0
  2. package/.releaserc.json +22 -0
  3. package/CHANGELOG.md +71 -0
  4. package/CODE_OF_CONDUCT.md +79 -0
  5. package/LICENSE +201 -0
  6. package/README.md +109 -0
  7. package/package.json +49 -0
  8. package/renovate.json +4 -0
  9. package/src/index.js +14 -0
  10. package/src/types/3DModel.js +21 -0
  11. package/src/types/AggregateOffer.js +23 -0
  12. package/src/types/AggregateRating.js +35 -0
  13. package/src/types/Answer.js +18 -0
  14. package/src/types/Article.js +26 -0
  15. package/src/types/Brand.js +18 -0
  16. package/src/types/BreadcrumbList.js +148 -0
  17. package/src/types/BroadcastEvent.js +23 -0
  18. package/src/types/Certification.js +26 -0
  19. package/src/types/Clip.js +25 -0
  20. package/src/types/DefinedRegion.js +38 -0
  21. package/src/types/Event.js +51 -0
  22. package/src/types/FAQPage.js +18 -0
  23. package/src/types/HowTo.js +27 -0
  24. package/src/types/HowToDirection.js +19 -0
  25. package/src/types/HowToSection.js +22 -0
  26. package/src/types/HowToStep.js +43 -0
  27. package/src/types/HowToTip.js +19 -0
  28. package/src/types/ImageObject.js +40 -0
  29. package/src/types/JobPosting.js +63 -0
  30. package/src/types/ListItem.js +28 -0
  31. package/src/types/LocalBusiness.js +30 -0
  32. package/src/types/MerchantReturnPolicy.js +96 -0
  33. package/src/types/Offer.js +39 -0
  34. package/src/types/OfferShippingDetails.js +27 -0
  35. package/src/types/Organization.js +18 -0
  36. package/src/types/PeopleAudience.js +37 -0
  37. package/src/types/Person.js +18 -0
  38. package/src/types/PriceSpecification.js +21 -0
  39. package/src/types/Product.js +90 -0
  40. package/src/types/ProductMerchant.js +88 -0
  41. package/src/types/QuantitativeValue.js +36 -0
  42. package/src/types/Question.js +21 -0
  43. package/src/types/Rating.js +56 -0
  44. package/src/types/Recipe.js +75 -0
  45. package/src/types/Review.js +35 -0
  46. package/src/types/SeekToAction.js +22 -0
  47. package/src/types/ShippingDeliveryTime.js +21 -0
  48. package/src/types/SizeSpecification.js +22 -0
  49. package/src/types/VideoObject.js +41 -0
  50. package/src/types/WebSite.js +23 -0
  51. package/src/types/base.js +201 -0
  52. package/src/types/schemaOrg.js +227 -0
  53. package/src/utils.js +15 -0
  54. package/src/validator.js +323 -0
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class LocalBusinessValidator extends BaseValidator {
15
+ getConditions() {
16
+ return [
17
+ this.required('name'),
18
+ this.required('address', 'object'),
19
+
20
+ this.recommended('aggregateRating', 'object'),
21
+ this.recommended('geo', 'object'),
22
+ this.recommended('image', 'url'),
23
+ this.recommended('openingHoursSpecification', 'arrayOrObject'),
24
+ this.recommended('priceRange'),
25
+ this.recommended('review', 'arrayOrObject'),
26
+ this.recommended('telephone'),
27
+ this.recommended('url', 'url'),
28
+ ].map((c) => c.bind(this));
29
+ }
30
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class MerchantReturnPolicyValidator extends BaseValidator {
15
+ getConditions(data) {
16
+ const conditions = [this.countryOrLink];
17
+
18
+ const category = data.returnPolicyCategory;
19
+
20
+ // If returnPolicyCategory is present, we need to validate the return window
21
+ if (
22
+ category &&
23
+ (category.includes('MerchantReturnFiniteReturnWindow') ||
24
+ category.includes('MerchantReturnUnlimitedWindow'))
25
+ ) {
26
+ if (category.includes('MerchantReturnFiniteReturnWindow')) {
27
+ conditions.push(this.recommended('merchantReturnDays', 'number'));
28
+ }
29
+ conditions.push(
30
+ this.recommended('returnFees'),
31
+ this.recommended('returnMethod'),
32
+ );
33
+
34
+ if (data.returnFees && data.returnFees.includes('ReturnShippingFees')) {
35
+ conditions.push(this.recommended('returnShippingFeesAmount'));
36
+ }
37
+
38
+ // Additional properties for Organization context
39
+ // TODO: Consider other subtypes of Organization
40
+ if (
41
+ this.inType('Organization') &&
42
+ this.inProperty('hasMerchantReturnPolicy')
43
+ ) {
44
+ conditions.push(
45
+ this.recommended('customerRemorseReturnFees'),
46
+ this.recommended('customerRemorseReturnLabelSource'),
47
+ this.recommended('customerRemorseReturnShippingFeesAmount'),
48
+ this.recommended('itemCondition'),
49
+ this.recommended('itemDefectReturnFees'),
50
+ this.recommended('itemDefectReturnLabelSource'),
51
+ this.recommended('itemDefectReturnShippingFeesAmount'),
52
+ this.recommended('refundType'),
53
+ this.recommended('restockingFee'),
54
+ this.recommended('returnLabelSource'),
55
+ this.recommended('returnPolicyCountry'),
56
+ );
57
+ }
58
+ }
59
+
60
+ return conditions.map((c) => c.bind(this));
61
+ }
62
+
63
+ countryOrLink(data) {
64
+ // Either applicableCountry and returnPolicyCategory need to be present
65
+ // or merchantReturnLink needs to be present, but only when nested under Organization
66
+
67
+ let valid = false;
68
+ if (
69
+ this.inProperty('hasMerchantReturnPolicy') &&
70
+ this.inType('Organization') &&
71
+ this.required('merchantReturnLink')(data) === null
72
+ ) {
73
+ valid = true;
74
+ } else if (
75
+ this.required('applicableCountry')(data) === null &&
76
+ this.required('returnPolicyCategory')(data) === null
77
+ ) {
78
+ valid = true;
79
+ }
80
+
81
+ if (!valid) {
82
+ return {
83
+ issueMessage:
84
+ 'Either applicableCountry and returnPolicyCategory or merchantReturnLink must be present',
85
+ severity: 'ERROR',
86
+ path: this.path,
87
+ fieldName: 'applicableCountry',
88
+ fieldNames: [
89
+ 'applicableCountry',
90
+ 'returnPolicyCategory',
91
+ 'merchantReturnLink',
92
+ ],
93
+ };
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class PriceSpecificationValidator extends BaseValidator {
15
+ getConditions() {
16
+ const conditions = [];
17
+ const offerIndex = this.path.findIndex(
18
+ (pathElement) => pathElement.type === 'Offer',
19
+ );
20
+ const productIsParent =
21
+ offerIndex > 0 ? this.path[offerIndex - 1].type === 'Product' : false;
22
+
23
+ if (productIsParent) {
24
+ conditions.push(
25
+ this.or(
26
+ this.required('price', 'number'),
27
+ this.required('priceSpecification.price', 'number'),
28
+ ),
29
+ this.recommended('availability'),
30
+ this.or(
31
+ this.recommended('priceCurrency', 'currency'),
32
+ this.recommended('priceSpecification.priceCurrency', 'currency'),
33
+ ),
34
+ this.recommended('priceValidUntil', 'date'),
35
+ );
36
+ }
37
+ return conditions.map((c) => c.bind(this));
38
+ }
39
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class OfferShippingDetailsValidator extends BaseValidator {
15
+ getConditions() {
16
+ return [
17
+ this.required('deliveryTime'),
18
+ this.required('shippingDestination'),
19
+ this.required('shippingRate'),
20
+ this.required('shippingRate.currency'),
21
+ this.or(
22
+ this.required('shippingRate.value'),
23
+ this.required('shippingRate.maxValue'),
24
+ ),
25
+ ].map((c) => c.bind(this));
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class OrganizationValidator extends BaseValidator {
15
+ getConditions() {
16
+ return [this.required('name')].map((c) => c.bind(this));
17
+ }
18
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class PeopleAudienceValidator extends BaseValidator {
15
+ getConditions(data) {
16
+ const conditions = [
17
+ this.recommended('suggestedGender'),
18
+
19
+ this.or(
20
+ this.recommended('suggestedMinAge', 'number'),
21
+ this.recommended('suggestedAge.minValue', 'number'),
22
+ ),
23
+ ];
24
+
25
+ const minAge = data.suggestedMinAge || data.suggestedAge?.minValue;
26
+ if (minAge && minAge < 13) {
27
+ conditions.push(
28
+ this.or(
29
+ this.recommended('suggestedMaxAge', 'number'),
30
+ this.recommended('suggestedAge.maxValue', 'number'),
31
+ ),
32
+ );
33
+ }
34
+
35
+ return conditions.map((c) => c.bind(this));
36
+ }
37
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class PersonValidator extends BaseValidator {
15
+ getConditions() {
16
+ return [this.required('name')].map((c) => c.bind(this));
17
+ }
18
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class PriceSpecificationValidator extends BaseValidator {
15
+ getConditions() {
16
+ return [
17
+ this.required('price', 'number'),
18
+ this.recommended('priceCurrency', 'currency'),
19
+ ].map((c) => c.bind(this));
20
+ }
21
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class ProductValidator extends BaseValidator {
15
+ getConditions() {
16
+ return [
17
+ this.required('name'),
18
+ this.ratingReviewOrOffers,
19
+ this.notesCount,
20
+ ].map((c) => c.bind(this));
21
+ }
22
+
23
+ notesCount(data) {
24
+ if (!data.review) {
25
+ return null;
26
+ }
27
+
28
+ const issues = [];
29
+ let notes = 0;
30
+
31
+ const reviews = Array.isArray(data.review) ? data.review : [data.review];
32
+ for (const review of reviews) {
33
+ // positiveNotes and negativeNotes are optional, but if they are present, they must be correct
34
+ if (
35
+ (review.positiveNotes && review.positiveNotes.itemListElement) ||
36
+ (review.negativeNotes && review.negativeNotes.itemListElement)
37
+ ) {
38
+ notes += review.positiveNotes?.itemListElement?.length || 0;
39
+ notes += review.negativeNotes?.itemListElement?.length || 0;
40
+ }
41
+ }
42
+
43
+ // Need to have at least 2 or zero notes
44
+ if (notes === 1) {
45
+ issues.push({
46
+ issueMessage:
47
+ 'At least 2 notes, either positive or negative, are required',
48
+ severity: 'WARNING',
49
+ path: this.path,
50
+ fieldName: 'review',
51
+ fieldNames: ['review.positiveNotes', 'review.negativeNotes'],
52
+ });
53
+ }
54
+
55
+ return issues;
56
+ }
57
+
58
+ ratingReviewOrOffers(data) {
59
+ const issues = [];
60
+
61
+ // One of the three is required
62
+ if (!data.aggregateRating && !data.offers && !data.review) {
63
+ issues.push({
64
+ issueMessage:
65
+ 'One of the following attributes is required: "aggregateRating", "offers" or "review"',
66
+ severity: 'ERROR',
67
+ path: this.path,
68
+ fieldName: 'aggregateRating',
69
+ fieldNames: ['aggregateRating', 'offers', 'review'],
70
+ });
71
+ }
72
+
73
+ // If only offers is present, then aggregateRating and review are recommended
74
+ if (data.offers && (!data.aggregateRating || !data.review)) {
75
+ const aggregateRating = this.recommended(
76
+ 'aggregateRating',
77
+ 'object',
78
+ )(data);
79
+ if (aggregateRating) {
80
+ issues.push(aggregateRating);
81
+ }
82
+ const review = this.recommended('review', 'object')(data);
83
+ if (review) {
84
+ issues.push(review);
85
+ }
86
+ }
87
+
88
+ return issues;
89
+ }
90
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class ProductMerchantValidator extends BaseValidator {
15
+ getConditions() {
16
+ return [
17
+ this.required('image'),
18
+ this.required('offers'),
19
+
20
+ this.recommended('audience'),
21
+ this.recommended('brand'),
22
+ this.recommended('color', 'string'),
23
+ this.recommended('description', 'string'),
24
+ this.recommended('hasCertification'),
25
+ this.recommended('inProductGroupWithID', 'string'),
26
+ this.recommended('isVariantOf'),
27
+ this.recommended('material', 'string'),
28
+ this.recommended('mpn', 'string'),
29
+ this.recommended('pattern', 'string'),
30
+ this.recommended('size'),
31
+ this.recommended('sku', 'string'),
32
+ this.recommended('subjectOf'),
33
+ this.validateGtin,
34
+ ].map((c) => c.bind(this));
35
+ }
36
+
37
+ validateGtin(data) {
38
+ let gtinFields = ['gtin', 'gtin8', 'gtin12', 'gtin13', 'gtin14', 'isbn'];
39
+
40
+ // Check if gtin is present on product
41
+ const productPass =
42
+ this.or(...gtinFields.map((field) => this.recommended(field, 'string')))(
43
+ data,
44
+ ) === null;
45
+
46
+ // Check if gtin is present on offers object
47
+ let offerPass = false;
48
+ if (
49
+ data.offers &&
50
+ typeof data.offers === 'object' &&
51
+ !Array.isArray(data.offers)
52
+ ) {
53
+ offerPass =
54
+ this.or(
55
+ ...gtinFields.map((field) =>
56
+ this.recommended(`offers.${field}`, 'string'),
57
+ ),
58
+ )(data.offers) === null;
59
+ }
60
+
61
+ // Check if gtin is present on offers array
62
+ let allOffersPass = false;
63
+ if (data.offers && Array.isArray(data.offers)) {
64
+ allOffersPass = true;
65
+ data.offers.forEach((offer, index) => {
66
+ const offerPass =
67
+ this.or(
68
+ ...gtinFields.map((field) => this.recommended(field, 'string')),
69
+ )(offer, index, data) === null;
70
+ if (!offerPass) {
71
+ allOffersPass = false;
72
+ }
73
+ });
74
+ }
75
+
76
+ if (productPass || offerPass || allOffersPass) {
77
+ return null;
78
+ }
79
+
80
+ return {
81
+ issueMessage: `Missing one of field ${gtinFields.map((a) => `"${a}"`).join(', ')} on either product or all offers`,
82
+ severity: 'WARNING',
83
+ path: this.path,
84
+ fieldName: 'gtin',
85
+ fieldNames: gtinFields,
86
+ };
87
+ }
88
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class QuantitativeValueValidator extends BaseValidator {
15
+ getConditions() {
16
+ if (this.inType('UnitPriceSpecification')) {
17
+ // If used in the context of unit pricing
18
+ return [
19
+ this.required('unitCode'),
20
+ this.required('value'),
21
+
22
+ this.recommended('valueReference'),
23
+ ].map((c) => c.bind(this));
24
+ }
25
+
26
+ if (this.inType('ShippingDeliveryTime')) {
27
+ return [
28
+ this.required('maxValue'),
29
+ this.required('minValue'),
30
+ this.required('unitCode'),
31
+ ].map((c) => c.bind(this));
32
+ }
33
+
34
+ return [];
35
+ }
36
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class QuestionValidator extends BaseValidator {
15
+ getConditions() {
16
+ return [
17
+ this.required('name'),
18
+ this.required('acceptedAnswer', 'object'),
19
+ ].map((c) => c.bind(this));
20
+ }
21
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class RatingValidator extends BaseValidator {
15
+ getConditions() {
16
+ const conditions = [
17
+ this.required('ratingValue'),
18
+ this.validateRange,
19
+
20
+ // Those fields are listed as recommended in documentation
21
+ // BUT: Google validator does not show warnings and assumes default values 0 and 5.
22
+ this.recommended('bestRating'),
23
+ this.recommended('worstRating'),
24
+ ];
25
+
26
+ return conditions.map((c) => c.bind(this));
27
+ }
28
+
29
+ validateRange(data) {
30
+ // If number or if it can be parsed as number
31
+ // For % and / values, Google ignores the range
32
+ const from = data.worstRating || 0;
33
+ const to = data.bestRating || 5;
34
+ let value = data.ratingValue;
35
+ if (typeof value === 'string') {
36
+ // Try to parse as number
37
+ value = parseFloat(value);
38
+ if (!isNaN(value)) {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ if (typeof value === 'number') {
44
+ if (value < from || value > to) {
45
+ return {
46
+ issueMessage: `Rating is outside the specified or default range`,
47
+ severity: 'ERROR',
48
+ path: this.path,
49
+ fieldName: 'ratingValue',
50
+ };
51
+ }
52
+ }
53
+
54
+ return null;
55
+ }
56
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import BaseValidator from './base.js';
13
+
14
+ export default class RecipeValidator extends BaseValidator {
15
+ getConditions() {
16
+ const conditions = [
17
+ this.validateImage,
18
+ this.required('name', 'string'),
19
+
20
+ this.recommended('aggregateRating'),
21
+ this.recommended('author'),
22
+ this.recommended('datePublished', 'date'),
23
+ this.recommended('description', 'string'),
24
+ this.recommended('keywords', 'string'),
25
+ this.recommended('recipeCategory', 'string'),
26
+ this.recommended('recipeCuisine', 'string'),
27
+ this.recommended('recipeIngredient'),
28
+ this.recommended('recipeInstructions'),
29
+ this.validateNutritionAndYield,
30
+ this.validateCookTime,
31
+ this.recommended('video'),
32
+ ];
33
+ return conditions.map((c) => c.bind(this));
34
+ }
35
+
36
+ validateNutritionAndYield(data) {
37
+ const issues = [];
38
+ if (data.nutrition?.calories && !data.recipeYield) {
39
+ issues.push(this.required('recipeYield')(data));
40
+ } else {
41
+ issues.push(this.recommended('recipeYield')(data));
42
+ issues.push(this.recommended('nutrition.calories')(data));
43
+ }
44
+ return issues.filter((issue) => issue !== null);
45
+ }
46
+
47
+ validateCookTime(data) {
48
+ const issues = [];
49
+ const hasCookTime = data.cookTime !== undefined && data.cookTime !== null;
50
+ const hasPrepTime = data.prepTime !== undefined && data.prepTime !== null;
51
+ const hasTotalTime =
52
+ data.totalTime !== undefined && data.totalTime !== null;
53
+
54
+ if (hasCookTime || hasPrepTime) {
55
+ issues.push(this.recommended('prepTime', 'duration')(data));
56
+ issues.push(this.recommended('cookTime', 'duration')(data));
57
+ if (hasTotalTime) {
58
+ issues.push(this.recommended('totalTime', 'duration')(data));
59
+ }
60
+ } else {
61
+ issues.push(this.recommended('totalTime', 'duration')(data));
62
+ }
63
+ return issues.filter((issue) => issue !== null);
64
+ }
65
+
66
+ validateImage(data) {
67
+ const issues = [];
68
+ if (Array.isArray(data.image)) {
69
+ issues.push(this.required('image', 'url')(data));
70
+ } else {
71
+ issues.push(this.required('image')(data));
72
+ }
73
+ return issues.filter((issue) => issue !== null);
74
+ }
75
+ }