@travetto/schema 6.0.1 → 7.0.0-rc.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.
@@ -0,0 +1,340 @@
1
+ import type { RegistryAdapter } from '@travetto/registry';
2
+ import { AppError, castKey, castTo, Class, describeFunction, safeAssign } from '@travetto/runtime';
3
+
4
+ import {
5
+ SchemaClassConfig, SchemaMethodConfig, SchemaFieldConfig,
6
+ SchemaParameterConfig, SchemaInputConfig, SchemaFieldMap, SchemaCoreConfig,
7
+ CONSTRUCTOR_PROPERTY
8
+ } from './types';
9
+
10
+ const classToDiscriminatedType = (cls: Class): string => cls.name
11
+ .replace(/([A-Z])([A-Z][a-z])/g, (all, l, r) => `${l}_${r.toLowerCase()}`)
12
+ .replace(/([a-z]|\b)([A-Z])/g, (all, l, r) => l ? `${l}_${r.toLowerCase()}` : r.toLowerCase())
13
+ .toLowerCase();
14
+
15
+ function assignMetadata<T>(key: symbol, base: SchemaCoreConfig, data: Partial<T>[]): T {
16
+ const md = base.metadata ??= {};
17
+ const out = md[key] ??= {};
18
+ for (const d of data) {
19
+ safeAssign(out, d);
20
+ }
21
+ return castTo(out);
22
+ }
23
+
24
+ function combineCore<T extends SchemaCoreConfig>(base: T, config: Partial<T>): T {
25
+ return safeAssign(base, {
26
+ ...config.metadata ? { metadata: { ...base.metadata, ...config.metadata } } : {},
27
+ ...config.private ? { private: config.private ?? base.private } : {},
28
+ ...config.description ? { description: config.description || base.description } : {},
29
+ ...config.examples ? { examples: [...(base.examples ?? []), ...(config.examples ?? [])] } : {},
30
+ });
31
+ }
32
+
33
+ function combineInputs<T extends SchemaInputConfig>(base: T, configs: Partial<T>[]): T {
34
+ for (const config of configs) {
35
+ safeAssign(base, {
36
+ ...config,
37
+ ...config.aliases ? { aliases: [...base.aliases ?? [], ...config.aliases ?? []] } : {},
38
+ ...config.specifiers ? { specifiers: [...base.specifiers ?? [], ...config.specifiers ?? []] } : {},
39
+ ...config.enum ? {
40
+ enum: {
41
+ message: config.enum?.message ?? base.enum?.message,
42
+ values: (config.enum?.values ?? base.enum?.values ?? []).toSorted()
43
+ }
44
+ } : {},
45
+ });
46
+ combineCore(base, config);
47
+ }
48
+ return base;
49
+ }
50
+
51
+ function combineMethods<T extends SchemaMethodConfig>(base: T, configs: Partial<T>[]): T {
52
+ for (const config of configs) {
53
+ safeAssign(base, {
54
+ ...config,
55
+ parameters: config.parameters ?? base.parameters,
56
+ validators: [...base.validators, ...(config.validators ?? [])],
57
+ });
58
+ combineCore(base, config);
59
+ if (config.parameters) {
60
+ for (const param of config.parameters) {
61
+ safeAssign(base.parameters[param.index], param);
62
+ }
63
+ }
64
+ }
65
+ return base;
66
+ }
67
+
68
+ function getConstructorConfig<T extends SchemaClassConfig>(base: Partial<T>, parent?: Partial<T>): SchemaMethodConfig {
69
+ const parentCons = parent?.methods?.[CONSTRUCTOR_PROPERTY];
70
+ const baseCons = base.methods?.[CONSTRUCTOR_PROPERTY];
71
+ return {
72
+ parameters: [],
73
+ validators: [],
74
+ ...parentCons,
75
+ ...baseCons,
76
+ returnType: { type: base.class! }
77
+ };
78
+ }
79
+
80
+ function combineClassWithParent<T extends SchemaClassConfig>(base: T, parent: T): T {
81
+ safeAssign(base, {
82
+ ...base.views ? { views: { ...parent.views, ...base.views } } : {},
83
+ ...base.validators ? { validators: [...parent.validators, ...base.validators] } : {},
84
+ ...base.metadata ? { metadata: { ...parent.metadata, ...base.metadata } } : {},
85
+ interfaces: [...parent.interfaces, ...base.interfaces],
86
+ methods: { ...parent.methods, ...base.methods },
87
+ description: base.description || parent.description,
88
+ examples: [...(parent.examples ?? []), ...(base.examples ?? [])],
89
+ discriminatedField: base.discriminatedField ?? parent.discriminatedField,
90
+ });
91
+ switch (base.mappedOperation) {
92
+ case 'Required':
93
+ case 'Partial': {
94
+ base.fields = Object.fromEntries(
95
+ Object.entries(base.fields).map(([k, v]) => [k, {
96
+ ...v,
97
+ required: {
98
+ active: base.mappedOperation === 'Required'
99
+ }
100
+ }])
101
+ );
102
+ break;
103
+ }
104
+ case 'Pick':
105
+ case 'Omit': {
106
+ const keys = new Set<string>(base.mappedFields ?? []);
107
+ base.fields = Object.fromEntries(
108
+ Object.entries(parent.fields).filter(([k]) =>
109
+ base.mappedOperation === 'Pick' ? keys.has(k) : !keys.has(k)
110
+ )
111
+ );
112
+ break;
113
+ }
114
+ default: {
115
+ base.fields = { ...parent.fields, ...base.fields };
116
+ }
117
+ }
118
+ return base;
119
+ }
120
+
121
+ function combineClasses<T extends SchemaClassConfig>(base: T, configs: Partial<T>[]): T {
122
+ for (const config of configs) {
123
+ Object.assign(base, {
124
+ ...config,
125
+ ...config.views ? { views: { ...base.views, ...config.views } } : {},
126
+ ...config.validators ? { validators: [...base.validators, ...config.validators] } : {},
127
+ interfaces: [...base.interfaces, ...(config.interfaces ?? [])],
128
+ methods: { ...base.methods, ...config.methods },
129
+ fields: { ...base.fields, ...config.fields },
130
+ });
131
+ combineCore(base, config);
132
+ }
133
+ return base;
134
+ }
135
+
136
+ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig> {
137
+
138
+ #cls: Class;
139
+ #config: SchemaClassConfig;
140
+ #views: Map<string, SchemaFieldMap> = new Map();
141
+ #accessorDescriptors: Map<string, PropertyDescriptor> = new Map();
142
+
143
+ constructor(cls: Class) {
144
+ this.#cls = cls;
145
+ }
146
+
147
+ register(...data: Partial<SchemaClassConfig>[]): SchemaClassConfig {
148
+ const cfg = this.#config ??= {
149
+ methods: {},
150
+ class: this.#cls,
151
+ views: {},
152
+ validators: [],
153
+ interfaces: [],
154
+ fields: {},
155
+ };
156
+ return combineClasses(cfg, data);
157
+ }
158
+
159
+ registerMetadata<T>(key: symbol, ...data: Partial<T>[]): T {
160
+ const cfg = this.register({});
161
+ return assignMetadata(key, cfg, data);
162
+ }
163
+
164
+ getMetadata<T>(key: symbol): T | undefined {
165
+ const md = this.#config?.metadata;
166
+ return castTo<T>(md?.[key]);
167
+ }
168
+
169
+ registerField(field: string | symbol, ...data: Partial<SchemaFieldConfig>[]): SchemaFieldConfig {
170
+ const config = this.register({});
171
+ const cfg = config.fields[field] ??= { name: field, owner: this.#cls, type: null! };
172
+ const combined = combineInputs(cfg, data);
173
+ return combined;
174
+ }
175
+
176
+ registerFieldMetadata<T>(field: string | symbol, key: symbol, ...data: Partial<T>[]): T {
177
+ const cfg = this.registerField(field);
178
+ return assignMetadata(key, cfg, data);
179
+ }
180
+
181
+ getFieldMetadata<T>(field: string | symbol, key: symbol): T | undefined {
182
+ const md = this.#config?.fields[field]?.metadata;
183
+ return castTo<T>(md?.[key]);
184
+ }
185
+
186
+ registerClass({ methods, ...cfg }: Partial<SchemaClassConfig> = {}): SchemaClassConfig {
187
+ this.register({ ...cfg });
188
+ if (methods?.[CONSTRUCTOR_PROPERTY]) {
189
+ const { parameters, ...rest } = methods[CONSTRUCTOR_PROPERTY];
190
+ this.registerMethod(CONSTRUCTOR_PROPERTY, rest);
191
+ for (const param of parameters ?? []) {
192
+ this.registerParameter(CONSTRUCTOR_PROPERTY, param.index!, param);
193
+ }
194
+ }
195
+ return this.#config;
196
+ }
197
+
198
+ registerMethod(method: string | symbol, ...data: Partial<SchemaMethodConfig>[]): SchemaMethodConfig {
199
+ const config = this.register();
200
+ const cfg = config.methods[method] ??= { parameters: [], validators: [] };
201
+ return combineMethods(cfg, data);
202
+ }
203
+
204
+ registerMethodMetadata<T>(method: string | symbol, key: symbol, ...data: Partial<T>[]): T {
205
+ const cfg = this.registerMethod(method);
206
+ return assignMetadata(key, cfg, data);
207
+ }
208
+
209
+ getMethodMetadata<T>(method: string | symbol, key: symbol): T | undefined {
210
+ const md = this.#config?.methods[method]?.metadata;
211
+ return castTo<T>(md?.[key]);
212
+ }
213
+
214
+ registerParameter(method: string | symbol, idx: number, ...data: Partial<SchemaParameterConfig>[]): SchemaParameterConfig {
215
+ const params = this.registerMethod(method, {}).parameters;
216
+ const cfg = params[idx] ??= { method, index: idx, owner: this.#cls, array: false, type: null! };
217
+ return combineInputs(cfg, data);
218
+ }
219
+
220
+ registerParameterMetadata<T>(method: string | symbol, idx: number, key: symbol, ...data: Partial<T>[]): T {
221
+ const cfg = this.registerParameter(method, idx);
222
+ return assignMetadata(key, cfg, data);
223
+ }
224
+
225
+ getParameterMetadata<T>(method: string | symbol, idx: number, key: symbol): T | undefined {
226
+ const md = this.#config?.methods[method]?.parameters[idx]?.metadata;
227
+ return castTo<T>(md?.[key]);
228
+ }
229
+
230
+ finalize(parent?: SchemaClassConfig): void {
231
+ const config = this.#config;
232
+
233
+ if (parent) {
234
+ combineClassWithParent(config, parent);
235
+ }
236
+
237
+ if (config.discriminatedField && !config.discriminatedType && !describeFunction(this.#cls).abstract) {
238
+ config.discriminatedType = classToDiscriminatedType(this.#cls);
239
+ }
240
+
241
+ if (config.discriminatedField && config.discriminatedType) {
242
+ config.fields[config.discriminatedField] = {
243
+ ...config.fields[config.discriminatedField], // Make a full copy
244
+ required: {
245
+ active: false
246
+ },
247
+ enum: {
248
+ values: [config.discriminatedType],
249
+ message: `${config.discriminatedField} can only be '${config.discriminatedType}'`,
250
+ },
251
+ };
252
+ }
253
+
254
+ // Compute views on install
255
+ for (const view of Object.keys(config.views)) {
256
+ const fields = config.views[view];
257
+ const withoutSet = 'without' in fields ? new Set<string>(fields.without) : undefined;
258
+ const fieldList = withoutSet ?
259
+ Object.keys(config.fields).filter(x => !withoutSet.has(x)) :
260
+ ('with' in fields ? fields.with : []);
261
+
262
+ this.#views.set(view,
263
+ fieldList.reduce<SchemaFieldMap>((acc, v) => {
264
+ acc[v] = config.fields[v];
265
+ return acc;
266
+ }, {})
267
+ );
268
+ }
269
+
270
+ config.methods[CONSTRUCTOR_PROPERTY] = getConstructorConfig(config, parent);
271
+
272
+ for (const method of Object.values(config.methods)) {
273
+ method.parameters = method.parameters.toSorted((a, b) => (a.index! - b.index!));
274
+ }
275
+ }
276
+
277
+ get(): SchemaClassConfig {
278
+ return this.#config;
279
+ }
280
+
281
+ getField(field: string | symbol): SchemaFieldConfig {
282
+ return this.#config.fields[field];
283
+ }
284
+
285
+ getMethod(method: string | symbol): SchemaMethodConfig {
286
+ const res = this.#config.methods[method];
287
+ if (!res) {
288
+ throw new AppError(`Unknown method ${String(method)} on class ${this.#cls.Ⲑid}`);
289
+ }
290
+ return res;
291
+ }
292
+
293
+ getMethodReturnType(method: string | symbol): Class {
294
+ return this.getMethod(method).returnType!.type;
295
+ }
296
+
297
+ getSchema(view?: string): SchemaFieldMap {
298
+ if (!view) {
299
+ return this.#config.fields;
300
+ }
301
+ if (!this.#views.has(view)) {
302
+ throw new AppError(`Unknown view ${view} for class ${this.#cls.Ⲑid}`);
303
+ }
304
+ return this.#views.get(view)!;
305
+ }
306
+
307
+ /**
308
+ * Provides the prototype-derived descriptor for a property
309
+ */
310
+ getAccessorDescriptor(field: string): PropertyDescriptor {
311
+ if (!this.#accessorDescriptors.has(field)) {
312
+ let proto = this.#cls.prototype;
313
+ while (proto && !Object.hasOwn(proto, field)) {
314
+ proto = proto.prototype;
315
+ }
316
+ this.#accessorDescriptors.set(field, Object.getOwnPropertyDescriptor(proto, field)!);
317
+ }
318
+ return this.#accessorDescriptors.get(field)!;
319
+ }
320
+
321
+ /**
322
+ * Ensure type is set properly
323
+ */
324
+ ensureInstanceTypeField<T>(o: T): T {
325
+ const config = this.getDiscriminatedConfig();
326
+ if (config) {
327
+ const typeField = castKey<T>(config.discriminatedField);
328
+ o[typeField] ??= castTo(config.discriminatedType); // Assign if missing
329
+ }
330
+ return o;
331
+ }
332
+
333
+ getDiscriminatedConfig(): Required<Pick<SchemaClassConfig, 'discriminatedType' | 'discriminatedField' | 'discriminatedBase'>> | undefined {
334
+ const { discriminatedField, discriminatedType, discriminatedBase } = this.#config;
335
+ if (discriminatedType && discriminatedField) {
336
+ return { discriminatedType, discriminatedField, discriminatedBase: !!discriminatedBase };
337
+ }
338
+ return undefined;
339
+ }
340
+ }
@@ -0,0 +1,240 @@
1
+ import { ChangeEvent, RegistrationMethods, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
2
+ import { AppError, castKey, castTo, Class, classConstruct, getParentClass, Util } from '@travetto/runtime';
3
+
4
+ import { SchemaFieldConfig, SchemaClassConfig, SchemaFieldMap, SchemaMethodConfig } from './types.ts';
5
+ import { SchemaRegistryAdapter } from './registry-adapter.ts';
6
+ import { SchemaChangeListener } from './changes.ts';
7
+
8
+ /**
9
+ * Schema registry index for managing schema configurations across classes
10
+ */
11
+ export class SchemaRegistryIndex implements RegistryIndex {
12
+
13
+ static #instance = Registry.registerIndex(SchemaRegistryIndex);
14
+
15
+ static getForRegister(cls: Class, allowFinalized = false): SchemaRegistryAdapter {
16
+ return this.#instance.store.getForRegister(cls, allowFinalized);
17
+ }
18
+
19
+ static getConfig(cls: Class): SchemaClassConfig {
20
+ return this.#instance.store.get(cls).get();
21
+ }
22
+
23
+ static getDiscriminatedConfig<T>(cls: Class<T>): Required<Pick<SchemaClassConfig, 'discriminatedType' | 'discriminatedField' | 'discriminatedBase'>> | undefined {
24
+ return this.#instance.store.get(cls).getDiscriminatedConfig();
25
+ }
26
+
27
+ static getFieldMap(cls: Class, view?: string): SchemaFieldMap {
28
+ return this.#instance.store.get(cls).getSchema(view);
29
+ }
30
+
31
+ static getMethodConfig(cls: Class, method: string | symbol): SchemaMethodConfig {
32
+ return this.#instance.store.get(cls).getMethod(method);
33
+ }
34
+
35
+ static has(cls: Class): boolean {
36
+ return this.#instance.store.has(cls);
37
+ }
38
+
39
+ static getClassById(classId: string): Class {
40
+ return this.#instance.store.getClassById(classId);
41
+ }
42
+
43
+ static getDiscriminatedTypes(cls: Class): string[] | undefined {
44
+ return this.#instance.getDiscriminatedTypes(cls);
45
+ }
46
+
47
+ static resolveInstanceType<T>(cls: Class<T>, o: T): Class {
48
+ return this.#instance.resolveInstanceType(cls, o);
49
+ }
50
+
51
+ static visitFields<T>(cls: Class<T>, onField: (field: SchemaFieldConfig, path: SchemaFieldConfig[]) => void): void {
52
+ return this.#instance.visitFields(cls, onField);
53
+ }
54
+
55
+ static getDiscriminatedClasses(cls: Class): Class[] {
56
+ return this.#instance.getDiscriminatedClasses(cls);
57
+ }
58
+
59
+ static getBaseClass(cls: Class): Class {
60
+ return this.#instance.getBaseClass(cls);
61
+ }
62
+
63
+ static get(cls: Class): Omit<SchemaRegistryAdapter, RegistrationMethods> {
64
+ return this.#instance.store.get(cls);
65
+ }
66
+
67
+ static getOptionalConfig(cls: Class): SchemaClassConfig | undefined {
68
+ return this.#instance.store.getOptional(cls)?.get();
69
+ }
70
+
71
+ static getClasses(): Class[] {
72
+ return this.#instance.store.getClasses();
73
+ }
74
+
75
+ store = new RegistryIndexStore(SchemaRegistryAdapter);
76
+ #baseSchema = new Map<Class, Class>();
77
+ #byDiscriminatedTypes = new Map<Class, Map<string, Class>>();
78
+
79
+ /**
80
+ * Register discriminated types for a class
81
+ */
82
+ #registerDiscriminatedTypes(cls: Class): void {
83
+ // Mark as subtype
84
+ const config = this.getClassConfig(cls);
85
+ if (!config.discriminatedType) {
86
+ return;
87
+ }
88
+ const base = this.getBaseClass(cls);
89
+ if (!this.#byDiscriminatedTypes.has(base)) {
90
+ this.#byDiscriminatedTypes.set(base, new Map());
91
+ }
92
+ this.#byDiscriminatedTypes.get(base)!.set(config.discriminatedType, cls);
93
+ }
94
+
95
+ #onChanged(event: ChangeEvent<Class> & { type: 'changed' }): void {
96
+ Util.queueMacroTask().then(() => {
97
+ SchemaChangeListener.emitFieldChanges({
98
+ type: 'changed',
99
+ curr: this.getClassConfig(event.curr),
100
+ prev: this.getClassConfig(event.prev)
101
+ });
102
+ });
103
+ }
104
+
105
+ #onRemoving(event: ChangeEvent<Class> & { type: 'removing' }): void {
106
+ SchemaChangeListener.clearSchemaDependency(event.prev);
107
+ }
108
+
109
+ #onAdded(event: ChangeEvent<Class> & { type: 'added' }): void {
110
+ Util.queueMacroTask().then(() => {
111
+ SchemaChangeListener.emitFieldChanges({
112
+ type: 'added',
113
+ curr: this.getClassConfig(event.curr)
114
+ });
115
+ });
116
+ }
117
+
118
+ process(events: ChangeEvent<Class>[]): void {
119
+ for (const event of events) {
120
+ if (event.type === 'changed') {
121
+ this.#onChanged(event);
122
+ } else if (event.type === 'removing') {
123
+ this.#onRemoving(event);
124
+ } else if (event.type === 'added') {
125
+ this.#onAdded(event);
126
+ }
127
+ }
128
+
129
+ // Rebuild indices after every "process" batch
130
+ this.#byDiscriminatedTypes.clear();
131
+ for (const el of this.store.getClasses()) {
132
+ this.#registerDiscriminatedTypes(el);
133
+ }
134
+ }
135
+
136
+ getClassConfig(cls: Class): SchemaClassConfig {
137
+ return this.store.get(cls).get();
138
+ }
139
+
140
+ /**
141
+ * Find base schema class for a given class
142
+ */
143
+ getBaseClass(cls: Class): Class {
144
+ if (!this.#baseSchema.has(cls)) {
145
+ let conf = this.getClassConfig(cls);
146
+ let parent: Class | undefined = cls;
147
+ while (parent && conf.discriminatedType && !conf.discriminatedBase) {
148
+ parent = getParentClass(parent);
149
+ if (parent) {
150
+ conf = this.store.getOptional(parent)?.get() ?? conf;
151
+ }
152
+ }
153
+ this.#baseSchema.set(cls, conf.class);
154
+ }
155
+ return this.#baseSchema.get(cls)!;
156
+ }
157
+
158
+ /**
159
+ * Find the resolved type for a given instance
160
+ * @param cls Class for instance
161
+ * @param o Actual instance
162
+ */
163
+ resolveInstanceType<T>(requestedCls: Class<T>, o: T): Class {
164
+ const cls = this.store.getClassById(requestedCls.Ⲑid); // Resolve by id to handle any stale references
165
+ const adapter = this.store.get(cls);
166
+ const { discriminatedField, discriminatedType } = adapter.get();
167
+ if (!discriminatedField) {
168
+ return cls;
169
+ } else {
170
+ const base = this.getBaseClass(cls);
171
+ const map = this.#byDiscriminatedTypes.get(base);
172
+ const type = castTo<string>(o[castKey<T>(discriminatedField)]) ?? discriminatedType;
173
+ if (!type) {
174
+ throw new AppError(`Unable to resolve discriminated type for class ${base.name} without a type`);
175
+ }
176
+ if (!map?.has(type)) {
177
+ throw new AppError(`Unable to resolve discriminated type '${type}' for class ${base.name}`);
178
+ }
179
+ const requested = map.get(type)!;
180
+ if (!(classConstruct(requested) instanceof requestedCls)) {
181
+ throw new AppError(`Resolved discriminated type '${type}' for class ${base.name} is not an instance of requested type ${requestedCls.name}`);
182
+ }
183
+ return requested;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Track changes to schemas, and track the dependent changes
189
+ * @param cls The root class of the hierarchy
190
+ * @param curr The new class
191
+ * @param path The path within the object hierarchy
192
+ */
193
+ trackSchemaDependencies(cls: Class, curr: Class = cls, path: SchemaFieldConfig[] = []): void {
194
+ const config = this.getClassConfig(curr);
195
+
196
+ SchemaChangeListener.trackSchemaDependency(curr, cls, path, this.getClassConfig(cls));
197
+
198
+ // Read children
199
+ for (const field of Object.values(config.fields)) {
200
+ if (SchemaRegistryIndex.has(field.type) && field.type !== cls) {
201
+ this.trackSchemaDependencies(cls, field.type, [...path, field]);
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Visit fields recursively
208
+ */
209
+ visitFields<T>(cls: Class<T>, onField: (field: SchemaFieldConfig, path: SchemaFieldConfig[]) => void, _path: SchemaFieldConfig[] = [], root = cls): void {
210
+ const fields = SchemaRegistryIndex.has(cls) ?
211
+ Object.values(this.getClassConfig(cls).fields) :
212
+ [];
213
+ for (const field of fields) {
214
+ if (SchemaRegistryIndex.has(field.type)) {
215
+ this.visitFields(field.type, onField, [..._path, field], root);
216
+ } else {
217
+ onField(field, _path);
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Return all subtypes by discriminator for a given class
224
+ * @param cls The base class to resolve from
225
+ */
226
+ getDiscriminatedClasses(cls: Class): Class[] {
227
+ return [...this.#byDiscriminatedTypes.get(cls)?.values() ?? []];
228
+ }
229
+
230
+ /**
231
+ * Get all discriminated types for a given class
232
+ */
233
+ getDiscriminatedTypes(cls: Class): string[] | undefined {
234
+ const map = this.#byDiscriminatedTypes.get(cls);
235
+ if (map) {
236
+ return [...map.keys()];
237
+ }
238
+ return undefined;
239
+ }
240
+ }