Add local dispatch for OperationEnv with invoke() method, EventEnvelope wire format struct, 4-byte BE length-prefixed frame encoding/decoding, PendingRequestMap for call/subscribe correlation, call protocol event type constants, and default /services/list and /services/schema operations.
208 lines
6.7 KiB
Rust
208 lines
6.7 KiB
Rust
use std::sync::Arc;
|
|
|
|
use serde_json::Value;
|
|
|
|
use crate::call::context::OperationContext;
|
|
use crate::call::response::ResponseEnvelope;
|
|
use crate::call::spec::{AccessControl, OperationSpec, OperationType};
|
|
|
|
pub fn services_list_spec() -> OperationSpec {
|
|
OperationSpec {
|
|
name: super::events::SERVICE_LIST.to_string(),
|
|
namespace: "services".to_string(),
|
|
op_type: OperationType::Query,
|
|
input_schema: serde_json::json!({
|
|
"type": "object",
|
|
"properties": {},
|
|
}),
|
|
output_schema: serde_json::json!({
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"namespace": { "type": "string" },
|
|
"op_type": { "type": "string" },
|
|
},
|
|
},
|
|
}),
|
|
access_control: AccessControl {
|
|
required_scopes: vec![],
|
|
required_scopes_any: None,
|
|
resource_type: None,
|
|
resource_action: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn services_schema_spec() -> OperationSpec {
|
|
OperationSpec {
|
|
name: super::events::SERVICE_SCHEMA.to_string(),
|
|
namespace: "services".to_string(),
|
|
op_type: OperationType::Query,
|
|
input_schema: serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
},
|
|
"required": ["name"],
|
|
}),
|
|
output_schema: serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"namespace": { "type": "string" },
|
|
"op_type": { "type": "string" },
|
|
"input_schema": { "type": "object" },
|
|
"output_schema": { "type": "object" },
|
|
},
|
|
}),
|
|
access_control: AccessControl {
|
|
required_scopes: vec![],
|
|
required_scopes_any: None,
|
|
resource_type: None,
|
|
resource_action: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn register_default_operations(registry: &mut crate::call::OperationRegistry) {
|
|
registry.register(services_list_spec(), Arc::new(services_list_handler));
|
|
registry.register(services_schema_spec(), Arc::new(services_schema_handler));
|
|
}
|
|
|
|
fn services_list_handler(_input: Value, ctx: OperationContext) -> ResponseEnvelope {
|
|
let registry = &ctx.env.registry_ref();
|
|
let specs = registry.list_operations();
|
|
let ops: Vec<Value> = specs
|
|
.iter()
|
|
.map(|spec| {
|
|
serde_json::json!({
|
|
"name": spec.name,
|
|
"namespace": spec.namespace,
|
|
"op_type": format!("{:?}", spec.op_type).to_lowercase(),
|
|
})
|
|
})
|
|
.collect();
|
|
ResponseEnvelope::ok(&ctx.request_id, serde_json::json!({ "operations": ops }))
|
|
}
|
|
|
|
fn services_schema_handler(input: Value, ctx: OperationContext) -> ResponseEnvelope {
|
|
let name = match input.get("name").and_then(|v| v.as_str()) {
|
|
Some(n) => n.to_string(),
|
|
None => {
|
|
return ResponseEnvelope::err(
|
|
&ctx.request_id,
|
|
"INVALID_INPUT",
|
|
"missing required field: name",
|
|
false,
|
|
);
|
|
}
|
|
};
|
|
let registry = &ctx.env.registry_ref();
|
|
match registry.lookup(&name) {
|
|
Some((spec, _)) => ResponseEnvelope::ok(
|
|
&ctx.request_id,
|
|
serde_json::json!({
|
|
"name": spec.name,
|
|
"namespace": spec.namespace,
|
|
"op_type": format!("{:?}", spec.op_type).to_lowercase(),
|
|
"input_schema": spec.input_schema,
|
|
"output_schema": spec.output_schema,
|
|
}),
|
|
),
|
|
None => ResponseEnvelope::err(
|
|
&ctx.request_id,
|
|
"NOT_FOUND",
|
|
format!("operation not found: {name}"),
|
|
false,
|
|
),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::call::env::OperationEnv;
|
|
|
|
fn make_env() -> OperationEnv {
|
|
let mut registry = crate::call::OperationRegistry::new();
|
|
registry.register(services_list_spec(), Arc::new(services_list_handler));
|
|
registry.register(services_schema_spec(), Arc::new(services_schema_handler));
|
|
OperationEnv::local(registry)
|
|
}
|
|
|
|
#[test]
|
|
fn services_list_returns_operations() {
|
|
let env = make_env();
|
|
let result = env.invoke("services", "list", serde_json::json!({}));
|
|
assert!(result.result.is_ok());
|
|
let value = result.result.unwrap();
|
|
let ops = value.get("operations").unwrap().as_array().unwrap();
|
|
assert_eq!(ops.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn services_schema_returns_spec() {
|
|
let env = make_env();
|
|
let result = env.invoke(
|
|
"services",
|
|
"schema",
|
|
serde_json::json!({"name": "/services/list"}),
|
|
);
|
|
assert!(result.result.is_ok());
|
|
let value = result.result.unwrap();
|
|
assert_eq!(value["name"], "/services/list");
|
|
assert_eq!(value["namespace"], "services");
|
|
}
|
|
|
|
#[test]
|
|
fn services_schema_missing_name() {
|
|
let env = make_env();
|
|
let result = env.invoke("services", "schema", serde_json::json!({}));
|
|
assert!(result.result.is_err());
|
|
let err = result.result.unwrap_err();
|
|
assert_eq!(err.code, "INVALID_INPUT");
|
|
}
|
|
|
|
#[test]
|
|
fn services_schema_not_found() {
|
|
let env = make_env();
|
|
let result = env.invoke(
|
|
"services",
|
|
"schema",
|
|
serde_json::json!({"name": "/nonexistent/op"}),
|
|
);
|
|
assert!(result.result.is_err());
|
|
let err = result.result.unwrap_err();
|
|
assert_eq!(err.code, "NOT_FOUND");
|
|
}
|
|
|
|
#[test]
|
|
fn services_list_spec_fields() {
|
|
let spec = services_list_spec();
|
|
assert_eq!(spec.name, "/services/list");
|
|
assert_eq!(spec.namespace, "services");
|
|
assert_eq!(spec.op_type, OperationType::Query);
|
|
assert!(!spec.access_control.has_restrictions());
|
|
}
|
|
|
|
#[test]
|
|
fn services_schema_spec_fields() {
|
|
let spec = services_schema_spec();
|
|
assert_eq!(spec.name, "/services/schema");
|
|
assert_eq!(spec.namespace, "services");
|
|
assert_eq!(spec.op_type, OperationType::Query);
|
|
assert!(!spec.access_control.has_restrictions());
|
|
}
|
|
|
|
#[test]
|
|
fn register_default_operations_adds_both() {
|
|
let mut registry = crate::call::OperationRegistry::new();
|
|
register_default_operations(&mut registry);
|
|
assert!(registry.lookup("/services/list").is_some());
|
|
assert!(registry.lookup("/services/schema").is_some());
|
|
assert_eq!(registry.list_operations().len(), 2);
|
|
}
|
|
}
|