Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support a standard api for parsing media types #376

Merged
merged 10 commits into from
Oct 10, 2018
51 changes: 47 additions & 4 deletions src/webob/acceptparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,18 +407,61 @@ def generator(value):
)
return generator(value=value)

def _parse_and_normalize_offers(self, offers):
@classmethod
def parse_offer(cls, offer):
mmerickel marked this conversation as resolved.
Show resolved Hide resolved
"""
Parse an offer into its component parts.

:param offer: A media type or range in the format
``type/subtype[;params]``.
:return: A tuple of ``(*type*, *subtype*, *params*)``.

| *params* is a list containing ``(*parameter name*, *value*)``
values.

:raises ValueError: If the offer does not match the required format.

"""
match = cls.media_type_compiled_re.match(offer)
if not match:
raise ValueError('Invalid value for an Accept offer.')

groups = match.groups()
offer_type, offer_subtype = groups[0].split('/')
offer_params = cls._parse_media_type_params(
media_type_params_segment=groups[1],
)
# offer_type, offer_subtype, offer_params, invalid, example
# == * == * true Y/N
# N N N N a/b
# N N Y N a/b;x=y
# N Y N N a/*
# N Y Y Y a/*;x=y
# Y N N Y */b
# Y N Y Y */b;x=y
# Y Y N N */*
# Y Y Y Y */*;x=y
# simplifies to (A and not B or B and C)
stevepiercy marked this conversation as resolved.
Show resolved Hide resolved
if (
(offer_type == '*' and offer_subtype != '*')
or (offer_subtype == '*' and offer_params)
):
raise ValueError('Invalid value for an Accept offer.')
return (offer_type, offer_subtype, offer_params)

@classmethod
def _parse_and_normalize_offers(cls, offers):
"""
Throw out any offers that do not match the media type ABNF.
Throw out any offers that do not match the media range ABNF.

:return: A list of offers split into the format ``[offer_index,
offer_type_subtype, offer_media_type_params]``.

"""
lowercased_offers_parsed = []
for index, offer in enumerate(offers):
match = self.media_type_compiled_re.match(offer.lower())
# we're willing to try to match any offer that matches the
match = cls.media_type_compiled_re.match(offer.lower())
# we're willing to try to match any range that matches the
# media type grammar can parse, but we'll throw out anything
# that doesn't fit the correct syntax - this is not saying that
# the media type is actually a real media type, just that it looks
Expand Down
27 changes: 27 additions & 0 deletions tests/test_acceptparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,33 @@ def test_parse__valid_header(self, value, expected_list):
list_of_returned = list(returned)
assert list_of_returned == expected_list

@pytest.mark.parametrize('offer, expected_return', [
['text/html', ('text', 'html', [])],
['text/html;charset=utf8', ('text', 'html', [('charset', 'utf8')])],
['text/html;charset=utf8;x-version=1', ('text', 'html', [
('charset', 'utf8'),
('x-version', '1'),
])],
['text/*', ('text', '*', [])],
['*/*', ('*', '*', [])],
])
def test_parse_offer__valid(self, offer, expected_return):
assert Accept.parse_offer(offer) == expected_return

@pytest.mark.parametrize('offer', [
'',
'foo',
'foo/bar/baz',
'*/plain',
'*/plain;charset=utf8',
'*/plain;charset=utf8;x-version=1',
'*/*;charset=utf8',
'text/*;charset=utf8',
])
def test_parse_offer__invalid(self, offer):
with pytest.raises(ValueError):
Accept.parse_offer(offer)


class TestAcceptValidHeader(object):
def test_parse__inherited(self):
Expand Down