From 3b7d52abb0e7de00bdf92ee3482a4cae1f6b7d64 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Mon, 3 Jan 2022 01:02:00 +0100 Subject: feat: Add Profile and Service stores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the main process, it is optional to specify the ID of a Profile or a Service. The missing ID will be filled in with a randomly generated one. Moreover, services without a profile will get a profile generated with the same name. Signed-off-by: Kristóf Marussy --- packages/main/src/controllers/initConfig.ts | 12 +- packages/main/src/stores/Config.ts | 32 ++++- packages/main/src/stores/Profile.ts | 51 +++++++ packages/main/src/stores/Service.ts | 63 +++++++++ packages/main/src/stores/__tests__/Config.spec.ts | 156 ++++++++++++++++++++++ packages/main/src/utils/generateId.ts | 27 ++++ 6 files changed, 333 insertions(+), 8 deletions(-) create mode 100644 packages/main/src/stores/Profile.ts create mode 100644 packages/main/src/stores/Service.ts create mode 100644 packages/main/src/stores/__tests__/Config.spec.ts create mode 100644 packages/main/src/utils/generateId.ts (limited to 'packages/main/src') diff --git a/packages/main/src/controllers/initConfig.ts b/packages/main/src/controllers/initConfig.ts index 915f451..93be978 100644 --- a/packages/main/src/controllers/initConfig.ts +++ b/packages/main/src/controllers/initConfig.ts @@ -19,11 +19,11 @@ */ import { debounce } from 'lodash-es'; -import { applySnapshot, getSnapshot, onSnapshot } from 'mobx-state-tree'; +import { getSnapshot, onSnapshot } from 'mobx-state-tree'; import ms from 'ms'; -import type ConfigPersistenceService from '../services/ConfigPersistenceService'; -import type { Config, ConfigSnapshotOut } from '../stores/Config'; +import type ConfigPersistenceService from '../services/ConfigPersistenceService.js'; +import { Config, ConfigFileIn, ConfigSnapshotOut } from '../stores/Config.js'; import type Disposer from '../utils/Disposer'; import { getLogger } from '../utils/log'; @@ -44,12 +44,14 @@ export default async function initConfig( const result = await persistenceService.readConfig(); if (result.found) { try { - applySnapshot(config, result.data); - lastSnapshotOnDisk = getSnapshot(config); + // This cast is unsound if the config file is invalid, + // but we'll throw an error in the end anyways. + config.loadFromConfigFile(result.data as ConfigFileIn); } catch (error) { log.error('Failed to apply config snapshot', result.data, error); } } + lastSnapshotOnDisk = getSnapshot(config); return result.found; } diff --git a/packages/main/src/stores/Config.ts b/packages/main/src/stores/Config.ts index ca90c0c..e7fc360 100644 --- a/packages/main/src/stores/Config.ts +++ b/packages/main/src/stores/Config.ts @@ -19,14 +19,40 @@ */ import { config as originalConfig, ThemeSource } from '@sophie/shared'; -import { Instance } from 'mobx-state-tree'; +import { applySnapshot, Instance, SnapshotIn } from 'mobx-state-tree'; + +import { addMissingProfileIds, PartialProfileSnapshotIn } from './Profile'; +import { + addMissingServiceIdsAndProfiles, + PartialServiceSnapshotIn, +} from './Service'; export const config = originalConfig.actions((self) => ({ - setThemeSource(mode: ThemeSource) { + loadFromConfigFile(snapshot: ConfigFileIn): void { + const profiles = addMissingProfileIds(snapshot.profiles); + const services = addMissingServiceIdsAndProfiles( + snapshot.services, + profiles, + ); + applySnapshot(self, { + ...snapshot, + profiles, + services, + }); + }, + setThemeSource(mode: ThemeSource): void { self.themeSource = mode; }, })); export interface Config extends Instance {} -export type { ConfigSnapshotIn, ConfigSnapshotOut } from '@sophie/shared'; +export interface ConfigSnapshotIn extends SnapshotIn {} + +export interface ConfigFileIn + extends Omit { + profiles?: PartialProfileSnapshotIn[] | undefined; + services?: PartialServiceSnapshotIn[] | undefined; +} + +export type { ConfigSnapshotOut } from '@sophie/shared'; diff --git a/packages/main/src/stores/Profile.ts b/packages/main/src/stores/Profile.ts new file mode 100644 index 0000000..4705862 --- /dev/null +++ b/packages/main/src/stores/Profile.ts @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { ProfileSnapshotIn } from '@sophie/shared'; + +import generateId from '../utils/generateId'; + +export interface PartialProfileSnapshotIn + extends Omit { + id?: string | undefined; +} + +export function addMissingProfileIds( + partialProfiles: PartialProfileSnapshotIn[] | undefined, +): ProfileSnapshotIn[] { + return (partialProfiles ?? []).map((profile) => { + const { name } = profile; + let { id } = profile; + if (typeof id === 'undefined') { + id = generateId(name); + } + return { + ...profile, + id, + }; + }); +} + +export type { + Profile, + ProfileSnapshotOut, + ProfileSnapshotIn, +} from '@sophie/shared'; +export { profile } from '@sophie/shared'; diff --git a/packages/main/src/stores/Service.ts b/packages/main/src/stores/Service.ts new file mode 100644 index 0000000..9bc6a43 --- /dev/null +++ b/packages/main/src/stores/Service.ts @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { ServiceSnapshotIn } from '@sophie/shared'; + +import generateId from '../utils/generateId'; + +import type { ProfileSnapshotIn } from './Profile'; + +export interface PartialServiceSnapshotIn + extends Omit { + id?: string | undefined; + profile?: string | undefined; +} + +export function addMissingServiceIdsAndProfiles( + partialServices: PartialServiceSnapshotIn[] | undefined, + profiles: ProfileSnapshotIn[], +): ServiceSnapshotIn[] { + return (partialServices ?? []).map((service) => { + const { name } = service; + let { id, profile } = service; + if (typeof id === 'undefined') { + id = generateId(name); + } + if (typeof profile === 'undefined') { + profile = generateId(name); + profiles.push({ + id: profile, + name: service.name, + }); + } + return { + ...service, + id, + profile, + }; + }); +} + +export type { + Service, + ServiceSnapshotOut, + ServiceSnapshotIn, +} from '@sophie/shared'; +export { service } from '@sophie/shared'; diff --git a/packages/main/src/stores/__tests__/Config.spec.ts b/packages/main/src/stores/__tests__/Config.spec.ts new file mode 100644 index 0000000..22ccbc7 --- /dev/null +++ b/packages/main/src/stores/__tests__/Config.spec.ts @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { config, Config, ConfigFileIn } from '../Config'; +import type { PartialProfileSnapshotIn } from '../Profile'; +import type { PartialServiceSnapshotIn } from '../Service'; + +const profileProps: PartialProfileSnapshotIn = { + name: 'Test profile', +}; + +const serviceProps: PartialServiceSnapshotIn = { + name: 'Test service', + url: 'https://example.com', +}; + +let sut: Config; + +beforeEach(() => { + sut = config.create(); +}); + +describe('preprocessConfigFile', () => { + it('should load profiles with an ID', () => { + sut.loadFromConfigFile({ + profiles: [ + { + id: 'someId', + ...profileProps, + }, + ], + }); + expect(sut.profiles[0].id).toBe('someId'); + }); + + it('should generate an ID for profiles without and ID', () => { + sut.loadFromConfigFile({ + profiles: [profileProps], + }); + expect(sut.profiles[0].id).toBeDefined(); + }); + + it('should load services with an ID and a profile', () => { + sut.loadFromConfigFile({ + profiles: [ + { + id: 'someProfileId', + ...profileProps, + }, + ], + services: [ + { + id: 'someServiceId', + profile: 'someProfileId', + ...serviceProps, + }, + ], + }); + expect(sut.services[0].id).toBe('someServiceId'); + expect(sut.services[0].profile).toBe(sut.profiles[0]); + }); + + it('should refuse to load a profile without a name', () => { + expect(() => { + sut.loadFromConfigFile({ + profiles: [ + { + id: 'someProfileId', + ...profileProps, + name: undefined, + }, + ], + } as unknown as ConfigFileIn); + }).toThrow(); + expect(sut.profiles).toHaveLength(0); + }); + + it('should load services without an ID but with a profile', () => { + sut.loadFromConfigFile({ + profiles: [ + { + id: 'someProfileId', + ...profileProps, + }, + ], + services: [ + { + profile: 'someProfileId', + ...serviceProps, + }, + ], + }); + expect(sut.services[0].id).toBeDefined(); + expect(sut.services[0].profile).toBe(sut.profiles[0]); + }); + + it('should create a profile for a service with an ID but no profile', () => { + sut.loadFromConfigFile({ + services: [ + { + id: 'someServiceId', + ...serviceProps, + }, + ], + }); + expect(sut.services[0].id).toBe('someServiceId'); + expect(sut.services[0].profile).toBeDefined(); + expect(sut.services[0].profile.name).toBe(serviceProps.name); + }); + + it('should create a profile for a service without an ID or profile', () => { + sut.loadFromConfigFile({ + services: [ + { + ...serviceProps, + }, + ], + }); + expect(sut.services[0].id).toBeDefined(); + expect(sut.services[0].profile).toBeDefined(); + expect(sut.services[0].profile.name).toBe(serviceProps.name); + }); + + it('should refuse to load a service without a name', () => { + expect(() => { + sut.loadFromConfigFile({ + services: [ + { + id: 'someServiceId', + ...serviceProps, + name: undefined, + }, + ], + } as unknown as ConfigFileIn); + }).toThrow(); + expect(sut.profiles).toHaveLength(0); + expect(sut.services).toHaveLength(0); + }); +}); diff --git a/packages/main/src/utils/generateId.ts b/packages/main/src/utils/generateId.ts new file mode 100644 index 0000000..8a87e5a --- /dev/null +++ b/packages/main/src/utils/generateId.ts @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021-2022 Kristóf Marussy + * + * This file is part of Sophie. + * + * Sophie is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { nanoid } from 'nanoid'; +import slug from 'slug'; + +export default function generateId(name?: string | undefined) { + const nameSlug = typeof name === 'undefined' ? '' : slug(name); + return `${nameSlug}_${nanoid()}`; +} -- cgit v1.2.3-54-g00ecf