use super::{compare_schema, field_path, PageProps, PropsError, PropsSource, SchemaBuilder};
use crate::{
schema::{PropField, PropType},
IrModule,
};
use serde_json::Value;
pub trait PropValue: Sized + 'static {
fn describe(builder: &mut SchemaBuilder) -> Result<PropType, PropsError>;
fn encode_value(
&self,
context: &EncodeContext<'_>,
kind: &PropType,
path: &str,
) -> Result<Option<Value>, PropsError>;
fn source() -> PropsSource {
PropsSource::new(std::any::type_name::<Self>(), 0, 0)
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct EncodeContext<'a> {
module: &'a IrModule,
route: &'a str,
typescript: Option<PropsSource>,
rust: Option<PropsSource>,
depth: usize,
}
impl<'a> EncodeContext<'a> {
pub fn required<'b>(&self, mut kind: &'b PropType) -> &'b PropType {
while let PropType::Optional(inner) = kind {
kind = inner;
}
kind
}
pub fn error(&self, code: &'static str, path: &str, message: impl Into<String>) -> PropsError {
PropsError {
code,
route: self.route.into(),
path: path.into(),
message: message.into(),
typescript: self.typescript.clone().map(Box::new),
rust: self.rust.clone().map(Box::new),
}
}
pub fn object(&self, kind: &PropType, path: &str) -> Result<&'a [PropField], PropsError> {
if let PropType::Object(index) = self.required(kind) {
return self
.module
.schema
.objects
.get(*index as usize)
.map(Vec::as_slice)
.ok_or_else(|| self.error("RENDER-404", path, "invalid object reference"));
}
Err(self.error("RENDER-404", path, "expected a declared object"))
}
pub fn name(&self, index: u32, path: &str) -> Result<&'a str, PropsError> {
self.module
.strings
.get(index)
.map_err(|e| self.error("RENDER-404", path, e.to_string()))
}
pub fn field(
&self,
field: &PropField,
rust: PropsSource,
path: &str,
) -> Result<Self, PropsError> {
self.descend(path).map(|mut child| {
child.typescript = self
.module
.strings
.get(field.file)
.ok()
.map(|file| PropsSource::new(file, field.line, field.column));
child.rust = Some(rust);
child
})
}
fn descend(&self, path: &str) -> Result<Self, PropsError> {
if self.depth >= crate::format::MAX_SCHEMA_ITEMS {
return Err(self.error(
"RENDER-404",
path,
"props value depth exceeds the schema limit",
));
}
Ok(Self {
module: self.module,
route: self.route,
typescript: self.typescript.clone(),
rust: self.rust.clone(),
depth: self.depth + 1,
})
}
pub fn enumeration(
&self,
kind: &PropType,
path: &str,
literal: &str,
) -> Result<Option<Value>, PropsError> {
if let PropType::Enum(names) = self.required(kind) {
for index in names {
if self.name(*index, path)? == literal {
return Ok(Some(Value::String(literal.into())));
}
}
}
Err(self.error(
"RENDER-404",
path,
"enum value is absent from the declared literal set",
))
}
}
#[derive(Debug)]
pub struct BoundProps<P: PageProps> {
module: std::sync::Arc<IrModule>,
route: String,
source: PropsSource,
warnings: Vec<super::PropsWarning>,
marker: std::marker::PhantomData<fn() -> P>,
}
impl<P: PageProps> BoundProps<P> {
pub fn bind(module: std::sync::Arc<IrModule>, route: &str) -> Result<Self, Vec<PropsError>> {
let rust = P::rust_type().map_err(|e| vec![e.with_route(route)])?;
let warnings = compare_schema(&module, &rust, route)?;
Ok(Self {
module,
route: route.into(),
source: rust.source,
warnings,
marker: std::marker::PhantomData,
})
}
pub fn module(&self) -> &IrModule {
&self.module
}
pub fn module_arc(&self) -> &std::sync::Arc<IrModule> {
&self.module
}
pub fn warnings(&self) -> &[super::PropsWarning] {
&self.warnings
}
pub fn encode(&self, props: &P) -> Result<Value, PropsError> {
project(&self.module, props, &self.route, self.source.clone())
}
}
fn project<P: PageProps>(
module: &IrModule,
props: &P,
route: &str,
source: PropsSource,
) -> Result<Value, PropsError> {
let context = EncodeContext {
module,
route,
typescript: None,
rust: Some(source),
depth: 0,
};
props
.encode_value(&context, &PropType::Object(0), "")?
.ok_or_else(|| context.error("RENDER-404", "", "page props cannot be absent"))
}
pub fn encode_props<P: PageProps>(
module: &IrModule,
props: &P,
route: &str,
) -> Result<Value, PropsError> {
let rust = P::rust_type().map_err(|e| e.with_route(route))?;
if let Err(errors) = compare_schema(module, &rust, route) {
if let Some(error) = errors.into_iter().next() {
return Err(error);
}
}
project(module, props, route, rust.source)
}
macro_rules! primitive {
($ty:ty,$kind:ident,$convert:expr) => {
impl PropValue for $ty {
fn describe(_: &mut SchemaBuilder) -> Result<PropType, PropsError> {
Ok(PropType::$kind)
}
fn encode_value(
&self,
context: &EncodeContext<'_>,
kind: &PropType,
path: &str,
) -> Result<Option<Value>, PropsError> {
if !matches!(context.required(kind), PropType::$kind) {
return Err(context.error(
"RENDER-404",
path,
"value does not match the declared type",
));
}
($convert)(self, context, path).map(Some)
}
}
};
}
primitive!(bool, Boolean, |value: &bool,
_: &EncodeContext<'_>,
_: &str| Ok::<
Value,
PropsError,
>(
Value::Bool(*value)
));
primitive!(String, String, |value: &String,
_: &EncodeContext<'_>,
_: &str| Ok::<
Value,
PropsError,
>(
Value::String(value.clone())
));
const MAX_SAFE: i64 = 9_007_199_254_740_991;
macro_rules! integers {
($($ty:ty),*)=>{$(primitive!($ty,Number,|value:&$ty,context:&EncodeContext<'_>,path:&str|{
let number=*value as i128;
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)"));}
Ok(Value::Number(serde_json::Number::from(number as i64)))
});)*};
}
integers!(i8, i16, i32, i64, u8, u16, u32, u64, isize, usize);
macro_rules! floats {($($ty:ty),*)=>{$(primitive!($ty,Number,|value:&$ty,context:&EncodeContext<'_>,path:&str|{
serde_json::Number::from_f64(*value as f64).map(Value::Number).ok_or_else(||context.error("RENDER-402",path,"number must be finite"))
});)*};}
floats!(f32, f64);
fn date_milliseconds(negative: bool, duration: std::time::Duration) -> Option<i64> {
let millis = duration.as_millis();
if millis > 8_640_000_000_000_000 || millis > MAX_SAFE as u128 {
return None;
}
Some(millis as i64 * if negative { -1 } else { 1 })
}
primitive!(
std::time::SystemTime,
Date,
|value: &std::time::SystemTime, context: &EncodeContext<'_>, path: &str| {
let (negative, duration) = match value.duration_since(std::time::UNIX_EPOCH) {
Ok(d) => (false, d),
Err(e) => (true, e.duration()),
};
date_milliseconds(negative, duration)
.map(|value| Value::Number(value.into()))
.ok_or_else(|| {
context.error(
"RENDER-403",
path,
"date exceeds the valid JavaScript Date range ±8.64e15 milliseconds",
)
})
}
);
impl<T: PropValue> PropValue for Option<T> {
fn describe(builder: &mut SchemaBuilder) -> Result<PropType, PropsError> {
Ok(PropType::Optional(Box::new(builder.nested::<T>()?)))
}
fn encode_value(
&self,
context: &EncodeContext<'_>,
kind: &PropType,
path: &str,
) -> Result<Option<Value>, PropsError> {
let PropType::Optional(inner) = kind else {
return Err(context.error(
"RENDER-404",
path,
"Option needs an optional declared type",
));
};
match self {
Some(value) => value.encode_value(context, inner, path),
None => Ok(None),
}
}
}
impl<T: PropValue> PropValue for Vec<T> {
fn describe(builder: &mut SchemaBuilder) -> Result<PropType, PropsError> {
Ok(PropType::Array(Box::new(builder.nested::<T>()?)))
}
fn encode_value(
&self,
context: &EncodeContext<'_>,
kind: &PropType,
path: &str,
) -> Result<Option<Value>, PropsError> {
let PropType::Array(inner) = context.required(kind) else {
return Err(context.error("RENDER-404", path, "Vec needs an array declared type"));
};
let context = context.descend(path)?;
let mut values = Vec::with_capacity(self.len());
for (index, value) in self.iter().enumerate() {
values.push(
value
.encode_value(&context, inner, &format!("{path}[{index}]"))?
.unwrap_or(Value::Null),
);
}
Ok(Some(Value::Array(values)))
}
}
#[doc(hidden)]
pub use serde_json::{Map as JsonMap, Value as JsonValue};
#[doc(hidden)]
pub fn child_path(parent: &str, name: &str) -> String {
field_path(parent, name)
}
#[cfg(test)]
mod tests {
use super::date_milliseconds;
use std::time::Duration;
#[test]
fn date_conversion_checks_both_signed_bounds_without_system_time_limits() {
let boundary = 8_640_000_000_000_000;
for negative in [false, true] {
let sign = if negative { -1 } else { 1 };
for (duration, expected) in [
(Duration::ZERO, 0),
(Duration::from_nanos(999_999), 0),
(Duration::from_nanos(1_999_999), 1),
(Duration::from_millis(boundary), boundary as i64),
(
Duration::from_millis(boundary) + Duration::from_nanos(999_999),
boundary as i64,
),
] {
assert_eq!(date_milliseconds(negative, duration), Some(expected * sign));
}
for duration in [
Duration::from_millis(boundary + 1),
Duration::from_millis(i64::MAX as u64 + 1),
Duration::from_secs(u64::MAX),
] {
assert_eq!(date_milliseconds(negative, duration), None);
}
}
}
}