Skip to main content

operator/content/
mod.rs

1mod body;
2mod content_directory;
3mod content_engine;
4mod content_index;
5mod content_item;
6mod content_registry;
7mod handlebars_helpers;
8mod mime;
9mod route;
10mod test_lib;
11
12use crate::bug_message;
13use bytes::Bytes;
14use content_item::RenderingFailedError;
15use futures::Stream;
16use serde::Serialize;
17use std::collections::HashMap;
18use std::io;
19use thiserror::Error;
20
21pub use self::mime::{MediaRange, MediaType};
22pub use content_directory::ContentDirectory;
23pub use content_engine::{
24    ContentEngine, ContentLoadingError, FilesystemBasedContentEngine, TemplateError,
25};
26pub use content_index::ContentIndex;
27pub use content_item::UnregisteredTemplate;
28pub use content_registry::{ContentRepresentations, RegisteredContent};
29pub use route::Route;
30
31// This is just a trait alias to help make type signatures a bit saner.
32pub trait ByteStream: Stream<Item = Result<Bytes, StreamError>>
33where
34    Self: Unpin,
35{
36}
37impl<T> ByteStream for T where T: Stream<Item = Result<Bytes, StreamError>> + Unpin {}
38
39/// A piece of rendered content along with its media type.
40pub struct Media<Content: ByteStream> {
41    pub media_type: MediaType,
42    pub content: Content,
43}
44impl<Content: ByteStream> Media<Content> {
45    fn new(media_type: MediaType, content: Content) -> Self {
46        Self {
47            media_type,
48            content,
49        }
50    }
51}
52
53/// Indicates that it was not possible to produce rendered output, either
54/// because rendering was attempted and failed or because no acceptable media
55/// types are available.
56#[derive(Error, Debug)]
57pub enum RenderError {
58    #[error(transparent)]
59    RenderingFailed(RenderingFailedError),
60
61    #[error("The requested content cannot be rendered as an acceptable media type.")]
62    CannotProvideAcceptableMediaType,
63
64    #[doc(hidden)]
65    #[error("{} This should never happen: {}", bug_message!(), .0)]
66    Bug(String),
67}
68
69/// Indicates that something went wrong after starting to stream content.
70#[derive(Error, Debug)]
71pub enum StreamError {
72    #[error(
73        "Process exited with {}{}",
74        match .exit_code {
75            Some(code) => format!("code {code}"),
76            None => String::from("unknown code"),
77        },
78        .stderr_contents.as_ref().map(|message| format!(": {message}")).unwrap_or_default(),
79    )]
80    ExecutableExitedWithNonzero {
81        pid: u32,
82        exit_code: Option<i32>,
83        stderr_contents: Option<String>,
84    },
85
86    #[error("Executable output could not be captured")]
87    ExecutableOutputCouldNotBeCaptured { pid: u32 },
88
89    #[error("Input/output error during rendering")]
90    IOError {
91        #[from]
92        source: io::Error,
93    },
94
95    #[error("Stream was cancelled")]
96    Canceled,
97}
98
99pub trait Render {
100    type Output;
101    fn render<'engine, 'accept, ServerInfo, Engine, Accept>(
102        &self,
103        context: RenderContext<'engine, ServerInfo, Engine>,
104        acceptable_media_ranges: Accept,
105    ) -> Result<Media<Self::Output>, RenderError>
106    where
107        ServerInfo: Clone + Serialize,
108        Engine: ContentEngine<ServerInfo>,
109        Accept: IntoIterator<Item = &'accept MediaRange>,
110        Self::Output: ByteStream;
111}
112
113// These must match up with serialized property names in RequestData and
114// RenderData.
115const TARGET_MEDIA_TYPE_PROPERTY_NAME: &str = "target-media-type";
116const REQUEST_DATA_PROPERTY_NAME: &str = "request";
117const ROUTE_PROPERTY_NAME: &str = "route";
118const QUERY_PARAMETERS_PROPERTY_NAME: &str = "query-parameters";
119const REQUEST_HEADERS_PROPERTY_NAME: &str = "request-headers";
120
121/// Render data that comes from requests.
122#[derive(Clone, Serialize)]
123#[serde(rename_all = "kebab-case")]
124pub struct RequestData {
125    /// The request [`Route`] that caused this content to be rendered, if any.
126    pub route: Option<Route>,
127
128    /// A parsed version of the request URI's query string.
129    pub query_parameters: HashMap<String, String>,
130
131    /// Headers that were sent in the request.
132    pub request_headers: HashMap<String, String>,
133}
134
135/// Data passed to handlebars templates and executables.
136///
137/// Fields serialize into kebab-case (e.g. `server_info` becomes `server-info`).
138#[derive(Clone, Serialize)]
139#[serde(rename_all = "kebab-case")]
140pub struct RenderData<ServerInfo: Clone + Serialize> {
141    /// A hierarchial index of the content. This is serialized with the name
142    /// `/` (with handlebars escaping this looks like `[/].[foo/].bar`).
143    #[serde(rename = "/")]
144    pub index: ContentIndex,
145
146    /// Metadata about the server, such as its version.
147    pub server_info: ServerInfo,
148
149    /// The best [`MediaType`] as determined by content negotiation. Rendering
150    /// must emit content in this media type.
151    pub target_media_type: Option<MediaType>,
152
153    /// Data that comes from requests.
154    pub request: RequestData,
155
156    /// An [HTTP `4xx` or `5xx` status code](https://datatracker.ietf.org/doc/html/rfc7231#section-6)
157    /// indicating that something went wrong. This will be set while rendering
158    /// content for the `--error-handler-route`.
159    pub error_code: Option<u16>,
160}
161
162/// Values used during rendering, including the data passed to handlebars
163/// templates and executables.
164pub struct RenderContext<'engine, ServerInfo, Engine>
165where
166    ServerInfo: Clone + Serialize,
167    Engine: ContentEngine<ServerInfo>,
168{
169    content_engine: &'engine Engine,
170    data: RenderData<ServerInfo>,
171    handlebars_render_context: Option<handlebars::RenderContext<'engine, 'engine>>,
172}
173
174impl<'engine, ServerInfo, Engine> RenderContext<'engine, ServerInfo, Engine>
175where
176    ServerInfo: Clone + Serialize,
177    Engine: ContentEngine<ServerInfo>,
178{
179    pub fn into_error_context(self, error_code: u16) -> Self {
180        RenderContext {
181            data: RenderData {
182                error_code: Some(error_code),
183                ..self.data
184            },
185            ..self
186        }
187    }
188
189    pub fn with_handlebars_render_context(
190        self,
191        handlebars_render_context: handlebars::RenderContext<'engine, 'engine>,
192    ) -> Self {
193        RenderContext {
194            handlebars_render_context: Some(handlebars_render_context),
195            ..self
196        }
197    }
198}