use crate::schema::graph::{RefResolutionError, SchemaGraph};
use crate::schema::metadata::Role;
use crate::schema::schema_type::{
BinaryRestrictions, SchemaType, TextRestrictions, UrlRestrictions, VariantCaseType,
};
use crate::schema::schema_value::{SchemaValue, VariantValuePayload};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnstructuredError(pub String);
impl std::fmt::Display for UnstructuredError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for UnstructuredError {}
impl From<RefResolutionError> for UnstructuredError {
fn from(err: RefResolutionError) -> Self {
UnstructuredError(err.to_string())
}
}
pub const INLINE_CASE: u32 = 0;
pub const URL_CASE: u32 = 1;
const INLINE_CASE_NAME: &str = "inline";
const URL_CASE_NAME: &str = "url";
pub fn unstructured_text_schema_type(restrictions: TextRestrictions) -> SchemaType {
let mut ty = SchemaType::variant(vec![
VariantCaseType {
name: INLINE_CASE_NAME.to_string(),
payload: Some(SchemaType::text(restrictions)),
metadata: Default::default(),
},
VariantCaseType {
name: URL_CASE_NAME.to_string(),
payload: Some(SchemaType::url(UrlRestrictions::default())),
metadata: Default::default(),
},
]);
ty.metadata_mut().role = Some(Role::UnstructuredText);
ty
}
pub fn unstructured_binary_schema_type(restrictions: BinaryRestrictions) -> SchemaType {
let mut ty = SchemaType::variant(vec![
VariantCaseType {
name: INLINE_CASE_NAME.to_string(),
payload: Some(SchemaType::binary(restrictions)),
metadata: Default::default(),
},
VariantCaseType {
name: URL_CASE_NAME.to_string(),
payload: Some(SchemaType::url(UrlRestrictions::default())),
metadata: Default::default(),
},
]);
ty.metadata_mut().role = Some(Role::UnstructuredBinary);
ty
}
pub fn is_unstructured_variant(ty: &SchemaType) -> bool {
matches!(
ty,
SchemaType::Variant { metadata, .. }
if matches!(
metadata.role,
Some(Role::UnstructuredText) | Some(Role::UnstructuredBinary)
)
)
}
pub fn unstructured_text_restrictions<'a>(
graph: &'a SchemaGraph,
ty: &'a SchemaType,
) -> Result<Option<&'a TextRestrictions>, UnstructuredError> {
let resolved = graph.resolve_ref(ty)?;
if resolved.metadata().role != Some(Role::UnstructuredText) {
return Ok(None);
}
let SchemaType::Variant { cases, .. } = resolved else {
return Err(UnstructuredError(
"Role::UnstructuredText must mark a variant type".into(),
));
};
let [inline, url] = cases.as_slice() else {
return Err(UnstructuredError(
"Role::UnstructuredText variant must have exactly an `inline` and a `url` case".into(),
));
};
if inline.name != INLINE_CASE_NAME || url.name != URL_CASE_NAME {
return Err(UnstructuredError(
"Role::UnstructuredText variant cases must be exactly `inline`, `url` in that order"
.into(),
));
}
let inline_payload = inline.payload.as_ref().ok_or_else(|| {
UnstructuredError("Role::UnstructuredText `inline` case must carry a text payload".into())
})?;
let SchemaType::Text { restrictions, .. } = graph.resolve_ref(inline_payload)? else {
return Err(UnstructuredError(
"Role::UnstructuredText `inline` case payload must be a text type".into(),
));
};
check_url_case(graph, url, "Role::UnstructuredText")?;
Ok(Some(restrictions))
}
pub fn unstructured_binary_restrictions<'a>(
graph: &'a SchemaGraph,
ty: &'a SchemaType,
) -> Result<Option<&'a BinaryRestrictions>, UnstructuredError> {
let resolved = graph.resolve_ref(ty)?;
if resolved.metadata().role != Some(Role::UnstructuredBinary) {
return Ok(None);
}
let SchemaType::Variant { cases, .. } = resolved else {
return Err(UnstructuredError(
"Role::UnstructuredBinary must mark a variant type".into(),
));
};
let [inline, url] = cases.as_slice() else {
return Err(UnstructuredError(
"Role::UnstructuredBinary variant must have exactly an `inline` and a `url` case"
.into(),
));
};
if inline.name != INLINE_CASE_NAME || url.name != URL_CASE_NAME {
return Err(UnstructuredError(
"Role::UnstructuredBinary variant cases must be exactly `inline`, `url` in that order"
.into(),
));
}
let inline_payload = inline.payload.as_ref().ok_or_else(|| {
UnstructuredError(
"Role::UnstructuredBinary `inline` case must carry a binary payload".into(),
)
})?;
let SchemaType::Binary { restrictions, .. } = graph.resolve_ref(inline_payload)? else {
return Err(UnstructuredError(
"Role::UnstructuredBinary `inline` case payload must be a binary type".into(),
));
};
check_url_case(graph, url, "Role::UnstructuredBinary")?;
Ok(Some(restrictions))
}
fn check_url_case(
graph: &SchemaGraph,
url: &VariantCaseType,
role: &str,
) -> Result<(), UnstructuredError> {
let url_payload = url
.payload
.as_ref()
.ok_or_else(|| UnstructuredError(format!("{role} `url` case must carry a url payload")))?;
if !matches!(graph.resolve_ref(url_payload)?, SchemaType::Url { .. }) {
return Err(UnstructuredError(format!(
"{role} `url` case payload must be a url type"
)));
}
Ok(())
}
pub enum UnstructuredKind<'a> {
Text(&'a TextRestrictions),
Binary(&'a BinaryRestrictions),
}
pub fn unstructured_kind<'a>(
graph: &'a SchemaGraph,
ty: &'a SchemaType,
) -> Result<Option<UnstructuredKind<'a>>, UnstructuredError> {
if let Some(restrictions) = unstructured_text_restrictions(graph, ty)? {
Ok(Some(UnstructuredKind::Text(restrictions)))
} else if let Some(restrictions) = unstructured_binary_restrictions(graph, ty)? {
Ok(Some(UnstructuredKind::Binary(restrictions)))
} else {
Ok(None)
}
}
pub fn binary_body_restrictions<'a>(
graph: &'a SchemaGraph,
ty: &'a SchemaType,
) -> Result<&'a BinaryRestrictions, UnstructuredError> {
if let Some(restrictions) = unstructured_binary_restrictions(graph, ty)? {
return Ok(restrictions);
}
match graph.resolve_ref(ty)? {
SchemaType::Binary { restrictions, .. } => Ok(restrictions),
_ => Err(UnstructuredError(
"binary body schema root is neither an unstructured-binary wrapper nor a bare binary type"
.into(),
)),
}
}
pub fn text_body_restrictions<'a>(
graph: &'a SchemaGraph,
ty: &'a SchemaType,
) -> Result<&'a TextRestrictions, UnstructuredError> {
if let Some(restrictions) = unstructured_text_restrictions(graph, ty)? {
return Ok(restrictions);
}
match graph.resolve_ref(ty)? {
SchemaType::Text { restrictions, .. } => Ok(restrictions),
_ => Err(UnstructuredError(
"text body schema root is neither an unstructured-text wrapper nor a bare text type"
.into(),
)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnstructuredPayloadKind {
Text,
Binary,
}
pub fn unstructured_or_raw_kind(
graph: &SchemaGraph,
ty: &SchemaType,
) -> Result<Option<UnstructuredPayloadKind>, UnstructuredError> {
match unstructured_kind(graph, ty)? {
Some(UnstructuredKind::Text(_)) => return Ok(Some(UnstructuredPayloadKind::Text)),
Some(UnstructuredKind::Binary(_)) => return Ok(Some(UnstructuredPayloadKind::Binary)),
None => {}
}
Ok(match graph.resolve_ref(ty)? {
SchemaType::Text { .. } => Some(UnstructuredPayloadKind::Text),
SchemaType::Binary { .. } => Some(UnstructuredPayloadKind::Binary),
_ => None,
})
}
pub fn wrap_unstructured_inline_for_schema(
graph: &SchemaGraph,
ty: &SchemaType,
raw: SchemaValue,
) -> Result<SchemaValue, UnstructuredError> {
if unstructured_kind(graph, ty)?.is_some() {
Ok(unstructured_inline_value(raw))
} else {
Ok(raw)
}
}
pub enum UnstructuredOutput<'a> {
Inline(&'a SchemaValue),
Url(&'a str),
}
pub fn decode_unstructured_output<'a>(
graph: &SchemaGraph,
ty: &SchemaType,
value: &'a SchemaValue,
) -> Result<Option<UnstructuredOutput<'a>>, UnstructuredError> {
if let Some(kind) = unstructured_kind(graph, ty)? {
let expected = match kind {
UnstructuredKind::Text(_) => UnstructuredPayloadKind::Text,
UnstructuredKind::Binary(_) => UnstructuredPayloadKind::Binary,
};
return Ok(Some(match value {
SchemaValue::Variant(_) => match decode_unstructured_value(value)? {
UnstructuredValueCase::Url(url) => UnstructuredOutput::Url(url),
UnstructuredValueCase::Inline(inline) => {
check_unstructured_value_kind(inline, expected)?;
UnstructuredOutput::Inline(inline)
}
},
raw => {
check_unstructured_value_kind(raw, expected)?;
UnstructuredOutput::Inline(raw)
}
}));
}
let expected = match graph.resolve_ref(ty)? {
SchemaType::Text { .. } => UnstructuredPayloadKind::Text,
SchemaType::Binary { .. } => UnstructuredPayloadKind::Binary,
_ => return Ok(None),
};
check_unstructured_value_kind(value, expected)?;
Ok(Some(UnstructuredOutput::Inline(value)))
}
fn check_unstructured_value_kind(
value: &SchemaValue,
expected: UnstructuredPayloadKind,
) -> Result<(), UnstructuredError> {
let matches = match expected {
UnstructuredPayloadKind::Text => matches!(value, SchemaValue::Text(_)),
UnstructuredPayloadKind::Binary => matches!(value, SchemaValue::Binary(_)),
};
if matches {
Ok(())
} else {
let kind = match expected {
UnstructuredPayloadKind::Text => "text",
UnstructuredPayloadKind::Binary => "binary",
};
Err(UnstructuredError(format!(
"expected a {kind} value for an unstructured {kind} output"
)))
}
}
pub enum UnstructuredValueCase<'a> {
Inline(&'a SchemaValue),
Url(&'a str),
}
pub fn decode_unstructured_value(
value: &SchemaValue,
) -> Result<UnstructuredValueCase<'_>, UnstructuredError> {
let SchemaValue::Variant(VariantValuePayload { case, payload }) = value else {
return Err(UnstructuredError(
"expected a variant value for an unstructured text/binary type".into(),
));
};
match *case {
INLINE_CASE => {
let payload = payload.as_deref().ok_or_else(|| {
UnstructuredError("unstructured `inline` value must carry a payload".into())
})?;
Ok(UnstructuredValueCase::Inline(payload))
}
URL_CASE => {
let payload = payload.as_deref().ok_or_else(|| {
UnstructuredError("unstructured `url` value must carry a payload".into())
})?;
let SchemaValue::Url { url } = payload else {
return Err(UnstructuredError(
"unstructured `url` value payload must be a url".into(),
));
};
Ok(UnstructuredValueCase::Url(url))
}
other => Err(UnstructuredError(format!(
"unknown unstructured variant case {other}"
))),
}
}
pub fn unstructured_inline_value(inline: SchemaValue) -> SchemaValue {
SchemaValue::Variant(VariantValuePayload {
case: INLINE_CASE,
payload: Some(Box::new(inline)),
})
}
pub fn unstructured_url_value(url: String) -> SchemaValue {
SchemaValue::Variant(VariantValuePayload {
case: URL_CASE,
payload: Some(Box::new(SchemaValue::Url { url })),
})
}