@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.
- package/.eslint-header.txt +9 -0
- package/.releaserc.json +22 -0
- package/CHANGELOG.md +71 -0
- package/CODE_OF_CONDUCT.md +79 -0
- package/LICENSE +201 -0
- package/README.md +109 -0
- package/package.json +49 -0
- package/renovate.json +4 -0
- package/src/index.js +14 -0
- package/src/types/3DModel.js +21 -0
- package/src/types/AggregateOffer.js +23 -0
- package/src/types/AggregateRating.js +35 -0
- package/src/types/Answer.js +18 -0
- package/src/types/Article.js +26 -0
- package/src/types/Brand.js +18 -0
- package/src/types/BreadcrumbList.js +148 -0
- package/src/types/BroadcastEvent.js +23 -0
- package/src/types/Certification.js +26 -0
- package/src/types/Clip.js +25 -0
- package/src/types/DefinedRegion.js +38 -0
- package/src/types/Event.js +51 -0
- package/src/types/FAQPage.js +18 -0
- package/src/types/HowTo.js +27 -0
- package/src/types/HowToDirection.js +19 -0
- package/src/types/HowToSection.js +22 -0
- package/src/types/HowToStep.js +43 -0
- package/src/types/HowToTip.js +19 -0
- package/src/types/ImageObject.js +40 -0
- package/src/types/JobPosting.js +63 -0
- package/src/types/ListItem.js +28 -0
- package/src/types/LocalBusiness.js +30 -0
- package/src/types/MerchantReturnPolicy.js +96 -0
- package/src/types/Offer.js +39 -0
- package/src/types/OfferShippingDetails.js +27 -0
- package/src/types/Organization.js +18 -0
- package/src/types/PeopleAudience.js +37 -0
- package/src/types/Person.js +18 -0
- package/src/types/PriceSpecification.js +21 -0
- package/src/types/Product.js +90 -0
- package/src/types/ProductMerchant.js +88 -0
- package/src/types/QuantitativeValue.js +36 -0
- package/src/types/Question.js +21 -0
- package/src/types/Rating.js +56 -0
- package/src/types/Recipe.js +75 -0
- package/src/types/Review.js +35 -0
- package/src/types/SeekToAction.js +22 -0
- package/src/types/ShippingDeliveryTime.js +21 -0
- package/src/types/SizeSpecification.js +22 -0
- package/src/types/VideoObject.js +41 -0
- package/src/types/WebSite.js +23 -0
- package/src/types/base.js +201 -0
- package/src/types/schemaOrg.js +227 -0
- package/src/utils.js +15 -0
- package/src/validator.js +323 -0
|
@@ -0,0 +1,35 @@
|
|
|
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 ReviewValidator extends BaseValidator {
|
|
15
|
+
getConditions() {
|
|
16
|
+
const conditions = [
|
|
17
|
+
this.required('author'),
|
|
18
|
+
|
|
19
|
+
// Documentation states reviewRating as required
|
|
20
|
+
// Validator allows it to be missing
|
|
21
|
+
this.required('reviewRating'),
|
|
22
|
+
|
|
23
|
+
this.recommended('datePublished', 'date'),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
if (this.path.length === 1) {
|
|
27
|
+
conditions.push(
|
|
28
|
+
this.required('itemReviewed'),
|
|
29
|
+
this.required('itemReviewed.name'),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return conditions.map((c) => c.bind(this));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 SeekToActionValidator extends BaseValidator {
|
|
15
|
+
getConditions() {
|
|
16
|
+
const conditions = [
|
|
17
|
+
this.required('target'),
|
|
18
|
+
this.required('startOffset-input'),
|
|
19
|
+
];
|
|
20
|
+
return conditions.map((c) => c.bind(this));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -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 ShippingDeliveryTimeValidator extends BaseValidator {
|
|
15
|
+
getConditions() {
|
|
16
|
+
return [
|
|
17
|
+
this.recommended('handlingTime'),
|
|
18
|
+
this.recommended('transitTime'),
|
|
19
|
+
].map((c) => c.bind(this));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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 SizeSpecificationValidator extends BaseValidator {
|
|
15
|
+
getConditions() {
|
|
16
|
+
return [
|
|
17
|
+
this.recommended('name'),
|
|
18
|
+
this.recommended('sizeGroup'),
|
|
19
|
+
this.recommended('sizeSystem'),
|
|
20
|
+
].map((c) => c.bind(this));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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 VideoObjectValidator extends BaseValidator {
|
|
15
|
+
getConditions() {
|
|
16
|
+
const conditions = [
|
|
17
|
+
this.required('name', 'string'),
|
|
18
|
+
this.required('thumbnailUrl'),
|
|
19
|
+
this.required('uploadDate', 'date'),
|
|
20
|
+
|
|
21
|
+
this.recommended('description', 'string'),
|
|
22
|
+
this.recommended('duration', 'duration'),
|
|
23
|
+
this.recommended('expires', 'date'),
|
|
24
|
+
this.recommended('hasPart'),
|
|
25
|
+
this.recommended('publication'),
|
|
26
|
+
this.or(
|
|
27
|
+
this.recommended('contentUrl', 'url'),
|
|
28
|
+
this.recommended('embedUrl', 'url'),
|
|
29
|
+
),
|
|
30
|
+
this.or(
|
|
31
|
+
this.recommended('ineligibleRegion'),
|
|
32
|
+
this.recommended('regionsAllowed'),
|
|
33
|
+
),
|
|
34
|
+
this.or(
|
|
35
|
+
this.recommended('interactionStatistic'),
|
|
36
|
+
this.recommended('interactionCount'),
|
|
37
|
+
),
|
|
38
|
+
];
|
|
39
|
+
return conditions.map((c) => c.bind(this));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 WebSiteValidator extends BaseValidator {
|
|
15
|
+
getConditions() {
|
|
16
|
+
return [
|
|
17
|
+
this.required('name'),
|
|
18
|
+
this.required('url', 'url'),
|
|
19
|
+
|
|
20
|
+
this.recommended('potentialAction', 'arrayOrObject'),
|
|
21
|
+
].map((c) => c.bind(this));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
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 { isObject } from '../utils.js';
|
|
13
|
+
|
|
14
|
+
export default class BaseValidator {
|
|
15
|
+
constructor({ dataFormat, path }) {
|
|
16
|
+
this.dataFormat = dataFormat;
|
|
17
|
+
this.path = path;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getConditions() {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
validate(data) {
|
|
25
|
+
const issues = [];
|
|
26
|
+
|
|
27
|
+
for (const condition of this.getConditions(data)) {
|
|
28
|
+
const issue = condition(data);
|
|
29
|
+
if (Array.isArray(issue)) {
|
|
30
|
+
issues.push(...issue);
|
|
31
|
+
} else if (issue) {
|
|
32
|
+
issues.push(issue);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return issues;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#valueByPath(data, path) {
|
|
40
|
+
const parts = path.split('.');
|
|
41
|
+
let value = data;
|
|
42
|
+
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
if (value === undefined || typeof value !== 'object') {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
value = value[part];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
required(name, type, ...opts) {
|
|
54
|
+
return (data) => {
|
|
55
|
+
const value = this.#valueByPath(data, name);
|
|
56
|
+
if (value === undefined || value === null || value === '') {
|
|
57
|
+
return {
|
|
58
|
+
issueMessage: `Required attribute "${name}" is missing`,
|
|
59
|
+
severity: 'ERROR',
|
|
60
|
+
path: this.path,
|
|
61
|
+
fieldName: name,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (type && !this.checkType(value, type, ...opts)) {
|
|
65
|
+
return {
|
|
66
|
+
issueMessage: `Invalid type for attribute "${name}"`,
|
|
67
|
+
severity: 'ERROR',
|
|
68
|
+
path: this.path,
|
|
69
|
+
fieldName: name,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
or(...conditions) {
|
|
77
|
+
return (element, index, data) => {
|
|
78
|
+
const issues = conditions.map((c) => c(element, index, data));
|
|
79
|
+
const pass = issues.some(
|
|
80
|
+
(i) => i === null || (Array.isArray(i) && i.length === 0),
|
|
81
|
+
);
|
|
82
|
+
if (pass) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Use highest severity of the issues
|
|
87
|
+
const severity = issues.reduce((max, i) => {
|
|
88
|
+
if (i && i.severity === 'ERROR') {
|
|
89
|
+
return 'ERROR';
|
|
90
|
+
}
|
|
91
|
+
return max;
|
|
92
|
+
}, 'WARNING');
|
|
93
|
+
|
|
94
|
+
// Collect all field names from the conditions
|
|
95
|
+
const fieldNames = issues
|
|
96
|
+
.flat()
|
|
97
|
+
.filter((i) => i && i.fieldName)
|
|
98
|
+
.map((i) => i.fieldName);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
issueMessage: `One of the following conditions needs to be met: ${issues
|
|
102
|
+
.flat()
|
|
103
|
+
.map((c) => c.issueMessage)
|
|
104
|
+
.join(' or ')}`,
|
|
105
|
+
severity,
|
|
106
|
+
path: this.path,
|
|
107
|
+
fieldName: fieldNames[0] || null,
|
|
108
|
+
fieldNames: fieldNames.length > 0 ? fieldNames : undefined,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
recommended(name, type, ...opts) {
|
|
114
|
+
return (data) => {
|
|
115
|
+
const value = this.#valueByPath(data, name);
|
|
116
|
+
if (value === undefined || value === null || value === '') {
|
|
117
|
+
return {
|
|
118
|
+
issueMessage: `Missing field "${name}" (optional)`,
|
|
119
|
+
severity: 'WARNING',
|
|
120
|
+
path: this.path,
|
|
121
|
+
fieldName: name,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (type && !this.checkType(value, type, ...opts)) {
|
|
125
|
+
return {
|
|
126
|
+
issueMessage: `Invalid type for attribute "${name}"`,
|
|
127
|
+
severity: 'WARNING',
|
|
128
|
+
path: this.path,
|
|
129
|
+
fieldName: name,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
checkType(data, type, ...value) {
|
|
137
|
+
// TODO: Write tests for all type checks
|
|
138
|
+
if (type === 'string' && typeof data !== 'string') {
|
|
139
|
+
return false;
|
|
140
|
+
} else if (type === 'arrayOrObject') {
|
|
141
|
+
return isObject(data) || Array.isArray(data);
|
|
142
|
+
} else if (type === 'array' && !Array.isArray(data)) {
|
|
143
|
+
return false;
|
|
144
|
+
} else if (type === 'object') {
|
|
145
|
+
return isObject(data);
|
|
146
|
+
} else if (type === 'number') {
|
|
147
|
+
if (typeof data === 'number') {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (typeof data === 'string') {
|
|
151
|
+
const num = Number(data);
|
|
152
|
+
return !isNaN(num);
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
} else if (type === 'date') {
|
|
156
|
+
const date = new Date(data);
|
|
157
|
+
return !isNaN(date.getTime());
|
|
158
|
+
} else if (type === 'url') {
|
|
159
|
+
// Absolute or relative URL, but no data: URLs
|
|
160
|
+
let urlValues = Array.isArray(data) ? data : [data];
|
|
161
|
+
for (const url of urlValues) {
|
|
162
|
+
if (url.startsWith('data:')) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
new URL(url, 'https://example.com');
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else if (type === 'currency') {
|
|
172
|
+
return typeof data === 'string' && /^[A-Z]{3}$/.test(data);
|
|
173
|
+
} else if (type === 'enum' && !value.includes(data)) {
|
|
174
|
+
return false;
|
|
175
|
+
} else if (type === 'regex' && !value.test(data)) {
|
|
176
|
+
return false;
|
|
177
|
+
} else if (type === 'duration' && !this.validDurationFormat(data)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
inType(type) {
|
|
184
|
+
return (
|
|
185
|
+
this.path.length > 1 && this.path[this.path.length - 2].type === type
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
inProperty(property) {
|
|
190
|
+
return (
|
|
191
|
+
this.path.length > 1 &&
|
|
192
|
+
this.path[this.path.length - 1].property === property
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
validDurationFormat(time) {
|
|
197
|
+
const durationRegex =
|
|
198
|
+
/^P(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$/;
|
|
199
|
+
return durationRegex.test(time);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
export default class SchemaOrgValidator {
|
|
13
|
+
// Cache schema globally to improve performance
|
|
14
|
+
static schemaCache = null;
|
|
15
|
+
|
|
16
|
+
constructor({ dataFormat, path, type, schemaOrgJson }) {
|
|
17
|
+
this.dataFormat = dataFormat;
|
|
18
|
+
this.path = path;
|
|
19
|
+
this.type = type;
|
|
20
|
+
this.schemaOrgJson = schemaOrgJson;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#stripSchema(name) {
|
|
24
|
+
if (name.startsWith('schema:')) {
|
|
25
|
+
return name.replace('schema:', '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (name.startsWith('http://schema.org/')) {
|
|
29
|
+
return name.replace('http://schema.org/', '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (name.startsWith('https://schema.org/')) {
|
|
33
|
+
return name.replace('https://schema.org/', '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async #loadSchema() {
|
|
40
|
+
if (SchemaOrgValidator.schemaCache instanceof Promise) {
|
|
41
|
+
return SchemaOrgValidator.schemaCache;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
SchemaOrgValidator.schemaCache = new Promise((resolve) => {
|
|
45
|
+
const schema = {};
|
|
46
|
+
|
|
47
|
+
// Get all types
|
|
48
|
+
const entites = this.schemaOrgJson['@graph'];
|
|
49
|
+
entites
|
|
50
|
+
.filter((entity) => entity['@type'] === 'rdfs:Class')
|
|
51
|
+
.forEach((type) => {
|
|
52
|
+
const name = this.#stripSchema(type['@id']);
|
|
53
|
+
schema[name] = {
|
|
54
|
+
properties: [],
|
|
55
|
+
propertiesFromParent: {},
|
|
56
|
+
};
|
|
57
|
+
if (Array.isArray(type['rdfs:subClassOf'])) {
|
|
58
|
+
schema[name].parents = type['rdfs:subClassOf'].map((parent) =>
|
|
59
|
+
this.#stripSchema(parent['@id']),
|
|
60
|
+
);
|
|
61
|
+
} else if (type['rdfs:subClassOf']) {
|
|
62
|
+
schema[name].parents = [
|
|
63
|
+
this.#stripSchema(type['rdfs:subClassOf']['@id']),
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Add all properties to types
|
|
69
|
+
entites
|
|
70
|
+
.filter((entity) => entity['@type'] === 'rdf:Property')
|
|
71
|
+
.forEach((property) => {
|
|
72
|
+
const domainIncludes = property['schema:domainIncludes'];
|
|
73
|
+
const types = Array.isArray(domainIncludes)
|
|
74
|
+
? domainIncludes.map((domain) => this.#stripSchema(domain['@id']))
|
|
75
|
+
: domainIncludes
|
|
76
|
+
? [this.#stripSchema(domainIncludes['@id'])]
|
|
77
|
+
: [];
|
|
78
|
+
types.forEach((type) => {
|
|
79
|
+
if (schema[type]) {
|
|
80
|
+
schema[type].properties.push(this.#stripSchema(property['@id']));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// TODO: Add property types for validation
|
|
86
|
+
|
|
87
|
+
// Sort properties for each type alphabetically
|
|
88
|
+
Object.keys(schema).forEach((type) => {
|
|
89
|
+
schema[type].properties.sort();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Add inherited properties
|
|
93
|
+
const processOrder = this.#getTopologicalOrder(schema);
|
|
94
|
+
this.#addInheritedProperties(schema, processOrder);
|
|
95
|
+
|
|
96
|
+
resolve(schema);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return SchemaOrgValidator.schemaCache;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#getTopologicalOrder(schema) {
|
|
103
|
+
const visited = new Set();
|
|
104
|
+
const temp = new Set(); // For cycle detection
|
|
105
|
+
const order = [];
|
|
106
|
+
|
|
107
|
+
// Helper function for DFS
|
|
108
|
+
const visit = (typeId) => {
|
|
109
|
+
if (temp.has(typeId)) {
|
|
110
|
+
throw new Error('Cyclic inheritance detected');
|
|
111
|
+
}
|
|
112
|
+
if (visited.has(typeId)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
temp.add(typeId);
|
|
117
|
+
|
|
118
|
+
const type = schema[typeId];
|
|
119
|
+
if (type && type.parents) {
|
|
120
|
+
// Visit all parents before this type
|
|
121
|
+
for (const parentId of type.parents) {
|
|
122
|
+
if (schema[parentId]) {
|
|
123
|
+
visit(parentId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
temp.delete(typeId);
|
|
129
|
+
visited.add(typeId);
|
|
130
|
+
order.push(typeId);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Process all types
|
|
134
|
+
Object.keys(schema).forEach((typeId) => {
|
|
135
|
+
if (!visited.has(typeId)) {
|
|
136
|
+
visit(typeId);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return order;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#addInheritedProperties(schema, processOrder) {
|
|
144
|
+
processOrder.forEach((typeId) => {
|
|
145
|
+
const type = schema[typeId];
|
|
146
|
+
if (type.parents && type.parents.length > 0) {
|
|
147
|
+
// Process each parent
|
|
148
|
+
for (const parentId of type.parents) {
|
|
149
|
+
if (schema[parentId]) {
|
|
150
|
+
// Add direct properties from this parent
|
|
151
|
+
type.propertiesFromParent[parentId] = [
|
|
152
|
+
...schema[parentId].properties,
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
// Add inherited properties from this parent's ancestors
|
|
156
|
+
Object.keys(schema[parentId].propertiesFromParent).forEach(
|
|
157
|
+
(ancestorId) => {
|
|
158
|
+
if (
|
|
159
|
+
!type.propertiesFromParent[ancestorId] &&
|
|
160
|
+
schema[parentId].propertiesFromParent[ancestorId].length > 0
|
|
161
|
+
) {
|
|
162
|
+
type.propertiesFromParent[ancestorId] =
|
|
163
|
+
schema[parentId].propertiesFromParent[ancestorId];
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async validateProperty(type, property) {
|
|
174
|
+
const schema = await this.#loadSchema();
|
|
175
|
+
|
|
176
|
+
// Check if type exists
|
|
177
|
+
if (!schema[type]) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Check if property is directly supported
|
|
182
|
+
if (schema[type].properties.includes(property)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if property is supported through inheritance
|
|
187
|
+
return Object.keys(schema[type].propertiesFromParent).some((parent) => {
|
|
188
|
+
return schema[type].propertiesFromParent[parent].includes(property);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async validate(data) {
|
|
193
|
+
const issues = [];
|
|
194
|
+
|
|
195
|
+
if (typeof data === 'object' && data !== null) {
|
|
196
|
+
if (!this.type) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Get list of properties, any other keys which do not start with @
|
|
201
|
+
const properties = Object.keys(data).filter(
|
|
202
|
+
(key) => !key.startsWith('@'),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Check in schema.org schema if all properties are supported within the given type
|
|
206
|
+
await Promise.all(
|
|
207
|
+
properties.map(async (property) => {
|
|
208
|
+
const propertyId = this.#stripSchema(property);
|
|
209
|
+
const typeId = this.#stripSchema(this.type);
|
|
210
|
+
|
|
211
|
+
const isValid = await this.validateProperty(typeId, propertyId);
|
|
212
|
+
if (!isValid) {
|
|
213
|
+
issues.push({
|
|
214
|
+
issueMessage: `Property "${propertyId}" for type "${typeId}" is not supported by the schema.org specification`,
|
|
215
|
+
severity: 'WARNING',
|
|
216
|
+
path: this.path,
|
|
217
|
+
errorType: 'schemaOrg',
|
|
218
|
+
fieldName: propertyId,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return issues;
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
|
|
13
|
+
export function isObject(obj) {
|
|
14
|
+
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
|
|
15
|
+
}
|