z-schema 6.0.2 → 7.0.0-beta.2
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/README.md +154 -134
- package/bin/z-schema +128 -124
- package/cjs/ZSchema.d.ts +227 -0
- package/cjs/ZSchema.js +13785 -0
- package/dist/Errors.js +50 -0
- package/dist/FormatValidators.js +136 -0
- package/{src → dist}/JsonValidation.js +184 -213
- package/dist/Report.js +220 -0
- package/{src → dist}/SchemaCache.js +67 -82
- package/{src → dist}/SchemaCompilation.js +89 -129
- package/dist/SchemaValidation.js +631 -0
- package/{src → dist}/Utils.js +96 -104
- package/dist/ZSchema.js +365 -0
- package/dist/index.js +2 -0
- package/dist/schemas/hyper-schema.json +156 -0
- package/dist/schemas/schema.json +151 -0
- package/dist/types/Errors.d.ts +44 -0
- package/dist/types/FormatValidators.d.ts +12 -0
- package/dist/types/JsonValidation.d.ts +37 -0
- package/dist/types/Report.d.ts +87 -0
- package/dist/types/SchemaCache.d.ts +26 -0
- package/dist/types/SchemaCompilation.d.ts +1 -0
- package/dist/types/SchemaValidation.d.ts +6 -0
- package/dist/types/Utils.d.ts +64 -0
- package/dist/types/ZSchema.d.ts +97 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +59 -45
- package/src/Errors.ts +56 -0
- package/src/FormatValidators.ts +136 -0
- package/src/JsonValidation.ts +624 -0
- package/src/Report.ts +337 -0
- package/src/SchemaCache.ts +189 -0
- package/src/SchemaCompilation.ts +293 -0
- package/src/SchemaValidation.ts +629 -0
- package/src/Utils.ts +286 -0
- package/src/ZSchema.ts +467 -0
- package/src/index.ts +3 -0
- package/src/schemas/_ +0 -0
- package/umd/ZSchema.js +13791 -0
- package/umd/ZSchema.min.js +1 -0
- package/dist/ZSchema-browser-min.js +0 -2
- package/dist/ZSchema-browser-min.js.map +0 -1
- package/dist/ZSchema-browser-test.js +0 -32247
- package/dist/ZSchema-browser.js +0 -12745
- package/index.d.ts +0 -175
- package/src/Errors.js +0 -60
- package/src/FormatValidators.js +0 -129
- package/src/Polyfills.js +0 -16
- package/src/Report.js +0 -299
- package/src/SchemaValidation.js +0 -619
- package/src/ZSchema.js +0 -409
package/dist/Report.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import get from 'lodash.get';
|
|
2
|
+
import { Errors } from './Errors.js';
|
|
3
|
+
import * as Utils from './Utils.js';
|
|
4
|
+
export class Report {
|
|
5
|
+
errors;
|
|
6
|
+
parentReport;
|
|
7
|
+
options;
|
|
8
|
+
reportOptions;
|
|
9
|
+
path;
|
|
10
|
+
asyncTasks;
|
|
11
|
+
rootSchema;
|
|
12
|
+
commonErrorMessage;
|
|
13
|
+
json;
|
|
14
|
+
constructor(parentOrOptions, reportOptions) {
|
|
15
|
+
this.parentReport = parentOrOptions instanceof Report ? parentOrOptions : undefined;
|
|
16
|
+
this.options = parentOrOptions instanceof Report ? parentOrOptions.options : parentOrOptions || {};
|
|
17
|
+
this.reportOptions = reportOptions || {};
|
|
18
|
+
this.errors = [];
|
|
19
|
+
/**
|
|
20
|
+
* @type {string[]}
|
|
21
|
+
*/
|
|
22
|
+
this.path = [];
|
|
23
|
+
this.asyncTasks = [];
|
|
24
|
+
this.rootSchema = undefined;
|
|
25
|
+
this.commonErrorMessage = undefined;
|
|
26
|
+
this.json = undefined;
|
|
27
|
+
}
|
|
28
|
+
isValid() {
|
|
29
|
+
if (this.asyncTasks.length > 0) {
|
|
30
|
+
throw new Error("Async tasks pending, can't answer isValid");
|
|
31
|
+
}
|
|
32
|
+
return this.errors.length === 0;
|
|
33
|
+
}
|
|
34
|
+
addAsyncTask(fn, args, asyncTaskResultProcessFn) {
|
|
35
|
+
this.asyncTasks.push([fn, args, asyncTaskResultProcessFn]);
|
|
36
|
+
}
|
|
37
|
+
getAncestor(id) {
|
|
38
|
+
if (!this.parentReport) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (this.parentReport.getSchemaId() === id) {
|
|
42
|
+
return this.parentReport;
|
|
43
|
+
}
|
|
44
|
+
return this.parentReport.getAncestor(id);
|
|
45
|
+
}
|
|
46
|
+
processAsyncTasks(timeout, callback) {
|
|
47
|
+
const validationTimeout = timeout || 2000;
|
|
48
|
+
let tasksCount = this.asyncTasks.length;
|
|
49
|
+
let idx = tasksCount;
|
|
50
|
+
let timedOut = false;
|
|
51
|
+
const finish = () => {
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
const valid = this.errors.length === 0, err = valid ? null : this.errors;
|
|
54
|
+
callback(err, valid);
|
|
55
|
+
}, 0);
|
|
56
|
+
};
|
|
57
|
+
function respond(asyncTaskResultProcessFn) {
|
|
58
|
+
return function (asyncTaskResult) {
|
|
59
|
+
if (timedOut) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
asyncTaskResultProcessFn(asyncTaskResult);
|
|
63
|
+
if (--tasksCount === 0) {
|
|
64
|
+
finish();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// finish if tasks are completed or there are any errors and breaking on first error was requested
|
|
69
|
+
if (tasksCount === 0 || (this.errors.length > 0 && this.options.breakOnFirstError)) {
|
|
70
|
+
finish();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
while (idx--) {
|
|
74
|
+
const [fn, fnArgs, processFn] = this.asyncTasks[idx];
|
|
75
|
+
const respondCallback = respond(processFn);
|
|
76
|
+
fn(...fnArgs, respondCallback);
|
|
77
|
+
}
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
if (tasksCount > 0) {
|
|
80
|
+
timedOut = true;
|
|
81
|
+
this.addError('ASYNC_TIMEOUT', [tasksCount, validationTimeout]);
|
|
82
|
+
callback(this.errors, false);
|
|
83
|
+
}
|
|
84
|
+
}, validationTimeout);
|
|
85
|
+
}
|
|
86
|
+
getPath(returnPathAsString) {
|
|
87
|
+
/**
|
|
88
|
+
* @type {string[]|string}
|
|
89
|
+
*/
|
|
90
|
+
let path = [];
|
|
91
|
+
if (this.parentReport) {
|
|
92
|
+
path = path.concat(this.parentReport.path);
|
|
93
|
+
}
|
|
94
|
+
path = path.concat(this.path);
|
|
95
|
+
if (returnPathAsString !== true) {
|
|
96
|
+
// Sanitize the path segments (http://tools.ietf.org/html/rfc6901#section-4)
|
|
97
|
+
return ('#/' +
|
|
98
|
+
path
|
|
99
|
+
.map(function (segment) {
|
|
100
|
+
segment = segment.toString();
|
|
101
|
+
if (Utils.isAbsoluteUri(segment)) {
|
|
102
|
+
return 'uri(' + segment + ')';
|
|
103
|
+
}
|
|
104
|
+
return segment.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
105
|
+
})
|
|
106
|
+
.join('/'));
|
|
107
|
+
}
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
getSchemaId() {
|
|
111
|
+
if (!this.rootSchema) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
// get the error path as an array
|
|
115
|
+
let path = [];
|
|
116
|
+
if (this.parentReport) {
|
|
117
|
+
path = path.concat(this.parentReport.path);
|
|
118
|
+
}
|
|
119
|
+
path = path.concat(this.path);
|
|
120
|
+
// try to find id in the error path
|
|
121
|
+
while (path.length > 0) {
|
|
122
|
+
const obj = get(this.rootSchema, path);
|
|
123
|
+
if (obj && obj.id) {
|
|
124
|
+
return obj.id;
|
|
125
|
+
}
|
|
126
|
+
path.pop();
|
|
127
|
+
}
|
|
128
|
+
// return id of the root
|
|
129
|
+
return this.rootSchema.id;
|
|
130
|
+
}
|
|
131
|
+
hasError(errorCode, params) {
|
|
132
|
+
let idx = this.errors.length;
|
|
133
|
+
while (idx--) {
|
|
134
|
+
if (this.errors[idx].code === errorCode) {
|
|
135
|
+
// assume match
|
|
136
|
+
let match = true;
|
|
137
|
+
// check the params too
|
|
138
|
+
let idx2 = this.errors[idx].params.length;
|
|
139
|
+
while (idx2--) {
|
|
140
|
+
if (this.errors[idx].params[idx2] !== params[idx2]) {
|
|
141
|
+
match = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// if match, return true
|
|
145
|
+
if (match) {
|
|
146
|
+
return match;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
addError(errorCode, params, subReports, schema) {
|
|
153
|
+
if (!errorCode) {
|
|
154
|
+
throw new Error('No errorCode passed into addError()');
|
|
155
|
+
}
|
|
156
|
+
this.addCustomError(errorCode, Errors[errorCode], params, subReports, schema);
|
|
157
|
+
}
|
|
158
|
+
getJson() {
|
|
159
|
+
if (this.json) {
|
|
160
|
+
return this.json;
|
|
161
|
+
}
|
|
162
|
+
if (this.parentReport) {
|
|
163
|
+
return this.parentReport.getJson();
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
addCustomError(errorCode, errorMessage, params, subReports, schema) {
|
|
168
|
+
if (this.errors.length >= this.reportOptions.maxErrors) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!errorMessage) {
|
|
172
|
+
throw new Error('No errorMessage known for code ' + errorCode);
|
|
173
|
+
}
|
|
174
|
+
params = params || [];
|
|
175
|
+
let idx = params.length;
|
|
176
|
+
while (idx--) {
|
|
177
|
+
const whatIs = Utils.whatIs(params[idx]);
|
|
178
|
+
const param = whatIs === 'object' || whatIs === 'null' ? JSON.stringify(params[idx]) : params[idx];
|
|
179
|
+
errorMessage = errorMessage.replace('{' + idx + '}', param);
|
|
180
|
+
}
|
|
181
|
+
const err = {
|
|
182
|
+
code: errorCode,
|
|
183
|
+
params: params,
|
|
184
|
+
message: errorMessage,
|
|
185
|
+
path: this.getPath(this.options.reportPathAsArray),
|
|
186
|
+
schemaId: this.getSchemaId(),
|
|
187
|
+
};
|
|
188
|
+
err[Utils.schemaSymbol] = schema;
|
|
189
|
+
err[Utils.jsonSymbol] = this.getJson();
|
|
190
|
+
if (schema && typeof schema === 'string') {
|
|
191
|
+
err.description = schema;
|
|
192
|
+
}
|
|
193
|
+
else if (schema && typeof schema === 'object') {
|
|
194
|
+
if (schema.title) {
|
|
195
|
+
err.title = schema.title;
|
|
196
|
+
}
|
|
197
|
+
if (schema.description) {
|
|
198
|
+
err.description = schema.description;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (subReports != null) {
|
|
202
|
+
if (!Array.isArray(subReports)) {
|
|
203
|
+
subReports = [subReports];
|
|
204
|
+
}
|
|
205
|
+
err.inner = [];
|
|
206
|
+
idx = subReports.length;
|
|
207
|
+
while (idx--) {
|
|
208
|
+
const subReport = subReports[idx];
|
|
209
|
+
let idx2 = subReport.errors.length;
|
|
210
|
+
while (idx2--) {
|
|
211
|
+
err.inner.push(subReport.errors[idx2]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (err.inner.length === 0) {
|
|
215
|
+
err.inner = undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
this.errors.push(err);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -1,69 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var SchemaValidation = require("./SchemaValidation");
|
|
7
|
-
var Utils = require("./Utils");
|
|
8
|
-
|
|
1
|
+
import isequal from 'lodash.isequal';
|
|
2
|
+
import { Report } from './Report.js';
|
|
3
|
+
import { compileSchema } from './SchemaCompilation.js';
|
|
4
|
+
import * as SchemaValidation from './SchemaValidation.js';
|
|
5
|
+
import * as Utils from './Utils.js';
|
|
9
6
|
function decodeJSONPointer(str) {
|
|
10
7
|
// http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07#section-3
|
|
11
8
|
return decodeURIComponent(str).replace(/~[0-1]/g, function (x) {
|
|
12
|
-
return x ===
|
|
9
|
+
return x === '~1' ? '/' : '~';
|
|
13
10
|
});
|
|
14
11
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
var io = uri.indexOf("#");
|
|
12
|
+
export function getRemotePath(uri) {
|
|
13
|
+
const io = uri.indexOf('#');
|
|
18
14
|
return io === -1 ? uri : uri.slice(0, io);
|
|
19
15
|
}
|
|
20
|
-
|
|
21
16
|
function getQueryPath(uri) {
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
const io = uri.indexOf('#');
|
|
18
|
+
const res = io === -1 ? undefined : uri.slice(io + 1);
|
|
24
19
|
// WARN: do not slice slash, #/ means take root and go down from it
|
|
25
20
|
// if (res && res[0] === "/") { res = res.slice(1); }
|
|
26
21
|
return res;
|
|
27
22
|
}
|
|
28
|
-
|
|
29
23
|
function findId(schema, id) {
|
|
30
24
|
// process only arrays and objects
|
|
31
|
-
if (typeof schema !==
|
|
25
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
32
26
|
return;
|
|
33
27
|
}
|
|
34
|
-
|
|
35
28
|
// no id means root so return itself
|
|
36
29
|
if (!id) {
|
|
37
30
|
return schema;
|
|
38
31
|
}
|
|
39
|
-
|
|
40
32
|
if (schema.id) {
|
|
41
|
-
if (schema.id === id || schema.id[0] ===
|
|
33
|
+
if (schema.id === id || (schema.id[0] === '#' && schema.id.substring(1) === id)) {
|
|
42
34
|
return schema;
|
|
43
35
|
}
|
|
44
36
|
}
|
|
45
|
-
|
|
46
|
-
var idx, result;
|
|
37
|
+
let idx, result;
|
|
47
38
|
if (Array.isArray(schema)) {
|
|
48
39
|
idx = schema.length;
|
|
49
40
|
while (idx--) {
|
|
50
41
|
result = findId(schema[idx], id);
|
|
51
|
-
if (result) {
|
|
42
|
+
if (result) {
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
52
45
|
}
|
|
53
|
-
}
|
|
54
|
-
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const keys = Object.keys(schema);
|
|
55
49
|
idx = keys.length;
|
|
56
50
|
while (idx--) {
|
|
57
|
-
|
|
58
|
-
if (k.indexOf(
|
|
51
|
+
const k = keys[idx];
|
|
52
|
+
if (k.indexOf('__$') === 0) {
|
|
59
53
|
continue;
|
|
60
54
|
}
|
|
61
55
|
result = findId(schema[k], id);
|
|
62
|
-
if (result) {
|
|
56
|
+
if (result) {
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
63
59
|
}
|
|
64
60
|
}
|
|
65
61
|
}
|
|
66
|
-
|
|
67
62
|
/**
|
|
68
63
|
*
|
|
69
64
|
* @param {*} uri
|
|
@@ -71,118 +66,108 @@ function findId(schema, id) {
|
|
|
71
66
|
*
|
|
72
67
|
* @returns {void}
|
|
73
68
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
export function cacheSchemaByUri(uri, schema) {
|
|
70
|
+
const remotePath = getRemotePath(uri);
|
|
76
71
|
if (remotePath) {
|
|
77
72
|
this.cache[remotePath] = schema;
|
|
78
73
|
}
|
|
79
|
-
}
|
|
80
|
-
|
|
74
|
+
}
|
|
81
75
|
/**
|
|
82
76
|
*
|
|
83
77
|
* @param {*} uri
|
|
84
78
|
*
|
|
85
79
|
* @returns {void}
|
|
86
80
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
export function removeFromCacheByUri(uri) {
|
|
82
|
+
const remotePath = getRemotePath(uri);
|
|
89
83
|
if (remotePath) {
|
|
90
84
|
delete this.cache[remotePath];
|
|
91
85
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
86
|
+
}
|
|
94
87
|
/**
|
|
95
88
|
*
|
|
96
89
|
* @param {*} uri
|
|
97
90
|
*
|
|
98
91
|
* @returns {boolean}
|
|
99
92
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
93
|
+
export function checkCacheForUri(uri) {
|
|
94
|
+
const remotePath = getRemotePath(uri);
|
|
102
95
|
return remotePath ? this.cache[remotePath] != null : false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
schema = exports.getSchemaByReference.call(this, report, schema);
|
|
96
|
+
}
|
|
97
|
+
export function getSchema(report, schema) {
|
|
98
|
+
if (typeof schema === 'object') {
|
|
99
|
+
schema = getSchemaByReference.call(this, report, schema);
|
|
108
100
|
}
|
|
109
|
-
if (typeof schema ===
|
|
110
|
-
schema =
|
|
101
|
+
if (typeof schema === 'string') {
|
|
102
|
+
schema = getSchemaByUri.call(this, report, schema);
|
|
111
103
|
}
|
|
112
104
|
return schema;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
var i = this.referenceCache.length;
|
|
105
|
+
}
|
|
106
|
+
export function getSchemaByReference(report, key) {
|
|
107
|
+
let i = this.referenceCache.length;
|
|
117
108
|
while (i--) {
|
|
118
109
|
if (isequal(this.referenceCache[i][0], key)) {
|
|
119
110
|
return this.referenceCache[i][1];
|
|
120
111
|
}
|
|
121
112
|
}
|
|
122
113
|
// not found
|
|
123
|
-
|
|
114
|
+
const schema = Utils.cloneDeep(key);
|
|
124
115
|
this.referenceCache.push([key, schema]);
|
|
125
116
|
return schema;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
result = remotePath ? this.cache[remotePath] : root;
|
|
132
|
-
|
|
117
|
+
}
|
|
118
|
+
export function getSchemaByUri(report, uri, root) {
|
|
119
|
+
const remotePath = getRemotePath(uri);
|
|
120
|
+
const queryPath = getQueryPath(uri);
|
|
121
|
+
let result = remotePath ? this.cache[remotePath] : root;
|
|
133
122
|
if (result && remotePath) {
|
|
134
123
|
// we need to avoid compiling schemas in a recursive loop
|
|
135
|
-
|
|
124
|
+
const compileRemote = result !== root;
|
|
136
125
|
// now we need to compile and validate resolved schema (in case it's not already)
|
|
137
126
|
if (compileRemote) {
|
|
138
|
-
|
|
139
127
|
report.path.push(remotePath);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
var anscestorReport = report.getAncestor(result.id);
|
|
128
|
+
let remoteReport;
|
|
129
|
+
const anscestorReport = report.getAncestor(result.id);
|
|
144
130
|
if (anscestorReport) {
|
|
145
131
|
remoteReport = anscestorReport;
|
|
146
|
-
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
147
134
|
remoteReport = new Report(report);
|
|
148
|
-
if (
|
|
149
|
-
|
|
135
|
+
if (compileSchema.call(this, remoteReport, result)) {
|
|
136
|
+
const savedOptions = this.options;
|
|
150
137
|
try {
|
|
151
138
|
// If custom validationOptions were provided to setRemoteReference(),
|
|
152
139
|
// use them instead of the default options
|
|
153
140
|
this.options = result.__$validationOptions || this.options;
|
|
154
141
|
SchemaValidation.validateSchema.call(this, remoteReport, result);
|
|
155
|
-
}
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
156
144
|
this.options = savedOptions;
|
|
157
145
|
}
|
|
158
146
|
}
|
|
159
147
|
}
|
|
160
|
-
|
|
148
|
+
const remoteReportIsValid = remoteReport.isValid();
|
|
161
149
|
if (!remoteReportIsValid) {
|
|
162
|
-
report.addError(
|
|
150
|
+
report.addError('REMOTE_NOT_VALID', [uri], remoteReport);
|
|
163
151
|
}
|
|
164
|
-
|
|
165
152
|
report.path.pop();
|
|
166
|
-
|
|
167
153
|
if (!remoteReportIsValid) {
|
|
168
154
|
return undefined;
|
|
169
155
|
}
|
|
170
156
|
}
|
|
171
157
|
}
|
|
172
|
-
|
|
173
158
|
if (result && queryPath) {
|
|
174
|
-
|
|
175
|
-
for (
|
|
176
|
-
|
|
177
|
-
if (idx === 0) {
|
|
159
|
+
const parts = queryPath.split('/');
|
|
160
|
+
for (let idx = 0, lim = parts.length; result && idx < lim; idx++) {
|
|
161
|
+
const key = decodeJSONPointer(parts[idx]);
|
|
162
|
+
if (idx === 0) {
|
|
163
|
+
// it's an id
|
|
178
164
|
result = findId(result, key);
|
|
179
|
-
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// it's a path behind id
|
|
180
168
|
result = result[key];
|
|
181
169
|
}
|
|
182
170
|
}
|
|
183
171
|
}
|
|
184
|
-
|
|
185
172
|
return result;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
exports.getRemotePath = getRemotePath;
|
|
173
|
+
}
|