mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 06:04:05 +00:00
refactor: improve payload type for OSC (#954)
--------- Co-authored-by: Joel Wetzell <jwetzell@yahoo.com> Co-authored-by: arc-alex <ac@omnivox.dk>
This commit is contained in:
@@ -9,4 +9,5 @@ export const startsWithHttp = /^http:\/\//;
|
|||||||
export const startsWithSlash = /^\//;
|
export const startsWithSlash = /^\//;
|
||||||
export const isAlphanumeric = /^[a-z0-9]+$/i;
|
export const isAlphanumeric = /^[a-z0-9]+$/i;
|
||||||
export const isASCII = /^[ -~]+$/; //https://catonmat.net/my-favorite-regex
|
export const isASCII = /^[ -~]+$/; //https://catonmat.net/my-favorite-regex
|
||||||
|
export const isASCIIorEmpty = /^$|^[ -~]+$/; //https://catonmat.net/my-favorite-regex
|
||||||
export const isNotEmpty = /\S/;
|
export const isNotEmpty = /\S/;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { generateId } from 'ontime-utils';
|
|||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
||||||
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { isASCII, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
import { isASCII, isASCIIorEmpty, isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import { cycles } from './integrationUtils';
|
import { cycles } from './integrationUtils';
|
||||||
@@ -221,7 +221,7 @@ export default function OscIntegrations() {
|
|||||||
<th>Enabled</th>
|
<th>Enabled</th>
|
||||||
<th>Cycle</th>
|
<th>Cycle</th>
|
||||||
<th className={style.halfWidth}>Address</th>
|
<th className={style.halfWidth}>Address</th>
|
||||||
<th className={style.halfWidth}>Payload</th>
|
<th className={style.halfWidth}>Arguments</th>
|
||||||
<th />
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -277,7 +277,7 @@ export default function OscIntegrations() {
|
|||||||
{...register(`subscriptions.${index}.payload`, {
|
{...register(`subscriptions.${index}.payload`, {
|
||||||
validate: {
|
validate: {
|
||||||
oscStringIsAscii: (value) =>
|
oscStringIsAscii: (value) =>
|
||||||
isASCII.test(value) || 'OSC payloads only allow ASCII characters',
|
isASCIIorEmpty.test(value) || 'OSC arguments only allow ASCII characters',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { LogOrigin, MaybeNumber, MaybeString, OSCSettings, OscSubscription } fro
|
|||||||
|
|
||||||
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
import IIntegration, { TimerLifeCycleKey } from './IIntegration.js';
|
||||||
import { parseTemplateNested } from './integrationUtils.js';
|
import { parseTemplateNested } from './integrationUtils.js';
|
||||||
import { isObject } from '../../utils/varUtils.js';
|
|
||||||
import { logger } from '../../classes/Logger.js';
|
import { logger } from '../../classes/Logger.js';
|
||||||
import { OscServer } from '../../adapters/OscAdapter.js';
|
import { OscServer } from '../../adapters/OscAdapter.js';
|
||||||
|
import { stringToOSCArgs } from '../../utils/oscArgParser.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Class contains logic towards outgoing OSC communications
|
* @description Class contains logic towards outgoing OSC communications
|
||||||
@@ -60,27 +60,24 @@ export class OscIntegration implements IIntegration<OscSubscription, OSCSettings
|
|||||||
}
|
}
|
||||||
const parsedAddress = parseTemplateNested(address, state || {});
|
const parsedAddress = parseTemplateNested(address, state || {});
|
||||||
const parsedPayload = payload ? parseTemplateNested(payload, state || {}) : undefined;
|
const parsedPayload = payload ? parseTemplateNested(payload, state || {}) : undefined;
|
||||||
|
const parsedArguments = stringToOSCArgs(parsedPayload);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.emit(parsedAddress, parsedPayload);
|
this.emit(parsedAddress, parsedArguments);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(LogOrigin.Tx, `OSC Integration: ${error}`);
|
logger.error(LogOrigin.Tx, `OSC Integration: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(address: string, payload?: ArgumentType) {
|
emit(address: string, args: ArgumentType[]) {
|
||||||
if (!this.oscClient) {
|
if (!this.oscClient) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Look into using bundles
|
||||||
const message = new Message(address);
|
const message = new Message(address);
|
||||||
if (payload) {
|
message.append(args);
|
||||||
if (isObject(payload)) {
|
|
||||||
message.append(JSON.stringify(payload));
|
|
||||||
} else {
|
|
||||||
message.append(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.oscClient.send(message);
|
this.oscClient.send(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { stringToOSCArgs } from '../../utils/oscArgParser.js';
|
||||||
import { parseTemplateNested } from './integrationUtils.js';
|
import { parseTemplateNested } from './integrationUtils.js';
|
||||||
|
|
||||||
describe('parseTemplateNested()', () => {
|
describe('parseTemplateNested()', () => {
|
||||||
@@ -96,3 +97,61 @@ describe('parseNestedTemplate() -> resolveAliasData()', () => {
|
|||||||
expect(easyParse).toBe('5 to testing-3 {{human.not.found}}');
|
expect(easyParse).toBe('5 to testing-3 {{human.not.found}}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseNestedTemplate() -> stringToOSCArgs()', () => {
|
||||||
|
it('specific osc requirements', () => {
|
||||||
|
const data = {
|
||||||
|
not: {
|
||||||
|
so: {
|
||||||
|
easy: 'data with space',
|
||||||
|
empty: '',
|
||||||
|
number: 1234,
|
||||||
|
stringNumber: '1234',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const payloads = [
|
||||||
|
{
|
||||||
|
test: '"string with space and {{not.so.easy}}"',
|
||||||
|
expect: [{ type: 'string', value: 'string with space and data with space' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: '"string with space and {{not.so.empty}}"',
|
||||||
|
expect: [{ type: 'string', value: 'string with space and ' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: '"string with space and {{not.so.number}}"',
|
||||||
|
expect: [{ type: 'string', value: 'string with space and 1234' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: '"string with space and {{not.so.stringNumber}}"',
|
||||||
|
expect: [{ type: 'string', value: 'string with space and 1234' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: '"{{not.so.easy}}" 1',
|
||||||
|
expect: [
|
||||||
|
{ type: 'string', value: 'data with space' },
|
||||||
|
{ type: 'integer', value: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: '"{{not.so.empty}}" 1',
|
||||||
|
expect: [
|
||||||
|
{ type: 'string', value: '' },
|
||||||
|
{ type: 'integer', value: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: '',
|
||||||
|
expect: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
payloads.forEach((payload) => {
|
||||||
|
const parsedPayload = parseTemplateNested(payload.test, data);
|
||||||
|
const parsedArguments = stringToOSCArgs(parsedPayload);
|
||||||
|
expect(parsedArguments).toStrictEqual(payload.expect);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { stringToOSCArgs } from '../oscArgParser.js';
|
||||||
|
|
||||||
|
describe('test stringToOSCArgs()', () => {
|
||||||
|
it('all types', () => {
|
||||||
|
const test = 'test 1111 0.1111 TRUE FALSE';
|
||||||
|
const expected = [
|
||||||
|
{ type: 'string', value: 'test' },
|
||||||
|
{ type: 'integer', value: 1111 },
|
||||||
|
{ type: 'float', value: 0.1111 },
|
||||||
|
{ type: 'T', value: true },
|
||||||
|
{ type: 'F', value: false },
|
||||||
|
];
|
||||||
|
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empty is nothing', () => {
|
||||||
|
const test = undefined;
|
||||||
|
const expected = [];
|
||||||
|
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empty is nothing', () => {
|
||||||
|
const test = '';
|
||||||
|
const expected = [];
|
||||||
|
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keep other types in strings', () => {
|
||||||
|
const test = 'test "1111" "0.1111" "TRUE" "FALSE"';
|
||||||
|
const expected = [
|
||||||
|
{ type: 'string', value: 'test' },
|
||||||
|
{ type: 'string', value: '1111' },
|
||||||
|
{ type: 'string', value: '0.1111' },
|
||||||
|
{ type: 'string', value: 'TRUE' },
|
||||||
|
{ type: 'string', value: 'FALSE' },
|
||||||
|
];
|
||||||
|
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keep spaces in quoted strings', () => {
|
||||||
|
const test = '"test space" 1111 0.1111 TRUE FALSE';
|
||||||
|
const expected = [
|
||||||
|
{ type: 'string', value: 'test space' },
|
||||||
|
{ type: 'integer', value: 1111 },
|
||||||
|
{ type: 'float', value: 0.1111 },
|
||||||
|
{ type: 'T', value: true },
|
||||||
|
{ type: 'F', value: false },
|
||||||
|
];
|
||||||
|
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keep spaces escaped quotes', () => {
|
||||||
|
const test = '"test \\" space" 1111 0.1111 TRUE FALSE';
|
||||||
|
const expected = [
|
||||||
|
{ type: 'string', value: 'test " space' },
|
||||||
|
{ type: 'integer', value: 1111 },
|
||||||
|
{ type: 'float', value: 0.1111 },
|
||||||
|
{ type: 'T', value: true },
|
||||||
|
{ type: 'F', value: false },
|
||||||
|
];
|
||||||
|
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2 spaces', () => {
|
||||||
|
const test = '1111 0.1111 TRUE FALSE';
|
||||||
|
const expected = [
|
||||||
|
{ type: 'integer', value: 1111 },
|
||||||
|
{ type: 'float', value: 0.1111 },
|
||||||
|
{ type: 'T', value: true },
|
||||||
|
{ type: 'F', value: false },
|
||||||
|
];
|
||||||
|
expect(stringToOSCArgs(test)).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Argument } from 'node-osc';
|
||||||
|
import { splitWhitespace } from 'ontime-utils';
|
||||||
|
|
||||||
|
export function stringToOSCArgs(argsString: string | undefined): Argument[] {
|
||||||
|
if (typeof argsString === 'undefined' || argsString === '') {
|
||||||
|
return new Array<Argument>();
|
||||||
|
}
|
||||||
|
const matches = splitWhitespace(argsString);
|
||||||
|
|
||||||
|
const parsedArguments: Argument[] = matches.map((argString: string) => {
|
||||||
|
const argAsNum = Number(argString);
|
||||||
|
// NOTE: number like: 1 2.0 33333
|
||||||
|
if (!Number.isNaN(argAsNum)) {
|
||||||
|
return { type: argString.includes('.') ? 'float' : 'integer', value: argAsNum };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argString.startsWith('"') && argString.endsWith('"')) {
|
||||||
|
// NOTE: "quoted string"
|
||||||
|
return { type: 'string', value: argString.substring(1, argString.length - 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argString === 'TRUE') {
|
||||||
|
// NOTE: Boolean true
|
||||||
|
return { type: 'T', value: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argString === 'FALSE') {
|
||||||
|
// NOTE: Boolean false
|
||||||
|
return { type: 'F', value: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: string
|
||||||
|
return { type: 'string', value: argString };
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsedArguments;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ export type OscSubscription = {
|
|||||||
id: string;
|
id: string;
|
||||||
cycle: TimerLifeCycleKey;
|
cycle: TimerLifeCycleKey;
|
||||||
address: string;
|
address: string;
|
||||||
payload: string;
|
payload: string; // TODO: we should be using arguments to keep in line with protocol language
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export {
|
|||||||
export { parseUserTime } from './src/date-utils/parseUserTime.js';
|
export { parseUserTime } from './src/date-utils/parseUserTime.js';
|
||||||
export { isAlphanumeric } from './src/regex-utils/isAlphanumeric.js';
|
export { isAlphanumeric } from './src/regex-utils/isAlphanumeric.js';
|
||||||
export { isColourHex } from './src/regex-utils/isColourHex.js';
|
export { isColourHex } from './src/regex-utils/isColourHex.js';
|
||||||
|
export { splitWhitespace } from './src/regex-utils/splitWhitespace.js';
|
||||||
|
|
||||||
// helpers from externals
|
// helpers from externals
|
||||||
export { deepmerge } from './src/externals/deepmerge.js';
|
export { deepmerge } from './src/externals/deepmerge.js';
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { splitWhitespace } from './splitWhitespace';
|
||||||
|
|
||||||
|
describe('test splitWhitespace() function', () => {
|
||||||
|
it('empty string', () => {
|
||||||
|
const test = '';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('just space', () => {
|
||||||
|
const test = ' ';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('1 item', () => {
|
||||||
|
const test = 'test';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(['test']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2 items', () => {
|
||||||
|
const test = 'test test';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(['test', 'test']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2 items and quoted string', () => {
|
||||||
|
const test = 'test test "more test"';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(['test', 'test', '"more test"']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2 sapces', () => {
|
||||||
|
const test = 'test test "more test"';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(['test', 'test', '"more test"']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('quotes without spaces', () => {
|
||||||
|
const test = 'test test "moreTest"';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(['test', 'test', '"moreTest"']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('escaped quotes', () => {
|
||||||
|
const test = 'test test "more \\" test"';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(['test', 'test', '"more " test"']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('missing end quotes', () => {
|
||||||
|
const test = 'test test "more test';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(['test', 'test', '"more test']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('missing start quotes', () => {
|
||||||
|
const test = 'test test more test"';
|
||||||
|
expect(splitWhitespace(test)).toStrictEqual(['test', 'test', 'more', 'test"']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const splitRegex = /\\?.|^$/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adapted from {@link https://stackoverflow.com/questions/4031900/split-a-string-by-whitespace-keeping-quoted-segments-allowing-escaped-quotes this}
|
||||||
|
* @param str string to split
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const splitWhitespace = (str: string, keepQuotes = true): null | string[] => {
|
||||||
|
const match = str.match(splitRegex);
|
||||||
|
if (!match || match[0] == '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const array = match
|
||||||
|
.reduce(
|
||||||
|
(accumulator, current) => {
|
||||||
|
if (current === '"') {
|
||||||
|
accumulator.inQuotes ^= 1;
|
||||||
|
if (keepQuotes) {
|
||||||
|
accumulator.array[accumulator.array.length - 1] += current.replace(/\\(.)/, '$1');
|
||||||
|
}
|
||||||
|
} else if (!accumulator.inQuotes && current === ' ') {
|
||||||
|
accumulator.array.push('');
|
||||||
|
} else {
|
||||||
|
accumulator.array[accumulator.array.length - 1] += current.replace(/\\(.)/, '$1');
|
||||||
|
}
|
||||||
|
return accumulator;
|
||||||
|
},
|
||||||
|
{ array: [''], inQuotes: 0 },
|
||||||
|
)
|
||||||
|
.array.filter((value) => value != '');
|
||||||
|
|
||||||
|
if (!array.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user