yaz-rs 0.1.0

Rust crate for Z39.50 using YAZ toolkit
//! Generic safe wrappers around YAZ ZOOM connection/resultset APIs.

use crate::error::{Error, Result};
use std::ffi::{CStr, CString};

pub struct ZoomConnection {
    raw: crate::ZOOM_connection,
}

pub struct ZoomResultSet {
    raw: crate::ZOOM_resultset,
}

impl ZoomConnection {
    pub fn connect(target: &str) -> Result<Self> {
        let c_target = CString::new(target)
            .map_err(|_| Error::ConnectionError("Invalid target string".into()))?;

        unsafe {
            let connection = crate::ZOOM_connection_create(std::ptr::null_mut());
            if connection.is_null() {
                return Err(Error::ConnectionError("Failed to create ZOOM connection".into()));
            }

            crate::ZOOM_connection_connect(connection, c_target.as_ptr(), 0);

            let err_code = crate::ZOOM_connection_errcode(connection);
            if err_code != 0 {
                let err_msg_ptr = crate::ZOOM_connection_errmsg(connection);
                let err_msg = if !err_msg_ptr.is_null() {
                    CStr::from_ptr(err_msg_ptr).to_string_lossy().to_string()
                } else {
                    format!("ZOOM connection error code: {}", err_code)
                };
                crate::ZOOM_connection_destroy(connection);
                return Err(Error::ConnectionError(err_msg));
            }

            Ok(Self { raw: connection })
        }
    }

    pub fn option_set(&mut self, key: &str, value: &str) -> Result<()> {
        let c_key = CString::new(key).map_err(|_| Error::Other("Invalid option key".into()))?;
        let c_value = CString::new(value).map_err(|_| Error::Other("Invalid option value".into()))?;

        unsafe {
            crate::ZOOM_connection_option_set(self.raw, c_key.as_ptr(), c_value.as_ptr());
        }

        Ok(())
    }

    pub fn option_get(&self, key: &str) -> Result<Option<String>> {
        let c_key = CString::new(key).map_err(|_| Error::Other("Invalid option key".into()))?;

        unsafe {
            let ptr = crate::ZOOM_connection_option_get(self.raw, c_key.as_ptr());
            if ptr.is_null() {
                return Ok(None);
            }
            Ok(Some(CStr::from_ptr(ptr).to_string_lossy().to_string()))
        }
    }

    pub fn search_pqf(&mut self, pqf: &str) -> Result<ZoomResultSet> {
        let c_query = CString::new(pqf)
            .map_err(|_| Error::SearchError("Invalid PQF query".into()))?;

        unsafe {
            let rs = crate::ZOOM_connection_search_pqf(self.raw, c_query.as_ptr());
            if rs.is_null() {
                return Err(Error::SearchError("Search failed".into()));
            }

            let err_code = crate::ZOOM_connection_errcode(self.raw);
            if err_code != 0 {
                let err_msg_ptr = crate::ZOOM_connection_errmsg(self.raw);
                let err_msg = if !err_msg_ptr.is_null() {
                    CStr::from_ptr(err_msg_ptr).to_string_lossy().to_string()
                } else {
                    format!("ZOOM search error code: {}", err_code)
                };
                crate::ZOOM_resultset_destroy(rs);
                return Err(Error::SearchError(err_msg));
            }

            Ok(ZoomResultSet { raw: rs })
        }
    }
}

impl Drop for ZoomConnection {
    fn drop(&mut self) {
        unsafe {
            if !self.raw.is_null() {
                crate::ZOOM_connection_destroy(self.raw);
            }
        }
    }
}

impl ZoomResultSet {
    pub fn size(&self) -> usize {
        unsafe { crate::ZOOM_resultset_size(self.raw) }
    }

    pub fn option_get(&self, key: &str) -> Result<Option<String>> {
        let c_key = CString::new(key).map_err(|_| Error::Other("Invalid result set option key".into()))?;

        unsafe {
            let ptr = crate::ZOOM_resultset_option_get(self.raw, c_key.as_ptr());
            if ptr.is_null() {
                return Ok(None);
            }
            Ok(Some(CStr::from_ptr(ptr).to_string_lossy().to_string()))
        }
    }

    pub fn fetch(&mut self, start: usize, count: usize) {
        if count == 0 {
            return;
        }
        unsafe {
            crate::ZOOM_resultset_records(self.raw, std::ptr::null_mut(), start, count);
        }
    }

    pub fn record_data(&self, position: usize, record_type: &str) -> Result<Option<Vec<u8>>> {
        let c_type = CString::new(record_type).map_err(|_| Error::Other("Invalid record type".into()))?;

        unsafe {
            let record = crate::ZOOM_resultset_record(self.raw, position);
            if record.is_null() {
                return Ok(None);
            }

            let mut len: i32 = 0;
            let data_ptr = crate::ZOOM_record_get(record, c_type.as_ptr(), &mut len);
            if data_ptr.is_null() {
                return Ok(None);
            }

            if len > 0 {
                let bytes = std::slice::from_raw_parts(data_ptr as *const u8, len as usize).to_vec();
                return Ok(Some(bytes));
            }

            let bytes = CStr::from_ptr(data_ptr).to_bytes().to_vec();
            Ok(Some(bytes))
        }
    }

    pub fn record_text(&self, position: usize, record_type: &str) -> Result<Option<String>> {
        match self.record_data(position, record_type)? {
            Some(bytes) => Ok(Some(String::from_utf8_lossy(&bytes).to_string())),
            None => Ok(None),
        }
    }
}

impl Drop for ZoomResultSet {
    fn drop(&mut self) {
        unsafe {
            if !self.raw.is_null() {
                crate::ZOOM_resultset_destroy(self.raw);
            }
        }
    }
}