rosu_render/request/
render.rs1use 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#[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 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}