Skip to content

Commit b0dcbd5

Browse files
authored
Expose SDP grammar converters for application-defined attributes (#512)
session_description already accepted custom attribute_converters, but the helper converters needed to build them were internal, and the lifetime of those converters relative to the returned grammar was undocumented. Declare the reusable converters and generators, document the by-reference capture and static-initialization constraints, and add a test that extends the default grammar with a=x-example-foo.
1 parent 6826af3 commit b0dcbd5

3 files changed

Lines changed: 137 additions & 2 deletions

File tree

Development/sdp/sdp_grammar.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ namespace sdp
139139
};
140140
}
141141

142-
converter array_converter(const converter& converter, const std::string& delimiter = " ")
142+
converter array_converter(const converter& converter, const std::string& delimiter)
143143
{
144144
return{
145145
[=](const web::json::value& v) {
@@ -211,7 +211,7 @@ namespace sdp
211211
// in other RFCs and SMPTE standards are inconsistent, so allow additional whitespace
212212
const converter named_values_converter = array_converter(key_value_converter('=', { sdp::fields::name, string_converter }, { sdp::fields::value, string_converter }), "; ", "[ \\t]*(;[ \\t]*|$)");
213213

214-
converter object_converter(const std::vector<std::pair<utility::string_t, converter>>& field_converters, const std::string& delimiter = " ")
214+
converter object_converter(const std::vector<std::pair<utility::string_t, converter>>& field_converters, const std::string& delimiter)
215215
{
216216
return{
217217
[=](const web::json::value& v) {

Development/sdp/sdp_grammar.h

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,45 @@ namespace sdp
2626
parser parse;
2727
};
2828

29+
// converters for the value types used by this grammar, and generators for compound
30+
// values, which may also be used to define additional attribute converters
31+
// see https://tools.ietf.org/html/rfc4566#section-9
32+
// none of these, or get_default_attribute_converters, default_attribute_converter
33+
// or session_description below, are safe to use during static initialization;
34+
// prefer a function-local static for application-defined grammars
35+
36+
// <byte-string>
37+
extern const converter string_converter;
38+
39+
// a number, e.g. <frame rate> in "a=framerate:<frame rate>"
40+
extern const converter number_converter;
41+
42+
// 1*DIGIT
43+
extern const converter digits_converter;
44+
45+
// 1*DIGIT, represented as a numeric string, for values which may exceed the range
46+
// of a number, e.g. <sess-id>
47+
extern const converter big_digits_converter;
48+
49+
// a space-separated list of <byte-string>
50+
extern const converter strings_converter;
51+
52+
// a semicolon-separated list of <name>[=<value>], e.g. <format specific parameters>
53+
extern const converter named_values_converter;
54+
55+
// <key>[<separator><value>]
56+
converter key_value_converter(char separator, const std::pair<utility::string_t, converter>& key_converter, const std::pair<utility::string_t, converter>& value_converter);
57+
58+
// a delimited list of values, represented as a json array
59+
converter array_converter(const converter& converter, const std::string& delimiter = " ");
60+
61+
// identical to above except that parse_delimiter is a regex pattern
62+
converter array_converter(const converter& converter, const std::string& format_delimiter, const std::string& parse_delimiter);
63+
64+
// a delimited sequence of values, represented as a json object with the specified field names
65+
// an empty field name indicates the values of the converted json object are merged into the result
66+
converter object_converter(const std::vector<std::pair<utility::string_t, converter>>& field_converters, const std::string& delimiter = " ");
67+
2968
// "An SDP session description consists of a number of lines of text of
3069
// the form: <type>=<value>
3170
// where <type> MUST be exactly one case-significant character and
@@ -80,7 +119,12 @@ namespace sdp
80119
// See https://tools.ietf.org/html/rfc4566#section-6
81120
attribute_converters get_default_attribute_converters();
82121

122+
// converter for an attribute with no specific converter, which treats the <att-value> as an opaque <byte-string>
123+
// and an attribute with no <att-value> as a property attribute
124+
extern const converter default_attribute_converter;
125+
83126
// construct a grammar with the specified attribute converters
127+
// the converters and default_converter are captured by reference and must outlive the returned grammar
84128
description session_description(const attribute_converters& converters, const converter& default_converter);
85129
}
86130

Development/sdp/test/sdp_test.cpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "bst/test/test.h"
55
#include "sdp/json.h"
6+
#include "sdp/sdp_grammar.h"
67

78
////////////////////////////////////////////////////////////////////////////////////////////
89
BST_TEST_CASE(testSdpRoundtrip)
@@ -1120,3 +1121,93 @@ a=mid:SECONDARY
11201121
} while (!expected.fail() && !actual.fail());
11211122
}
11221123
}
1124+
1125+
////////////////////////////////////////////////////////////////////////////////////////////
1126+
namespace
1127+
{
1128+
// an example application-defined attribute
1129+
// a=x-example-foo:<format> <bar>
1130+
const utility::string_t example_foo{ U("x-example-foo") };
1131+
1132+
namespace example_fields
1133+
{
1134+
const web::json::field_as_integer format{ U("format") };
1135+
const web::json::field_as_string bar{ U("bar") };
1136+
}
1137+
1138+
// session_description captures converters by reference, so they must outlive
1139+
// the returned grammar; a function-local static is the natural application pattern
1140+
const sdp::grammar::attribute_converters& example_attribute_converters()
1141+
{
1142+
static const auto converters = [] {
1143+
auto converters = sdp::grammar::get_default_attribute_converters();
1144+
converters[example_foo] = sdp::grammar::object_converter({
1145+
{ example_fields::format, sdp::grammar::digits_converter },
1146+
{ example_fields::bar, sdp::grammar::string_converter }
1147+
});
1148+
return converters;
1149+
}();
1150+
return converters;
1151+
}
1152+
1153+
const sdp::grammar::description& example_grammar()
1154+
{
1155+
static const auto grammar = sdp::grammar::session_description(
1156+
example_attribute_converters(),
1157+
sdp::grammar::default_attribute_converter
1158+
);
1159+
return grammar;
1160+
}
1161+
}
1162+
1163+
BST_TEST_CASE(testSdpApplicationDefinedAttributes)
1164+
{
1165+
const auto& grammar = example_grammar();
1166+
1167+
const std::string test_sdp = R"(v=0
1168+
o=- 3745911798 3745911798 IN IP4 192.168.9.142
1169+
s=Example Sender 1 (Video)
1170+
t=0 0
1171+
a=x-example-foo:0 session-level
1172+
m=video 50020 RTP/AVP 96
1173+
c=IN IP4 239.22.142.1/32
1174+
a=rtpmap:96 raw/90000
1175+
a=x-example-foo:96 media-level
1176+
a=x-example-bar:unknown attributes are still handled by the default converter
1177+
a=recvonly
1178+
)";
1179+
1180+
auto session_description = sdp::parse_session_description(test_sdp, grammar);
1181+
1182+
auto& session_attributes = sdp::fields::attributes(session_description).as_array();
1183+
auto session_foo = sdp::find_name(session_attributes, example_foo);
1184+
BST_REQUIRE(session_attributes.end() != session_foo);
1185+
BST_REQUIRE_EQUAL(0, example_fields::format(sdp::fields::value(*session_foo)));
1186+
BST_REQUIRE_EQUAL(U("session-level"), example_fields::bar(sdp::fields::value(*session_foo)));
1187+
1188+
auto& media_attributes = sdp::fields::attributes(sdp::fields::media_descriptions(session_description).at(0)).as_array();
1189+
auto media_foo = sdp::find_name(media_attributes, example_foo);
1190+
BST_REQUIRE(media_attributes.end() != media_foo);
1191+
BST_REQUIRE_EQUAL(96, example_fields::format(sdp::fields::value(*media_foo)));
1192+
BST_REQUIRE_EQUAL(U("media-level"), example_fields::bar(sdp::fields::value(*media_foo)));
1193+
1194+
auto media_bar = sdp::find_name(media_attributes, U("x-example-bar"));
1195+
BST_REQUIRE(media_attributes.end() != media_bar);
1196+
BST_REQUIRE_EQUAL(U("unknown attributes are still handled by the default converter"), sdp::fields::value(*media_bar).as_string());
1197+
1198+
auto recvonly = sdp::find_name(media_attributes, sdp::attributes::recvonly);
1199+
BST_REQUIRE(media_attributes.end() != recvonly);
1200+
BST_REQUIRE(sdp::fields::value(*recvonly).is_null());
1201+
1202+
auto test_sdp2 = sdp::make_session_description(session_description, grammar);
1203+
std::istringstream expected(test_sdp), actual(test_sdp2);
1204+
do
1205+
{
1206+
std::string expected_line, actual_line;
1207+
std::getline(expected, expected_line);
1208+
std::getline(actual, actual_line);
1209+
// CR cannot appear in a raw string literal, so remove it from the actual line
1210+
if (!actual_line.empty() && '\r' == actual_line.back()) actual_line.pop_back();
1211+
BST_CHECK_EQUAL(expected_line, actual_line);
1212+
} while (!expected.fail() && !actual.fail());
1213+
}

0 commit comments

Comments
 (0)