@featurevisor/core 1.5.1 → 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/.eslintcache +1 -1
- package/CHANGELOG.md +11 -0
- package/coverage/clover.xml +2 -2
- package/coverage/lcov-report/index.html +1 -1
- package/coverage/lcov-report/lib/builder/allocator.js.html +1 -1
- package/coverage/lcov-report/lib/builder/index.html +1 -1
- package/coverage/lcov-report/lib/builder/traffic.js.html +1 -1
- package/coverage/lcov-report/lib/tester/checkIfObjectsAreEqual.js.html +1 -1
- package/coverage/lcov-report/lib/tester/index.html +1 -1
- package/coverage/lcov-report/lib/tester/matrix.js.html +1 -1
- package/coverage/lcov-report/src/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/src/builder/index.html +1 -1
- package/coverage/lcov-report/src/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/src/tester/checkIfObjectsAreEqual.ts.html +1 -1
- package/coverage/lcov-report/src/tester/index.html +1 -1
- package/coverage/lcov-report/src/tester/matrix.ts.html +1 -1
- package/lib/linter/attributeSchema.d.ts +17 -2
- package/lib/linter/attributeSchema.js +13 -11
- package/lib/linter/attributeSchema.js.map +1 -1
- package/lib/linter/checkPercentageExceedingSlot.d.ts +3 -0
- package/lib/linter/checkPercentageExceedingSlot.js +86 -0
- package/lib/linter/checkPercentageExceedingSlot.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +2 -2
- package/lib/linter/conditionSchema.js +112 -57
- package/lib/linter/conditionSchema.js.map +1 -1
- package/lib/linter/featureSchema.d.ts +229 -2
- package/lib/linter/featureSchema.js +195 -139
- package/lib/linter/featureSchema.js.map +1 -1
- package/lib/linter/groupSchema.d.ts +32 -2
- package/lib/linter/groupSchema.js +28 -97
- package/lib/linter/groupSchema.js.map +1 -1
- package/lib/linter/lintProject.js +169 -118
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/printError.d.ts +2 -0
- package/lib/linter/printError.js +20 -0
- package/lib/linter/printError.js.map +1 -0
- package/lib/linter/segmentSchema.d.ts +14 -2
- package/lib/linter/segmentSchema.js +12 -10
- package/lib/linter/segmentSchema.js.map +1 -1
- package/lib/linter/testSchema.d.ts +90 -2
- package/lib/linter/testSchema.js +49 -38
- package/lib/linter/testSchema.js.map +1 -1
- package/lib/tester/cliFormat.d.ts +1 -0
- package/lib/tester/cliFormat.js +2 -1
- package/lib/tester/cliFormat.js.map +1 -1
- package/package.json +4 -4
- package/src/linter/attributeSchema.ts +11 -9
- package/src/linter/checkPercentageExceedingSlot.ts +41 -0
- package/src/linter/conditionSchema.ts +120 -97
- package/src/linter/featureSchema.ts +241 -177
- package/src/linter/groupSchema.ts +38 -54
- package/src/linter/lintProject.ts +144 -62
- package/src/linter/printError.ts +21 -0
- package/src/linter/segmentSchema.ts +10 -8
- package/src/linter/testSchema.ts +67 -50
- package/src/tester/cliFormat.ts +1 -0
- package/lib/linter/printJoiError.d.ts +0 -2
- package/lib/linter/printJoiError.js +0 -14
- package/lib/linter/printJoiError.js.map +0 -1
- package/src/linter/printJoiError.ts +0 -11
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import { ProjectConfig } from "../config";
|
|
4
4
|
|
|
5
5
|
const tagRegex = /^[a-z0-9-]+$/;
|
|
6
6
|
|
|
7
|
-
export function
|
|
7
|
+
export function getFeatureZodSchema(
|
|
8
8
|
projectConfig: ProjectConfig,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
conditionsZodSchema,
|
|
10
|
+
availableAttributeKeys: [string, ...string[]],
|
|
11
|
+
availableSegmentKeys: [string, ...string[]],
|
|
12
|
+
availableFeatureKeys: [string, ...string[]],
|
|
12
13
|
) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Joi.object().custom(function (value) {
|
|
14
|
+
const variationValueZodSchema = z.string().min(1);
|
|
15
|
+
const variableValueZodSchema = z.union([
|
|
16
|
+
z.string(),
|
|
17
|
+
z.number(),
|
|
18
|
+
z.boolean(),
|
|
19
|
+
z.array(z.string()),
|
|
20
|
+
z.record(z.unknown()).refine(
|
|
21
|
+
(value) => {
|
|
22
22
|
let isFlat = true;
|
|
23
23
|
|
|
24
24
|
Object.keys(value).forEach((key) => {
|
|
@@ -27,177 +27,241 @@ export function getFeatureJoiSchema(
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Joi.object({
|
|
44
|
-
and: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
|
|
45
|
-
}),
|
|
46
|
-
Joi.object({
|
|
47
|
-
or: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
|
|
48
|
-
}),
|
|
49
|
-
Joi.object({
|
|
50
|
-
// @TODO: allow plainGroupSegment as well?
|
|
51
|
-
not: Joi.array().items(Joi.link("#andOrNotGroupSegment"), plainGroupSegment),
|
|
52
|
-
}),
|
|
53
|
-
)
|
|
54
|
-
.id("andOrNotGroupSegment");
|
|
55
|
-
|
|
56
|
-
const groupSegment = Joi.alternatives().try(andOrNotGroupSegment, plainGroupSegment);
|
|
57
|
-
|
|
58
|
-
const groupSegmentsJoiSchema = Joi.alternatives().try(
|
|
59
|
-
Joi.array().items(groupSegment),
|
|
60
|
-
groupSegment,
|
|
30
|
+
return isFlat;
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
message: "object is not flat",
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const plainGroupSegment = z.string().refine(
|
|
39
|
+
(value) => value === "*" || availableSegmentKeys.includes(value),
|
|
40
|
+
(value) => ({
|
|
41
|
+
message: `Unknown segment key "${value}"`,
|
|
42
|
+
}),
|
|
61
43
|
);
|
|
62
44
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
45
|
+
const andOrNotGroupSegment = z.union([
|
|
46
|
+
z
|
|
47
|
+
.object({
|
|
48
|
+
and: z.array(z.lazy(() => groupSegmentZodSchema)),
|
|
49
|
+
})
|
|
50
|
+
.strict(),
|
|
51
|
+
z
|
|
52
|
+
.object({
|
|
53
|
+
or: z.array(z.lazy(() => groupSegmentZodSchema)),
|
|
54
|
+
})
|
|
55
|
+
.strict(),
|
|
56
|
+
z
|
|
57
|
+
.object({
|
|
58
|
+
not: z.array(z.lazy(() => groupSegmentZodSchema)),
|
|
59
|
+
})
|
|
60
|
+
.strict(),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const groupSegmentZodSchema = z.union([andOrNotGroupSegment, plainGroupSegment]);
|
|
64
|
+
|
|
65
|
+
const groupSegmentsZodSchema = z.union([z.array(groupSegmentZodSchema), groupSegmentZodSchema]);
|
|
66
|
+
|
|
67
|
+
const environmentZodSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
expose: z
|
|
70
|
+
.union([
|
|
71
|
+
z.boolean(),
|
|
72
|
+
z.array(z.string().refine((value) => projectConfig.tags.includes(value))),
|
|
73
|
+
])
|
|
74
|
+
.optional(),
|
|
75
|
+
rules: z
|
|
76
|
+
.array(
|
|
77
|
+
z
|
|
78
|
+
.object({
|
|
79
|
+
key: z.string(),
|
|
80
|
+
description: z.string().optional(),
|
|
81
|
+
segments: groupSegmentsZodSchema,
|
|
82
|
+
percentage: z.number().min(0).max(100),
|
|
83
|
+
|
|
84
|
+
enabled: z.boolean().optional(),
|
|
85
|
+
variation: variationValueZodSchema.optional(),
|
|
86
|
+
variables: z.record(variableValueZodSchema).optional(),
|
|
87
|
+
})
|
|
88
|
+
.strict(),
|
|
89
|
+
)
|
|
90
|
+
.refine(
|
|
91
|
+
(value) => {
|
|
92
|
+
const keys = value.map((v) => v.key);
|
|
93
|
+
return keys.length === new Set(keys).size;
|
|
94
|
+
},
|
|
95
|
+
(value) => ({
|
|
96
|
+
message: "Duplicate rule keys found: " + value.map((v) => v.key).join(", "),
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
force: z
|
|
100
|
+
.array(
|
|
101
|
+
z.union([
|
|
102
|
+
z
|
|
103
|
+
.object({
|
|
104
|
+
segments: groupSegmentsZodSchema,
|
|
105
|
+
enabled: z.boolean().optional(),
|
|
106
|
+
variation: variationValueZodSchema.optional(),
|
|
107
|
+
variables: z.record(variableValueZodSchema).optional(),
|
|
108
|
+
})
|
|
109
|
+
.strict(),
|
|
110
|
+
z
|
|
111
|
+
.object({
|
|
112
|
+
conditions: conditionsZodSchema,
|
|
113
|
+
enabled: z.boolean().optional(),
|
|
114
|
+
variation: variationValueZodSchema.optional(),
|
|
115
|
+
variables: z.record(variableValueZodSchema).optional(),
|
|
116
|
+
})
|
|
117
|
+
.strict(),
|
|
118
|
+
]),
|
|
119
|
+
)
|
|
120
|
+
.optional(),
|
|
121
|
+
})
|
|
122
|
+
.strict();
|
|
95
123
|
|
|
96
124
|
const allEnvironmentsSchema = {};
|
|
97
125
|
projectConfig.environments.forEach((environmentKey) => {
|
|
98
|
-
allEnvironmentsSchema[environmentKey] =
|
|
126
|
+
allEnvironmentsSchema[environmentKey] = environmentZodSchema;
|
|
99
127
|
});
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
const allEnvironmentsZodSchema = z.object(allEnvironmentsSchema).strict();
|
|
129
|
+
|
|
130
|
+
const attributeKeyZodSchema = z.string().refine(
|
|
131
|
+
(value) => value === "*" || availableAttributeKeys.includes(value),
|
|
132
|
+
(value) => ({
|
|
133
|
+
message: `Unknown attribute "${value}"`,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const featureKeyZodSchema = z.string().refine(
|
|
138
|
+
(value) => availableFeatureKeys.includes(value),
|
|
139
|
+
(value) => ({
|
|
140
|
+
message: `Unknown feature "${value}"`,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
const featureZodSchema = z
|
|
145
|
+
.object({
|
|
146
|
+
archived: z.boolean().optional(),
|
|
147
|
+
deprecated: z.boolean().optional(),
|
|
148
|
+
description: z.string(),
|
|
149
|
+
tags: z
|
|
150
|
+
.array(
|
|
151
|
+
z.string().refine(
|
|
152
|
+
(value) => tagRegex.test(value),
|
|
153
|
+
(value) => ({
|
|
154
|
+
message: `Tag "${value}" must be lower cased and alphanumeric, and may contain hyphens.`,
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
.refine(
|
|
159
|
+
(value) => {
|
|
160
|
+
return value.length === new Set(value).size;
|
|
161
|
+
},
|
|
162
|
+
(value) => ({
|
|
163
|
+
message: "Duplicate tags found: " + value.join(", "),
|
|
129
164
|
}),
|
|
130
165
|
),
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
166
|
+
required: z
|
|
167
|
+
.array(
|
|
168
|
+
z.union([
|
|
169
|
+
featureKeyZodSchema,
|
|
170
|
+
z
|
|
171
|
+
.object({
|
|
172
|
+
key: featureKeyZodSchema,
|
|
173
|
+
variation: z.string().optional(),
|
|
174
|
+
})
|
|
175
|
+
.strict(),
|
|
176
|
+
]),
|
|
177
|
+
)
|
|
178
|
+
.optional(),
|
|
179
|
+
bucketBy: z.union([
|
|
180
|
+
attributeKeyZodSchema,
|
|
181
|
+
z.array(attributeKeyZodSchema),
|
|
182
|
+
z
|
|
183
|
+
.object({
|
|
184
|
+
or: z.array(attributeKeyZodSchema),
|
|
185
|
+
})
|
|
186
|
+
.strict(),
|
|
187
|
+
]),
|
|
188
|
+
variablesSchema: z
|
|
189
|
+
.array(
|
|
190
|
+
z
|
|
191
|
+
.object({
|
|
192
|
+
key: z
|
|
193
|
+
.string()
|
|
194
|
+
.min(1)
|
|
195
|
+
.refine((value) => value !== "variation", {
|
|
196
|
+
message: `variable key cannot be "variation"`,
|
|
197
|
+
}),
|
|
198
|
+
type: z.enum(["string", "integer", "boolean", "double", "array", "object", "json"]),
|
|
199
|
+
description: z.string().optional(),
|
|
200
|
+
defaultValue: variableValueZodSchema,
|
|
201
|
+
})
|
|
202
|
+
.strict(),
|
|
203
|
+
)
|
|
204
|
+
.refine(
|
|
205
|
+
(value) => {
|
|
206
|
+
const keys = value.map((v) => v.key);
|
|
207
|
+
return keys.length === new Set(keys).size;
|
|
208
|
+
},
|
|
209
|
+
(value) => ({
|
|
210
|
+
message: "Duplicate variable keys found: " + value.map((v) => v.key).join(", "),
|
|
211
|
+
}),
|
|
212
|
+
)
|
|
213
|
+
.optional(),
|
|
214
|
+
variations: z
|
|
215
|
+
.array(
|
|
216
|
+
z
|
|
217
|
+
.object({
|
|
218
|
+
description: z.string().optional(),
|
|
219
|
+
value: variationValueZodSchema,
|
|
220
|
+
weight: z.number().min(0).max(100),
|
|
221
|
+
variables: z
|
|
222
|
+
.array(
|
|
223
|
+
z
|
|
224
|
+
.object({
|
|
225
|
+
key: z.string().min(1),
|
|
226
|
+
value: variableValueZodSchema,
|
|
227
|
+
overrides: z
|
|
228
|
+
.array(
|
|
229
|
+
z.union([
|
|
230
|
+
z
|
|
231
|
+
.object({
|
|
232
|
+
conditions: conditionsZodSchema,
|
|
233
|
+
value: variableValueZodSchema,
|
|
234
|
+
})
|
|
235
|
+
.strict(),
|
|
236
|
+
z
|
|
237
|
+
.object({
|
|
238
|
+
segments: groupSegmentsZodSchema,
|
|
239
|
+
value: variableValueZodSchema,
|
|
240
|
+
})
|
|
241
|
+
.strict(),
|
|
242
|
+
]),
|
|
243
|
+
)
|
|
244
|
+
.optional(),
|
|
245
|
+
})
|
|
246
|
+
.strict(),
|
|
247
|
+
)
|
|
248
|
+
.optional(),
|
|
249
|
+
})
|
|
250
|
+
.strict(),
|
|
251
|
+
)
|
|
252
|
+
.refine(
|
|
253
|
+
(value) => {
|
|
254
|
+
const variationValues = value.map((v) => v.value);
|
|
255
|
+
return variationValues.length === new Set(variationValues).size;
|
|
256
|
+
},
|
|
257
|
+
(value) => ({
|
|
258
|
+
message: "Duplicate variation values found: " + value.map((v) => v.value).join(", "),
|
|
259
|
+
}),
|
|
260
|
+
)
|
|
261
|
+
.optional(),
|
|
262
|
+
environments: allEnvironmentsZodSchema,
|
|
263
|
+
})
|
|
264
|
+
.strict();
|
|
201
265
|
|
|
202
|
-
return
|
|
266
|
+
return featureZodSchema;
|
|
203
267
|
}
|
|
@@ -1,63 +1,47 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import { ProjectConfig } from "../config";
|
|
4
4
|
import { Datasource } from "../datasource";
|
|
5
5
|
|
|
6
|
-
export function
|
|
6
|
+
export function getGroupZodSchema(
|
|
7
7
|
projectConfig: ProjectConfig,
|
|
8
8
|
datasource: Datasource,
|
|
9
9
|
availableFeatureKeys: string[],
|
|
10
10
|
) {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// @TODO: this does not help with same feature belonging to multiple slots. fix that.
|
|
48
|
-
throw new Error(
|
|
49
|
-
`Feature ${featureKey}'s rule ${rule.key} in ${environmentKey} has a percentage of ${rule.percentage} which is greater than the maximum percentage of ${maxPercentageForRule} for the slot`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return value;
|
|
58
|
-
})
|
|
59
|
-
.required(),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return groupJoiSchema;
|
|
11
|
+
const groupZodSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
description: z.string(),
|
|
14
|
+
slots: z
|
|
15
|
+
.array(
|
|
16
|
+
z
|
|
17
|
+
.object({
|
|
18
|
+
feature: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.refine(
|
|
22
|
+
(value) => {
|
|
23
|
+
if (value && availableFeatureKeys.indexOf(value) === -1) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
(value) => ({ message: `Unknown feature "${value}"` }),
|
|
30
|
+
),
|
|
31
|
+
percentage: z.number().min(0).max(100),
|
|
32
|
+
})
|
|
33
|
+
.strict(),
|
|
34
|
+
)
|
|
35
|
+
.refine(
|
|
36
|
+
(value) => {
|
|
37
|
+
const totalPercentage = value.reduce((acc, slot) => acc + slot.percentage, 0);
|
|
38
|
+
|
|
39
|
+
return totalPercentage === 100;
|
|
40
|
+
},
|
|
41
|
+
{ message: "Total percentage of all slots is not 100" },
|
|
42
|
+
),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
|
|
46
|
+
return groupZodSchema;
|
|
63
47
|
}
|