Skip to content

Commit

Permalink
[lib] Move conversion function out of validation utils
Browse files Browse the repository at this point in the history
Summary: Changes required for fixing [ENG-4670](https://linear.app/comm/issue/ENG-4670/) introduce some cyclic dependencies so we need to move some things around.

Test Plan: `yarn test-all`, `yarn eslint`, `yarn flow-all`

Reviewers: tomek, inka, kamil, ginsu

Reviewed By: kamil

Subscribers: ashoat

Differential Revision: https://phab.comm.dev/D8879
  • Loading branch information
MichalGniadek committed Aug 22, 2023
1 parent 76f6c0d commit feb5be9
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 194 deletions.
8 changes: 5 additions & 3 deletions keyserver/src/utils/validation-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import {
hasMinStateVersion,
} from 'lib/shared/version-utils.js';
import { type PlatformDetails } from 'lib/types/device-types.js';
import {
convertClientIDsToServerIDs,
convertObject,
convertServerIDsToClientIDs,
} from 'lib/utils/conversion-utils.js';
import { ServerError } from 'lib/utils/errors.js';
import {
tCookie,
Expand All @@ -16,9 +21,6 @@ import {
tPlatformDetails,
assertWithValidator,
ashoatKeyserverID,
convertClientIDsToServerIDs,
convertObject,
convertServerIDsToClientIDs,
} from 'lib/utils/validation-utils.js';

import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js';
Expand Down
7 changes: 2 additions & 5 deletions lib/selectors/socket-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,10 @@ import {
userInfosValidator,
} from '../types/user-types.js';
import { getConfig } from '../utils/config.js';
import { convertClientIDsToServerIDs } from '../utils/conversion-utils.js';
import { minimumOneTimeKeysRequired } from '../utils/crypto-utils.js';
import { values, hash } from '../utils/objects.js';
import {
tID,
convertClientIDsToServerIDs,
ashoatKeyserverID,
} from '../utils/validation-utils.js';
import { tID, ashoatKeyserverID } from '../utils/validation-utils.js';

const queuedReports: (
state: AppState,
Expand Down
127 changes: 127 additions & 0 deletions lib/utils/conversion-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// @flow

import _mapKeys from 'lodash/fp/mapKeys.js';
import _mapValues from 'lodash/fp/mapValues.js';
import type { TInterface, TType } from 'tcomb';

import { assertWithValidator, tID } from './validation-utils.js';

function convertServerIDsToClientIDs<T>(
serverPrefixID: string,
outputValidator: TType<T>,
data: T,
): T {
const conversionFunction = id => {
if (id.indexOf('|') !== -1) {
console.warn(`Server id '${id}' already has a prefix`);
return id;
}
return `${serverPrefixID}|${id}`;
};

return convertObject(outputValidator, data, [tID], conversionFunction);
}

function convertClientIDsToServerIDs<T>(
serverPrefixID: string,
outputValidator: TType<T>,
data: T,
): T {
const prefix = serverPrefixID + '|';
const conversionFunction = id => {
if (id.startsWith(prefix)) {
return id.substr(prefix.length);
}

throw new Error('invalid_client_id_prefix');
};

return convertObject(outputValidator, data, [tID], conversionFunction);
}

function convertObject<T, I>(
validator: TType<I>,
input: I,
typesToConvert: $ReadOnlyArray<TType<T>>,
conversionFunction: T => T,
): I {
if (input === null || input === undefined) {
return input;
}

// While they should be the same runtime object,
// `TValidator` is `TType<T>` and `validator` is `TType<I>`.
// Having them have different types allows us to use `assertWithValidator`
// to change `input` flow type
const TValidator = typesToConvert[typesToConvert.indexOf(validator)];
if (TValidator && TValidator.is(input)) {
const TInput = assertWithValidator(input, TValidator);
const converted = conversionFunction(TInput);
return assertWithValidator(converted, validator);
}

if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') {
return convertObject(
validator.meta.type,
input,
typesToConvert,
conversionFunction,
);
}
if (validator.meta.kind === 'interface' && typeof input === 'object') {
const recastValidator: TInterface<typeof input> = (validator: any);
const result = {};
for (const key in input) {
const innerValidator = recastValidator.meta.props[key];
result[key] = convertObject(
innerValidator,
input[key],
typesToConvert,
conversionFunction,
);
}
return assertWithValidator(result, recastValidator);
}
if (validator.meta.kind === 'union') {
for (const innerValidator of validator.meta.types) {
if (innerValidator.is(input)) {
return convertObject(
innerValidator,
input,
typesToConvert,
conversionFunction,
);
}
}
return input;
}
if (validator.meta.kind === 'list' && Array.isArray(input)) {
const innerValidator = validator.meta.type;
return (input.map(value =>
convertObject(innerValidator, value, typesToConvert, conversionFunction),
): any);
}
if (validator.meta.kind === 'dict' && typeof input === 'object') {
const domainValidator = validator.meta.domain;
const codomainValidator = validator.meta.codomain;
if (typesToConvert.includes(domainValidator)) {
input = _mapKeys(key => conversionFunction(key))(input);
}
return _mapValues(value =>
convertObject(
codomainValidator,
value,
typesToConvert,
conversionFunction,
),
)(input);
}

return input;
}

export {
convertClientIDsToServerIDs,
convertServerIDsToClientIDs,
convertObject,
};
69 changes: 69 additions & 0 deletions lib/utils/conversion-utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// @flow

import invariant from 'invariant';
import t from 'tcomb';

import {
convertServerIDsToClientIDs,
convertClientIDsToServerIDs,
} from './conversion-utils.js';
import { tShape, tID, idSchemaRegex } from './validation-utils.js';

describe('id conversion', () => {
it('should convert string id', () => {
const validator = tShape({ id: tID });
const serverData = { id: '1' };
const clientData = { id: '0|1' };

expect(
convertServerIDsToClientIDs('0', validator, serverData),
).toStrictEqual(clientData);
expect(
convertClientIDsToServerIDs('0', validator, clientData),
).toStrictEqual(serverData);
});

it('should convert a complex type', () => {
const validator = tShape({ ids: t.dict(tID, t.list(tID)) });
const serverData = { ids: { '1': ['11', '12'], '2': [], '3': ['13'] } };
const clientData = {
ids: { '0|1': ['0|11', '0|12'], '0|2': [], '0|3': ['0|13'] },
};

expect(
convertServerIDsToClientIDs('0', validator, serverData),
).toStrictEqual(clientData);
expect(
convertClientIDsToServerIDs('0', validator, clientData),
).toStrictEqual(serverData);
});

it('should convert a refinement', () => {
const validator = t.refinement(tID, () => true);
const serverData = '1';
const clientData = '0|1';

expect(
convertServerIDsToClientIDs('0', validator, serverData),
).toStrictEqual(clientData);
expect(
convertClientIDsToServerIDs('0', validator, clientData),
).toStrictEqual(serverData);
});
});

describe('idSchemaRegex tests', () => {
it('should capture ids', () => {
const regex = new RegExp(`^(${idSchemaRegex})$`);
const ids = ['123|123', '0|0', '123', '0'];

for (const id of ids) {
const result = regex.exec(id);
expect(result).not.toBeNull();
invariant(result, 'result is not null');
const matches = [...result];
expect(matches).toHaveLength(2);
expect(matches[1]).toBe(id);
}
});
});
119 changes: 0 additions & 119 deletions lib/utils/validation-utils.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// @flow

import invariant from 'invariant';
import _mapKeys from 'lodash/fp/mapKeys.js';
import _mapValues from 'lodash/fp/mapValues.js';
import t from 'tcomb';
import type {
TStructProps,
Expand Down Expand Up @@ -105,120 +103,6 @@ function assertWithValidator<T>(data: mixed, validator: TType<T>): T {

const ashoatKeyserverID = '256';

function convertServerIDsToClientIDs<T>(
serverPrefixID: string,
outputValidator: TType<T>,
data: T,
): T {
const conversionFunction = id => {
if (id.indexOf('|') !== -1) {
console.warn(`Server id '${id}' already has a prefix`);
return id;
}
return `${serverPrefixID}|${id}`;
};

return convertObject(outputValidator, data, [tID], conversionFunction);
}

function convertClientIDsToServerIDs<T>(
serverPrefixID: string,
outputValidator: TType<T>,
data: T,
): T {
const prefix = serverPrefixID + '|';
const conversionFunction = id => {
if (id.startsWith(prefix)) {
return id.substr(prefix.length);
}

throw new Error('invalid_client_id_prefix');
};

return convertObject(outputValidator, data, [tID], conversionFunction);
}

function convertObject<T, I>(
validator: TType<I>,
input: I,
typesToConvert: $ReadOnlyArray<TType<T>>,
conversionFunction: T => T,
): I {
if (input === null || input === undefined) {
return input;
}

// While they should be the same runtime object,
// `TValidator` is `TType<T>` and `validator` is `TType<I>`.
// Having them have different types allows us to use `assertWithValidator`
// to change `input` flow type
const TValidator = typesToConvert[typesToConvert.indexOf(validator)];
if (TValidator && TValidator.is(input)) {
const TInput = assertWithValidator(input, TValidator);
const converted = conversionFunction(TInput);
return assertWithValidator(converted, validator);
}

if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') {
return convertObject(
validator.meta.type,
input,
typesToConvert,
conversionFunction,
);
}
if (validator.meta.kind === 'interface' && typeof input === 'object') {
const recastValidator: TInterface<typeof input> = (validator: any);
const result = {};
for (const key in input) {
const innerValidator = recastValidator.meta.props[key];
result[key] = convertObject(
innerValidator,
input[key],
typesToConvert,
conversionFunction,
);
}
return assertWithValidator(result, recastValidator);
}
if (validator.meta.kind === 'union') {
for (const innerValidator of validator.meta.types) {
if (innerValidator.is(input)) {
return convertObject(
innerValidator,
input,
typesToConvert,
conversionFunction,
);
}
}
return input;
}
if (validator.meta.kind === 'list' && Array.isArray(input)) {
const innerValidator = validator.meta.type;
return (input.map(value =>
convertObject(innerValidator, value, typesToConvert, conversionFunction),
): any);
}
if (validator.meta.kind === 'dict' && typeof input === 'object') {
const domainValidator = validator.meta.domain;
const codomainValidator = validator.meta.codomain;
if (typesToConvert.includes(domainValidator)) {
input = _mapKeys(key => conversionFunction(key))(input);
}
return _mapValues(value =>
convertObject(
codomainValidator,
value,
typesToConvert,
conversionFunction,
),
)(input);
}

return input;
}

const idSchemaRegex = '(?:[0-9]+\\|)?[0-9]+';

export {
Expand All @@ -244,8 +128,5 @@ export {
tMediaMessageMedia,
assertWithValidator,
ashoatKeyserverID,
convertClientIDsToServerIDs,
convertServerIDsToClientIDs,
convertObject,
idSchemaRegex,
};
Loading

0 comments on commit feb5be9

Please sign in to comment.