///|
fn parse_media_parts(
raw : String,
item : Int,
allow_quality : Bool,
) -> Result[(String, String, Array[Param], Int, Array[Param]), ParseError] {
let pieces = split_quoted(raw, ';')
if pieces.length() == 0 {
return Err(EmptyItem(item))
}
let main = clean(pieces[0][:])
if main.is_empty() {
return Err(EmptyItem(item))
}
let (top, sub) = match main.split_once("/") {
Some((top_view, sub_view)) =>
(clean(top_view).to_lower(), clean(sub_view).to_lower())
None => return Err(MissingSlash(item, main))
}
if !is_token(top) {
return Err(InvalidToken(item, "media type", top))
}
if !is_token(sub) {
return Err(InvalidToken(item, "media subtype", sub))
}
if top == "*" && sub != "*" {
return Err(InvalidWildcard(item, main))
}
let params : Array[Param] = []
let extensions : Array[Param] = []
let mut q = 1000
let mut after_q = false
for i in 1..
if allow_quality && param.name == "q" {
match parse_q(param.value, item) {
Ok(parsed) => {
q = parsed
after_q = true
}
Err(err) => return Err(err)
}
} else if allow_quality && after_q {
extensions.push(param)
} else {
params.push(param)
}
Err(err) => return Err(err)
}
}
}
Ok((top, sub, params, q, extensions))
}
///|
fn default_media_range() -> MediaRange {
{
top: "*",
sub: "*",
params: [],
extensions: [],
q: 1000,
order: 0,
raw: "*/*",
}
}
///|
fn parse_media_range_item(
raw : String,
order : Int,
) -> Result[MediaRange, ParseError] {
match parse_media_parts(raw, order, true) {
Ok((top, sub, params, q, extensions)) =>
Ok({ top, sub, params, extensions, q, order, raw: clean(raw[:]) })
Err(err) => Err(err)
}
}
///|
fn parse_media_offer_item(
raw : String,
index : Int,
) -> Result[MediaOffer, ParseError] {
match parse_media_parts(raw, index, false) {
Ok((top, sub, params, _, _)) =>
if top == "*" || sub == "*" {
Err(InvalidOffer(index, raw))
} else {
Ok({ value: clean(raw[:]), top, sub, params, index })
}
Err(err) => Err(err)
}
}
///|
pub fn parse_accept(
header : StringView,
) -> Result[Array[MediaRange], ParseError] {
let text = clean(header)
let ranges : Array[MediaRange] = []
if text.is_empty() {
return Ok(ranges)
}
let parts = split_quoted(text, ',')
for i in 0.. ranges.push(range)
Err(err) => return Err(err)
}
}
Ok(ranges)
}
///|
pub fn parse_content_type(value : StringView) -> Result[MediaOffer, ParseError] {
parse_media_offer_item(clean(value), 0)
}
///|
fn media_specificity(range : MediaRange) -> Int {
if range.top == "*" {
0
} else if range.sub == "*" {
1
} else {
2
}
}
///|
fn media_range_matches(range : MediaRange, offer : MediaOffer) -> Bool {
if range.top != "*" && range.top != offer.top {
return false
}
if range.sub != "*" && range.sub != offer.sub {
return false
}
params_match(range.params, offer.params)
}
///|
fn format_media_range(range : MediaRange) -> String {
let buf = StringBuilder()
buf.write_string(range.top)
buf.write_char('/')
buf.write_string(range.sub)
append_params(buf, range.params)
if range.q != 1000 || range.extensions.length() > 0 {
buf.write_string(";q=")
buf.write_string(quality_to_string(range.q))
append_params(buf, range.extensions)
}
buf.to_string()
}
///|
fn format_media_offer(offer : MediaOffer) -> String {
let buf = StringBuilder()
buf.write_string(offer.top)
buf.write_char('/')
buf.write_string(offer.sub)
append_params(buf, offer.params)
buf.to_string()
}
///|
fn media_reason(
offer : MediaOffer,
range : MediaRange,
specificity : Int,
) -> String {
let buf = StringBuilder()
buf.write_string(format_media_offer(offer))
buf.write_string(" matched ")
buf.write_string(format_media_range(range))
buf.write_string(" with q=")
buf.write_string(quality_to_string(range.q))
buf.write_string(", specificity=")
buf.write_string(specificity.to_string())
buf.write_string(", params=")
buf.write_string(range.params.length().to_string())
buf.to_string()
}
///|
fn better_media(candidate : MediaCandidate, current : MediaCandidate) -> Bool {
if candidate.quality != current.quality {
candidate.quality > current.quality
} else if candidate.specificity != current.specificity {
candidate.specificity > current.specificity
} else if candidate.parameter_matches != current.parameter_matches {
candidate.parameter_matches > current.parameter_matches
} else if candidate.matched_range.order != current.matched_range.order {
candidate.matched_range.order < current.matched_range.order
} else {
candidate.offer.index < current.offer.index
}
}
///|
fn better_media_range_for_offer(
candidate : MediaRange,
current : MediaRange,
) -> Bool {
let candidate_specificity = media_specificity(candidate)
let current_specificity = media_specificity(current)
if candidate_specificity != current_specificity {
candidate_specificity > current_specificity
} else if candidate.params.length() != current.params.length() {
candidate.params.length() > current.params.length()
} else {
candidate.order < current.order
}
}
///|
fn effective_media_range(
offer : MediaOffer,
ranges : Array[MediaRange],
) -> MediaRange? {
let mut best : MediaRange? = None
for range in ranges {
if media_range_matches(range, offer) {
if (match best {
None => true
Some(current) => better_media_range_for_offer(range, current)
}) {
best = Some(range)
}
}
}
best
}
///|
fn media_explanation(best : MediaCandidate?) -> String {
match best {
Some(candidate) => {
let buf = StringBuilder()
buf.write_string("selected ")
buf.write_string(candidate.offer.value)
buf.write_string(": ")
buf.write_string(candidate.reason)
buf.to_string()
}
None => "no acceptable media offer matched"
}
}
///|
pub fn negotiate_media(
accept_header : StringView,
offers : Array[String],
) -> Result[MediaDecision, ParseError] {
let ranges = match parse_accept(accept_header) {
Ok(parsed) =>
if parsed.length() == 0 {
[default_media_range()]
} else {
parsed
}
Err(err) => return Err(err)
}
let matches : Array[MediaCandidate] = []
let mut best : MediaCandidate? = None
for offer_index in 0.. offer
Err(err) => return Err(err)
}
match effective_media_range(offer, ranges) {
Some(range) =>
if range.q > 0 {
let specificity = media_specificity(range)
let candidate = {
offer,
matched_range: range,
quality: range.q,
specificity,
parameter_matches: range.params.length(),
reason: media_reason(offer, range, specificity),
}
matches.push(candidate)
if (match best {
None => true
Some(current) => better_media(candidate, current)
}) {
best = Some(candidate)
}
}
None => ()
}
}
let selected = match best {
Some(candidate) => Some(candidate.offer.value)
None => None
}
let quality = match best {
Some(candidate) => candidate.quality
None => 0
}
Ok({
selected,
quality,
candidate: best,
matches,
explanation: media_explanation(best),
})
}