mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
234 lines
7.4 KiB
Rust
234 lines
7.4 KiB
Rust
//! Shared core-level schemas and contracts used across adapters (RPC, CLI, etc.).
|
|
//!
|
|
//! This module defines the foundational types for OpenHuman's controller system,
|
|
//! which provides a transport-agnostic way to define and invoke domain logic.
|
|
//! It also exports submodules for CLI handling, event bus, and RPC server.
|
|
|
|
use serde::Serialize;
|
|
|
|
pub mod agent_cli;
|
|
pub mod all;
|
|
pub mod auth;
|
|
pub mod autocomplete_cli_adapter;
|
|
pub mod cli;
|
|
pub mod dispatch;
|
|
pub mod event_bind_tokens;
|
|
pub mod event_bus;
|
|
pub mod jsonrpc;
|
|
pub mod legacy_aliases;
|
|
pub mod logging;
|
|
pub mod memory_cli;
|
|
pub mod observability;
|
|
pub mod rpc_log;
|
|
pub mod shutdown;
|
|
pub mod socketio;
|
|
pub mod types;
|
|
|
|
/// Canonical function contract for domain controllers.
|
|
///
|
|
/// This shape is transport-agnostic and can be consumed by RPC and CLI layers
|
|
/// in different ways. It defines the identity, purpose, and I/O signature
|
|
/// of a specific piece of domain logic.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
pub struct ControllerSchema {
|
|
/// Domain/group identifier, e.g. `memory`, `config`, `credentials`.
|
|
/// This forms the first part of the RPC method name.
|
|
pub namespace: &'static str,
|
|
/// Function identifier inside namespace, e.g. `doc_put`.
|
|
/// This forms the second part of the RPC method name.
|
|
pub function: &'static str,
|
|
/// One-line human-readable purpose, used for CLI help and API documentation.
|
|
pub description: &'static str,
|
|
/// Ordered input parameters accepted by the controller function.
|
|
/// Each input is a field with a name, type, and description.
|
|
pub inputs: Vec<FieldSchema>,
|
|
/// Ordered output fields returned by the controller function.
|
|
/// This defines the structure of the successful response.
|
|
pub outputs: Vec<FieldSchema>,
|
|
}
|
|
|
|
impl ControllerSchema {
|
|
/// Canonical dotted name for routing, e.g. `memory.doc_put`.
|
|
/// This is used internally to identify the controller.
|
|
pub fn method_name(&self) -> String {
|
|
format!("{}.{}", self.namespace, self.function)
|
|
}
|
|
}
|
|
|
|
/// Schema for one input/output field.
|
|
///
|
|
/// Defines the properties of a single parameter or return value,
|
|
/// enabling validation and documentation generation.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
pub struct FieldSchema {
|
|
/// Field name. Used as the key in JSON objects or as a CLI flag.
|
|
pub name: &'static str,
|
|
/// Field type, defining the expected data shape and enabling validation.
|
|
pub ty: TypeSchema,
|
|
/// Human-readable description for docs/help. Should explain what the field is for.
|
|
pub comment: &'static str,
|
|
/// Requiredness for adapters:
|
|
/// - input: if true, the argument/flag MUST be provided.
|
|
/// - output: if true, the field is guaranteed to be present in the response.
|
|
pub required: bool,
|
|
}
|
|
|
|
/// Type-system shape used by controller input/output schema fields.
|
|
///
|
|
/// This enum represents the set of supported types that can be passed
|
|
/// across the controller boundary.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
pub enum TypeSchema {
|
|
/// A boolean value (true/false).
|
|
Bool,
|
|
/// A 64-bit signed integer.
|
|
I64,
|
|
/// A 64-bit unsigned integer.
|
|
U64,
|
|
/// A 64-bit floating point number.
|
|
F64,
|
|
/// A UTF-8 encoded string.
|
|
String,
|
|
/// A generic JSON value (serde_json::Value).
|
|
Json,
|
|
/// Raw binary data.
|
|
Bytes,
|
|
/// An ordered list of values of a specific type.
|
|
Array(Box<TypeSchema>),
|
|
/// String-keyed map/object with homogeneous values.
|
|
Map(Box<TypeSchema>),
|
|
/// An optional value that may be null or a value of the inner type.
|
|
Option(Box<TypeSchema>),
|
|
/// A string that must match one of the predefined variants.
|
|
Enum {
|
|
/// The list of allowed string variants.
|
|
variants: Vec<&'static str>,
|
|
},
|
|
/// A nested object with its own set of fields.
|
|
Object {
|
|
/// The fields defining the object's structure.
|
|
fields: Vec<FieldSchema>,
|
|
},
|
|
/// Reference to a named shared/domain type defined elsewhere.
|
|
Ref(&'static str),
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn mk(namespace: &'static str, function: &'static str) -> ControllerSchema {
|
|
ControllerSchema {
|
|
namespace,
|
|
function,
|
|
description: "",
|
|
inputs: vec![],
|
|
outputs: vec![],
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn method_name_joins_namespace_and_function_with_dot() {
|
|
let s = mk("memory", "doc_put");
|
|
assert_eq!(s.method_name(), "memory.doc_put");
|
|
}
|
|
|
|
#[test]
|
|
fn method_name_is_not_an_rpc_method_name() {
|
|
// The dotted controller key and the `openhuman.<ns>_<fn>` RPC method
|
|
// name are intentionally different — guard against drift.
|
|
let s = mk("memory", "doc_put");
|
|
assert_eq!(s.method_name(), "memory.doc_put");
|
|
assert_eq!(
|
|
crate::core::all::rpc_method_name(&s),
|
|
"openhuman.memory_doc_put"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn method_name_preserves_underscores_in_function() {
|
|
let s = mk("team", "change_member_role");
|
|
assert_eq!(s.method_name(), "team.change_member_role");
|
|
}
|
|
|
|
#[test]
|
|
fn controller_schema_equality_considers_all_fields() {
|
|
let a = ControllerSchema {
|
|
namespace: "a",
|
|
function: "b",
|
|
description: "x",
|
|
inputs: vec![],
|
|
outputs: vec![],
|
|
};
|
|
let b = ControllerSchema {
|
|
namespace: "a",
|
|
function: "b",
|
|
description: "x",
|
|
inputs: vec![],
|
|
outputs: vec![],
|
|
};
|
|
let c = ControllerSchema {
|
|
namespace: "a",
|
|
function: "b",
|
|
description: "different",
|
|
inputs: vec![],
|
|
outputs: vec![],
|
|
};
|
|
assert_eq!(a, b);
|
|
assert_ne!(a, c);
|
|
}
|
|
|
|
#[test]
|
|
fn type_schema_nesting_is_equality_comparable() {
|
|
let a = TypeSchema::Array(Box::new(TypeSchema::Option(Box::new(TypeSchema::String))));
|
|
let b = TypeSchema::Array(Box::new(TypeSchema::Option(Box::new(TypeSchema::String))));
|
|
let c = TypeSchema::Array(Box::new(TypeSchema::Option(Box::new(TypeSchema::I64))));
|
|
assert_eq!(a, b);
|
|
assert_ne!(a, c);
|
|
}
|
|
|
|
#[test]
|
|
fn field_schema_required_flag_changes_equality() {
|
|
let a = FieldSchema {
|
|
name: "x",
|
|
ty: TypeSchema::Bool,
|
|
comment: "",
|
|
required: true,
|
|
};
|
|
let b = FieldSchema {
|
|
name: "x",
|
|
ty: TypeSchema::Bool,
|
|
comment: "",
|
|
required: false,
|
|
};
|
|
assert_ne!(a, b);
|
|
}
|
|
|
|
#[test]
|
|
fn controller_schema_serializes_to_json() {
|
|
// Schema must be JSON-serializable: the /schema endpoint depends on it.
|
|
let s = ControllerSchema {
|
|
namespace: "health",
|
|
function: "snapshot",
|
|
description: "d",
|
|
inputs: vec![FieldSchema {
|
|
name: "limit",
|
|
ty: TypeSchema::U64,
|
|
comment: "cap",
|
|
required: false,
|
|
}],
|
|
outputs: vec![FieldSchema {
|
|
name: "ok",
|
|
ty: TypeSchema::Bool,
|
|
comment: "",
|
|
required: true,
|
|
}],
|
|
};
|
|
let json = serde_json::to_value(&s).unwrap();
|
|
assert_eq!(json["namespace"], "health");
|
|
assert_eq!(json["function"], "snapshot");
|
|
assert_eq!(json["inputs"][0]["name"], "limit");
|
|
assert_eq!(json["outputs"][0]["required"], true);
|
|
}
|
|
}
|