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

Commit 317adcb

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 317adcb

File tree

8 files changed

+249
-28
lines changed

8 files changed

+249
-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: 135 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,95 @@ 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+
import { SnakedObject } from "./utils/SnakedObject";
2220

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

27108
logout_redirect_url?: string;
28109

@@ -31,45 +112,76 @@ export interface ConfigOptions {
31112
sso_redirect_options?: ISsoRedirectOptions;
32113

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

37-
export const DEFAULTS: ConfigOptions = {
38-
// Brand name of the app
147+
// see element-web config.md for docs, or the ConfigOptions interface for dev docs
148+
export const DEFAULTS: Partial<IConfigOptions> = {
39149
brand: "Element",
40-
// URL to a page we show in an iframe to configure integrations
41150
integrations_ui_url: "https://scalar.vector.im/",
42-
// Base URL to the REST interface of the integrations server
43151
integrations_rest_url: "https://scalar.vector.im/api",
44-
// Where to send bug reports. If not specified, bugs cannot be sent.
45152
bug_report_endpoint_url: null,
46-
// Jitsi conference options
47153
jitsi: {
48-
// Default conference domain
49-
preferredDomain: "meet.element.io",
154+
preferred_domain: "meet.element.io",
50155
},
51-
desktopBuilds: {
156+
desktop_builds: {
52157
available: true,
53158
logo: require("../res/img/element-desktop-logo.svg").default,
54159
url: "https://element.io/get-started",
55160
},
56161
};
57162

58163
export default class SdkConfig {
59-
private static instance: ConfigOptions;
164+
private static instance: IConfigOptions;
165+
private static fallback: SnakedObject<IConfigOptions>;
60166

61-
private static setInstance(i: ConfigOptions) {
167+
private static setInstance(i: IConfigOptions) {
62168
SdkConfig.instance = i;
169+
SdkConfig.fallback = new SnakedObject(i);
63170

64171
// For debugging purposes
65172
window.mxReactSdkConfig = i;
66173
}
67174

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

72-
public static put(cfg: ConfigOptions) {
184+
public static put(cfg: IConfigOptions) {
73185
const defaultKeys = Object.keys(DEFAULTS);
74186
for (let i = 0; i < defaultKeys.length; ++i) {
75187
if (cfg[defaultKeys[i]] === undefined) {
@@ -83,14 +195,14 @@ export default class SdkConfig {
83195
SdkConfig.setInstance({});
84196
}
85197

86-
public static add(cfg: ConfigOptions) {
198+
public static add(cfg: IConfigOptions) {
87199
const liveConfig = SdkConfig.get();
88200
const newConfig = Object.assign({}, liveConfig, cfg);
89201
SdkConfig.put(newConfig);
90202
}
91203
}
92204

93-
export function parseSsoRedirectOptions(config: ConfigOptions): ISsoRedirectOptions {
205+
export function parseSsoRedirectOptions(config: IConfigOptions): ISsoRedirectOptions {
94206
// Ignore deprecated options if the config is using new ones
95207
if (config.sso_redirect_options) return config.sso_redirect_options;
96208

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 (keyof T) & string>(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)