Skip to content

Commit

Permalink
Project settings validators (#816)
Browse files Browse the repository at this point in the history
  • Loading branch information
jolierabideau authored Apr 9, 2024
2 parents 76f2274 + 98fd206 commit 2876053
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 24 deletions.
53 changes: 50 additions & 3 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2487,10 +2487,14 @@ declare module 'papi-shared-types' {
/**
* Set the value of the specified project setting on this project.
*
* Note: `setSetting` must call `papi.projectSettings.isValid` before allowing the setting
* change.
*
* @param key The string id of the project setting to change
* @param newSetting The value that is to be set to the project setting.
* @returns Information that papi uses to interpret whether to send out updates. Defaults to
* `true` (meaning send updates only for this data type).
* @throws If the setting validator failed.
* @see {@link DataProviderUpdateInstructions} for more info on what to return
*/
setSetting: <ProjectSettingName extends ProjectSettingNames>(
Expand Down Expand Up @@ -4709,6 +4713,24 @@ declare module 'renderer/hooks/papi-hooks/use-dialog-callback.hook' {
}
declare module 'shared/services/project-settings.service-model' {
import { ProjectSettingNames, ProjectSettingTypes, ProjectTypes } from 'papi-shared-types';
import { UnsubscriberAsync } from 'platform-bible-utils';
/** Name prefix for registered commands that call project settings validators */
export const CATEGORY_EXTENSION_PROJECT_SETTING_VALIDATOR = 'extensionProjectSettingValidator';
export const projectSettingsServiceNetworkObjectName = 'ProjectSettingsService';
export const projectSettingsServiceObjectToProxy: Readonly<{
/**
*
* Registers a function that validates whether a new project setting value is allowed to be set.
*
* @param key The string id of the setting to validate
* @param validator Function to call to validate the new setting value
* @returns Unsubscriber that should be called whenever the providing extension is deactivated
*/
registerValidator: <ProjectSettingName extends keyof ProjectSettingTypes>(
key: ProjectSettingName,
validator: ProjectSettingValidator<ProjectSettingName>,
) => Promise<UnsubscriberAsync>;
}>;
/**
*
* Provides utility functions that project storage interpreters should call when handling project
Expand All @@ -4730,11 +4752,11 @@ declare module 'shared/services/project-settings.service-model' {
* @returns `true` if change is valid, `false` otherwise
*/
isValid<ProjectSettingName extends ProjectSettingNames>(
key: ProjectSettingName,
newValue: ProjectSettingTypes[ProjectSettingName],
currentValue: ProjectSettingTypes[ProjectSettingName],
key: ProjectSettingName,
allChanges: SimultaneousProjectSettingsChanges,
projectType: ProjectTypes,
allChanges?: SimultaneousProjectSettingsChanges,
): Promise<boolean>;
/**
* Gets default value for a project setting
Expand All @@ -4753,6 +4775,18 @@ declare module 'shared/services/project-settings.service-model' {
key: ProjectSettingName,
projectType: ProjectTypes,
): Promise<ProjectSettingTypes[ProjectSettingName]>;
/**
*
* Registers a function that validates whether a new project setting value is allowed to be set.
*
* @param key The string id of the setting to validate
* @param validator Function to call to validate the new setting value
* @returns Unsubscriber that should be called whenever the providing extension is deactivated
*/
registerValidator<ProjectSettingName extends ProjectSettingNames>(
key: ProjectSettingName,
validatorCallback: ProjectSettingValidator<ProjectSettingName>,
): Promise<UnsubscriberAsync>;
}
/**
* All project settings changes being set in one batch
Expand All @@ -4768,7 +4802,20 @@ declare module 'shared/services/project-settings.service-model' {
currentValue: ProjectSettingTypes[ProjectSettingName];
};
};
export const projectSettingsServiceNetworkObjectName = 'ProjectSettingsService';
/** Function that validates whether a new project setting value should be allowed to be set */
export type ProjectSettingValidator<ProjectSettingName extends ProjectSettingNames> = (
newValue: ProjectSettingTypes[ProjectSettingName],
currentValue: ProjectSettingTypes[ProjectSettingName],
allChanges: SimultaneousProjectSettingsChanges,
projectType: ProjectTypes,
) => Promise<boolean>;
/**
* Validators for all project settings. Keys are setting keys, values are functions to validate new
* settings
*/
export type AllProjectSettingsValidators = {
[ProjectSettingName in ProjectSettingNames]: ProjectSettingValidator<ProjectSettingName>;
};
}
declare module '@papi/core' {
/** Exporting empty object so people don't have to put 'type' in their import statements */
Expand Down
4 changes: 4 additions & 0 deletions src/declarations/papi-shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,14 @@ declare module 'papi-shared-types' {
/**
* Set the value of the specified project setting on this project.
*
* Note: `setSetting` must call `papi.projectSettings.isValid` before allowing the setting
* change.
*
* @param key The string id of the project setting to change
* @param newSetting The value that is to be set to the project setting.
* @returns Information that papi uses to interpret whether to send out updates. Defaults to
* `true` (meaning send updates only for this data type).
* @throws If the setting validator failed.
* @see {@link DataProviderUpdateInstructions} for more info on what to return
*/
setSetting: <ProjectSettingName extends ProjectSettingNames>(
Expand Down
28 changes: 28 additions & 0 deletions src/main/data/core-project-settings-info.data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {
AllProjectSettingsValidators,
ProjectSettingValidator,
} from '@shared/services/project-settings.service-model';
import { ProjectSettingNames, ProjectSettingTypes } from 'papi-shared-types';
import { stringLength } from 'platform-bible-utils';

/** Information about one project setting */
type ProjectSettingInfo<ProjectSettingName extends ProjectSettingNames> = {
Expand Down Expand Up @@ -27,4 +32,27 @@ const coreProjectSettingsInfo: Partial<AllProjectSettingsInfo> = {
'platformScripture.versification': { default: 4 },
};

// Based on https://github.com/paranext/paranext-core/blob/5c403e272b002ddd8970f735bc119f335c78c509/extensions/src/usfm-data-provider/index.d.ts#L401
// Should be 123 characters long
// todo Check that it only includes 1 or 0
export const booksPresentSettingsValidator: ProjectSettingValidator<
'platformScripture.booksPresent'
> = async (newValue: string): Promise<boolean> => {
return stringLength(newValue) === 123;
};

// Based on https://github.com/paranext/paranext-core/blob/5c403e272b002ddd8970f735bc119f335c78c509/extensions/src/usfm-data-provider/index.d.ts#L391
// There are 7 options in the enum
export const versificationSettingsValidator: ProjectSettingValidator<
'platformScripture.versification'
> = async (newValue: number): Promise<boolean> => {
return newValue >= 0 && newValue <= 6;
};

/** Info about all settings built into core. Does not contain info for extensions' settings */
export const coreProjectSettingsValidators: Partial<AllProjectSettingsValidators> = {
'platformScripture.booksPresent': booksPresentSettingsValidator,
'platformScripture.versification': versificationSettingsValidator,
};

export default coreProjectSettingsInfo;
47 changes: 42 additions & 5 deletions src/main/services/project-settings.service-host.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { testingProjectSettingsService } from '@main/services/project-settings.service-host';
import { ProjectSettingValidator } from '@shared/services/project-settings.service-model';

jest.mock('@shared/services/network.service', () => ({
...jest.requireActual('@shared/services/network.service'),
registerRequestHandler: () => {
return {};
},
}));
jest.mock('@main/data/core-project-settings-info.data', () => ({
...jest.requireActual('@main/data/core-project-settings-info.data'),
__esModule: true,
default: {
'platform.fullName': { default: '%test_project_full_name_missing%' },
Expand All @@ -10,19 +18,34 @@ jest.mock('@main/data/core-project-settings-info.data', () => ({
},
// Not present! Should throw error 'platformScripture.versification': { default: 1629326 },
},
coreProjectSettingsValidators: {
'platform.language': async (newValue: string): Promise<boolean> => {
return newValue === 'eng' || newValue === 'fre';
},
},
}));

describe('isValid', () => {
it('should return true always - TEMP. TODO: Fix when we implement validation #511', async () => {
it('should return true', async () => {
const projectSettingKey = 'platform.language';
const isSettingChangeValid = await testingProjectSettingsService.isValid(
'',
'',
'platform.fullName',
{},
projectSettingKey,
'eng',
'%test_project_language_missing%',
'ParatextStandard',
);
expect(isSettingChangeValid).toBe(true);
});
it('should return false', async () => {
const projectSettingKey = 'platform.language';
const isSettingChangeValid = await testingProjectSettingsService.isValid(
projectSettingKey,
'ger',
'%test_project_language_missing%',
'ParatextStandard',
);
expect(isSettingChangeValid).toBe(false);
});
});

describe('getDefault', () => {
Expand All @@ -42,3 +65,17 @@ describe('getDefault', () => {
).rejects.toThrow(new RegExp(`default value for project setting ${projectSettingKey}`));
});
});

describe('registerValidator', () => {
it('should resolve', async () => {
const projectSettingKey = 'platform.fullName';
const fullNameSettingsValidator: ProjectSettingValidator<
'platform.fullName'
> = async (): Promise<boolean> => {
return true;
};
await expect(
testingProjectSettingsService.registerValidator(projectSettingKey, fullNameSettingsValidator),
).resolves.toStrictEqual({});
});
});
40 changes: 37 additions & 3 deletions src/main/services/project-settings.service-host.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import * as networkService from '@shared/services/network.service';
import coreProjectSettingsInfo, {
AllProjectSettingsInfo,
coreProjectSettingsValidators,
} from '@main/data/core-project-settings-info.data';
import networkObjectService from '@shared/services/network-object.service';
import {
CATEGORY_EXTENSION_PROJECT_SETTING_VALIDATOR,
IProjectSettingsService,
SimultaneousProjectSettingsChanges,
projectSettingsServiceNetworkObjectName,
projectSettingsServiceObjectToProxy,
} from '@shared/services/project-settings.service-model';
import { serializeRequestType } from '@shared/utils/util';
import { ProjectSettingNames, ProjectSettingTypes, ProjectTypes } from 'papi-shared-types';
import { includes } from 'platform-bible-utils';

/**
* Our internal list of project settings information for each setting. Theoretically this should not
Expand Down Expand Up @@ -35,9 +42,34 @@ async function initialize(): Promise<void> {
return initializationPromise;
}

// TODO: Implement validators in https://github.com/paranext/paranext-core/issues/511
async function isValid(): Promise<boolean> {
return true;
async function isValid<ProjectSettingName extends ProjectSettingNames>(
key: ProjectSettingName,
newValue: ProjectSettingTypes[ProjectSettingName],
currentValue: ProjectSettingTypes[ProjectSettingName],
projectType: ProjectTypes,
allChanges?: SimultaneousProjectSettingsChanges,
): Promise<boolean> {
if (key in coreProjectSettingsValidators) {
const projectSettingValidator = coreProjectSettingsValidators[key];
if (projectSettingValidator)
return projectSettingValidator(newValue, currentValue, allChanges ?? {}, projectType);
// If key exists in coreProjectSettingsValidators but there is no validator, let the change go through
return true;
}
try {
return networkService.request(
serializeRequestType(CATEGORY_EXTENSION_PROJECT_SETTING_VALIDATOR, key),
newValue,
currentValue,
allChanges ?? {},
projectType,
);
} catch (error) {
// If there is no validator just let the change go through
const missingValidatorMsg = `No handler was found to process the request of type`;
if (includes(`${error}`, missingValidatorMsg)) return true;
throw error;
}
}

async function getDefault<ProjectSettingName extends ProjectSettingNames>(
Expand All @@ -55,9 +87,11 @@ async function getDefault<ProjectSettingName extends ProjectSettingNames>(
return projectSettingInfo.default;
}

const { registerValidator } = projectSettingsServiceObjectToProxy;
const projectSettingsService: IProjectSettingsService = {
isValid,
getDefault,
registerValidator,
};

/** This is an internal-only export for testing purposes, and should not be used in development */
Expand Down
52 changes: 49 additions & 3 deletions src/shared/services/project-settings.service-model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
import { serializeRequestType } from '@shared/utils/util';
import * as networkService from '@shared/services/network.service';
import { ProjectSettingNames, ProjectSettingTypes, ProjectTypes } from 'papi-shared-types';
import { UnsubscriberAsync } from 'platform-bible-utils';

/** Name prefix for registered commands that call project settings validators */
export const CATEGORY_EXTENSION_PROJECT_SETTING_VALIDATOR = 'extensionProjectSettingValidator';

export const projectSettingsServiceNetworkObjectName = 'ProjectSettingsService';
export const projectSettingsServiceObjectToProxy = Object.freeze({
/** JSDOC DESTINATION projectSettingsServiceRegisterValidator */
registerValidator: async <ProjectSettingName extends ProjectSettingNames>(
key: ProjectSettingName,
validator: ProjectSettingValidator<ProjectSettingName>,
): Promise<UnsubscriberAsync> => {
return networkService.registerRequestHandler(
serializeRequestType(CATEGORY_EXTENSION_PROJECT_SETTING_VALIDATOR, key),
validator,
);
},
});

/**
* JSDOC SOURCE projectSettingsService
Expand All @@ -22,11 +42,11 @@ export interface IProjectSettingsService {
* @returns `true` if change is valid, `false` otherwise
*/
isValid<ProjectSettingName extends ProjectSettingNames>(
key: ProjectSettingName,
newValue: ProjectSettingTypes[ProjectSettingName],
currentValue: ProjectSettingTypes[ProjectSettingName],
key: ProjectSettingName,
allChanges: SimultaneousProjectSettingsChanges,
projectType: ProjectTypes,
allChanges?: SimultaneousProjectSettingsChanges,
): Promise<boolean>;
/**
* Gets default value for a project setting
Expand All @@ -45,6 +65,19 @@ export interface IProjectSettingsService {
key: ProjectSettingName,
projectType: ProjectTypes,
): Promise<ProjectSettingTypes[ProjectSettingName]>;
/**
* JSDOC SOURCE projectSettingsServiceRegisterValidator
*
* Registers a function that validates whether a new project setting value is allowed to be set.
*
* @param key The string id of the setting to validate
* @param validator Function to call to validate the new setting value
* @returns Unsubscriber that should be called whenever the providing extension is deactivated
*/
registerValidator<ProjectSettingName extends ProjectSettingNames>(
key: ProjectSettingName,
validatorCallback: ProjectSettingValidator<ProjectSettingName>,
): Promise<UnsubscriberAsync>;
}

/**
Expand All @@ -61,5 +94,18 @@ export type SimultaneousProjectSettingsChanges = {
currentValue: ProjectSettingTypes[ProjectSettingName];
};
};
/** Function that validates whether a new project setting value should be allowed to be set */
export type ProjectSettingValidator<ProjectSettingName extends ProjectSettingNames> = (
newValue: ProjectSettingTypes[ProjectSettingName],
currentValue: ProjectSettingTypes[ProjectSettingName],
allChanges: SimultaneousProjectSettingsChanges,
projectType: ProjectTypes,
) => Promise<boolean>;

export const projectSettingsServiceNetworkObjectName = 'ProjectSettingsService';
/**
* Validators for all project settings. Keys are setting keys, values are functions to validate new
* settings
*/
export type AllProjectSettingsValidators = {
[ProjectSettingName in ProjectSettingNames]: ProjectSettingValidator<ProjectSettingName>;
};
Loading

0 comments on commit 2876053

Please sign in to comment.