mod compare;
mod value;
use crate::schema::{PropField, PropType, PropsSchema};
pub use compare::compare_schema;
use std::any::TypeId;
use std::collections::HashMap;
#[doc(hidden)]
pub use value::{child_path, JsonMap, JsonValue};
pub use value::{encode_props, BoundProps, EncodeContext, PropValue};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropsSource {
pub file: String,
pub line: u32,
pub column: u32,
}
impl PropsSource {
pub fn new(file: &str, line: u32, column: u32) -> Self {
Self {
file: file.into(),
line,
column,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropsError {
pub code: &'static str,
pub route: String,
pub path: String,
pub message: String,
pub typescript: Option<Box<PropsSource>>,
pub rust: Option<Box<PropsSource>>,
}
impl PropsError {
pub fn code(&self) -> &'static str {
self.code
}
pub fn route(&self) -> &str {
&self.route
}
pub fn path(&self) -> &str {
&self.path
}
pub fn with_route(mut self, route: &str) -> Self {
self.route = route.into();
self
}
}
impl std::fmt::Display for PropsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} route {} props {}: {}",
self.code,
self.route,
if self.path.is_empty() {
"<root>"
} else {
&self.path
},
self.message
)?;
for (label, source) in [("TypeScript", &self.typescript), ("Rust", &self.rust)] {
if let Some(source) = source {
write!(
f,
"; {label} {}:{}:{}",
source.file, source.line, source.column
)?;
}
}
Ok(())
}
}
impl std::error::Error for PropsError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropsWarning {
pub code: &'static str,
pub route: String,
pub path: String,
pub message: String,
pub typescript: Option<PropsSource>,
pub rust: Option<PropsSource>,
}
impl std::fmt::Display for PropsWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
PropsError {
code: self.code,
route: self.route.clone(),
path: self.path.clone(),
message: self.message.clone(),
typescript: self.typescript.clone().map(Box::new),
rust: self.rust.clone().map(Box::new),
}
.fmt(f)
}
}
#[derive(Debug, Clone)]
pub struct RustType {
pub schema: PropsSchema,
pub strings: Vec<String>,
pub root: PropType,
pub source: PropsSource,
}
pub trait PageProps: PropValue {
fn rust_type() -> Result<RustType, PropsError> {
let mut builder = SchemaBuilder::default();
let root = Self::describe(&mut builder)?;
Ok(RustType {
schema: PropsSchema {
objects: builder.objects,
},
strings: builder.strings,
root,
source: Self::source(),
})
}
}
#[doc(hidden)]
#[derive(Debug, Default)]
pub struct SchemaBuilder {
strings: Vec<String>,
names: HashMap<String, u32>,
objects: Vec<Vec<PropField>>,
types: HashMap<TypeId, u16>,
active: Vec<TypeId>,
wrappers: usize,
}
impl SchemaBuilder {
fn failure(message: &str) -> PropsError {
PropsError {
code: "BIND-105",
route: String::new(),
path: String::new(),
message: message.into(),
typescript: None,
rust: None,
}
}
pub fn intern(&mut self, name: &str) -> Result<u32, PropsError> {
if let Some(index) = self.names.get(name) {
return Ok(*index);
}
if name.len() > u16::MAX as usize || self.strings.len() >= crate::format::MAX_STRINGS {
return Err(Self::failure("Rust schema string exceeds the FMIR limit"));
}
let index = self.strings.len() as u32;
self.strings.push(name.into());
self.names.insert(name.into(), index);
Ok(index)
}
pub fn object<T: 'static>(
&mut self,
fields: impl FnOnce(&mut Self) -> Result<Vec<PropField>, PropsError>,
) -> Result<PropType, PropsError> {
let ty = TypeId::of::<T>();
if self.active.contains(&ty) {
return Err(Self::failure("recursive Rust props are not supported"));
}
if let Some(index) = self.types.get(&ty) {
return Ok(PropType::Object(*index));
}
if self.objects.len() >= crate::format::MAX_SCHEMA_ITEMS {
return Err(Self::failure("too many Rust schema objects"));
}
let index = self.objects.len() as u16;
self.objects.push(Vec::new());
self.types.insert(ty, index);
self.active.push(ty);
let wrappers = std::mem::replace(&mut self.wrappers, 0);
let result = fields(self);
self.wrappers = wrappers;
self.active.pop();
let fields = result?;
if fields.len() > crate::format::MAX_SCHEMA_ITEMS {
return Err(Self::failure("too many Rust schema fields"));
}
let mut names = std::collections::HashSet::new();
if fields.iter().any(|f| !names.insert(f.name)) {
return Err(Self::failure("duplicate Rust props field names"));
}
self.objects[index as usize] = fields;
Ok(PropType::Object(index))
}
pub fn field(
&mut self,
name: &str,
source: PropsSource,
kind: PropType,
) -> Result<PropField, PropsError> {
Ok(PropField {
name: self.intern(name)?,
file: self.intern(&source.file)?,
line: source.line,
column: source.column,
kind,
})
}
pub fn nested<T: PropValue>(&mut self) -> Result<PropType, PropsError> {
if self.wrappers >= crate::format::MAX_TYPE_DEPTH {
return Err(Self::failure(
"Rust props type nesting exceeds the FMIR limit",
));
}
self.wrappers += 1;
let result = T::describe(self);
self.wrappers -= 1;
result
}
pub fn enumeration(&mut self, names: &[&str]) -> Result<PropType, PropsError> {
if names.is_empty() || names.len() > crate::format::MAX_SCHEMA_ITEMS {
return Err(Self::failure(
"Rust enum must contain a bounded nonempty set of literals",
));
}
let names = names
.iter()
.map(|name| self.intern(name))
.collect::<Result<Vec<_>, _>>()?;
Ok(PropType::Enum(names))
}
}
#[doc(hidden)]
pub fn field_path(parent: &str, name: &str) -> String {
let simple = !name.is_empty()
&& name.chars().enumerate().all(|(i, c)| {
c == '_' || c == '$' || c.is_ascii_alphabetic() || (i > 0 && c.is_ascii_digit())
});
if simple {
if parent.is_empty() {
name.into()
} else {
format!("{parent}.{name}")
}
} else {
format!("{parent}[{}]", serde_json::Value::String(name.into()))
}
}