Skip to main content

rosu_render/request/
render.rs

1use std::future::IntoFuture;
2
3use crate::{
4    model::{RenderAdded, RenderOptions, RenderSkinOption},
5    routing::Route,
6    util::multipart::Form,
7    ClientError, OrdrClient,
8};
9
10use super::{OrdrFuture, Request};
11
12enum ReplaySource<'a> {
13    File(&'a [u8]),
14    Url(&'a str),
15}
16
17/// Commission a render job to o!rdr.
18///
19/// If successful, progress of the rendering can be tracking through the [`OrdrWebsocket`](crate::OrdrWebsocket).
20#[must_use]
21pub struct CommissionRender<'a> {
22    ordr: &'a OrdrClient,
23    replay_source: ReplaySource<'a>,
24    username: &'a str,
25    skin: &'a RenderSkinOption<'a>,
26    options: Option<&'a RenderOptions>,
27}
28
29impl<'a> CommissionRender<'a> {
30    pub(crate) const fn with_file(
31        ordr: &'a OrdrClient,
32        replay_file: &'a [u8],
33        username: &'a str,
34        skin: &'a RenderSkinOption<'a>,
35    ) -> Self {
36        Self {
37            ordr,
38            replay_source: ReplaySource::File(replay_file),
39            username,
40            skin,
41            options: None,
42        }
43    }
44
45    pub(crate) const fn with_url(
46        ordr: &'a OrdrClient,
47        replay_url: &'a str,
48        username: &'a str,
49        skin: &'a RenderSkinOption<'a>,
50    ) -> Self {
51        Self {
52            ordr,
53            replay_source: ReplaySource::Url(replay_url),
54            username,
55            skin,
56            options: None,
57        }
58    }
59
60    /// Specify rendering options.
61    pub fn options(mut self, options: &'a RenderOptions) -> Self {
62        self.options = Some(options);
63
64        self
65    }
66
67    fn request(&self) -> Request {
68        let missing_options = self.options.is_none();
69
70        let mut form = self.options.map_or_else(Form::new, Form::serialize);
71
72        if missing_options {
73            form.push_text("resolution", RenderOptions::DEFAULT_RESOLUTION.as_str());
74        }
75
76        match self.replay_source {
77            ReplaySource::File(bytes) => form.push_replay("replayFile", bytes),
78            ReplaySource::Url(url) => form.push_text("replayURL", url),
79        };
80
81        form.push_text("username", self.username);
82
83        match self.skin {
84            RenderSkinOption::Official { name } => {
85                form.push_text("skin", name.as_ref())
86                    .push_text("customSkin", "false");
87            }
88            RenderSkinOption::Custom { id } => {
89                form.push_text("skin", id.to_string())
90                    .push_text("customSkin", "true");
91            }
92        }
93
94        if let Some(verification) = self.ordr.verification() {
95            form.push_text("verificationKey", verification.as_str());
96        }
97
98        Request::builder(Route::Render).form(form).build()
99    }
100}
101
102impl IntoFuture for &mut CommissionRender<'_> {
103    type Output = Result<RenderAdded, ClientError>;
104    type IntoFuture = OrdrFuture<RenderAdded>;
105
106    fn into_future(self) -> Self::IntoFuture {
107        self.ordr.request(self.request())
108    }
109}
110
111impl IntoFuture for CommissionRender<'_> {
112    type Output = Result<RenderAdded, ClientError>;
113    type IntoFuture = OrdrFuture<RenderAdded>;
114
115    fn into_future(mut self) -> Self::IntoFuture {
116        (&mut self).into_future()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::{
123        model::{RenderOptions, RenderSkinOption},
124        OrdrClient,
125    };
126
127    use super::CommissionRender;
128
129    fn form_body(render: &CommissionRender<'_>) -> String {
130        let request = render.request();
131        let form = request
132            .form
133            .expect("commission request must carry a multipart form");
134
135        String::from_utf8(form.build()).expect("form body is UTF-8")
136    }
137
138    #[test]
139    fn options_form() {
140        let client = OrdrClient::builder().build();
141        let skin = RenderSkinOption::Official {
142            name: "Kuro".into(),
143        };
144        let options = RenderOptions::default();
145
146        let render = client
147            .render_with_replay_url("https://replay.watch/x.osr", "TestUser", &skin)
148            .options(&options);
149
150        let body = form_body(&render);
151
152        assert!(body.contains(r#"name="replayURL""#));
153        assert!(body.contains("https://replay.watch/x.osr"));
154        assert!(body.contains(r#"name="username""#));
155        assert!(body.contains("TestUser"));
156        assert!(body.contains(r#"name="skin""#));
157        assert!(body.contains("Kuro"));
158        assert!(body.contains(r#"name="customSkin""#));
159        assert!(body.contains(r#"name="resolution""#));
160        assert!(body.contains("1280x720"));
161        assert!(body.contains(r#"name="globalVolume""#));
162        assert!(!body.contains(r#"name="verificationKey""#));
163    }
164
165    #[test]
166    fn missing_options_default_resolution() {
167        let client = OrdrClient::builder().build();
168        let skin = RenderSkinOption::Official {
169            name: "Kuro".into(),
170        };
171
172        let render = client.render_with_replay_url("https://replay.watch/x.osr", "TestUser", &skin);
173
174        let body = form_body(&render);
175
176        assert!(body.contains(r#"name="resolution""#));
177        assert!(body.contains("1280x720"));
178        assert!(!body.contains(r#"name="globalVolume""#));
179    }
180
181    #[test]
182    fn custom_skin() {
183        let client = OrdrClient::builder().build();
184        let skin = RenderSkinOption::from(42u32);
185
186        let render = client.render_with_replay_url("https://replay.watch/x.osr", "TestUser", &skin);
187
188        let body = form_body(&render);
189        let lines: Vec<&str> = body.lines().collect();
190
191        let skin_idx = lines
192            .iter()
193            .position(|line| line.contains(r#"name="skin""#))
194            .unwrap();
195        let custom_idx = lines
196            .iter()
197            .position(|line| line.contains(r#"name="customSkin""#))
198            .unwrap();
199
200        assert_eq!(lines[skin_idx + 2], "42");
201        assert_eq!(lines[custom_idx + 2], "true");
202    }
203}