Skip to main content

json_ld/core/loader/reqwest/
mod.rs

1//! Simple document and context loader based on [`reqwest`](https://crates.io/crates/reqwest)
2use 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
21/// Loader options.
22pub struct JsonLdReqwestOptions {
23	/// One or more IRIs to use in the request as a profile parameter.
24	///
25	/// (See [IANA Considerations](https://www.w3.org/TR/json-ld11/#iana-considerations)).
26	pub request_profile: Vec<JsonLdProfile>,
27
28	/// Maximum number of allowed `Link` header redirections before the loader
29	/// fails.
30	///
31	/// Defaults to 8.
32	///
33	/// Note: this only controls how many times the loader will use a `Link`
34	/// HTTP header to find the target JSON-LD document. The number of allowed
35	/// regular HTTP redirections is controlled by the HTTP
36	/// [`client`](Self::client).
37	pub max_redirections: usize,
38
39	/// HTTP client.
40	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/// Loading error.
54#[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
75/// `reqwest`-based loader.
76///
77/// Only works with the [`tokio`](https://tokio.rs/) runtime.
78///
79/// The loader will follow indirections and `Link` headers.
80///
81/// Loaded documents are not cached: a new network query is made each time
82/// an URL is loaded even if it has already been queried before.
83pub 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	/// Creates a new loader with the given parsing function.
96	pub fn new() -> Self {
97		Self::default()
98	}
99
100	/// Creates a new leader with the given options.
101	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 decoder = utf8_decode::Decoder::new(bytes.iter().copied());
202							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}