at main 368 lines 14 kB view raw
1use dioxus::prelude::*; 2use jacquard::bytes::Bytes; 3use jacquard::common::{Data, IntoStatic}; 4use jacquard::smol_str::{SmolStr, format_smolstr}; 5use jacquard::types::LexiconStringType; 6use jacquard::types::string::AtprotoStr; 7use jacquard_lexicon::validation::{ 8 ConstraintError, StructuralError, ValidationError, ValidationResult, 9}; 10 11// ============================================================================ 12// Validation Helper Functions 13// ============================================================================ 14 15/// Parse UI path into segments (fields and indices only) 16fn parse_ui_path(ui_path: &str) -> Vec<UiPathSegment> { 17 if ui_path.is_empty() { 18 return vec![]; 19 } 20 21 let mut segments = Vec::new(); 22 let mut current = String::new(); 23 24 for ch in ui_path.chars() { 25 match ch { 26 '.' => { 27 if !current.is_empty() { 28 segments.push(UiPathSegment::Field(current.clone())); 29 current.clear(); 30 } 31 } 32 '[' => { 33 if !current.is_empty() { 34 segments.push(UiPathSegment::Field(current.clone())); 35 current.clear(); 36 } 37 } 38 ']' => { 39 if !current.is_empty() { 40 if let Ok(idx) = current.parse::<usize>() { 41 segments.push(UiPathSegment::Index(idx)); 42 } 43 current.clear(); 44 } 45 } 46 c => current.push(c), 47 } 48 } 49 50 if !current.is_empty() { 51 segments.push(UiPathSegment::Field(current)); 52 } 53 54 segments 55} 56 57#[derive(Debug, PartialEq)] 58enum UiPathSegment { 59 Field(String), 60 Index(usize), 61} 62 63/// Get all validation errors at exactly this path (not children) 64pub fn get_errors_at_exact_path( 65 validation_result: &Option<ValidationResult>, 66 ui_path: &str, 67) -> Vec<String> { 68 use jacquard_lexicon::validation::PathSegment; 69 70 if let Some(result) = validation_result { 71 let ui_segments = parse_ui_path(ui_path); 72 73 result 74 .all_errors() 75 .filter_map(|err| { 76 let validation_path = match &err { 77 ValidationError::Structural(s) => match s { 78 StructuralError::TypeMismatch { path, .. } => Some(path), 79 StructuralError::MissingRequiredField { path, .. } => Some(path), 80 StructuralError::MissingUnionDiscriminator { path } => Some(path), 81 StructuralError::UnionNoMatch { path, .. } => Some(path), 82 StructuralError::UnresolvedRef { path, .. } => Some(path), 83 StructuralError::RefCycle { path, .. } => Some(path), 84 StructuralError::MaxDepthExceeded { path, .. } => Some(path), 85 86 _ => None, 87 }, 88 ValidationError::Constraint(c) => match c { 89 ConstraintError::MaxLength { path, .. } => Some(path), 90 ConstraintError::MaxGraphemes { path, .. } => Some(path), 91 ConstraintError::MinLength { path, .. } => Some(path), 92 ConstraintError::MinGraphemes { path, .. } => Some(path), 93 ConstraintError::Maximum { path, .. } => Some(path), 94 ConstraintError::Minimum { path, .. } => Some(path), 95 96 _ => None, 97 }, 98 99 _ => None, 100 }; 101 102 if let Some(path) = validation_path { 103 // Convert validation path to UI segments 104 let validation_ui_segments: Vec<_> = path 105 .segments() 106 .iter() 107 .filter_map(|seg| match seg { 108 PathSegment::Field(name) => { 109 Some(UiPathSegment::Field(name.to_string())) 110 } 111 PathSegment::Index(idx) => Some(UiPathSegment::Index(*idx)), 112 PathSegment::UnionVariant(_) => None, 113 }) 114 .collect(); 115 116 // Exact match only 117 if validation_ui_segments == ui_segments { 118 return Some(err.to_string()); 119 } 120 } 121 None 122 }) 123 .collect() 124 } else { 125 Vec::new() 126 } 127} 128 129// ============================================================================ 130// Pretty Editor: Helper Functions 131// ============================================================================ 132 133/// Infer Data type from text input 134pub fn infer_data_from_text(text: &str) -> Result<Data<'static>, String> { 135 let trimmed = text.trim(); 136 137 if trimmed == "true" || trimmed == "false" { 138 Ok(Data::Boolean(trimmed == "true")) 139 } else if trimmed == "{}" { 140 use jacquard::types::value::Object; 141 use std::collections::BTreeMap; 142 Ok(Data::Object(Object(BTreeMap::new()))) 143 } else if trimmed == "[]" { 144 use jacquard::types::value::Array; 145 Ok(Data::Array(Array(Vec::new()))) 146 } else if trimmed == "null" { 147 Ok(Data::Null) 148 } else if let Ok(num) = trimmed.parse::<i64>() { 149 Ok(Data::Integer(num)) 150 } else { 151 // Smart string parsing 152 use jacquard::types::value::parsing; 153 Ok(Data::String(parsing::parse_string(trimmed).into_static())) 154 } 155} 156 157/// Parse text as specific AtprotoStr type, preserving type information 158pub fn try_parse_as_type( 159 text: &str, 160 string_type: LexiconStringType, 161) -> Result<AtprotoStr<'static>, String> { 162 use jacquard::types::string::*; 163 use std::str::FromStr; 164 165 match string_type { 166 LexiconStringType::Datetime => Datetime::from_str(text) 167 .map(AtprotoStr::Datetime) 168 .map_err(|e| format_smolstr!("Invalid datetime: {}", e).to_string()), 169 LexiconStringType::Did => Did::new(text) 170 .map(|v| AtprotoStr::Did(v.into_static())) 171 .map_err(|e| format_smolstr!("Invalid DID: {}", e).to_string()), 172 LexiconStringType::Handle => Handle::new(text) 173 .map(|v| AtprotoStr::Handle(v.into_static())) 174 .map_err(|e| format_smolstr!("Invalid handle: {}", e).to_string()), 175 LexiconStringType::AtUri => AtUri::new(text) 176 .map(|v| AtprotoStr::AtUri(v.into_static())) 177 .map_err(|e| format_smolstr!("Invalid AT-URI: {}", e).to_string()), 178 LexiconStringType::AtIdentifier => AtIdentifier::new(text) 179 .map(|v| AtprotoStr::AtIdentifier(v.into_static())) 180 .map_err(|e| format_smolstr!("Invalid identifier: {}", e).to_string()), 181 LexiconStringType::Nsid => Nsid::new(text) 182 .map(|v| AtprotoStr::Nsid(v.into_static())) 183 .map_err(|e| format_smolstr!("Invalid NSID: {}", e).to_string()), 184 LexiconStringType::Tid => Tid::new(text) 185 .map(|v| AtprotoStr::Tid(v.into_static())) 186 .map_err(|e| format_smolstr!("Invalid TID: {}", e).to_string()), 187 LexiconStringType::RecordKey => Rkey::new(text) 188 .map(|rk| AtprotoStr::RecordKey(RecordKey::from(rk))) 189 .map_err(|e| format_smolstr!("Invalid record key: {}", e).to_string()), 190 LexiconStringType::Cid => Cid::new(text.as_bytes()) 191 .map(|v| AtprotoStr::Cid(v.into_static())) 192 .map_err(|_| SmolStr::new_inline("Invalid CID").to_string()), 193 LexiconStringType::Language => Language::new(text) 194 .map(AtprotoStr::Language) 195 .map_err(|e| format_smolstr!("Invalid language: {}", e).to_string()), 196 LexiconStringType::Uri(_) => Uri::new(text) 197 .map(|u| AtprotoStr::Uri(u.into_static())) 198 .map_err(|e| format_smolstr!("Invalid URI: {}", e).to_string()), 199 LexiconStringType::String => { 200 // Plain strings: use smart inference 201 use jacquard::types::value::parsing; 202 Ok(parsing::parse_string(text).into_static()) 203 } 204 } 205} 206 207/// Create default value for new array item by cloning structure of existing items 208pub fn create_array_item_default(arr: &jacquard::types::value::Array) -> Data<'static> { 209 if let Some(existing) = arr.0.first() { 210 clone_structure(existing) 211 } else { 212 // Empty array, default to null (user can change type) 213 Data::Null 214 } 215} 216 217/// Clone structure of Data, setting sensible defaults for leaf values 218pub fn clone_structure(data: &Data) -> Data<'static> { 219 use jacquard::types::string::*; 220 use jacquard::types::value::{Array, Object}; 221 use jacquard::types::{LexiconStringType, blob::*}; 222 use std::collections::BTreeMap; 223 224 match data { 225 Data::Object(obj) => { 226 let mut new_obj = BTreeMap::new(); 227 for (key, value) in obj.0.iter() { 228 new_obj.insert(key.clone(), clone_structure(value)); 229 } 230 Data::Object(Object(new_obj)) 231 } 232 Data::Array(_) => Data::Array(Array(Vec::new())), 233 Data::String(s) => match s.string_type() { 234 LexiconStringType::Datetime => { 235 // Sensible default: now 236 Data::String(AtprotoStr::Datetime(Datetime::now())) 237 } 238 LexiconStringType::Tid => Data::String(AtprotoStr::Tid(Tid::now_0())), 239 _ => { 240 // Empty string, type inference will handle it 241 Data::String(AtprotoStr::String("".into())) 242 } 243 }, 244 Data::Integer(_) => Data::Integer(0), 245 Data::Boolean(_) => Data::Boolean(false), 246 Data::Blob(blob) => { 247 // Placeholder blob 248 Data::Blob( 249 Blob { 250 r#ref: CidLink::str( 251 "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 252 ), 253 mime_type: blob.mime_type.clone(), 254 size: 0, 255 } 256 .into_static(), 257 ) 258 } 259 Data::CidLink(_) => Data::CidLink(Cid::str( 260 "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 261 )), 262 Data::Bytes(_) => Data::Bytes(Bytes::new()), 263 Data::Null => Data::Null, 264 } 265} 266 267/// Get expected string format from schema by navigating path 268pub fn get_expected_string_format( 269 root_data: &Data<'_>, 270 current_path: &str, 271) -> Option<jacquard_lexicon::lexicon::LexStringFormat> { 272 use jacquard_lexicon::lexicon::*; 273 274 // Get root type discriminator 275 let root_nsid = root_data.type_discriminator()?; 276 277 // Look up schema in global registry 278 let validator = jacquard_lexicon::validation::SchemaValidator::global(); 279 let registry = validator.registry(); 280 let schema = registry.get(root_nsid)?; 281 282 // Navigate to the property at this path 283 let segments: Vec<&str> = if current_path.is_empty() { 284 vec![] 285 } else { 286 current_path.split('.').collect() 287 }; 288 289 // Start with the record's main object definition 290 let main_obj = match schema.defs.get("main")? { 291 LexUserType::Record(rec) => match &rec.record { 292 LexRecordRecord::Object(obj) => obj, 293 }, 294 _ => return None, 295 }; 296 297 // Track current position in schema 298 enum SchemaType<'a> { 299 ObjectProp(&'a LexObjectProperty<'a>), 300 ArrayItem(&'a LexArrayItem<'a>), 301 } 302 303 let mut current_type: Option<SchemaType> = None; 304 let mut current_obj = Some(main_obj); 305 306 for segment in segments { 307 // Handle array indices - strip them to get field name 308 let field_name = segment.trim_end_matches(|c: char| c.is_numeric() || c == '[' || c == ']'); 309 310 if field_name.is_empty() { 311 continue; // Pure array index like [0], skip 312 } 313 314 if let Some(obj) = current_obj.take() { 315 if let Some(prop) = obj.properties.get(field_name) { 316 current_type = Some(SchemaType::ObjectProp(prop)); 317 } 318 } 319 320 // Process current type 321 match current_type { 322 Some(SchemaType::ObjectProp(LexObjectProperty::Array(arr))) => { 323 // Array - unwrap to item type 324 current_type = Some(SchemaType::ArrayItem(&arr.items)); 325 } 326 Some(SchemaType::ObjectProp(LexObjectProperty::Object(obj))) => { 327 // Nested object - descend into it 328 current_obj = Some(obj); 329 current_type = None; 330 } 331 Some(SchemaType::ArrayItem(LexArrayItem::Object(obj))) => { 332 // Array of objects - descend into object 333 current_obj = Some(obj); 334 current_type = None; 335 } 336 _ => {} 337 } 338 } 339 340 // Check if final type is a string with format 341 match current_type? { 342 SchemaType::ObjectProp(LexObjectProperty::String(lex_string)) => lex_string.format, 343 SchemaType::ArrayItem(LexArrayItem::String(lex_string)) => lex_string.format, 344 _ => None, 345 } 346} 347 348pub fn get_hex_rep(byte_array: &mut [u8]) -> String { 349 let build_string_vec: Vec<String> = byte_array 350 .chunks(2) 351 .enumerate() 352 .map(|(i, c)| { 353 let sep = if i % 16 == 0 && i > 0 { 354 "\n" 355 } else if i == 0 { 356 "" 357 } else { 358 " " 359 }; 360 if c.len() == 2 { 361 format!("{}{:02x}{:02x}", sep, c[0], c[1]) 362 } else { 363 format!("{}{:02x}", sep, c[0]) 364 } 365 }) 366 .collect(); 367 build_string_vec.join("") 368}