atproto blogging
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}