lino_rest_api/
media_type.rs1pub const LINO_CONTENT_TYPE: &str = "text/lino";
10
11pub const LINO_LINE_CONTENT_TYPE: &str = "text/lino-line";
13
14pub const LINO_COMPACT_CONTENT_TYPE: &str = "text/lino-compact";
16
17pub const JSON_CONTENT_TYPE: &str = "application/json";
19
20pub const LINO_PROBLEM_CONTENT_TYPE: &str = "application/problem+lino";
22
23pub const JSON_PROBLEM_CONTENT_TYPE: &str = "application/problem+json";
25
26pub const SUPPORTED_MEDIA_TYPES: [&str; 4] = [
28 LINO_CONTENT_TYPE,
29 LINO_LINE_CONTENT_TYPE,
30 LINO_COMPACT_CONTENT_TYPE,
31 JSON_CONTENT_TYPE,
32];
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct AcceptRange {
37 pub media_type: String,
39 pub quality: f64,
41 pub specificity: u8,
43}
44
45pub fn with_charset(media_type: &str) -> String {
47 format!("{media_type}; charset=utf-8")
48}
49
50pub fn parse_content_type(header_value: Option<&str>) -> String {
54 match header_value {
55 None => String::new(),
56 Some(value) => value
57 .split(';')
58 .next()
59 .unwrap_or("")
60 .trim()
61 .to_ascii_lowercase(),
62 }
63}
64
65pub fn parse_accept(header_value: Option<&str>) -> Vec<AcceptRange> {
67 let header = header_value.unwrap_or("").trim();
68 if header.is_empty() {
69 return vec![AcceptRange {
70 media_type: "*/*".to_string(),
71 quality: 1.0,
72 specificity: 0,
73 }];
74 }
75
76 let mut ranges: Vec<(usize, AcceptRange)> = Vec::new();
77 for (index, part) in header.split(',').enumerate() {
78 let mut pieces = part.split(';');
79 let media_type = pieces.next().unwrap_or("").trim().to_ascii_lowercase();
80 if media_type.is_empty() {
81 continue;
82 }
83
84 let mut quality = 1.0;
85 for parameter in pieces {
86 let (name, value) = match parameter.split_once('=') {
87 Some((name, value)) => (name, value),
88 None => (parameter, ""),
89 };
90 if name.trim().eq_ignore_ascii_case("q") {
91 quality = value.trim().parse::<f64>().unwrap_or(0.0);
92 }
93 }
94
95 let specificity = if media_type == "*/*" {
96 0
97 } else if media_type.ends_with("/*") {
98 1
99 } else {
100 2
101 };
102
103 ranges.push((
104 index,
105 AcceptRange {
106 media_type,
107 quality,
108 specificity,
109 },
110 ));
111 }
112
113 ranges.sort_by(|left, right| {
114 right
115 .1
116 .quality
117 .partial_cmp(&left.1.quality)
118 .unwrap_or(std::cmp::Ordering::Equal)
119 .then(right.1.specificity.cmp(&left.1.specificity))
120 .then(left.0.cmp(&right.0))
121 });
122 ranges.into_iter().map(|entry| entry.1).collect()
123}
124
125fn range_matches(range_type: &str, media_type: &str) -> bool {
127 if range_type == "*/*" || range_type == media_type {
128 return true;
129 }
130 match range_type.strip_suffix('*') {
131 Some(prefix) => media_type.starts_with(prefix),
132 None => false,
133 }
134}
135
136pub fn negotiate_media_type(accept_header: Option<&str>, supported: &[String]) -> Option<String> {
141 for accepted in parse_accept(accept_header) {
142 if accepted.quality <= 0.0 {
143 continue;
144 }
145 for media_type in supported {
146 if range_matches(&accepted.media_type, media_type) {
147 return Some(media_type.clone());
148 }
149 }
150 }
151 None
152}
153
154pub fn supported_media_types() -> Vec<String> {
156 SUPPORTED_MEDIA_TYPES
157 .iter()
158 .map(|entry| entry.to_string())
159 .collect()
160}
161
162pub fn normalize_media_type(media_type: &str) -> &str {
167 match media_type {
168 LINO_PROBLEM_CONTENT_TYPE => LINO_CONTENT_TYPE,
169 JSON_PROBLEM_CONTENT_TYPE => JSON_CONTENT_TYPE,
170 other => other,
171 }
172}
173
174pub fn is_decodable_media_type(media_type: &str) -> bool {
176 SUPPORTED_MEDIA_TYPES.contains(&normalize_media_type(media_type))
177}
178
179pub fn problem_media_type(media_type: &str) -> &str {
181 match media_type {
182 LINO_CONTENT_TYPE => LINO_PROBLEM_CONTENT_TYPE,
183 JSON_CONTENT_TYPE => JSON_PROBLEM_CONTENT_TYPE,
184 other => other,
185 }
186}