1use crate::schema::graph::{RefResolutionError, SchemaGraph};
23use crate::schema::metadata::Role;
24use crate::schema::schema_type::{
25 BinaryRestrictions, SchemaType, TextRestrictions, UrlRestrictions, VariantCaseType,
26};
27use crate::schema::schema_value::{SchemaValue, VariantValuePayload};
28
29#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct UnstructuredError(pub String);
33
34impl std::fmt::Display for UnstructuredError {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 write!(f, "{}", self.0)
37 }
38}
39
40impl std::error::Error for UnstructuredError {}
41
42impl From<RefResolutionError> for UnstructuredError {
43 fn from(err: RefResolutionError) -> Self {
44 UnstructuredError(err.to_string())
45 }
46}
47
48pub const INLINE_CASE: u32 = 0;
49pub const URL_CASE: u32 = 1;
51
52const INLINE_CASE_NAME: &str = "inline";
53const URL_CASE_NAME: &str = "url";
54
55pub fn unstructured_text_schema_type(restrictions: TextRestrictions) -> SchemaType {
58 let mut ty = SchemaType::variant(vec![
59 VariantCaseType {
60 name: INLINE_CASE_NAME.to_string(),
61 payload: Some(SchemaType::text(restrictions)),
62 metadata: Default::default(),
63 },
64 VariantCaseType {
65 name: URL_CASE_NAME.to_string(),
66 payload: Some(SchemaType::url(UrlRestrictions::default())),
67 metadata: Default::default(),
68 },
69 ]);
70 ty.metadata_mut().role = Some(Role::UnstructuredText);
71 ty
72}
73
74pub fn unstructured_binary_schema_type(restrictions: BinaryRestrictions) -> SchemaType {
77 let mut ty = SchemaType::variant(vec![
78 VariantCaseType {
79 name: INLINE_CASE_NAME.to_string(),
80 payload: Some(SchemaType::binary(restrictions)),
81 metadata: Default::default(),
82 },
83 VariantCaseType {
84 name: URL_CASE_NAME.to_string(),
85 payload: Some(SchemaType::url(UrlRestrictions::default())),
86 metadata: Default::default(),
87 },
88 ]);
89 ty.metadata_mut().role = Some(Role::UnstructuredBinary);
90 ty
91}
92
93pub fn is_unstructured_variant(ty: &SchemaType) -> bool {
100 matches!(
101 ty,
102 SchemaType::Variant { metadata, .. }
103 if matches!(
104 metadata.role,
105 Some(Role::UnstructuredText) | Some(Role::UnstructuredBinary)
106 )
107 )
108}
109
110pub fn unstructured_text_restrictions<'a>(
117 graph: &'a SchemaGraph,
118 ty: &'a SchemaType,
119) -> Result<Option<&'a TextRestrictions>, UnstructuredError> {
120 let resolved = graph.resolve_ref(ty)?;
121 if resolved.metadata().role != Some(Role::UnstructuredText) {
122 return Ok(None);
123 }
124 let SchemaType::Variant { cases, .. } = resolved else {
128 return Err(UnstructuredError(
129 "Role::UnstructuredText must mark a variant type".into(),
130 ));
131 };
132 let [inline, url] = cases.as_slice() else {
133 return Err(UnstructuredError(
134 "Role::UnstructuredText variant must have exactly an `inline` and a `url` case".into(),
135 ));
136 };
137 if inline.name != INLINE_CASE_NAME || url.name != URL_CASE_NAME {
138 return Err(UnstructuredError(
139 "Role::UnstructuredText variant cases must be exactly `inline`, `url` in that order"
140 .into(),
141 ));
142 }
143 let inline_payload = inline.payload.as_ref().ok_or_else(|| {
144 UnstructuredError("Role::UnstructuredText `inline` case must carry a text payload".into())
145 })?;
146 let SchemaType::Text { restrictions, .. } = graph.resolve_ref(inline_payload)? else {
147 return Err(UnstructuredError(
148 "Role::UnstructuredText `inline` case payload must be a text type".into(),
149 ));
150 };
151 check_url_case(graph, url, "Role::UnstructuredText")?;
152 Ok(Some(restrictions))
153}
154
155pub fn unstructured_binary_restrictions<'a>(
158 graph: &'a SchemaGraph,
159 ty: &'a SchemaType,
160) -> Result<Option<&'a BinaryRestrictions>, UnstructuredError> {
161 let resolved = graph.resolve_ref(ty)?;
162 if resolved.metadata().role != Some(Role::UnstructuredBinary) {
163 return Ok(None);
164 }
165 let SchemaType::Variant { cases, .. } = resolved else {
169 return Err(UnstructuredError(
170 "Role::UnstructuredBinary must mark a variant type".into(),
171 ));
172 };
173 let [inline, url] = cases.as_slice() else {
174 return Err(UnstructuredError(
175 "Role::UnstructuredBinary variant must have exactly an `inline` and a `url` case"
176 .into(),
177 ));
178 };
179 if inline.name != INLINE_CASE_NAME || url.name != URL_CASE_NAME {
180 return Err(UnstructuredError(
181 "Role::UnstructuredBinary variant cases must be exactly `inline`, `url` in that order"
182 .into(),
183 ));
184 }
185 let inline_payload = inline.payload.as_ref().ok_or_else(|| {
186 UnstructuredError(
187 "Role::UnstructuredBinary `inline` case must carry a binary payload".into(),
188 )
189 })?;
190 let SchemaType::Binary { restrictions, .. } = graph.resolve_ref(inline_payload)? else {
191 return Err(UnstructuredError(
192 "Role::UnstructuredBinary `inline` case payload must be a binary type".into(),
193 ));
194 };
195 check_url_case(graph, url, "Role::UnstructuredBinary")?;
196 Ok(Some(restrictions))
197}
198
199fn check_url_case(
200 graph: &SchemaGraph,
201 url: &VariantCaseType,
202 role: &str,
203) -> Result<(), UnstructuredError> {
204 let url_payload = url
205 .payload
206 .as_ref()
207 .ok_or_else(|| UnstructuredError(format!("{role} `url` case must carry a url payload")))?;
208 if !matches!(graph.resolve_ref(url_payload)?, SchemaType::Url { .. }) {
209 return Err(UnstructuredError(format!(
210 "{role} `url` case payload must be a url type"
211 )));
212 }
213 Ok(())
214}
215
216pub enum UnstructuredKind<'a> {
219 Text(&'a TextRestrictions),
221 Binary(&'a BinaryRestrictions),
223}
224
225pub fn unstructured_kind<'a>(
231 graph: &'a SchemaGraph,
232 ty: &'a SchemaType,
233) -> Result<Option<UnstructuredKind<'a>>, UnstructuredError> {
234 if let Some(restrictions) = unstructured_text_restrictions(graph, ty)? {
235 Ok(Some(UnstructuredKind::Text(restrictions)))
236 } else if let Some(restrictions) = unstructured_binary_restrictions(graph, ty)? {
237 Ok(Some(UnstructuredKind::Binary(restrictions)))
238 } else {
239 Ok(None)
240 }
241}
242
243pub fn binary_body_restrictions<'a>(
253 graph: &'a SchemaGraph,
254 ty: &'a SchemaType,
255) -> Result<&'a BinaryRestrictions, UnstructuredError> {
256 if let Some(restrictions) = unstructured_binary_restrictions(graph, ty)? {
257 return Ok(restrictions);
258 }
259 match graph.resolve_ref(ty)? {
260 SchemaType::Binary { restrictions, .. } => Ok(restrictions),
261 _ => Err(UnstructuredError(
262 "binary body schema root is neither an unstructured-binary wrapper nor a bare binary type"
263 .into(),
264 )),
265 }
266}
267
268pub fn text_body_restrictions<'a>(
275 graph: &'a SchemaGraph,
276 ty: &'a SchemaType,
277) -> Result<&'a TextRestrictions, UnstructuredError> {
278 if let Some(restrictions) = unstructured_text_restrictions(graph, ty)? {
279 return Ok(restrictions);
280 }
281 match graph.resolve_ref(ty)? {
282 SchemaType::Text { restrictions, .. } => Ok(restrictions),
283 _ => Err(UnstructuredError(
284 "text body schema root is neither an unstructured-text wrapper nor a bare text type"
285 .into(),
286 )),
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum UnstructuredPayloadKind {
295 Text,
296 Binary,
297}
298
299pub fn unstructured_or_raw_kind(
308 graph: &SchemaGraph,
309 ty: &SchemaType,
310) -> Result<Option<UnstructuredPayloadKind>, UnstructuredError> {
311 match unstructured_kind(graph, ty)? {
312 Some(UnstructuredKind::Text(_)) => return Ok(Some(UnstructuredPayloadKind::Text)),
313 Some(UnstructuredKind::Binary(_)) => return Ok(Some(UnstructuredPayloadKind::Binary)),
314 None => {}
315 }
316 Ok(match graph.resolve_ref(ty)? {
317 SchemaType::Text { .. } => Some(UnstructuredPayloadKind::Text),
318 SchemaType::Binary { .. } => Some(UnstructuredPayloadKind::Binary),
319 _ => None,
320 })
321}
322
323pub fn wrap_unstructured_inline_for_schema(
333 graph: &SchemaGraph,
334 ty: &SchemaType,
335 raw: SchemaValue,
336) -> Result<SchemaValue, UnstructuredError> {
337 if unstructured_kind(graph, ty)?.is_some() {
338 Ok(unstructured_inline_value(raw))
339 } else {
340 Ok(raw)
341 }
342}
343
344pub enum UnstructuredOutput<'a> {
346 Inline(&'a SchemaValue),
351 Url(&'a str),
353}
354
355pub fn decode_unstructured_output<'a>(
371 graph: &SchemaGraph,
372 ty: &SchemaType,
373 value: &'a SchemaValue,
374) -> Result<Option<UnstructuredOutput<'a>>, UnstructuredError> {
375 if let Some(kind) = unstructured_kind(graph, ty)? {
381 let expected = match kind {
382 UnstructuredKind::Text(_) => UnstructuredPayloadKind::Text,
383 UnstructuredKind::Binary(_) => UnstructuredPayloadKind::Binary,
384 };
385 return Ok(Some(match value {
386 SchemaValue::Variant(_) => match decode_unstructured_value(value)? {
387 UnstructuredValueCase::Url(url) => UnstructuredOutput::Url(url),
388 UnstructuredValueCase::Inline(inline) => {
389 check_unstructured_value_kind(inline, expected)?;
390 UnstructuredOutput::Inline(inline)
391 }
392 },
393 raw => {
394 check_unstructured_value_kind(raw, expected)?;
395 UnstructuredOutput::Inline(raw)
396 }
397 }));
398 }
399
400 let expected = match graph.resolve_ref(ty)? {
402 SchemaType::Text { .. } => UnstructuredPayloadKind::Text,
403 SchemaType::Binary { .. } => UnstructuredPayloadKind::Binary,
404 _ => return Ok(None),
405 };
406 check_unstructured_value_kind(value, expected)?;
407 Ok(Some(UnstructuredOutput::Inline(value)))
408}
409
410fn check_unstructured_value_kind(
411 value: &SchemaValue,
412 expected: UnstructuredPayloadKind,
413) -> Result<(), UnstructuredError> {
414 let matches = match expected {
415 UnstructuredPayloadKind::Text => matches!(value, SchemaValue::Text(_)),
416 UnstructuredPayloadKind::Binary => matches!(value, SchemaValue::Binary(_)),
417 };
418 if matches {
419 Ok(())
420 } else {
421 let kind = match expected {
422 UnstructuredPayloadKind::Text => "text",
423 UnstructuredPayloadKind::Binary => "binary",
424 };
425 Err(UnstructuredError(format!(
426 "expected a {kind} value for an unstructured {kind} output"
427 )))
428 }
429}
430
431pub enum UnstructuredValueCase<'a> {
433 Inline(&'a SchemaValue),
435 Url(&'a str),
437}
438
439pub fn decode_unstructured_value(
446 value: &SchemaValue,
447) -> Result<UnstructuredValueCase<'_>, UnstructuredError> {
448 let SchemaValue::Variant(VariantValuePayload { case, payload }) = value else {
449 return Err(UnstructuredError(
450 "expected a variant value for an unstructured text/binary type".into(),
451 ));
452 };
453 match *case {
454 INLINE_CASE => {
455 let payload = payload.as_deref().ok_or_else(|| {
456 UnstructuredError("unstructured `inline` value must carry a payload".into())
457 })?;
458 Ok(UnstructuredValueCase::Inline(payload))
459 }
460 URL_CASE => {
461 let payload = payload.as_deref().ok_or_else(|| {
462 UnstructuredError("unstructured `url` value must carry a payload".into())
463 })?;
464 let SchemaValue::Url { url } = payload else {
465 return Err(UnstructuredError(
466 "unstructured `url` value payload must be a url".into(),
467 ));
468 };
469 Ok(UnstructuredValueCase::Url(url))
470 }
471 other => Err(UnstructuredError(format!(
472 "unknown unstructured variant case {other}"
473 ))),
474 }
475}
476
477pub fn unstructured_inline_value(inline: SchemaValue) -> SchemaValue {
480 SchemaValue::Variant(VariantValuePayload {
481 case: INLINE_CASE,
482 payload: Some(Box::new(inline)),
483 })
484}
485
486pub fn unstructured_url_value(url: String) -> SchemaValue {
488 SchemaValue::Variant(VariantValuePayload {
489 case: URL_CASE,
490 payload: Some(Box::new(SchemaValue::Url { url })),
491 })
492}