From c09425fa890a1f85bfdc372f79fc22ead831b5f2 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Wed, 15 Feb 2023 21:56:23 +0700 Subject: [PATCH] feat: use verb-specifications (#262) * feat: use verb-specifications * feat: use verb-specifications * fix: verb specification v2 * remove irrelevant tests * fix: verb-scpecification * update to use @jambonz/verb-specifications --------- Co-authored-by: Quan HL Co-authored-by: Dave Horton --- lib/http-routes/api/create-message.js | 2 +- lib/http-routes/api/messaging.js | 2 +- lib/middleware.js | 2 +- lib/session/call-session.js | 2 +- lib/tasks/conference.js | 2 +- lib/tasks/dialogflow/index.js | 2 +- lib/tasks/enqueue.js | 2 +- lib/tasks/lex.js | 2 +- lib/tasks/make_task.js | 4 +- lib/tasks/rest_dial.js | 2 +- lib/tasks/specs.json | 791 -------------------------- lib/tasks/task.js | 78 +-- lib/tasks/transcribe.js | 2 +- lib/utils/normalize-jambones.js | 31 - lib/utils/place-outdial.js | 2 +- package-lock.json | 17 + package.json | 1 + test/index.js | 2 +- test/unit-tests.js | 6 +- 19 files changed, 36 insertions(+), 916 deletions(-) delete mode 100644 lib/tasks/specs.json delete mode 100644 lib/utils/normalize-jambones.js diff --git a/lib/http-routes/api/create-message.js b/lib/http-routes/api/create-message.js index ab637aa9..a92da171 100644 --- a/lib/http-routes/api/create-message.js +++ b/lib/http-routes/api/create-message.js @@ -2,7 +2,7 @@ const router = require('express').Router(); const CallInfo = require('../../session/call-info'); const {CallDirection} = require('../../utils/constants'); const SmsSession = require('../../session/sms-call-session'); -const normalizeJambones = require('../../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); const makeTask = require('../../tasks/make_task'); router.post('/:sid', async(req, res) => { diff --git a/lib/http-routes/api/messaging.js b/lib/http-routes/api/messaging.js index 4448804a..16c16d35 100644 --- a/lib/http-routes/api/messaging.js +++ b/lib/http-routes/api/messaging.js @@ -4,7 +4,7 @@ const WsRequestor = require('../../utils/ws-requestor'); const CallInfo = require('../../session/call-info'); const {CallDirection} = require('../../utils/constants'); const SmsSession = require('../../session/sms-call-session'); -const normalizeJambones = require('../../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); const {TaskPreconditions} = require('../../utils/constants'); const makeTask = require('../../tasks/make_task'); diff --git a/lib/middleware.js b/lib/middleware.js index 000c46b9..84183f7c 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -6,7 +6,7 @@ const HttpRequestor = require('./utils/http-requestor'); const WsRequestor = require('./utils/ws-requestor'); const makeTask = require('./tasks/make_task'); const parseUri = require('drachtio-srf').parseUri; -const normalizeJambones = require('./utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); const dbUtils = require('./utils/db-utils'); const RootSpan = require('./utils/call-tracer'); const listTaskNames = require('./utils/summarize-tasks'); diff --git a/lib/session/call-session.js b/lib/session/call-session.js index 45b92cc2..be9fc8da 100644 --- a/lib/session/call-session.js +++ b/lib/session/call-session.js @@ -13,7 +13,7 @@ const moment = require('moment'); const assert = require('assert'); const sessionTracker = require('./session-tracker'); const makeTask = require('../tasks/make_task'); -const normalizeJambones = require('../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); const listTaskNames = require('../utils/summarize-tasks'); const HttpRequestor = require('../utils/http-requestor'); const WsRequestor = require('../utils/ws-requestor'); diff --git a/lib/tasks/conference.js b/lib/tasks/conference.js index 1a6a8e79..c31f73ea 100644 --- a/lib/tasks/conference.js +++ b/lib/tasks/conference.js @@ -2,7 +2,7 @@ const Task = require('./task'); const Emitter = require('events'); const ConfirmCallSession = require('../session/confirm-call-session'); const {TaskName, TaskPreconditions, BONG_TONE} = require('../utils/constants'); -const normalizeJambones = require('../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); const makeTask = require('./make_task'); const bent = require('bent'); const assert = require('assert'); diff --git a/lib/tasks/dialogflow/index.js b/lib/tasks/dialogflow/index.js index 70f75658..628e01b7 100644 --- a/lib/tasks/dialogflow/index.js +++ b/lib/tasks/dialogflow/index.js @@ -3,7 +3,7 @@ const {TaskName, TaskPreconditions} = require('../../utils/constants'); const Intent = require('./intent'); const DigitBuffer = require('./digit-buffer'); const Transcription = require('./transcription'); -const normalizeJambones = require('../../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); class Dialogflow extends Task { constructor(logger, opts) { diff --git a/lib/tasks/enqueue.js b/lib/tasks/enqueue.js index 1b8b7624..5132a7f1 100644 --- a/lib/tasks/enqueue.js +++ b/lib/tasks/enqueue.js @@ -1,7 +1,7 @@ const Task = require('./task'); const Emitter = require('events'); const ConfirmCallSession = require('../session/confirm-call-session'); -const normalizeJambones = require('../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); const makeTask = require('./make_task'); const {TaskName, TaskPreconditions, QueueResults, KillReason} = require('../utils/constants'); const bent = require('bent'); diff --git a/lib/tasks/lex.js b/lib/tasks/lex.js index bf3f032b..4d562952 100644 --- a/lib/tasks/lex.js +++ b/lib/tasks/lex.js @@ -1,6 +1,6 @@ const Task = require('./task'); const {TaskName, TaskPreconditions} = require('../utils/constants'); -const normalizeJambones = require('../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); class Lex extends Task { constructor(logger, opts) { diff --git a/lib/tasks/make_task.js b/lib/tasks/make_task.js index bed583ce..1d90fc76 100644 --- a/lib/tasks/make_task.js +++ b/lib/tasks/make_task.js @@ -1,4 +1,4 @@ -const Task = require('./task'); +const { validateVerb } = require('@jambonz/verb-specifications'); const {TaskName} = require('../utils/constants'); const errBadInstruction = new Error('malformed jambonz application payload'); @@ -12,7 +12,7 @@ function makeTask(logger, obj, parent) { if (typeof data !== 'object') { throw errBadInstruction; } - Task.validate(name, data); + validateVerb(name, data, logger); switch (name) { case TaskName.SipDecline: const TaskSipDecline = require('./sip_decline'); diff --git a/lib/tasks/rest_dial.js b/lib/tasks/rest_dial.js index 729e756b..926de3d3 100644 --- a/lib/tasks/rest_dial.js +++ b/lib/tasks/rest_dial.js @@ -1,7 +1,7 @@ const Task = require('./task'); const {TaskName} = require('../utils/constants'); const makeTask = require('./make_task'); -const normalizeJambones = require('../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); /** * Manages an outdial made via REST API diff --git a/lib/tasks/specs.json b/lib/tasks/specs.json deleted file mode 100644 index 76391aea..00000000 --- a/lib/tasks/specs.json +++ /dev/null @@ -1,791 +0,0 @@ -{ - "sip:decline": { - "properties": { - "id": "string", - "status": "number", - "reason": "string", - "headers": "object" - }, - "required": [ - "status" - ] - }, - "sip:request": { - "properties": { - "id": "string", - "method": "string", - "body": "string", - "headers": "object", - "actionHook": "object|string" - }, - "required": [ - "method" - ] - }, - "sip:refer": { - "properties": { - "id": "string", - "referTo": "string", - "referredBy": "string", - "headers": "object", - "actionHook": "object|string", - "eventHook": "object|string" - }, - "required": [ - "referTo" - ] - }, - "config": { - "properties": { - "id": "string", - "synthesizer": "#synthesizer", - "recognizer": "#recognizer", - "bargeIn": "#bargeIn", - "record": "#recordOptions", - "listen": "#listenOptions", - "amd": "#amd", - "notifyEvents": "boolean" - }, - "required": [] - }, - "listenOptions": { - "properties": { - "enable": "boolean", - "url": "string", - "sampleRate": "number", - "wsAuth": "#auth", - "mixType": { - "type": "string", - "enum": ["mono", "stereo", "mixed"] - }, - "metadata": "object" - }, - "required": [ - "enable" - ] - }, - "bargeIn": { - "properties": { - "enable": "boolean", - "sticky": "boolean", - "actionHook": "object|string", - "input": "array", - "finishOnKey": "string", - "numDigits": "number", - "minDigits": "number", - "maxDigits": "number", - "interDigitTimeout": "number", - "dtmfBargein": "boolean", - "minBargeinWordCount": "number" - }, - "required": [ - "enable" - ] - }, - "dequeue": { - "properties": { - "id": "string", - "name": "string", - "actionHook": "object|string", - "timeout": "number", - "beep": "boolean" - }, - "required": [ - "name" - ] - }, - "enqueue": { - "properties": { - "id": "string", - "name": "string", - "actionHook": "object|string", - "waitHook": "object|string", - "_": "object" - }, - "required": [ - "name" - ] - }, - "leave": { - "properties": { - "id": "string" - } - }, - "hangup": { - "properties": { - "id": "string", - "headers": "object" - }, - "required": [ - ] - }, - "play": { - "properties": { - "id": "string", - "url": "string|array", - "loop": "number|string", - "earlyMedia": "boolean", - "seekOffset": "number|string", - "timeoutSecs": "number|string", - "actionHook": "object|string" - }, - "required": [ - "url" - ] - }, - "say": { - "properties": { - "id": "string", - "text": "string|array", - "loop": "number|string", - "synthesizer": "#synthesizer", - "earlyMedia": "boolean", - "disableTtsCache": "boolean" - }, - "required": [ - "text" - ] - }, - "gather": { - "properties": { - "id": "string", - "actionHook": "object|string", - "finishOnKey": "string", - "input": "array", - "numDigits": "number", - "minDigits": "number", - "maxDigits": "number", - "interDigitTimeout": "number", - "partialResultHook": "object|string", - "speechTimeout": "number", - "listenDuringPrompt": "boolean", - "dtmfBargein": "boolean", - "bargein": "boolean", - "minBargeinWordCount": "number", - "timeout": "number", - "recognizer": "#recognizer", - "play": "#play", - "say": "#say" - }, - "required": [ - ] - }, - "conference": { - "properties": { - "id": "string", - "name": "string", - "beep": "boolean", - "startConferenceOnEnter": "boolean", - "endConferenceOnExit": "boolean", - "maxParticipants": "number", - "joinMuted": "boolean", - "actionHook": "object|string", - "waitHook": "object|string", - "statusEvents": "array", - "statusHook": "object|string", - "enterHook": "object|string", - "record": "#record" - }, - "required": [ - "name" - ] - }, - "dial": { - "properties": { - "id": "string", - "actionHook": "object|string", - "answerOnBridge": "boolean", - "callerId": "string", - "confirmHook": "object|string", - "referHook": "object|string", - "dialMusic": "string", - "dtmfCapture": "object", - "dtmfHook": "object|string", - "headers": "object", - "listen": "#listen", - "target": ["#target"], - "timeLimit": "number", - "timeout": "number", - "proxy": "string", - "transcribe": "#transcribe", - "amd": "#amd" - }, - "required": [ - "target" - ] - }, - "dialogflow": { - "properties": { - "id": "string", - "credentials": "object|string", - "project": "string", - "environment": "string", - "region": { - "type": "string", - "enum": ["europe-west1", "europe-west2", "australia-southeast1", "asia-northeast1"] - }, - "lang": "string", - "actionHook": "object|string", - "eventHook": "object|string", - "events": "[string]", - "welcomeEvent": "string", - "welcomeEventParams": "object", - "noInputTimeout": "number", - "noInputEvent": "string", - "passDtmfAsTextInput": "boolean", - "thinkingMusic": "string", - "tts": "#synthesizer", - "bargein": "boolean" - }, - "required": [ - "project", - "credentials", - "lang" - ] - }, - "dtmf": { - "properties": { - "id": "string", - "dtmf": "string", - "duration": "number" - }, - "required": [ - "dtmf" - ] - }, - "lex": { - "properties": { - "id": "string", - "botId": "string", - "botAlias": "string", - "credentials": "object", - "region": "string", - "locale": "string", - "intent": "#lexIntent", - "welcomeMessage": "string", - "metadata": "object", - "bargein": "boolean", - "passDtmf": "boolean", - "actionHook": "object|string", - "eventHook": "object|string", - "noInputTimeout": "number", - "tts": "#synthesizer" - }, - "required": [ - "botId", - "botAlias", - "region", - "credentials" - ] - }, - "listen": { - "properties": { - "id": "string", - "actionHook": "object|string", - "auth": "#auth", - "finishOnKey": "string", - "maxLength": "number", - "metadata": "object", - "mixType": { - "type": "string", - "enum": ["mono", "stereo", "mixed"] - }, - "passDtmf": "boolean", - "playBeep": "boolean", - "disableBidirectionalAudio": "boolean", - "sampleRate": "number", - "timeout": "number", - "transcribe": "#transcribe", - "url": "string", - "wsAuth": "#auth", - "earlyMedia": "boolean" - }, - "required": [ - "url" - ] - }, - "message": { - "properties": { - "id": "string", - "carrier": "string", - "account_sid": "string", - "message_sid": "string", - "to": "string", - "from": "string", - "text": "string", - "media": "string|array", - "actionHook": "object|string" - }, - "required": [ - "to", - "from" - ] - }, - "pause": { - "properties": { - "id": "string", - "length": "number" - }, - "required": [ - "length" - ] - }, - "rasa": { - "properties": { - "id": "string", - "url": "string", - "recognizer": "#recognizer", - "tts": "#synthesizer", - "prompt": "string", - "actionHook": "object|string", - "eventHook": "object|string" - }, - "required": [ - "url" - ] - }, - "record": { - "properties": { - "path": "string" - }, - "required": [ - "path" - ] - }, - "recordOptions": { - "properties": { - "action": { - "type": "string", - "enum": ["startCallRecording", "stopCallRecording", "pauseCallRecording", "resumeCallRecording"] - }, - "recordingID": "string", - "siprecServerURL": "string" - }, - "required": [ - "action" - ] - }, - "redirect": { - "properties": { - "id": "string", - "actionHook": "object|string" - }, - "required": [ - "actionHook" - ] - }, - "rest:dial": { - "properties": { - "id": "string", - "account_sid": "string", - "application_sid": "string", - "call_hook": "object|string", - "call_status_hook": "object|string", - "from": "string", - "fromHost": "string", - "speech_synthesis_vendor": "string", - "speech_synthesis_voice": "string", - "speech_synthesis_language": "string", - "speech_recognizer_vendor": "string", - "speech_recognizer_language": "string", - "tag": "object", - "to": "#target", - "headers": "object", - "timeout": "number" - }, - "required": [ - "call_hook", - "from", - "to" - ] - }, - "tag": { - "properties": { - "id": "string", - "data": "object" - }, - "required": [ - "data" - ] - }, - "transcribe": { - "properties": { - "id": "string", - "transcriptionHook": "string", - "recognizer": "#recognizer", - "earlyMedia": "boolean" - }, - "required": [ - "recognizer" - ] - }, - "target": { - "properties": { - "type": { - "type": "string", - "enum": ["phone", "sip", "user", "teams"] - }, - "confirmHook": "object|string", - "method": { - "type": "string", - "enum": ["GET", "POST"] - }, - "headers": "object", - "from": "#dialFrom", - "name": "string", - "number": "string", - "sipUri": "string", - "auth": "#auth", - "vmail": "boolean", - "tenant": "string", - "trunk": "string", - "overrideTo": "string" - }, - "required": [ - "type" - ] - }, - "dialFrom": { - "properties": { - "user": "string", - "host": "string" - }, - "required": [ - ] - }, - "auth": { - "properties": { - "username": "string", - "password": "string" - }, - "required": [ - "username", - "password" - ] - }, - "synthesizer": { - "properties": { - "vendor": { - "type": "string", - "enum": ["google", "aws", "polly", "microsoft", "nuance", "ibm", "nvidia", "default"] - }, - "language": "string", - "voice": "string", - "engine": { - "type": "string", - "enum": ["standard", "neural"] - }, - "gender": { - "type": "string", - "enum": ["MALE", "FEMALE", "NEUTRAL"] - } - }, - "required": [ - "vendor" - ] - }, - "recognizer": { - "properties": { - "vendor": { - "type": "string", - "enum": ["google", "aws", "microsoft", "nuance", "deepgram", "ibm", "nvidia", "default"] - }, - "language": "string", - "vad": "#vad", - "hints": "array", - "hintsBoost": "number", - "altLanguages": "array", - "profanityFilter": "boolean", - "interim": "boolean", - "singleUtterance": "boolean", - "dualChannel": "boolean", - "separateRecognitionPerChannel": "boolean", - "punctuation": "boolean", - "enhancedModel": "boolean", - "words": "boolean", - "diarization": "boolean", - "diarizationMinSpeakers": "number", - "diarizationMaxSpeakers": "number", - "interactionType": { - "type": "string", - "enum": [ - "unspecified", - "discussion", - "presentation", - "phone_call", - "voicemail", - "voice_search", - "voice_command", - "dictation" - ] - }, - "naicsCode": "number", - "identifyChannels": "boolean", - "vocabularyName": "string", - "vocabularyFilterName": "string", - "filterMethod": { - "type": "string", - "enum": [ - "remove", - "mask", - "tag" - ] - }, - "model": "string", - "outputFormat": { - "type": "string", - "enum": [ - "simple", - "detailed" - ] - }, - "profanityOption": { - "type": "string", - "enum": [ - "masked", - "removed", - "raw" - ] - }, - "requestSnr": "boolean", - "initialSpeechTimeoutMs": "number", - "azureServiceEndpoint": "string", - "azureSttEndpointId": "string", - "asrDtmfTerminationDigit": "string", - "asrTimeout": "number", - "nuanceOptions": "#nuanceOptions", - "deepgramOptions": "#deepgramOptions", - "ibmOptions": "#ibmOptions", - "nvidiaOptions": "#nvidiaOptions" - }, - "required": [ - "vendor" - ] - }, - "nvidiaOptions": { - "properties": { - "rivaUri": "string", - "maxAlternatives": "number", - "profanityFilter": "boolean", - "punctuation": "boolean", - "wordTimeOffsets": "boolean", - "verbatimTranscripts": "boolean", - "customConfiguration": "object" - }, - "required": [ - ] - }, - "ibmOptions": { - "properties": { - "sttApiKey": "string", - "sttRegion": "string", - "ttsApiKey": "string", - "ttsRegion": "string", - "instanceId": "string", - "model": "string", - "languageCustomizationId": "string", - "acousticCustomizationId": "string", - "baseModelVersion": "string", - "watsonMetadata": "string", - "watsonLearningOptOut": "boolean" - }, - "required": [ - ] - }, - "deepgramOptions": { - "properties": { - "apiKey": "string", - "tier": { - "type": "string", - "enum": [ - "enhanced", - "base" - ] - }, - "model": { - "type": "string", - "enum": [ - "general", - "meeting", - "phonecall", - "voicemail", - "finance", - "conversationalai", - "video", - "custom" - ] - }, - "customModel": "string", - "version": "string", - "punctuate": "boolean", - "profanityFilter": "boolean", - "redact": { - "type": "string", - "enum": [ - "pci", - "numbers", - "true", - "ssn" - ] - }, - "diarize": "boolean", - "diarizeVersion": "string", - "ner": "boolean", - "multichannel": "boolean", - "alternatives": "number", - "numerals": "boolean", - "search": "array", - "replace": "array", - "keywords": "array", - "endpointing": "boolean", - "vadTurnoff": "number", - "tag": "string" - } - }, - "nuanceOptions": { - "properties": { - "clientId": "string", - "secret": "string", - "kryptonEndpoint": "string", - "topic": "string", - "utteranceDetectionMode": { - "type": "string", - "enum": [ - "single", - "multiple", - "disabled" - ] - }, - "punctuation": "boolean", - "profanityFilter": "boolean", - "includeTokenization": "boolean", - "discardSpeakerAdaptation": "boolean", - "suppressCallRecording": "boolean", - "maskLoadFailures": "boolean", - "suppressInitialCapitalization": "boolean", - "allowZeroBaseLmWeight": "boolean", - "filterWakeupWord": "boolean", - "resultType": { - "type": "string", - "enum": [ - "final", - "partial", - "immutable_partial" - ] - }, - "noInputTimeoutMs": "number", - "recognitionTimeoutMs": "number", - "utteranceEndSilenceMs": "number", - "maxHypotheses": "number", - "speechDomain": "string", - "formatting": "#formatting", - "clientData": "object", - "userId": "string", - "speechDetectionSensitivity": "number", - "resources": ["#resource"] - }, - "required": [ - ] - }, - "resource": { - "properties": { - "externalReference": "#resourceReference", - "inlineWordset": "string", - "builtin": "string", - "inlineGrammar": "string", - "wakeupWord": "[string]", - "weightName": { - "type": "string", - "enum": [ - "defaultWeight", - "lowest", - "low", - "medium", - "high", - "highest" - ] - }, - "weightValue": "number", - "reuse": { - "type": "string", - "enum": [ - "undefined_reuse", - "low_reuse", - "high_reuse" - ] - } - }, - "required": [ - ] - }, - "resourceReference": { - "properties": { - "type": { - "type": "string", - "enum": [ - "undefined_resource_type", - "wordset", - "compiled_wordset", - "domain_lm", - "speaker_profile", - "grammar", - "settings" - ] - }, - "uri": "string", - "maxLoadFailures": "boolean", - "requestTimeoutMs": "number", - "headers": "object" - }, - "required": [ - ] - }, - "formatting": { - "properties": { - "scheme": "string", - "options": "object" - }, - "required": [ - "scheme", - "options" - ] - }, - "lexIntent": { - "properties": { - "name": "string", - "slots": "object" - }, - "required": [ - "name" - ] - }, - "vad": { - "properties": { - "enable": "boolean", - "voiceMs": "number", - "mode": "number" - }, - "required": [ - "enable" - ] - }, - "amd": { - "properties": { - "actionHook": "object|string", - "thresholdWordCount": "number", - "timers": "#amdTimers", - "recognizer": "#recognizer" - }, - "required": [ - "actionHook" - ] - }, - "amdTimers": { - "properties": { - "noSpeechTimeoutMs": "number", - "decisionTimeoutMs": "number", - "toneTimeoutMs": "number", - "greetingCompletionTimeoutMs": "number" - } - } -} diff --git a/lib/tasks/task.js b/lib/tasks/task.js index c6a2b80e..2583d7f7 100644 --- a/lib/tasks/task.js +++ b/lib/tasks/task.js @@ -1,15 +1,10 @@ const Emitter = require('events'); const uuidv4 = require('uuid-random'); -const debug = require('debug')('jambonz:feature-server'); -const assert = require('assert'); const {TaskPreconditions} = require('../utils/constants'); -const normalizeJambones = require('../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); const WsRequestor = require('../utils/ws-requestor'); const {TaskName} = require('../utils/constants'); const {trace} = require('@opentelemetry/api'); -const specs = new Map(); -const _specData = require('./specs'); -for (const key in _specData) {specs.set(key, _specData[key]);} /** * @classdesc Represents a jambonz verb. This is a superclass that is extended @@ -287,77 +282,6 @@ class Task extends Emitter { this.logger.error(err, 'Task:_doRefer error'); } } - - /** - * validate that the JSON task description is valid - * @param {string} name - verb name - * @param {object} data - verb properties - */ - static validate(name, data) { - debug(`validating ${name} with data ${JSON.stringify(data)}`); - // validate the instruction is supported - if (!specs.has(name)) throw new Error(`invalid instruction: ${name}`); - - // check type of each element and make sure required elements are present - const specData = specs.get(name); - let required = specData.required || []; - for (const dKey in data) { - if (dKey in specData.properties) { - const dVal = data[dKey]; - const dSpec = specData.properties[dKey]; - debug(`Task:validate validating property ${dKey} with value ${JSON.stringify(dVal)}`); - - if (typeof dSpec === 'string' && dSpec === 'array') { - if (!Array.isArray(dVal)) throw new Error(`${name}: property ${dKey} is not an array`); - } - else if (typeof dSpec === 'string' && dSpec.includes('|')) { - const types = dSpec.split('|').map((t) => t.trim()); - if (!types.includes(typeof dVal) && !(types.includes('array') && Array.isArray(dVal))) { - throw new Error(`${name}: property ${dKey} has invalid data type, must be one of ${types}`); - } - } - else if (typeof dSpec === 'string' && ['number', 'string', 'object', 'boolean'].includes(dSpec)) { - // simple types - if (typeof dVal !== specData.properties[dKey]) { - throw new Error(`${name}: property ${dKey} has invalid data type`); - } - } - else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) { - const name = dSpec[0].slice(1); - for (const item of dVal) { - Task.validate(name, item); - } - } - else if (typeof dSpec === 'object') { - // complex types - const type = dSpec.type; - assert.ok(['number', 'string', 'object', 'boolean'].includes(type), - `invalid or missing type in spec ${JSON.stringify(dSpec)}`); - if (type === 'string' && dSpec.enum) { - assert.ok(Array.isArray(dSpec.enum), `enum must be an array ${JSON.stringify(dSpec.enum)}`); - if (!dSpec.enum.includes(dVal)) throw new Error(`invalid value ${dVal} must be one of ${dSpec.enum}`); - } - } - else if (typeof dSpec === 'string' && dSpec.startsWith('#')) { - // reference to another datatype (i.e. nested type) - const name = dSpec.slice(1); - //const obj = {}; - //obj[name] = dVal; - Task.validate(name, dVal); - } - else { - assert.ok(0, `invalid spec ${JSON.stringify(dSpec)}`); - } - required = required.filter((item) => item !== dKey); - } - else if (dKey === '_') { - /* no op: allow arbitrary info to be carried here, used by conference e.g in transfer */ - } - else throw new Error(`${name}: unknown property ${dKey}`); - } - if (required.length > 0) throw new Error(`${name}: missing value for ${required}`); - } - } module.exports = Task; diff --git a/lib/tasks/transcribe.js b/lib/tasks/transcribe.js index 4dde3b59..3c672d01 100644 --- a/lib/tasks/transcribe.js +++ b/lib/tasks/transcribe.js @@ -10,7 +10,7 @@ const { IbmTranscriptionEvents, NvidiaTranscriptionEvents } = require('../utils/constants'); -const normalizeJambones = require('../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); class TaskTranscribe extends Task { constructor(logger, opts, parentTask) { diff --git a/lib/utils/normalize-jambones.js b/lib/utils/normalize-jambones.js deleted file mode 100644 index 74c847cf..00000000 --- a/lib/utils/normalize-jambones.js +++ /dev/null @@ -1,31 +0,0 @@ -function normalizeJambones(logger, obj) { - if (!Array.isArray(obj)) throw new Error('malformed jambonz payload: must be array'); - const document = []; - for (const tdata of obj) { - if (typeof tdata !== 'object') throw new Error('malformed jambonz payload: must be array of objects'); - if ('verb' in tdata) { - // {verb: 'say', text: 'foo..bar'..} - const name = tdata.verb; - const o = {}; - Object.keys(tdata) - .filter((k) => k !== 'verb') - .forEach((k) => o[k] = tdata[k]); - const o2 = {}; - o2[name] = o; - document.push(o2); - } - else if (Object.keys(tdata).length === 1) { - // {'say': {..}} - document.push(tdata); - } - else { - logger.info(tdata, 'malformed jambonz payload: missing verb property'); - throw new Error('malformed jambonz payload: missing verb property'); - } - } - logger.debug({document}, `normalizeJambones: returning document with ${document.length} tasks`); - return document; -} - -module.exports = normalizeJambones; - diff --git a/lib/utils/place-outdial.js b/lib/utils/place-outdial.js index a6f3b841..1e695896 100644 --- a/lib/utils/place-outdial.js +++ b/lib/utils/place-outdial.js @@ -4,7 +4,7 @@ const SipError = require('drachtio-srf').SipError; const {TaskPreconditions, CallDirection} = require('../utils/constants'); const CallInfo = require('../session/call-info'); const assert = require('assert'); -const normalizeJambones = require('../utils/normalize-jambones'); +const { normalizeJambones } = require('@jambonz/verb-specifications'); const makeTask = require('../tasks/make_task'); const ConfirmCallSession = require('../session/confirm-call-session'); const AdultingCallSession = require('../session/adulting-call-session'); diff --git a/package-lock.json b/package-lock.json index 634b092b..684054a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@jambonz/realtimedb-helpers": "^0.6.5", "@jambonz/stats-collector": "^0.1.6", "@jambonz/time-series": "^0.2.5", + "@jambonz/verb-specifications": "^0.0.3", "@opentelemetry/api": "^1.4.0", "@opentelemetry/exporter-jaeger": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.35.0", @@ -673,6 +674,14 @@ "influx": "^5.9.3" } }, + "node_modules/@jambonz/verb-specifications": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.3.tgz", + "integrity": "sha512-sCBpE2H97HV7t4v6AAALe9OhOQdj4nLwd4hHNoSc0A1aPCImgAoCK/AMJz0FaYGpLhbjvlRWVW4+cFUeuEkCeQ==", + "dependencies": { + "pino": "^8.8.0" + } + }, "node_modules/@jest/types": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", @@ -8121,6 +8130,14 @@ "influx": "^5.9.3" } }, + "@jambonz/verb-specifications": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@jambonz/verb-specifications/-/verb-specifications-0.0.3.tgz", + "integrity": "sha512-sCBpE2H97HV7t4v6AAALe9OhOQdj4nLwd4hHNoSc0A1aPCImgAoCK/AMJz0FaYGpLhbjvlRWVW4+cFUeuEkCeQ==", + "requires": { + "pino": "^8.8.0" + } + }, "@jest/types": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", diff --git a/package.json b/package.json index b9840dd8..26bd7c6e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@jambonz/realtimedb-helpers": "^0.6.5", "@jambonz/stats-collector": "^0.1.6", "@jambonz/time-series": "^0.2.5", + "@jambonz/verb-specifications": "^0.0.3", "@opentelemetry/api": "^1.4.0", "@opentelemetry/exporter-jaeger": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.35.0", diff --git a/test/index.js b/test/index.js index 720d6ad6..6861a47c 100644 --- a/test/index.js +++ b/test/index.js @@ -1,4 +1,4 @@ -require('./ws-requestor-unit-test') +require('./ws-requestor-unit-test'); require('./unit-tests'); require('./docker_start'); require('./create-test-db'); diff --git a/test/unit-tests.js b/test/unit-tests.js index c2739432..7fc38ee8 100644 --- a/test/unit-tests.js +++ b/test/unit-tests.js @@ -61,8 +61,8 @@ test('unit tests', (t) => { const alt = require('./data/good/alternate-syntax'); - const normalize = require('../lib/utils/normalize-jambones'); - normalize(logger, alt).forEach((t) => { + const { normalizeJambones } = require('@jambonz/verb-specifications'); + normalizeJambones(logger, alt).forEach((t) => { const task = makeTask(logger, t); }); t.pass('alternate syntax works'); @@ -77,4 +77,4 @@ const errMissingProperty = () => makeTask(logger, require('./data/bad/missing-re const errInvalidType = () => makeTask(logger, require('./data/bad/invalid-type')); const errBadEnum = () => makeTask(logger, require('./data/bad/bad-enum')); const errBadPayload = () => makeTask(logger, require('./data/bad/bad-payload')); -const errBadPayload2 = () => makeTask(logger, require('./data/bad/bad-payload2')); +const errBadPayload2 = () => makeTask(logger, require('./data/bad/bad-payload2')); \ No newline at end of file