Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions bandwidth/models/bxml/nestable_verb.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
@copyright Bandwidth INC
"""
from __future__ import annotations
import re
from typing import Union
import xml.etree.ElementTree as ET

Expand All @@ -16,7 +15,6 @@
class NestableVerb(Verb):
"""Base class for nestable BXML verbs
"""
ssml_regex = r"<([a-zA-Z//].*?)>"

def __init__(self, tag: str, content: str = None, nested_verbs: list[Verb] = None):
"""Initialize the verb model
Expand Down
6 changes: 2 additions & 4 deletions bandwidth/models/bxml/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@

@copyright Bandwidth INC
"""
import re
from typing import List
import xml.etree.ElementTree as ET

from bandwidth.models.bxml.verb import Verb
from bandwidth.models.bxml.verb import Verb, _unescape_ssml


class Root:
"""Base class for BXML roots
"""
ssml_regex = r"<([a-zA-Z//].*?)>"

def __init__(self, tag: str, nested_verbs: List[Verb] = None):
"""Initialize instance of class
Expand Down Expand Up @@ -76,4 +74,4 @@ def to_bxml(self) -> str:
str: Serialized BXML string
"""
xml_document = self._generate_xml()
return re.sub(self.ssml_regex, r"<\1>", ET.tostring(xml_document._root, encoding='UTF-8', method='xml', xml_declaration=True).decode("utf8"))
return _unescape_ssml(ET.tostring(xml_document._root, encoding='UTF-8', method='xml', xml_declaration=True).decode("utf8"))
18 changes: 14 additions & 4 deletions bandwidth/models/bxml/verb.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
from typing import Union
import xml.etree.ElementTree as ET

_SPEAK_SENTENCE_REGEX = re.compile(r"(<SpeakSentence\b[^>]*(?<!/)>)(.*?)(</SpeakSentence>)", re.DOTALL)
_SSML_REGEX = re.compile(r"&lt;([a-zA-Z//].*?)&gt;")


def _unescape_ssml(bxml: str) -> str:
"""Restore SSML tags inside SpeakSentence text content only.

Attribute values and all other elements keep their XML escaping.
"""
return _SPEAK_SENTENCE_REGEX.sub(
lambda m: m.group(1) + _SSML_REGEX.sub(r"<\1>", m.group(2)) + m.group(3), bxml
)


class Verb:
"""Base class for BXML verbs
Expand Down Expand Up @@ -93,8 +106,5 @@ def to_bxml(self) -> str:
Returns:
str: Serialized BXML string
"""
ssml_regex = r"&lt;([a-zA-Z//].*?)&gt;"

xml_document = self._generate_xml()
# if the root name is speakSentence, replace content with the SSML regex
return re.sub(ssml_regex, r"<\1>", ET.tostring(xml_document._root).decode('utf8'))
return _unescape_ssml(ET.tostring(xml_document._root).decode('utf8'))
8 changes: 0 additions & 8 deletions bandwidth/models/bxml/verbs/speak_sentence.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@

@copyright Bandwidth INC
"""
import re

import xml.etree.ElementTree as ET

from ..nestable_verb import NestableVerb


Expand Down Expand Up @@ -44,7 +40,3 @@ def _attributes(self):
"gender": self.gender,
"locale": self.locale,
}

def to_bxml(self) -> str:
xml_document = self._generate_xml()
return re.sub(self.ssml_regex, r"<\1>", ET.tostring(xml_document._root).decode('utf8'))
8 changes: 8 additions & 0 deletions test/unit/models/bxml/test_gather.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,11 @@ def test_add_verb(self):
expected = '<Gather gatherUrl="test.com" gatherMethod="POST" gatherFallbackUrl="fallback-test.com" gatherFallbackMethod="GET" username="user" password="pass" fallbackUsername="user" fallbackPassword="pass" tag="tag" terminatingDigits="2" maxDigits="5" interDigitTimeout="1" firstDigitTimeout="3" repeatCount="2" input="dtmf_speech" hints="yes,no" language="en-US" partialResultCallback="partial-test.com" partialResultCallbackMethod="POST" profanityFilter="true" speechModel="default" speechTimeout="4"><PlayAudio username="user" password="pass">https://audio.url/audio1.wav</PlayAudio><SpeakSentence>Hello. Your number is <say-as interpret-as="telephone">asdf</say-as>, lets play a game. What is 10 + 3. Press the pound key when finished.</SpeakSentence></Gather>'
self.gather.add_verb(self.speak_sentence)
assert expected == self.gather.to_bxml()

def test_ssml_unescaped_in_speak_sentence_but_not_in_gather_attributes(self):
expected = '<Gather gatherUrl="https://example.com/?a=&lt;b&gt;"><SpeakSentence>say <emphasis>this</emphasis></SpeakSentence></Gather>'
gather = Gather(
gather_url="https://example.com/?a=<b>",
audio_verbs=[SpeakSentence(text="say <emphasis>this</emphasis>")]
)
assert expected == gather.to_bxml()
24 changes: 23 additions & 1 deletion test/unit/models/bxml/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""
import unittest

from bandwidth.models.bxml import Response
from bandwidth.models.bxml import Response, Redirect, SpeakSentence


class TestResponse(unittest.TestCase):
Expand All @@ -23,3 +23,25 @@ def test_response_init(self):
"""
expected_bxml = "<?xml version='1.0' encoding='UTF-8'?>\n<Response />"
assert self.response.to_bxml() == expected_bxml

def test_ssml_unescaped_but_other_attributes_untouched(self):
"""SSML in <SpeakSentence> text is un-escaped; escaped characters in other elements' attributes are not
"""
expected_bxml = "<?xml version='1.0' encoding='UTF-8'?>\n<Response><Redirect redirectUrl=\"https://example.com/a?info=&lt;https://example.com/c.pem&gt;\" /><SpeakSentence voice=\"julie\" gender=\"female\" locale=\"en_US\">Hello. Your number is <say-as interpret-as=\"telephone\">asdf</say-as>, lets play a game.</SpeakSentence></Response>"
self.response.add_verb(Redirect(redirect_url="https://example.com/a?info=<https://example.com/c.pem>"))
self.response.add_verb(SpeakSentence(
text='Hello. Your number is <say-as interpret-as="telephone">asdf</say-as>, lets play a game.',
voice="julie",
gender="female",
locale="en_US"
))
assert expected_bxml == self.response.to_bxml()

def test_empty_speak_sentence_does_not_swallow_following_verbs(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick but can we give this a docstring to match the others

"""A <SpeakSentence> with no text must not capture the verbs that follow it
"""
expected_bxml = "<?xml version='1.0' encoding='UTF-8'?>\n<Response><SpeakSentence /><Redirect redirectUrl=\"https://example.com/a?info=&lt;b&gt;\" /><SpeakSentence>hi <break/></SpeakSentence></Response>"
self.response.add_verb(SpeakSentence(text=""))
self.response.add_verb(Redirect(redirect_url="https://example.com/a?info=<b>"))
self.response.add_verb(SpeakSentence(text="hi <break/>"))
assert expected_bxml == self.response.to_bxml()