providers-kit 0.0.2

Portable enum-dispatch Kit over multiple model provider APIs.
Documentation
use serde_json::{Map, Number, Value};

use crate::KitError;
use crate::error::{
    ErrorKind, ErrorPhase, JsonValueKind, Operation, ResponseDetails, ResponseViolation,
};
use crate::generation::{Citation, FileReference};

use super::super::wire::AnthropicCitation;

impl AnthropicCitation {
    pub(crate) fn decode(raw: &Value) -> Result<Self, KitError> {
        let object = raw
            .as_object()
            .ok_or_else(|| Self::field_error("citation", JsonValueKind::Object, Some(raw)))?;
        match Self::required_str(object, "type", "citation.type")? {
            "web_search_result_location" => {
                Self::required_str(object, "encrypted_index", "citation.encrypted_index")?;
                Ok(Self::WebSearch {
                    url: Self::required_str(object, "url", "citation.url")?.to_owned(),
                    title: Self::optional_string(object, "title", "citation.title")?,
                    cited_text: Self::required_str(object, "cited_text", "citation.cited_text")?
                        .to_owned(),
                })
            }
            "char_location" => Ok(Self::Character {
                document_index: Self::required_integer(
                    object,
                    "document_index",
                    "citation.document_index",
                )?,
                start_char_index: Self::required_integer(
                    object,
                    "start_char_index",
                    "citation.start_char_index",
                )?,
                end_char_index: Self::required_integer(
                    object,
                    "end_char_index",
                    "citation.end_char_index",
                )?,
                file_id: Self::optional_string(object, "file_id", "citation.file_id")?,
                document_title: Self::optional_string(
                    object,
                    "document_title",
                    "citation.document_title",
                )?,
                cited_text: Self::required_str(object, "cited_text", "citation.cited_text")?
                    .to_owned(),
            }),
            "page_location" => Ok(Self::Page {
                document_index: Self::required_integer(
                    object,
                    "document_index",
                    "citation.document_index",
                )?,
                start_page_number: Self::required_integer(
                    object,
                    "start_page_number",
                    "citation.start_page_number",
                )?,
                end_page_number: Self::required_integer(
                    object,
                    "end_page_number",
                    "citation.end_page_number",
                )?,
                file_id: Self::optional_string(object, "file_id", "citation.file_id")?,
                document_title: Self::optional_string(
                    object,
                    "document_title",
                    "citation.document_title",
                )?,
                cited_text: Self::required_str(object, "cited_text", "citation.cited_text")?
                    .to_owned(),
            }),
            "content_block_location" => Ok(Self::ContentBlock {
                document_index: Self::required_integer(
                    object,
                    "document_index",
                    "citation.document_index",
                )?,
                start_block_index: Self::required_integer(
                    object,
                    "start_block_index",
                    "citation.start_block_index",
                )?,
                end_block_index: Self::required_integer(
                    object,
                    "end_block_index",
                    "citation.end_block_index",
                )?,
                file_id: Self::optional_string(object, "file_id", "citation.file_id")?,
                document_title: Self::optional_string(
                    object,
                    "document_title",
                    "citation.document_title",
                )?,
                cited_text: Self::required_str(object, "cited_text", "citation.cited_text")?
                    .to_owned(),
            }),
            "search_result_location" => {
                Self::required_str(object, "cited_text", "citation.cited_text")?;
                Self::required_str(object, "source", "citation.source")?;
                Self::optional_string(object, "title", "citation.title")?;
                Ok(Self::SearchResult {
                    search_result_index: Self::required_integer(
                        object,
                        "search_result_index",
                        "citation.search_result_index",
                    )?,
                    start_block_index: Self::required_integer(
                        object,
                        "start_block_index",
                        "citation.start_block_index",
                    )?,
                    end_block_index: Self::required_integer(
                        object,
                        "end_block_index",
                        "citation.end_block_index",
                    )?,
                })
            }
            actual => Err(
                KitError::invalid_response(ResponseViolation::UnknownVariant {
                    field: "citation.type",
                })
                .with_response_details(ResponseDetails::Discriminator {
                    actual: actual.to_owned(),
                }),
            ),
        }
    }

    pub(crate) fn into_portable(self) -> Result<Citation, KitError> {
        match self {
            Self::WebSearch {
                url,
                title,
                cited_text,
            } => Ok(Citation::Url {
                url,
                title,
                snippet: Some(cited_text),
                start_index: None,
                end_index: None,
            }),
            Self::Character {
                document_index,
                start_char_index,
                end_char_index,
                file_id,
                document_title,
                cited_text,
            } => {
                Self::source_index(&document_index, "citation.document_index")?;
                let start = Self::source_index(&start_char_index, "citation.start_char_index")?;
                let end = Self::source_index(&end_char_index, "citation.end_char_index")?;
                if end < start {
                    return Err(Self::invalid_index("citation.end_char_index"));
                }
                Self::file_citation(file_id, document_title, cited_text)
            }
            Self::Page {
                document_index,
                start_page_number,
                end_page_number,
                file_id,
                document_title,
                cited_text,
            } => {
                Self::source_index(&document_index, "citation.document_index")?;
                let start = Self::source_index(&start_page_number, "citation.start_page_number")?;
                let end = Self::source_index(&end_page_number, "citation.end_page_number")?;
                if start == 0 {
                    return Err(Self::invalid_index("citation.start_page_number"));
                }
                if end == 0 || end < start {
                    return Err(Self::invalid_index("citation.end_page_number"));
                }
                Self::file_citation(file_id, document_title, cited_text)
            }
            Self::ContentBlock {
                document_index,
                start_block_index,
                end_block_index,
                file_id,
                document_title,
                cited_text,
            } => {
                Self::source_index(&document_index, "citation.document_index")?;
                let start = Self::source_index(&start_block_index, "citation.start_block_index")?;
                let end = Self::source_index(&end_block_index, "citation.end_block_index")?;
                if end <= start {
                    return Err(Self::invalid_index("citation.end_block_index"));
                }
                Self::file_citation(file_id, document_title, cited_text)
            }
            Self::SearchResult {
                search_result_index,
                start_block_index,
                end_block_index,
            } => {
                Self::source_index(&search_result_index, "citation.search_result_index")?;
                let start = Self::source_index(&start_block_index, "citation.start_block_index")?;
                let end = Self::source_index(&end_block_index, "citation.end_block_index")?;
                if end <= start {
                    return Err(Self::invalid_index("citation.end_block_index"));
                }
                Err(KitError::new(
                    ErrorKind::Unsupported,
                    Operation::Unspecified,
                    ErrorPhase::Normalize,
                )
                .with_message("Anthropic indexed search-result citations"))
            }
        }
    }

    fn required_str<'a>(
        object: &'a Map<String, Value>,
        key: &'static str,
        field: &'static str,
    ) -> Result<&'a str, KitError> {
        match object.get(key) {
            Some(Value::String(value)) => Ok(value),
            actual => Err(Self::field_error(field, JsonValueKind::String, actual)),
        }
    }

    fn optional_string(
        object: &Map<String, Value>,
        key: &'static str,
        field: &'static str,
    ) -> Result<Option<String>, KitError> {
        match object.get(key) {
            Some(Value::String(value)) => Ok(Some(value.clone())),
            None | Some(Value::Null) => Ok(None),
            actual => Err(Self::field_error(field, JsonValueKind::String, actual)),
        }
    }

    fn required_integer(
        object: &Map<String, Value>,
        key: &'static str,
        field: &'static str,
    ) -> Result<Number, KitError> {
        match object.get(key) {
            Some(Value::Number(value)) if value.is_i64() || value.is_u64() => Ok(value.clone()),
            actual => Err(Self::field_error(field, JsonValueKind::Number, actual)),
        }
    }

    fn field_error(
        field: &'static str,
        expected: JsonValueKind,
        actual: Option<&Value>,
    ) -> KitError {
        let violation = if actual.is_none() {
            ResponseViolation::MissingField { field }
        } else {
            ResponseViolation::InvalidField { field }
        };
        KitError::invalid_response(violation).with_response_details(ResponseDetails::FieldType {
            expected,
            actual: JsonValueKind::of(actual),
        })
    }

    fn source_index(value: &Number, field: &'static str) -> Result<u64, KitError> {
        value.as_u64().ok_or_else(|| Self::invalid_index(field))
    }

    fn invalid_index(field: &'static str) -> KitError {
        KitError::invalid_response(ResponseViolation::InvalidField { field }).with_response_details(
            ResponseDetails::FieldType {
                expected: JsonValueKind::Number,
                actual: JsonValueKind::Number,
            },
        )
    }

    fn file_citation(
        file_id: Option<String>,
        title: Option<String>,
        cited_text: String,
    ) -> Result<Citation, KitError> {
        let id = file_id.ok_or_else(|| {
            KitError::new(
                ErrorKind::Unsupported,
                Operation::Unspecified,
                ErrorPhase::Normalize,
            )
            .with_message("Anthropic indexed document citations without a native file ID")
        })?;
        Ok(Citation::File {
            reference: FileReference::Provider {
                id,
                media_type: None,
            },
            title,
            snippet: Some(cited_text),
            start_index: None,
            end_index: None,
        })
    }
}