Skip to content

Commit c913c5c

Browse files
authored
Merge pull request #66 from Gnuxie/gnuxie/persistent-config-protection-settings
When we decided to go with the config recovery in the safe mode work the-draupnir-project/planning#1, we also decided we would unify the wrappers used for all persistent config to be based on a system that leveraged TypeBox to provide us with schema. So this PR replaces `ProtectionSettings` with the new `ConfigDescription` system. This branch was written and linked with the work to migrate draupnir's protection's to the new system too. This slightly longer because the types are not liberal enough in what they accept when they are describing interfaces. This is a general bitch throughout the code base and I don't really know how to address it yet. Basically we need to allow interfaces for things like the methods on the `ConfigMirror` to accept arguments liberally. But the implementation needs to be as defensive as `unknown` etc. It's my fault for not really understanding variance properly. There was also an issue where the command reader from Draupnir's interface-manager produced objects that are different to the transform types in the `ConfigDescription` schema. So we had to update the `ConfigMirror` to accept serialized representations of values as a workaround. Not a big deal but it's a thing. Aside from that I've been at a low point these past weeks.
2 parents 3df5866 + 8f44e25 commit c913c5c

23 files changed

+568
-817
lines changed

src/Config/ConfigDescription.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// SPDX-License-Identifier: AFL-3.0
44

5-
import { TObject, TProperties, TSchema } from '@sinclair/typebox';
5+
import { TObject, TProperties, TSchema, Type } from '@sinclair/typebox';
66
import { EDStatic } from '../Interface/Static';
77
import { Ok, Result } from '@gnuxie/typescript-result';
88
import { Value as TBValue } from '@sinclair/typebox/value';
@@ -17,11 +17,16 @@ export type UnknownProperties<T extends TSchema> = {
1717
[K in keyof StaticProperties<T>]: unknown;
1818
};
1919

20+
export const UnknownConfig = Type.Object({}, { additionalProperties: true });
21+
export type UnknownConfig = typeof UnknownConfig;
22+
2023
export type ConfigPropertyDescription = {
2124
path: string;
2225
name: string;
2326
description: string | undefined;
2427
default: unknown;
28+
isArray: boolean;
29+
isUniqueItems: boolean;
2530
};
2631

2732
export type ConfigDescription<TConfigSchema extends TObject = TObject> = {
@@ -80,6 +85,8 @@ export class StandardConfigDescription<TConfigSchema extends TObject>
8085
path: '/' + name,
8186
description: schema.description,
8287
default: schema.default as unknown,
88+
isUniqueItems: 'uniqueItems' in schema && schema.uniqueItems === true,
89+
isArray: 'items' in schema,
8390
}));
8491
}
8592

@@ -93,6 +100,8 @@ export class StandardConfigDescription<TConfigSchema extends TObject>
93100
path: '/' + key,
94101
description: schema.description,
95102
default: schema.default as unknown,
103+
isUniqueItems: 'uniqueItems' in schema && schema.uniqueItems === true,
104+
isArray: 'items' in schema,
96105
};
97106
}
98107

src/Config/ConfigMirror.ts

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,33 @@ import {
1212
import { ConfigDescription } from './ConfigDescription';
1313
import { EDStatic } from '../Interface/Static';
1414
import { ConfigPropertyError } from './ConfigParseError';
15-
import { Ok, Result } from '@gnuxie/typescript-result';
15+
import { Ok, Result, isError } from '@gnuxie/typescript-result';
1616
import { Value as TBValue } from '@sinclair/typebox/value';
17+
import { Value } from '../Interface/Value';
1718

19+
// We should really have a conditional type here for unknown config.
1820
export interface ConfigMirror<TConfigSchema extends TObject = TObject> {
1921
readonly description: ConfigDescription<TConfigSchema>;
20-
setValue(
22+
setValue<TKey extends string>(
2123
config: EDStatic<TConfigSchema>,
22-
key: keyof EDStatic<TConfigSchema>,
24+
key: TKey,
2325
value: unknown
2426
): Result<EDStatic<TConfigSchema>, ConfigPropertyError>;
25-
addItem(
27+
setSerializedValue<TKey extends string>(
28+
config: EDStatic<TConfigSchema>,
29+
key: TKey,
30+
value: string
31+
): Result<EDStatic<TConfigSchema>, ConfigPropertyError>;
32+
addItem<TKey extends string>(
2633
config: EDStatic<TConfigSchema>,
27-
key: keyof EDStatic<TConfigSchema>,
34+
key: TKey,
2835
value: unknown
2936
): Result<EDStatic<TConfigSchema>, ConfigPropertyError>;
37+
addSerializedItem<TKey extends string>(
38+
config: EDStatic<TConfigSchema>,
39+
key: TKey,
40+
value: string
41+
): Result<EDStatic<TConfigSchema>, ConfigPropertyError>;
3042
// needed for when additionalProperties is true.
3143
removeProperty<TKey extends string>(
3244
key: TKey,
@@ -54,7 +66,7 @@ export class StandardConfigMirror<TConfigSchema extends TObject>
5466
}
5567
setValue(
5668
config: Evaluate<StaticDecode<TConfigSchema>>,
57-
key: keyof Evaluate<StaticDecode<TConfigSchema>>,
69+
key: string,
5870
value: unknown
5971
): Result<Evaluate<StaticDecode<TConfigSchema>>, ConfigPropertyError> {
6072
const schema = this.description.schema.properties[key as keyof TProperties];
@@ -108,7 +120,7 @@ export class StandardConfigMirror<TConfigSchema extends TObject>
108120
}
109121
addItem(
110122
config: Evaluate<StaticDecode<TConfigSchema>>,
111-
key: keyof Evaluate<StaticDecode<TConfigSchema>>,
123+
key: string,
112124
value: unknown
113125
): Result<Evaluate<StaticDecode<TConfigSchema>>, ConfigPropertyError> {
114126
const schema = this.description.schema.properties[key as keyof TProperties];
@@ -117,12 +129,15 @@ export class StandardConfigMirror<TConfigSchema extends TObject>
117129
`Property ${key.toString()} does not exist in schema`
118130
);
119131
}
120-
const currentItems = config[key];
132+
const currentItems = config[key as keyof EDStatic<TConfigSchema>];
121133
if (!Array.isArray(currentItems)) {
122134
throw new TypeError(`Property ${key.toString()} is not an array`);
123135
}
124136
const errors = [
125-
...TBValue.Errors(schema, [...(config[key] as unknown[]), value]),
137+
...TBValue.Errors(schema, [
138+
...(config[key as keyof EDStatic<TConfigSchema>] as unknown[]),
139+
value,
140+
]),
126141
];
127142
if (errors[0] !== undefined) {
128143
return ConfigPropertyError.Result(errors[0].message, {
@@ -131,7 +146,62 @@ export class StandardConfigMirror<TConfigSchema extends TObject>
131146
description: this.description as unknown as ConfigDescription,
132147
});
133148
}
134-
return Ok(this.addUnparsedItem(config, key, value));
149+
return Ok(
150+
this.addUnparsedItem(config, key as keyof EDStatic<TConfigSchema>, value)
151+
);
152+
}
153+
addSerializedItem<TKey extends string>(
154+
config: EDStatic<TConfigSchema>,
155+
key: TKey,
156+
value: string
157+
): Result<EDStatic<TConfigSchema>, ConfigPropertyError> {
158+
const propertySchema =
159+
this.description.schema.properties[key as keyof TProperties];
160+
if (propertySchema === undefined) {
161+
throw new TypeError(
162+
`Property ${key.toString()} does not exist in schema`
163+
);
164+
}
165+
if (!('items' in propertySchema)) {
166+
throw new TypeError(`Property ${key.toString()} is not an array`);
167+
}
168+
const itemSchema = (propertySchema as TArray).items;
169+
const decodeResult = Value.Decode(itemSchema, value);
170+
if (isError(decodeResult)) {
171+
return ConfigPropertyError.Result(decodeResult.error.message, {
172+
path: `/${key.toString()}`,
173+
value,
174+
description: this.description as unknown as ConfigDescription,
175+
});
176+
}
177+
return Ok(
178+
this.addUnparsedItem(
179+
config,
180+
key as unknown as keyof EDStatic<TConfigSchema>,
181+
decodeResult.ok
182+
)
183+
);
184+
}
185+
setSerializedValue<TKey extends string>(
186+
config: EDStatic<TConfigSchema>,
187+
key: TKey,
188+
value: string
189+
): Result<EDStatic<TConfigSchema>, ConfigPropertyError> {
190+
const schema = this.description.schema.properties[key as keyof TProperties];
191+
if (schema === undefined) {
192+
throw new TypeError(
193+
`Property ${key.toString()} does not exist in schema`
194+
);
195+
}
196+
const decodeResult = Value.Decode(schema, value);
197+
if (isError(decodeResult)) {
198+
return ConfigPropertyError.Result(decodeResult.error.message, {
199+
path: `/${key.toString()}`,
200+
value,
201+
description: this.description as unknown as ConfigDescription,
202+
});
203+
}
204+
return this.setValue(config, key, decodeResult.ok);
135205
}
136206
removeProperty<TKey extends string>(
137207
key: TKey,

src/Protection/Protection.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ import {
2020
StateChange,
2121
} from '../StateTracking/StateRevisionIssuer';
2222
import { ProtectedRoomsSet } from './ProtectedRoomsSet';
23-
import { UnknownSettings } from './ProtectionSettings/ProtectionSetting';
24-
import {
25-
ProtectionSettings,
26-
StandardProtectionSettings,
27-
} from './ProtectionSettings/ProtectionSettings';
2823
import {
2924
CapabilityInterfaceSet,
3025
CapabilityProviderSet,
@@ -42,6 +37,13 @@ import {
4237
MatrixRoomID,
4338
StringRoomID,
4439
} from '@the-draupnir-project/matrix-basic-types';
40+
import {
41+
ConfigDescription,
42+
StandardConfigDescription,
43+
UnknownConfig,
44+
} from '../Config/ConfigDescription';
45+
import { TObject, Type } from '@sinclair/typebox';
46+
import { EDStatic } from '../Interface/Static';
4547

4648
/**
4749
* @param description The description for the protection being constructed.
@@ -56,16 +58,16 @@ import {
5658
*/
5759
export type ProtectionFactoryMethod<
5860
Context = unknown,
59-
TSettings extends Record<string, unknown> = Record<string, unknown>,
61+
TConfigSchema extends TObject = UnknownConfig,
6062
TCapabilitySet extends CapabilitySet = CapabilitySet,
6163
> = (
62-
description: ProtectionDescription<Context, TSettings, TCapabilitySet>,
64+
description: ProtectionDescription<Context, TConfigSchema, TCapabilitySet>,
6365
protectedRoomsSet: ProtectedRoomsSet,
6466
context: Context,
6567
capabilities: TCapabilitySet,
66-
settings: TSettings
68+
settings: EDStatic<TConfigSchema>
6769
) => ActionResult<
68-
Protection<ProtectionDescription<Context, TSettings, TCapabilitySet>>
70+
Protection<ProtectionDescription<Context, TConfigSchema, TCapabilitySet>>
6971
>;
7072

7173
/**
@@ -74,14 +76,18 @@ export type ProtectionFactoryMethod<
7476
*/
7577
export interface ProtectionDescription<
7678
Context = unknown,
77-
TSettings extends UnknownSettings<string> = UnknownSettings<string>,
79+
TConfigSchema extends TObject = UnknownConfig,
7880
TCapabilitySet extends CapabilitySet = CapabilitySet,
7981
> {
8082
readonly name: string;
8183
readonly description: string;
8284
readonly capabilities: CapabilityInterfaceSet<TCapabilitySet>;
83-
readonly factory: ProtectionFactoryMethod<Context, TSettings, TCapabilitySet>;
84-
readonly protectionSettings: ProtectionSettings<TSettings>;
85+
readonly factory: ProtectionFactoryMethod<
86+
Context,
87+
TConfigSchema,
88+
TCapabilitySet
89+
>;
90+
readonly protectionSettings: ConfigDescription<TConfigSchema>;
8591
readonly defaultCapabilities: CapabilityProviderSet<TCapabilitySet>;
8692
}
8793

@@ -197,17 +203,20 @@ const PROTECTIONS = new Map<string, ProtectionDescription>();
197203

198204
export function registerProtection<
199205
Context = unknown,
200-
TSettings extends UnknownSettings<string> = UnknownSettings<string>,
206+
TConfigSchema extends TObject = UnknownConfig,
201207
TCapabilitySet extends CapabilitySet = CapabilitySet,
202208
>(
203-
description: ProtectionDescription<Context, TSettings, TCapabilitySet>
204-
): ProtectionDescription<Context, TSettings, TCapabilitySet> {
209+
description: ProtectionDescription<Context, TConfigSchema, TCapabilitySet>
210+
): ProtectionDescription<Context, TConfigSchema, TCapabilitySet> {
205211
if (PROTECTIONS.has(description.name)) {
206212
throw new TypeError(
207213
`There is already a protection registered with the name ${description.name}`
208214
);
209215
}
210-
PROTECTIONS.set(description.name, description as ProtectionDescription);
216+
PROTECTIONS.set(
217+
description.name,
218+
description as unknown as ProtectionDescription
219+
);
211220
return description;
212221
}
213222

@@ -220,25 +229,26 @@ export function findProtection(
220229
export function describeProtection<
221230
TCapabilitySet extends CapabilitySet = CapabilitySet,
222231
Context = unknown,
223-
TSettings extends Record<string, unknown> = Record<string, unknown>,
232+
TConfigSchema extends TObject = UnknownConfig,
224233
>({
225234
name,
226235
description,
227236
capabilityInterfaces,
228237
defaultCapabilities,
229238
factory,
230-
protectionSettings = new StandardProtectionSettings<TSettings>(
231-
{} as Record<keyof TSettings, never>,
232-
{} as TSettings
233-
),
239+
configSchema,
234240
}: {
235241
name: string;
236242
description: string;
237-
factory: ProtectionDescription<Context, TSettings, TCapabilitySet>['factory'];
243+
factory: ProtectionDescription<
244+
Context,
245+
TConfigSchema,
246+
TCapabilitySet
247+
>['factory'];
238248
capabilityInterfaces: GenericCapabilityDescription<TCapabilitySet>;
239249
defaultCapabilities: GenericCapabilityDescription<TCapabilitySet>;
240-
protectionSettings?: ProtectionSettings<TSettings>;
241-
}): ProtectionDescription<Context, TSettings, TCapabilitySet> {
250+
configSchema?: TConfigSchema;
251+
}): ProtectionDescription<Context, TConfigSchema, TCapabilitySet> {
242252
const capabilityInterfaceSet =
243253
findCapabilityInterfaceSet(capabilityInterfaces);
244254
const defaultCapabilitySet = findCapabilityProviderSet(defaultCapabilities);
@@ -248,10 +258,16 @@ export function describeProtection<
248258
capabilities: capabilityInterfaceSet,
249259
defaultCapabilities: defaultCapabilitySet,
250260
factory,
251-
protectionSettings,
261+
protectionSettings: new StandardConfigDescription(
262+
configSchema ?? Type.Object({})
263+
),
252264
};
253-
registerProtection(protectionDescription);
254-
return protectionDescription;
265+
registerProtection(protectionDescription as unknown as ProtectionDescription);
266+
return protectionDescription as unknown as ProtectionDescription<
267+
Context,
268+
TConfigSchema,
269+
TCapabilitySet
270+
>;
255271
}
256272

257273
export function getAllProtections(): IterableIterator<ProtectionDescription> {

src/Protection/ProtectionHandles.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
//
33
// SPDX-License-Identifier: AFL-3.0
44

5+
import { Type } from '@sinclair/typebox';
56
import { PowerLevelPermission } from '../Client/PowerLevelsMirror';
7+
import { StandardConfigDescription } from '../Config/ConfigDescription';
68
import { Ok, isError } from '../Interface/Action';
79
import { Logger } from '../Logging/Logger';
810
import { PowerLevelsEventContent } from '../MatrixTypes/PowerLevels';
@@ -13,8 +15,6 @@ import {
1315
} from '../StateTracking/DeclareRoomState';
1416
import { randomRoomID, randomUserID } from '../TestUtilities/EventGeneration';
1517
import { ProtectionDescription } from './Protection';
16-
import { ProtectionSetting } from './ProtectionSettings/ProtectionSetting';
17-
import { StandardProtectionSettings } from './ProtectionSettings/ProtectionSettings';
1818

1919
const log = new Logger('ProtectionHandles.test');
2020

@@ -50,10 +50,7 @@ test('handlePermissionRequirementsMet is called when a new room is added with me
5050
description: 'test description',
5151
capabilities: {},
5252
defaultCapabilities: {},
53-
protectionSettings: new StandardProtectionSettings(
54-
new Map<never, ProtectionSetting<string, Record<never, never>>>(),
55-
{}
56-
),
53+
protectionSettings: new StandardConfigDescription(Type.Object({})),
5754
factory(
5855
description,
5956
_protectedRoomsSet,
@@ -76,7 +73,6 @@ test('handlePermissionRequirementsMet is called when a new room is added with me
7673
};
7774
const protectionAddResult = await protectedRoomsSet.protections.addProtection(
7875
protectionDescription,
79-
{},
8076
protectedRoomsSet,
8177
undefined
8278
);

0 commit comments

Comments
 (0)