json_ld/core/loader/reqwest/
mod.rs1use hashbrown::HashSet;
3use json_syntax::{JsonParse, JsonValue, parse::JsonParseError};
4use rdf_syntax::Iri;
5use reqwest::{
6 StatusCode,
7 header::{ACCEPT, CONTENT_TYPE, LINK},
8};
9use reqwest_middleware::ClientWithMiddleware;
10
11use crate::{JsonLdDocument, JsonLdLoadError, JsonLdProfile, JsonLdSourceCode};
12
13use super::AsyncJsonLdLoader;
14
15mod content_type;
16mod link;
17
18use content_type::*;
19use link::*;
20
21pub struct JsonLdReqwestOptions {
23 pub request_profile: Vec<JsonLdProfile>,
27
28 pub max_redirections: usize,
38
39 pub client: ClientWithMiddleware,
41}
42
43impl Default for JsonLdReqwestOptions {
44 fn default() -> Self {
45 Self {
46 request_profile: Vec::new(),
47 max_redirections: 8,
48 client: reqwest_middleware::ClientBuilder::new(reqwest::Client::default()).build(),
49 }
50 }
51}
52
53#[derive(Debug, thiserror::Error)]
55pub enum Error {
56 #[error("internal error: {0}")]
57 Reqwest(reqwest_middleware::Error),
58
59 #[error("query failed: status code {0}")]
60 QueryFailed(StatusCode),
61
62 #[error("invalid content type")]
63 InvalidContentType,
64
65 #[error("multiple context link headers")]
66 MultipleContextLinkHeaders,
67
68 #[error("too many redirections")]
69 TooManyRedirections,
70
71 #[error("JSON parse error: {0}")]
72 Parse(JsonParseError),
73}
74
75pub struct JsonLdReqwestLoader {
84 options: JsonLdReqwestOptions,
85 accept_header: String,
86}
87
88impl Default for JsonLdReqwestLoader {
89 fn default() -> Self {
90 Self::new_using(JsonLdReqwestOptions::default())
91 }
92}
93
94impl JsonLdReqwestLoader {
95 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn new_using(options: JsonLdReqwestOptions) -> Self {
102 let mut json_ld_params = String::new();
103
104 if !options.request_profile.is_empty() {
105 json_ld_params.push_str("; profile=");
106
107 if options.request_profile.len() > 1 {
108 json_ld_params.push('"');
109 }
110
111 for (i, p) in options.request_profile.iter().enumerate() {
112 if i > 0 {
113 json_ld_params.push(' ');
114 }
115
116 json_ld_params.push_str(p.iri().as_str());
117 }
118
119 if options.request_profile.len() > 1 {
120 json_ld_params.push('"');
121 }
122 }
123
124 Self {
125 options,
126 accept_header: format!("application/ld+json{json_ld_params}, application/json"),
127 }
128 }
129}
130
131impl AsyncJsonLdLoader for JsonLdReqwestLoader {
132 async fn async_load(&self, url: &Iri) -> Result<JsonLdDocument, JsonLdLoadError> {
133 let mut redirection_number = 0;
134 let mut url = url.to_owned();
135 'next_url: loop {
136 if redirection_number > self.options.max_redirections {
137 return Err(JsonLdLoadError::new(
138 url.clone(),
139 Error::TooManyRedirections,
140 ));
141 }
142
143 log::debug!("downloading: {}", url);
144 let request = self
145 .options
146 .client
147 .get(url.as_str())
148 .header(ACCEPT, &self.accept_header);
149
150 let response = request
151 .send()
152 .await
153 .map_err(|e| JsonLdLoadError::new(url.clone(), e))?;
154
155 match response.status() {
156 StatusCode::OK => {
157 let mut content_types = response
158 .headers()
159 .get_all(CONTENT_TYPE)
160 .into_iter()
161 .filter_map(ContentType::new);
162
163 match content_types.find(ContentType::is_json_ld) {
164 Some(content_type) => {
165 let mut context_url = None;
166 if *content_type.media_type() != "application/ld+json" {
167 for link in response.headers().get_all(LINK).into_iter() {
168 if let Some(link) = Link::new(link)
169 && link.rel()
170 == Some(b"http://www.w3.org/ns/json-ld#context")
171 {
172 if context_url.is_some() {
173 return Err(JsonLdLoadError::new(
174 url,
175 Error::MultipleContextLinkHeaders,
176 ));
177 }
178
179 context_url = Some(link.href().resolved(&url));
180 }
181 }
182 }
183
184 let mut profile = HashSet::new();
185 for p in content_type
186 .profile()
187 .into_iter()
188 .flat_map(|p| p.split(|b| *b == b' '))
189 {
190 if let Ok(p) = std::str::from_utf8(p)
191 && let Ok(iri) = Iri::new(p)
192 {
193 profile.insert(JsonLdProfile::new(iri));
194 }
195 }
196
197 let bytes = response.bytes().await.map_err(|e| {
198 JsonLdLoadError::new(url.clone(), Error::Reqwest(e.into()))
199 })?;
200
201 let (document, code_map) = JsonValue::parse_slice(&bytes)
203 .map_err(|e| JsonLdLoadError::new(url.clone(), e))?;
204
205 break Ok(JsonLdDocument::new_full(
206 Some(url),
207 Some(content_type.into_media_type()),
208 context_url,
209 profile,
210 Some(JsonLdSourceCode::new(
211 String::from_utf8_lossy(&bytes).into_owned(),
212 code_map,
213 )),
214 document,
215 ));
216 }
217 None => {
218 log::debug!("no valid media type found");
219 for link in response.headers().get_all(LINK).into_iter() {
220 if let Some(link) = Link::new(link)
221 && link.rel() == Some(b"alternate")
222 && link.type_() == Some(b"application/ld+json")
223 {
224 log::debug!("link found");
225 url = link.href().resolved(&url);
226 redirection_number += 1;
227 continue 'next_url;
228 }
229 }
230
231 break Err(JsonLdLoadError::new(url, Error::InvalidContentType));
232 }
233 }
234 }
235 code => break Err(JsonLdLoadError::new(url, Error::QueryFailed(code))),
236 }
237 }
238 }
239}