orpc 0.1.0

Type-safe RPC framework for Rust, inspired by oRPC
Documentation
use std::marker::PhantomData;
use std::sync::Arc;

use orpc_procedure::{DynInput, ErasedSchema, ProcedureError};
use serde::Serialize;
use serde::de::DeserializeOwned;

use crate::error::ORPCError;

/// Unified Schema abstraction, counterpart to oRPC's Standard Schema.
///
/// Provides validation and JSON Schema generation for procedure input/output types.
pub trait Schema: Send + Sync + 'static {
    type Input: DeserializeOwned + Send;
    type Output: Serialize + Send;

    /// Validate and transform input.
    fn validate(&self, input: Self::Input) -> Result<Self::Output, ORPCError>;

    /// Generate JSON Schema representation (for OpenAPI generation).
    fn json_schema(&self) -> serde_json::Value;

    /// Whether this schema is a passthrough (Input == Output, validate is identity).
    /// When true, the framework skips the validate() call and deserializes directly
    /// into the Output type, avoiding a serialize/deserialize roundtrip.
    fn is_passthrough(&self) -> bool {
        false
    }

    /// Convert to type-erased schema. Override in extension crates (e.g., orpc-specta)
    /// to preserve additional type information through erasure.
    fn into_erased(self) -> Box<dyn ErasedSchema>
    where
        Self: Sized,
    {
        Box::new(SchemaAdapter(self))
    }
}

/// No-validation pass-through schema. Counterpart to oRPC's `type<T>()`.
///
/// Input passes through unchanged — no validation, no transformation.
pub struct Identity<T>(PhantomData<T>);

impl<T> Identity<T> {
    pub fn new() -> Self {
        Identity(PhantomData)
    }
}

impl<T> Default for Identity<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl<T: DeserializeOwned + Serialize + Send + Sync + 'static> Schema for Identity<T> {
    type Input = T;
    type Output = T;

    fn validate(&self, input: T) -> Result<T, ORPCError> {
        Ok(input)
    }

    fn json_schema(&self) -> serde_json::Value {
        serde_json::json!({})
    }

    fn is_passthrough(&self) -> bool {
        true
    }
}

/// Adapter: wraps a typed `Schema` into a type-erased `ErasedSchema` for storage
/// in `ErasedProcedure`.
pub(crate) struct SchemaAdapter<S: Schema>(pub S);

impl<S: Schema + 'static> ErasedSchema for SchemaAdapter<S> {
    fn json_schema(&self) -> serde_json::Value {
        self.0.json_schema()
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

/// Type-erased input validator. Calls `Schema::validate` at runtime.
/// `None` for passthrough schemas (Identity), avoiding serialize/deserialize roundtrip.
pub(crate) type InputValidator =
    Arc<dyn Fn(DynInput) -> Result<DynInput, ProcedureError> + Send + Sync>;

/// Create a type-erased input validator for non-passthrough schemas.
///
/// Currently returns `None` (validation deferred until a real non-passthrough Schema exists).
/// Called only when `is_passthrough() == false`.
pub(crate) fn make_input_validator() -> Option<InputValidator> {
    // TODO: implement actual validation when a non-passthrough Schema impl exists.
    // For now, all schemas (Identity, SpectaSchemaWrapper) are passthrough.
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn identity_passthrough() {
        let schema = Identity::<String>::new();
        let result = schema.validate("hello".to_string());
        assert_eq!(result.unwrap(), "hello");
    }

    #[test]
    fn identity_json_schema() {
        let schema = Identity::<u32>::new();
        assert_eq!(schema.json_schema(), serde_json::json!({}));
    }

    #[test]
    fn schema_adapter_erased() {
        let schema = Identity::<u32>::new();
        let erased: Box<dyn ErasedSchema> = Box::new(SchemaAdapter(schema));
        assert_eq!(erased.json_schema(), serde_json::json!({}));
    }
}