@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,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
+ }