Skip to content

Commit

Permalink
Change params casing for Voice RPC methods (#489)
Browse files Browse the repository at this point in the history
* add util for converting a record's props to snake_case

* export util from core

* update `record` method to use camelCase

* improve typings for toSnakeCaseProps

* rename function, update prompt method

* convert remaining prompt params

* use camelCase for devices and add conversion for the internal util

* convert digit props

* add util for checking typeof

* add ability to convert deeply nested arrays

* fix typeof

* fix condition

* remove need of the util
  • Loading branch information
framini committed Apr 14, 2022
1 parent 15154da commit 229cf97
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 32 deletions.
2 changes: 1 addition & 1 deletion internal/playground-realtime-api/src/voice/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ async function run() {
volume: 1.0,
digits: {
max: 4,
digit_timeout: 10,
digitTimeout: 10,
terminators: '#',
},
})
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getLogger,
isGlobalEvent,
toExternalJSON,
toSnakeCaseKeys,
toLocalEvent,
toSyntheticEvent,
extendComponent,
Expand Down Expand Up @@ -44,6 +45,7 @@ export {
getEventEmitter,
isGlobalEvent,
toExternalJSON,
toSnakeCaseKeys,
toLocalEvent,
toInternalEventName,
serializeableProxy,
Expand Down
28 changes: 14 additions & 14 deletions packages/core/src/types/voiceCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export interface VoiceCallSipParams {
timeout?: number
headers?: SipHeader[]
codecs?: SipCodec[]
webrtc_media?: boolean
webrtcMedia?: boolean
}

export interface NestedArray<T> extends Array<T | NestedArray<T>> {}
Expand Down Expand Up @@ -207,8 +207,8 @@ export interface VoiceCallRecordMethodParams {
format?: 'mp3' | 'wav'
stereo?: boolean
direction?: 'listen' | 'speak' | 'both'
initial_timeout?: number
end_silence_timeout?: number
initialTimeout?: number
endSilenceTimeout?: number
terminators?: string
}
}
Expand All @@ -217,43 +217,43 @@ type SpeechOrDigits =
| {
digits: {
max: number
digit_timeout?: number
digitTimeout?: number
terminators?: string
}
speech?: never
}
| {
digits?: never
speech: {
end_silence_timeout: number
speech_timeout: number
endSilenceTimeout: number
speechTimeout: number
language: number
hints: string[]
}
}
export type VoiceCallPromptMethodParams = SpeechOrDigits & {
media: NestedArray<VoiceCallPlayParams>
volume?: number
initial_timeout?: number
partial_results?: boolean
initialTimeout?: number
partialResults?: boolean
}
export type VoiceCallPromptAudioMethodParams = SpeechOrDigits &
Omit<VoiceCallPlayAudioParams, 'type'> & {
volume?: number
initial_timeout?: number
partial_results?: boolean
initialTimeout?: number
partialResults?: boolean
}
export type VoiceCallPromptRingtoneMethodParams = SpeechOrDigits &
Omit<VoiceCallPlayRingtoneParams, 'type'> & {
volume?: number
initial_timeout?: number
partial_results?: boolean
initialTimeout?: number
partialResults?: boolean
}
export type VoiceCallPromptTTSMethodParams = SpeechOrDigits &
Omit<VoiceCallPlayTTSParams, 'type'> & {
volume?: number
initial_timeout?: number
partial_results?: boolean
initialTimeout?: number
partialResults?: boolean
}
type TapCodec = 'OPUS' | 'PCMA' | 'PCMU'
interface TapDeviceWS {
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const UPPERCASE_REGEX = /[A-Z]/g
/**
* Converts values from camelCase to snake_case
* @internal
*/
export const fromCamelToSnakeCase = <T>(event: T): T => {
// @ts-ignore
return event.replace(UPPERCASE_REGEX, (letter) => {
return `_${letter.toLowerCase()}`
}) as T
}
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from './parseRPCResponse'
export * from './toExternalJSON'
export * from './toInternalEventName'
export * from './toInternalAction'
export * from './toSnakeCaseKeys'
export * from './extendComponent'
export * from './eventTransformUtils'
export * from './proxyUtils'
Expand Down
13 changes: 1 addition & 12 deletions packages/core/src/utils/toInternalEventName.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fromCamelToSnakeCase } from './common'
import { EVENT_NAMESPACE_DIVIDER } from './constants'
import { EventEmitter } from './EventEmitter'

Expand Down Expand Up @@ -27,18 +28,6 @@ export const toInternalEventName = <
return event
}

const UPPERCASE_REGEX = /[A-Z]/g
/**
* Converts values from camelCase to snake_case
* @internal
*/
const fromCamelToSnakeCase = <T>(event: T): T => {
// @ts-ignore
return event.replace(UPPERCASE_REGEX, (letter) => {
return `_${letter.toLowerCase()}`
}) as T
}

const getNamespacedEvent = ({
namespace,
event,
Expand Down
114 changes: 114 additions & 0 deletions packages/core/src/utils/toSnakeCaseKeys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { toSnakeCaseKeys } from './toSnakeCaseKeys'

describe('toSnakeCaseKeys', () => {
it('should convert properties from camelCase to snake_case', () => {
expect(
toSnakeCaseKeys({
someProperty: 'someValue',
someOtherProperty: 'someOtherValue',
nestedProperty: {
nestedProperty1: 'nestedValue',
nestedProperty2: 'nestedValue2',
nestedProperty3: {
nestedProperty4: 'nestedValue4',
nestedProperty5: {
nestedProperty6: 'nestedValue6',
nestedProperty7: {
nestedProperty8: 'nestedValue8',
},
},
},
nestedProperty4: [
{
somePropertyKey: 'value_in_key',
somePropertyNested: {
somePropertyNested1: 'value_in_key_nested1',
somePropertyNested2: [
{
somePropertyNested21: {
somePropertyNested211: 'value',
},
},
],
},
},
],
},
nestedPropertyWithLongName: 'nestedValueWithLongName',
})
).toEqual({
some_property: 'someValue',
some_other_property: 'someOtherValue',
nested_property: {
nested_property1: 'nestedValue',
nested_property2: 'nestedValue2',
nested_property3: {
nested_property4: 'nestedValue4',
nested_property5: {
nested_property6: 'nestedValue6',
nested_property7: {
nested_property8: 'nestedValue8',
},
},
},
nested_property4: [
{
some_property_key: 'value_in_key',
some_property_nested: {
some_property_nested1: 'value_in_key_nested1',
some_property_nested2: [
{
some_property_nested21: {
some_property_nested211: 'value',
},
},
],
},
},
],
},
nested_property_with_long_name: 'nestedValueWithLongName',
})
})

it('should allow passing a function for transforming values', () => {
expect(
toSnakeCaseKeys(
{
someProperty: 'someValue',
someOtherProperty: 'someOtherValue',
nestedProperty: {
nestedProperty1: 'nestedValue',
nestedProperty2: 'nestedValue2',
nestedProperty3: {
nestedProperty4: 'nestedValue4',
nestedProperty5: {
nestedProperty6: 'nestedValue6',
nestedProperty7: {
nestedProperty8: 'nestedValue8',
},
},
},
},
},
(value: string) => value.toUpperCase()
)
).toEqual({
some_property: 'SOMEVALUE',
some_other_property: 'SOMEOTHERVALUE',
nested_property: {
nested_property1: 'NESTEDVALUE',
nested_property2: 'NESTEDVALUE2',
nested_property3: {
nested_property4: 'NESTEDVALUE4',
nested_property5: {
nested_property6: 'NESTEDVALUE6',
nested_property7: {
nested_property8: 'NESTEDVALUE8',
},
},
},
},
})
})
})
32 changes: 32 additions & 0 deletions packages/core/src/utils/toSnakeCaseKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { CamelToSnakeCase } from '../types/utils'
import { fromCamelToSnakeCase } from './common'

type ToSnakeCaseKeys<T> = {
[Property in NonNullable<keyof T> as CamelToSnakeCase<
Extract<Property, string>
>]: T[Property]
}

export const toSnakeCaseKeys = <T extends Record<string, any>>(
obj: T,
transform: (value: string) => any = (value: string) => value,
result: Record<string, any> = {}
) => {
if (Array.isArray(obj)) {
result = obj.map((item: any, index: number) =>
toSnakeCaseKeys(item, transform, result[index])
)
} else {
Object.keys(obj).forEach((key) => {
const newKey = fromCamelToSnakeCase(key)
// Both 'object's and arrays will enter this branch
if (obj[key] && typeof obj[key] === 'object') {
result[newKey] = toSnakeCaseKeys(obj[key], transform, result[newKey])
} else {
result[newKey] = transform(obj[key])
}
})
}

return result as ToSnakeCaseKeys<T>
}
5 changes: 3 additions & 2 deletions packages/realtime-api/src/voice/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
EventTransform,
toLocalEvent,
toExternalJSON,
toSnakeCaseKeys,
CallingCallPlayEventParams,
VoiceCallTapMethodParams,
VoiceCallTapAudioMethodParams,
Expand Down Expand Up @@ -498,7 +499,7 @@ export class CallConsumer extends AutoApplyTransformsConsumer<RealTimeCallApiEve
// @ts-expect-error
this.on(callingRecordTriggerEvent, resolveHandler)

const record = { ...params }
const record = toSnakeCaseKeys(params)
this.execute({
method: 'calling.record',
params: {
Expand Down Expand Up @@ -564,7 +565,7 @@ export class CallConsumer extends AutoApplyTransformsConsumer<RealTimeCallApiEve
speech,
media,
volume,
} = params
} = toSnakeCaseKeys(params)
const collect = {
initial_timeout,
partial_results,
Expand Down
7 changes: 4 additions & 3 deletions packages/realtime-api/src/voice/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
VoiceCallDialMethodParams,
VoiceCallPlayParams,
VoiceCallPlayMethodParams,
toSnakeCaseKeys,
} from '@signalwire/core'

const toInternalDevice = (device: VoiceCallDeviceParams) => {
Expand All @@ -11,18 +12,18 @@ const toInternalDevice = (device: VoiceCallDeviceParams) => {
const { type, ...params } = device
return {
type,
params,
params: toSnakeCaseKeys(params),
}
}
case 'phone': {
const { to, from, type, ...rest } = device
return {
type,
params: {
params: toSnakeCaseKeys({
...rest,
to_number: to,
from_number: from,
},
}),
}
}

Expand Down

0 comments on commit 229cf97

Please sign in to comment.