1mod compare;
4mod value;
5use crate::schema::{PropField, PropType, PropsSchema};
6pub use compare::compare_schema;
7use std::any::TypeId;
8use std::collections::HashMap;
9#[doc(hidden)]
10pub use value::{child_path, JsonMap, JsonValue};
11pub use value::{encode_props, BoundProps, EncodeContext, PropValue};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct PropsSource {
16 pub file: String,
18 pub line: u32,
20 pub column: u32,
22}
23impl PropsSource {
24 pub fn new(file: &str, line: u32, column: u32) -> Self {
26 Self {
27 file: file.into(),
28 line,
29 column,
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct PropsError {
38 pub code: &'static str,
40 pub route: String,
42 pub path: String,
44 pub message: String,
46 pub typescript: Option<Box<PropsSource>>,
48 pub rust: Option<Box<PropsSource>>,
50}
51impl PropsError {
52 pub fn code(&self) -> &'static str {
54 self.code
55 }
56 pub fn route(&self) -> &str {
58 &self.route
59 }
60 pub fn path(&self) -> &str {
62 &self.path
63 }
64 pub fn with_route(mut self, route: &str) -> Self {
66 self.route = route.into();
67 self
68 }
69}
70impl std::fmt::Display for PropsError {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(
73 f,
74 "{} route {} props {}: {}",
75 self.code,
76 self.route,
77 if self.path.is_empty() {
78 "<root>"
79 } else {
80 &self.path
81 },
82 self.message
83 )?;
84 for (label, source) in [("TypeScript", &self.typescript), ("Rust", &self.rust)] {
85 if let Some(source) = source {
86 write!(
87 f,
88 "; {label} {}:{}:{}",
89 source.file, source.line, source.column
90 )?;
91 }
92 }
93 Ok(())
94 }
95}
96impl std::error::Error for PropsError {}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct PropsWarning {
101 pub code: &'static str,
103 pub route: String,
105 pub path: String,
107 pub message: String,
109 pub typescript: Option<PropsSource>,
111 pub rust: Option<PropsSource>,
113}
114impl std::fmt::Display for PropsWarning {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 PropsError {
117 code: self.code,
118 route: self.route.clone(),
119 path: self.path.clone(),
120 message: self.message.clone(),
121 typescript: self.typescript.clone().map(Box::new),
122 rust: self.rust.clone().map(Box::new),
123 }
124 .fmt(f)
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct RustType {
131 pub schema: PropsSchema,
133 pub strings: Vec<String>,
135 pub root: PropType,
137 pub source: PropsSource,
139}
140
141pub trait PageProps: PropValue {
144 fn rust_type() -> Result<RustType, PropsError> {
146 let mut builder = SchemaBuilder::default();
147 let root = Self::describe(&mut builder)?;
148 Ok(RustType {
149 schema: PropsSchema {
150 objects: builder.objects,
151 },
152 strings: builder.strings,
153 root,
154 source: Self::source(),
155 })
156 }
157}
158
159#[doc(hidden)]
161#[derive(Debug, Default)]
162pub struct SchemaBuilder {
163 strings: Vec<String>,
164 names: HashMap<String, u32>,
165 objects: Vec<Vec<PropField>>,
166 types: HashMap<TypeId, u16>,
167 active: Vec<TypeId>,
168 wrappers: usize,
169}
170impl SchemaBuilder {
171 fn failure(message: &str) -> PropsError {
172 PropsError {
173 code: "BIND-105",
174 route: String::new(),
175 path: String::new(),
176 message: message.into(),
177 typescript: None,
178 rust: None,
179 }
180 }
181 pub fn intern(&mut self, name: &str) -> Result<u32, PropsError> {
183 if let Some(index) = self.names.get(name) {
184 return Ok(*index);
185 }
186 if name.len() > u16::MAX as usize || self.strings.len() >= crate::format::MAX_STRINGS {
187 return Err(Self::failure("Rust schema string exceeds the FMIR limit"));
188 }
189 let index = self.strings.len() as u32;
190 self.strings.push(name.into());
191 self.names.insert(name.into(), index);
192 Ok(index)
193 }
194 pub fn object<T: 'static>(
196 &mut self,
197 fields: impl FnOnce(&mut Self) -> Result<Vec<PropField>, PropsError>,
198 ) -> Result<PropType, PropsError> {
199 let ty = TypeId::of::<T>();
200 if self.active.contains(&ty) {
201 return Err(Self::failure("recursive Rust props are not supported"));
202 }
203 if let Some(index) = self.types.get(&ty) {
204 return Ok(PropType::Object(*index));
205 }
206 if self.objects.len() >= crate::format::MAX_SCHEMA_ITEMS {
207 return Err(Self::failure("too many Rust schema objects"));
208 }
209 let index = self.objects.len() as u16;
210 self.objects.push(Vec::new());
211 self.types.insert(ty, index);
212 self.active.push(ty);
213 let wrappers = std::mem::replace(&mut self.wrappers, 0);
214 let result = fields(self);
215 self.wrappers = wrappers;
216 self.active.pop();
217 let fields = result?;
218 if fields.len() > crate::format::MAX_SCHEMA_ITEMS {
219 return Err(Self::failure("too many Rust schema fields"));
220 }
221 let mut names = std::collections::HashSet::new();
222 if fields.iter().any(|f| !names.insert(f.name)) {
223 return Err(Self::failure("duplicate Rust props field names"));
224 }
225 self.objects[index as usize] = fields;
226 Ok(PropType::Object(index))
227 }
228 pub fn field(
230 &mut self,
231 name: &str,
232 source: PropsSource,
233 kind: PropType,
234 ) -> Result<PropField, PropsError> {
235 Ok(PropField {
236 name: self.intern(name)?,
237 file: self.intern(&source.file)?,
238 line: source.line,
239 column: source.column,
240 kind,
241 })
242 }
243 pub fn nested<T: PropValue>(&mut self) -> Result<PropType, PropsError> {
245 if self.wrappers >= crate::format::MAX_TYPE_DEPTH {
246 return Err(Self::failure(
247 "Rust props type nesting exceeds the FMIR limit",
248 ));
249 }
250 self.wrappers += 1;
251 let result = T::describe(self);
252 self.wrappers -= 1;
253 result
254 }
255 pub fn enumeration(&mut self, names: &[&str]) -> Result<PropType, PropsError> {
257 if names.is_empty() || names.len() > crate::format::MAX_SCHEMA_ITEMS {
258 return Err(Self::failure(
259 "Rust enum must contain a bounded nonempty set of literals",
260 ));
261 }
262 let names = names
263 .iter()
264 .map(|name| self.intern(name))
265 .collect::<Result<Vec<_>, _>>()?;
266 Ok(PropType::Enum(names))
267 }
268}
269
270#[doc(hidden)]
272pub fn field_path(parent: &str, name: &str) -> String {
273 let simple = !name.is_empty()
274 && name.chars().enumerate().all(|(i, c)| {
275 c == '_' || c == '$' || c.is_ascii_alphabetic() || (i > 0 && c.is_ascii_digit())
276 });
277 if simple {
278 if parent.is_empty() {
279 name.into()
280 } else {
281 format!("{parent}.{name}")
282 }
283 } else {
284 format!("{parent}[{}]", serde_json::Value::String(name.into()))
285 }
286}