Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit f4b71d2

Browse files
committed
Document and support the established naming convention for config opts
This change: * Rename `ConfigOptions` to `IConfigOptions` to match code convention/style * Update comments and surrounding documentation * Define every single documented option (from element-web's config.md) * Try and enable a linter to enforce the convention * Invent a translation layer for a different change to use * No attempt to fix build errors from doing this (at this stage)
1 parent e1fdff4 commit f4b71d2

File tree

8 files changed

+250
-28
lines changed

8 files changed

+250
-28
lines changed

.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module.exports = {
22
plugins: [
33
"matrix-org",
4+
"snakecasejs", // for SdkConfig
45
],
56
extends: [
67
"plugin:matrix-org/babel",
@@ -15,6 +16,9 @@ module.exports = {
1516
LANGUAGES_FILE: "readonly",
1617
},
1718
rules: {
19+
// Disable snakecase as a default (re-enabled manually by SdkConfig)
20+
"snakecasejs/snakecasejs": "off",
21+
1822
// Things we do that break the ideal style
1923
"no-constant-condition": "off",
2024
"prefer-promise-reject-errors": "off",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
"eslint-plugin-matrix-org": "^0.4.0",
176176
"eslint-plugin-react": "^7.28.0",
177177
"eslint-plugin-react-hooks": "^4.3.0",
178+
"eslint-plugin-snakecasejs": "^2.2.0",
178179
"glob": "^7.1.6",
179180
"jest": "^27.4.0",
180181
"jest-canvas-mock": "^2.3.0",

src/@types/global.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
5252
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
5353
import { Skinner } from "../Skinner";
5454
import AutoRageshakeStore from "../stores/AutoRageshakeStore";
55-
import { ConfigOptions } from "../SdkConfig";
55+
import { IConfigOptions } from "../SdkConfig";
5656

5757
/* eslint-disable @typescript-eslint/naming-convention */
5858

@@ -63,7 +63,7 @@ declare global {
6363
Olm: {
6464
init: () => Promise<void>;
6565
};
66-
mxReactSdkConfig: ConfigOptions;
66+
mxReactSdkConfig: IConfigOptions;
6767

6868
// Needed for Safari, unknown to TypeScript
6969
webkitAudioContext: typeof AudioContext;

src/SdkConfig.ts

Lines changed: 136 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
Copyright 2016 OpenMarket Ltd
3-
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
3+
Copyright 2019 - 2022 The Matrix.org Foundation C.I.C.
44
55
Licensed under the Apache License, Version 2.0 (the "License");
66
you may not use this file except in compliance with the License.
@@ -15,14 +15,96 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
export interface ISsoRedirectOptions {
19-
immediate?: boolean;
20-
on_welcome_page?: boolean; // eslint-disable-line camelcase
21-
}
18+
import { IClientWellKnown } from "matrix-js-sdk/src/client";
19+
20+
import { SnakedObject } from "./utils/SnakedObject";
2221

2322
/* eslint-disable camelcase */
24-
export interface ConfigOptions {
25-
[key: string]: any;
23+
/* eslint-enable snakecasejs/snakecasejs */
24+
// Convention decision: All config options are lower_snake_case
25+
// see element-web config.md for non-developer docs
26+
export interface IConfigOptions { // eslint-disable-line snakecasejs/snakecasejs
27+
valToEnsureLinterWorks?: never;
28+
29+
// dev note: while true that this is arbitrary JSON, it's valuable to enforce that all
30+
// config options are documented for "find all usages" sort of searching.
31+
// [key: string]: any;
32+
33+
// Properties of this interface are roughly grouped by their subject matter, such as
34+
// "instance customisation", "login stuff", "branding", etc. Use blank lines to denote
35+
// a logical separation of properties, but keep similar ones near each other.
36+
37+
// Exactly one of the following must be supplied
38+
default_server_config?: IClientWellKnown; // copy/paste of client well-known
39+
default_server_name?: string; // domain to do well-known lookup on
40+
default_hs_url?: string; // http url
41+
42+
default_is_url?: string; // used in combination with default_hs_url, but for the identity server
43+
44+
disable_custom_urls?: boolean;
45+
disable_guests?: boolean;
46+
disable_login_language_selector?: boolean;
47+
disable_3pid_login?: boolean;
48+
49+
brand?: string;
50+
branding?: {
51+
welcome_background_url?: string;
52+
auth_header_logo_url?: string;
53+
auth_footer_links?: {text: string, url: string}[];
54+
};
55+
56+
map_style_url?: string; // for location-shared maps
57+
58+
embedded_pages?: {
59+
welcome_url?: string;
60+
home_url?: string;
61+
login_for_welcome?: boolean;
62+
};
63+
64+
permalink_prefix?: string;
65+
66+
update_base_url?: string;
67+
desktop_builds?: {
68+
available: boolean;
69+
logo: string; // url
70+
url: string; // download url
71+
};
72+
mobile_builds?: {
73+
ios?: string; // download url
74+
android?: string; // download url
75+
fdroid?: string; // download url
76+
};
77+
78+
mobile_guide_toast?: boolean;
79+
80+
default_theme?: "light" | "dark" | string; // custom themes are strings
81+
default_country_code?: string; // ISO 3166 alpha2 country code
82+
default_federate?: boolean;
83+
default_device_display_name?: string; // for device naming on login+registration
84+
85+
setting_defaults?: Record<string, any>; // <SettingName, Value>
86+
87+
integrations_ui_url?: string;
88+
integrations_rest_url?: string;
89+
integrations_widgets_urls?: string[];
90+
91+
show_labs_settings?: boolean;
92+
features?: Record<string, boolean>; // <FeatureName, EnabledBool>
93+
94+
bug_report_endpoint_url?: string; // omission disables bug reporting
95+
uisi_autorageshake_app?: string;
96+
sentry?: {
97+
dsn: string;
98+
environment?: string; // "production", etc
99+
};
100+
101+
audio_stream_url?: string;
102+
jitsi?: {
103+
preferred_domain: string;
104+
};
105+
voip?: {
106+
obey_asserted_identity?: boolean; // MSC3086
107+
};
26108

27109
logout_redirect_url?: string;
28110

@@ -31,45 +113,76 @@ export interface ConfigOptions {
31113
sso_redirect_options?: ISsoRedirectOptions;
32114

33115
custom_translations_url?: string;
116+
117+
report_event?: {
118+
admin_message_md: string; // message for how to contact the server owner when reporting an event
119+
};
120+
121+
welcome_user_id?: string;
122+
123+
room_directory?: {
124+
servers: string[];
125+
};
126+
127+
// piwik (matomo) is deprecated in favour of posthog
128+
piwik?: false | {
129+
url: string; // piwik instance
130+
site_id: string | number; // TODO: @@TR Typed correctly?
131+
policy_url: string; // cookie policy
132+
whitelisted_hs_urls: string[];
133+
};
134+
posthog?: {
135+
project_api_key: string;
136+
api_host: string; // hostname
137+
};
138+
analytics_owner?: string; // defaults to `brand`
139+
}
140+
141+
export interface ISsoRedirectOptions { // eslint-disable-line snakecasejs/snakecasejs
142+
immediate?: boolean;
143+
on_welcome_page?: boolean;
34144
}
145+
/* eslint-disable snakecasejs/snakecasejs */
35146
/* eslint-enable camelcase*/
36147

37-
export const DEFAULTS: ConfigOptions = {
38-
// Brand name of the app
148+
// see element-web config.md for docs, or the ConfigOptions interface for dev docs
149+
export const DEFAULTS: Partial<IConfigOptions> = {
39150
brand: "Element",
40-
// URL to a page we show in an iframe to configure integrations
41151
integrations_ui_url: "https://scalar.vector.im/",
42-
// Base URL to the REST interface of the integrations server
43152
integrations_rest_url: "https://scalar.vector.im/api",
44-
// Where to send bug reports. If not specified, bugs cannot be sent.
45153
bug_report_endpoint_url: null,
46-
// Jitsi conference options
47154
jitsi: {
48-
// Default conference domain
49-
preferredDomain: "meet.element.io",
155+
preferred_domain: "meet.element.io",
50156
},
51-
desktopBuilds: {
157+
desktop_builds: {
52158
available: true,
53159
logo: require("../res/img/element-desktop-logo.svg").default,
54160
url: "https://element.io/get-started",
55161
},
56162
};
57163

58164
export default class SdkConfig {
59-
private static instance: ConfigOptions;
165+
private static instance: IConfigOptions;
166+
private static fallback: SnakedObject<IConfigOptions>;
60167

61-
private static setInstance(i: ConfigOptions) {
168+
private static setInstance(i: IConfigOptions) {
62169
SdkConfig.instance = i;
170+
SdkConfig.fallback = new SnakedObject(i);
63171

64172
// For debugging purposes
65173
window.mxReactSdkConfig = i;
66174
}
67175

68-
public static get() {
69-
return SdkConfig.instance || {};
176+
public static get(): IConfigOptions;
177+
public static get<K extends keyof IConfigOptions>(key: K, altCaseName?: string): IConfigOptions[K];
178+
public static get<K extends keyof IConfigOptions = never>(
179+
key?: K, altCaseName?: string,
180+
): IConfigOptions | IConfigOptions[K] {
181+
if (key === undefined) return SdkConfig.instance || {};
182+
return SdkConfig.fallback.get(key, altCaseName);
70183
}
71184

72-
public static put(cfg: ConfigOptions) {
185+
public static put(cfg: IConfigOptions) {
73186
const defaultKeys = Object.keys(DEFAULTS);
74187
for (let i = 0; i < defaultKeys.length; ++i) {
75188
if (cfg[defaultKeys[i]] === undefined) {
@@ -83,14 +196,14 @@ export default class SdkConfig {
83196
SdkConfig.setInstance({});
84197
}
85198

86-
public static add(cfg: ConfigOptions) {
199+
public static add(cfg: IConfigOptions) {
87200
const liveConfig = SdkConfig.get();
88201
const newConfig = Object.assign({}, liveConfig, cfg);
89202
SdkConfig.put(newConfig);
90203
}
91204
}
92205

93-
export function parseSsoRedirectOptions(config: ConfigOptions): ISsoRedirectOptions {
206+
export function parseSsoRedirectOptions(config: IConfigOptions): ISsoRedirectOptions {
94207
// Ignore deprecated options if the config is using new ones
95208
if (config.sso_redirect_options) return config.sso_redirect_options;
96209

src/utils/SnakedObject.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
export function snakeToCamel(s: string): string {
18+
return s.replace(/._./g, v => `${v[0]}${v[2].toUpperCase()}`);
19+
}
20+
21+
export class SnakedObject<T = Record<string, any>> {
22+
public constructor(private obj: T) {
23+
}
24+
25+
public get<K extends string & keyof T>(key: K, altCaseName?: string): T[K] {
26+
const val = this.obj[key];
27+
if (val !== undefined) return val;
28+
29+
return this.obj[altCaseName ?? snakeToCamel(key)];
30+
}
31+
32+
// Make JSON.stringify() pretend that everything is fine
33+
public toJSON() {
34+
return this.obj;
35+
}
36+
}

src/utils/pages.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { ConfigOptions } from "../SdkConfig";
17+
import { IConfigOptions } from "../SdkConfig";
1818

19-
export function getHomePageUrl(appConfig: ConfigOptions): string | null {
19+
export function getHomePageUrl(appConfig: IConfigOptions): string | null {
2020
const pagesConfig = appConfig.embeddedPages;
2121
let pageUrl = pagesConfig?.homeUrl;
2222

@@ -30,7 +30,7 @@ export function getHomePageUrl(appConfig: ConfigOptions): string | null {
3030
return pageUrl;
3131
}
3232

33-
export function shouldUseLoginForWelcome(appConfig: ConfigOptions): boolean {
33+
export function shouldUseLoginForWelcome(appConfig: IConfigOptions): boolean {
3434
const pagesConfig = appConfig.embeddedPages;
3535
return pagesConfig?.loginForWelcome === true;
3636
}

test/utils/SnakedObject-test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { SnakedObject, snakeToCamel } from "../../src/utils/SnakedObject";
18+
19+
describe('snakeToCamel', () => {
20+
it('should convert snake_case to camelCase in simple scenarios', () => {
21+
expect(snakeToCamel("snake_case")).toBe("snakeCase");
22+
expect(snakeToCamel("snake_case_but_longer")).toBe("snakeCaseButLonger");
23+
expect(snakeToCamel("numbered_123")).toBe("numbered123"); // not a thing we would see normally
24+
});
25+
26+
// Not really something we expect to see, but it's defined behaviour of the function
27+
it('should not camelCase a trailing or leading underscore', () => {
28+
expect(snakeToCamel("_snake")).toBe("_snake");
29+
expect(snakeToCamel("snake_")).toBe("snake_");
30+
expect(snakeToCamel("_snake_case")).toBe("_snakeCase");
31+
expect(snakeToCamel("snake_case_")).toBe("snakeCase_");
32+
});
33+
34+
// Another thing we don't really expect to see, but is "defined behaviour"
35+
it('should be predictable with double underscores', () => {
36+
expect(snakeToCamel("__snake__")).toBe("_Snake_");
37+
expect(snakeToCamel("snake__case")).toBe("snake_case");
38+
});
39+
});
40+
41+
describe('SnakedObject', () => {
42+
/* eslint-disable camelcase*/
43+
const input = {
44+
snake_case: "woot",
45+
snakeCase: "oh no", // ensure different value from snake_case for tests
46+
camelCase: "fallback",
47+
};
48+
const snake = new SnakedObject(input);
49+
/* eslint-enable camelcase*/
50+
51+
it('should prefer snake_case keys', () => {
52+
expect(snake.get("snake_case")).toBe(input.snake_case);
53+
expect(snake.get("snake_case", "camelCase")).toBe(input.snake_case);
54+
});
55+
56+
it('should fall back to camelCase keys when needed', () => {
57+
// @ts-ignore - we're deliberately supplying a key that doesn't exist
58+
expect(snake.get("camel_case")).toBe(input.camelCase);
59+
60+
// @ts-ignore - we're deliberately supplying a key that doesn't exist
61+
expect(snake.get("e_no_exist", "camelCase")).toBe(input.camelCase);
62+
});
63+
});

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3837,6 +3837,11 @@ eslint-plugin-react@^7.28.0:
38373837
semver "^6.3.0"
38383838
string.prototype.matchall "^4.0.6"
38393839

3840+
eslint-plugin-snakecasejs@^2.2.0:
3841+
version "2.2.0"
3842+
resolved "https://registry.yarnpkg.com/eslint-plugin-snakecasejs/-/eslint-plugin-snakecasejs-2.2.0.tgz#e35bf4eee2f375a9ba79de68c96303e06f8acf86"
3843+
integrity sha512-vdQHT2VvzPpJHHPAVXjtyAZ/CfiJqNCa2d9kn6XMapWBN2Uio/nzL957TooNa6gumlHabBAhB5eSNmqwHgu8gA==
3844+
38403845
eslint-rule-composer@^0.3.0:
38413846
version "0.3.0"
38423847
resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9"

0 commit comments

Comments
 (0)