1use super::{compare_schema, field_path, PageProps, PropsError, PropsSource, SchemaBuilder};
2use crate::{
3 schema::{PropField, PropType},
4 IrModule,
5};
6use serde_json::Value;
7
8pub trait PropValue: Sized + 'static {
11 fn describe(builder: &mut SchemaBuilder) -> Result<PropType, PropsError>;
13 fn encode_value(
16 &self,
17 context: &EncodeContext<'_>,
18 kind: &PropType,
19 path: &str,
20 ) -> Result<Option<Value>, PropsError>;
21 fn source() -> PropsSource {
23 PropsSource::new(std::any::type_name::<Self>(), 0, 0)
24 }
25}
26
27#[doc(hidden)]
30#[derive(Debug)]
31pub struct EncodeContext<'a> {
32 module: &'a IrModule,
33 route: &'a str,
34 typescript: Option<PropsSource>,
35 rust: Option<PropsSource>,
36 depth: usize,
37}
38impl<'a> EncodeContext<'a> {
39 pub fn required<'b>(&self, mut kind: &'b PropType) -> &'b PropType {
41 while let PropType::Optional(inner) = kind {
42 kind = inner;
43 }
44 kind
45 }
46 pub fn error(&self, code: &'static str, path: &str, message: impl Into<String>) -> PropsError {
48 PropsError {
49 code,
50 route: self.route.into(),
51 path: path.into(),
52 message: message.into(),
53 typescript: self.typescript.clone().map(Box::new),
54 rust: self.rust.clone().map(Box::new),
55 }
56 }
57 pub fn object(&self, kind: &PropType, path: &str) -> Result<&'a [PropField], PropsError> {
59 if let PropType::Object(index) = self.required(kind) {
60 return self
61 .module
62 .schema
63 .objects
64 .get(*index as usize)
65 .map(Vec::as_slice)
66 .ok_or_else(|| self.error("RENDER-404", path, "invalid object reference"));
67 }
68 Err(self.error("RENDER-404", path, "expected a declared object"))
69 }
70 pub fn name(&self, index: u32, path: &str) -> Result<&'a str, PropsError> {
72 self.module
73 .strings
74 .get(index)
75 .map_err(|e| self.error("RENDER-404", path, e.to_string()))
76 }
77 pub fn field(
79 &self,
80 field: &PropField,
81 rust: PropsSource,
82 path: &str,
83 ) -> Result<Self, PropsError> {
84 self.descend(path).map(|mut child| {
85 child.typescript = self
86 .module
87 .strings
88 .get(field.file)
89 .ok()
90 .map(|file| PropsSource::new(file, field.line, field.column));
91 child.rust = Some(rust);
92 child
93 })
94 }
95 fn descend(&self, path: &str) -> Result<Self, PropsError> {
96 if self.depth >= crate::format::MAX_SCHEMA_ITEMS {
97 return Err(self.error(
98 "RENDER-404",
99 path,
100 "props value depth exceeds the schema limit",
101 ));
102 }
103 Ok(Self {
104 module: self.module,
105 route: self.route,
106 typescript: self.typescript.clone(),
107 rust: self.rust.clone(),
108 depth: self.depth + 1,
109 })
110 }
111 pub fn enumeration(
113 &self,
114 kind: &PropType,
115 path: &str,
116 literal: &str,
117 ) -> Result<Option<Value>, PropsError> {
118 if let PropType::Enum(names) = self.required(kind) {
119 for index in names {
120 if self.name(*index, path)? == literal {
121 return Ok(Some(Value::String(literal.into())));
122 }
123 }
124 }
125 Err(self.error(
126 "RENDER-404",
127 path,
128 "enum value is absent from the declared literal set",
129 ))
130 }
131}
132
133#[derive(Debug)]
136pub struct BoundProps<P: PageProps> {
137 module: std::sync::Arc<IrModule>,
138 route: String,
139 source: PropsSource,
140 warnings: Vec<super::PropsWarning>,
141 marker: std::marker::PhantomData<fn() -> P>,
142}
143impl<P: PageProps> BoundProps<P> {
144 pub fn bind(module: std::sync::Arc<IrModule>, route: &str) -> Result<Self, Vec<PropsError>> {
146 let rust = P::rust_type().map_err(|e| vec![e.with_route(route)])?;
147 let warnings = compare_schema(&module, &rust, route)?;
148 Ok(Self {
149 module,
150 route: route.into(),
151 source: rust.source,
152 warnings,
153 marker: std::marker::PhantomData,
154 })
155 }
156 pub fn module(&self) -> &IrModule {
158 &self.module
159 }
160 pub fn module_arc(&self) -> &std::sync::Arc<IrModule> {
162 &self.module
163 }
164 pub fn warnings(&self) -> &[super::PropsWarning] {
166 &self.warnings
167 }
168 pub fn encode(&self, props: &P) -> Result<Value, PropsError> {
170 project(&self.module, props, &self.route, self.source.clone())
171 }
172}
173
174fn project<P: PageProps>(
175 module: &IrModule,
176 props: &P,
177 route: &str,
178 source: PropsSource,
179) -> Result<Value, PropsError> {
180 let context = EncodeContext {
181 module,
182 route,
183 typescript: None,
184 rust: Some(source),
185 depth: 0,
186 };
187 props
188 .encode_value(&context, &PropType::Object(0), "")?
189 .ok_or_else(|| context.error("RENDER-404", "", "page props cannot be absent"))
190}
191
192pub fn encode_props<P: PageProps>(
195 module: &IrModule,
196 props: &P,
197 route: &str,
198) -> Result<Value, PropsError> {
199 let rust = P::rust_type().map_err(|e| e.with_route(route))?;
200 if let Err(errors) = compare_schema(module, &rust, route) {
201 if let Some(error) = errors.into_iter().next() {
202 return Err(error);
203 }
204 }
205 project(module, props, route, rust.source)
206}
207
208macro_rules! primitive {
209 ($ty:ty,$kind:ident,$convert:expr) => {
210 impl PropValue for $ty {
211 fn describe(_: &mut SchemaBuilder) -> Result<PropType, PropsError> {
212 Ok(PropType::$kind)
213 }
214 fn encode_value(
215 &self,
216 context: &EncodeContext<'_>,
217 kind: &PropType,
218 path: &str,
219 ) -> Result<Option<Value>, PropsError> {
220 if !matches!(context.required(kind), PropType::$kind) {
221 return Err(context.error(
222 "RENDER-404",
223 path,
224 "value does not match the declared type",
225 ));
226 }
227 ($convert)(self, context, path).map(Some)
228 }
229 }
230 };
231}
232primitive!(bool, Boolean, |value: &bool,
233 _: &EncodeContext<'_>,
234 _: &str| Ok::<
235 Value,
236 PropsError,
237>(
238 Value::Bool(*value)
239));
240primitive!(String, String, |value: &String,
241 _: &EncodeContext<'_>,
242 _: &str| Ok::<
243 Value,
244 PropsError,
245>(
246 Value::String(value.clone())
247));
248const MAX_SAFE: i64 = 9_007_199_254_740_991;
249macro_rules! integers {
250 ($($ty:ty),*)=>{$(primitive!($ty,Number,|value:&$ty,context:&EncodeContext<'_>,path:&str|{
251 let number=*value as i128;
253 if !(-(MAX_SAFE as i128)..=MAX_SAFE as i128).contains(&number){return Err(context.error("RENDER-401",path,"integer exceeds JavaScript's exact range ±(2^53 - 1)"));}
254 Ok(Value::Number(serde_json::Number::from(number as i64)))
255 });)*};
256}
257integers!(i8, i16, i32, i64, u8, u16, u32, u64, isize, usize);
258macro_rules! floats {($($ty:ty),*)=>{$(primitive!($ty,Number,|value:&$ty,context:&EncodeContext<'_>,path:&str|{
259 serde_json::Number::from_f64(*value as f64).map(Value::Number).ok_or_else(||context.error("RENDER-402",path,"number must be finite"))
260});)*};}
261floats!(f32, f64);
262
263fn date_milliseconds(negative: bool, duration: std::time::Duration) -> Option<i64> {
264 let millis = duration.as_millis();
267 if millis > 8_640_000_000_000_000 || millis > MAX_SAFE as u128 {
268 return None;
269 }
270 Some(millis as i64 * if negative { -1 } else { 1 })
271}
272
273primitive!(
274 std::time::SystemTime,
275 Date,
276 |value: &std::time::SystemTime, context: &EncodeContext<'_>, path: &str| {
277 let (negative, duration) = match value.duration_since(std::time::UNIX_EPOCH) {
278 Ok(d) => (false, d),
279 Err(e) => (true, e.duration()),
280 };
281 date_milliseconds(negative, duration)
282 .map(|value| Value::Number(value.into()))
283 .ok_or_else(|| {
284 context.error(
285 "RENDER-403",
286 path,
287 "date exceeds the valid JavaScript Date range ±8.64e15 milliseconds",
288 )
289 })
290 }
291);
292impl<T: PropValue> PropValue for Option<T> {
293 fn describe(builder: &mut SchemaBuilder) -> Result<PropType, PropsError> {
294 Ok(PropType::Optional(Box::new(builder.nested::<T>()?)))
295 }
296 fn encode_value(
297 &self,
298 context: &EncodeContext<'_>,
299 kind: &PropType,
300 path: &str,
301 ) -> Result<Option<Value>, PropsError> {
302 let PropType::Optional(inner) = kind else {
303 return Err(context.error(
304 "RENDER-404",
305 path,
306 "Option needs an optional declared type",
307 ));
308 };
309 match self {
310 Some(value) => value.encode_value(context, inner, path),
311 None => Ok(None),
312 }
313 }
314}
315impl<T: PropValue> PropValue for Vec<T> {
316 fn describe(builder: &mut SchemaBuilder) -> Result<PropType, PropsError> {
317 Ok(PropType::Array(Box::new(builder.nested::<T>()?)))
318 }
319 fn encode_value(
320 &self,
321 context: &EncodeContext<'_>,
322 kind: &PropType,
323 path: &str,
324 ) -> Result<Option<Value>, PropsError> {
325 let PropType::Array(inner) = context.required(kind) else {
326 return Err(context.error("RENDER-404", path, "Vec needs an array declared type"));
327 };
328 let context = context.descend(path)?;
329 let mut values = Vec::with_capacity(self.len());
330 for (index, value) in self.iter().enumerate() {
331 values.push(
332 value
333 .encode_value(&context, inner, &format!("{path}[{index}]"))?
334 .unwrap_or(Value::Null),
335 );
336 }
337 Ok(Some(Value::Array(values)))
338 }
339}
340
341#[doc(hidden)]
343pub use serde_json::{Map as JsonMap, Value as JsonValue};
344#[doc(hidden)]
345pub fn child_path(parent: &str, name: &str) -> String {
346 field_path(parent, name)
347}
348
349#[cfg(test)]
350mod tests {
351 use super::date_milliseconds;
352 use std::time::Duration;
353
354 #[test]
355 fn date_conversion_checks_both_signed_bounds_without_system_time_limits() {
356 let boundary = 8_640_000_000_000_000;
359 for negative in [false, true] {
360 let sign = if negative { -1 } else { 1 };
361 for (duration, expected) in [
362 (Duration::ZERO, 0),
363 (Duration::from_nanos(999_999), 0),
364 (Duration::from_nanos(1_999_999), 1),
365 (Duration::from_millis(boundary), boundary as i64),
366 (
367 Duration::from_millis(boundary) + Duration::from_nanos(999_999),
368 boundary as i64,
369 ),
370 ] {
371 assert_eq!(date_milliseconds(negative, duration), Some(expected * sign));
372 }
373 for duration in [
374 Duration::from_millis(boundary + 1),
375 Duration::from_millis(i64::MAX as u64 + 1),
376 Duration::from_secs(u64::MAX),
377 ] {
378 assert_eq!(date_milliseconds(negative, duration), None);
379 }
380 }
381 }
382}