chore: rebrand project to crank
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crank_adapter_graphql::{GraphqlAdapter, GraphqlRequest};
|
||||
use crank_adapter_grpc::{GrpcAdapter, GrpcRequest};
|
||||
use crank_adapter_rest::{RestAdapter, RestRequest};
|
||||
use crank_core::Target;
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::{AdapterResponse, PreparedRequest, RuntimeError, RuntimeOperation};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RuntimeExecutor {
|
||||
graphql_adapter: GraphqlAdapter,
|
||||
grpc_adapter: GrpcAdapter,
|
||||
rest_adapter: RestAdapter,
|
||||
}
|
||||
|
||||
impl Default for RuntimeExecutor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeExecutor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
graphql_adapter: GraphqlAdapter::new(),
|
||||
grpc_adapter: GrpcAdapter::new(),
|
||||
rest_adapter: RestAdapter::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
&self,
|
||||
operation: &RuntimeOperation,
|
||||
input: &Value,
|
||||
) -> Result<Value, RuntimeError> {
|
||||
operation.input_schema.validate_shape(input)?;
|
||||
|
||||
let mapped_input = operation.input_mapping.apply(&json!({ "mcp": input }))?;
|
||||
let prepared_request = PreparedRequest::from_mapping_output(&mapped_input)?;
|
||||
|
||||
let adapter_response = match &operation.target {
|
||||
Target::Grpc(target) => {
|
||||
let request = GrpcRequest {
|
||||
headers: merge_headers(
|
||||
&BTreeMap::new(),
|
||||
&operation.execution_config.headers,
|
||||
&prepared_request.headers,
|
||||
),
|
||||
body: prepared_request
|
||||
.grpc
|
||||
.clone()
|
||||
.unwrap_or(Value::Object(Map::new())),
|
||||
timeout_ms: operation.execution_config.timeout_ms,
|
||||
};
|
||||
let response = self.grpc_adapter.execute(target, &request).await?;
|
||||
|
||||
AdapterResponse {
|
||||
status_code: response.status_code,
|
||||
headers: response.headers,
|
||||
body: response.body.clone(),
|
||||
data: response.body,
|
||||
}
|
||||
}
|
||||
Target::Graphql(target) => {
|
||||
let request = GraphqlRequest {
|
||||
headers: merge_headers(
|
||||
&BTreeMap::new(),
|
||||
&operation.execution_config.headers,
|
||||
&prepared_request.headers,
|
||||
),
|
||||
variables: prepared_request.variables.clone(),
|
||||
timeout_ms: operation.execution_config.timeout_ms,
|
||||
};
|
||||
let response = self.graphql_adapter.execute(target, &request).await?;
|
||||
|
||||
AdapterResponse {
|
||||
status_code: response.status_code,
|
||||
headers: response.headers,
|
||||
body: response.body,
|
||||
data: response.data,
|
||||
}
|
||||
}
|
||||
Target::Rest(target) => {
|
||||
let request = RestRequest {
|
||||
path_params: prepared_request.path_params.clone(),
|
||||
query_params: prepared_request.query_params.clone(),
|
||||
headers: merge_headers(
|
||||
&target.static_headers,
|
||||
&operation.execution_config.headers,
|
||||
&prepared_request.headers,
|
||||
),
|
||||
body: prepared_request.body.clone(),
|
||||
timeout_ms: operation.execution_config.timeout_ms,
|
||||
};
|
||||
let response = self.rest_adapter.execute(target, &request).await?;
|
||||
|
||||
AdapterResponse {
|
||||
status_code: response.status_code,
|
||||
headers: response.headers,
|
||||
body: response.body,
|
||||
data: Value::Null,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let finalized_output = finalize_output(operation, &adapter_response)?;
|
||||
|
||||
operation.output_schema.validate_shape(&finalized_output)?;
|
||||
|
||||
Ok(finalized_output)
|
||||
}
|
||||
}
|
||||
|
||||
impl PreparedRequest {
|
||||
pub fn from_mapping_output(mapped: &Value) -> Result<Self, RuntimeError> {
|
||||
let request =
|
||||
mapped
|
||||
.get("request")
|
||||
.ok_or_else(|| RuntimeError::InvalidPreparedRequest {
|
||||
details: "mapped input must contain request root".to_owned(),
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
path_params: read_string_map(request.get("path"), "request.path")?,
|
||||
query_params: read_string_map(request.get("query"), "request.query")?,
|
||||
headers: read_string_map(request.get("headers"), "request.headers")?,
|
||||
grpc: non_empty_payload(request.get("grpc").cloned()),
|
||||
variables: non_empty_payload(request.get("variables").cloned()),
|
||||
body: request.get("body").and_then(non_empty_body).cloned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_output(
|
||||
operation: &RuntimeOperation,
|
||||
response: &AdapterResponse,
|
||||
) -> Result<Value, RuntimeError> {
|
||||
let mapped = operation.output_mapping.apply(&json!({
|
||||
"response": {
|
||||
"body": response.body,
|
||||
"data": response.data,
|
||||
"headers": response.headers,
|
||||
"status": response.status_code
|
||||
}
|
||||
}))?;
|
||||
|
||||
Ok(mapped
|
||||
.get("output")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::Object(Map::new())))
|
||||
}
|
||||
|
||||
fn merge_headers(
|
||||
static_headers: &BTreeMap<String, String>,
|
||||
execution_headers: &BTreeMap<String, String>,
|
||||
request_headers: &BTreeMap<String, String>,
|
||||
) -> BTreeMap<String, String> {
|
||||
let mut headers = static_headers.clone();
|
||||
headers.extend(execution_headers.clone());
|
||||
headers.extend(request_headers.clone());
|
||||
headers
|
||||
}
|
||||
|
||||
fn read_string_map(
|
||||
value: Option<&Value>,
|
||||
field_name: &str,
|
||||
) -> Result<BTreeMap<String, String>, RuntimeError> {
|
||||
let Some(value) = value else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
|
||||
let Some(object) = value.as_object() else {
|
||||
return Err(RuntimeError::InvalidPreparedRequest {
|
||||
details: format!("{field_name} must be an object"),
|
||||
});
|
||||
};
|
||||
|
||||
object
|
||||
.iter()
|
||||
.map(|(key, value)| stringify_value(value).map(|value| (key.clone(), value)))
|
||||
.collect::<Result<BTreeMap<_, _>, _>>()
|
||||
.map_err(|details| RuntimeError::InvalidPreparedRequest { details })
|
||||
}
|
||||
|
||||
fn stringify_value(value: &Value) -> Result<String, String> {
|
||||
match value {
|
||||
Value::Null => Ok("null".to_owned()),
|
||||
Value::Bool(value) => Ok(value.to_string()),
|
||||
Value::Number(value) => Ok(value.to_string()),
|
||||
Value::String(value) => Ok(value.clone()),
|
||||
Value::Array(_) | Value::Object(_) => {
|
||||
Err("request path/query/headers accept only scalar values".to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_body(value: &Value) -> Option<&Value> {
|
||||
match value {
|
||||
Value::Null => None,
|
||||
Value::Object(object) if object.is_empty() => None,
|
||||
_ => Some(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn non_empty_payload(value: Option<Value>) -> Option<Value> {
|
||||
match value {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::Object(object)) if object.is_empty() => None,
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::{Json, Router, routing::post};
|
||||
use crank_adapter_grpc::test_support as grpc_test_support;
|
||||
use crank_core::{
|
||||
DescriptorId, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus, GraphqlOperationType,
|
||||
GraphqlTarget, GrpcTarget, HttpMethod, Operation, OperationId, OperationStatus, Protocol,
|
||||
RestTarget, Samples, Target, ToolDescription, ToolExample,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use crate::{RuntimeError, RuntimeExecutor, RuntimeOperation};
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_rest_operation_end_to_end() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_rest_operation(&base_url, false, false);
|
||||
|
||||
let output = executor
|
||||
.execute(&operation, &json!({ "email": "user@example.com" }))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, json!({ "id": "lead_123" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_graphql_operation_end_to_end() {
|
||||
let endpoint = spawn_graphql_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_graphql_operation(&endpoint, false);
|
||||
|
||||
let output = executor
|
||||
.execute(&operation, &json!({ "email": "user@example.com" }))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, json!({ "id": "lead_123" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn executes_grpc_operation_end_to_end() {
|
||||
let server_addr = grpc_test_support::spawn_unary_echo_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_grpc_operation(&server_addr);
|
||||
|
||||
let output = executor
|
||||
.execute(&operation, &json!({ "message": "hello" }))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output, json!({ "message": "hello" }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_invalid_input_shape() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_rest_operation(&base_url, false, false);
|
||||
|
||||
let error = executor.execute(&operation, &json!({})).await.unwrap_err();
|
||||
|
||||
assert!(matches!(error, RuntimeError::Schema(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn propagates_external_rest_errors() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_rest_operation(&base_url, true, false);
|
||||
|
||||
let error = executor
|
||||
.execute(&operation, &json!({ "email": "user@example.com" }))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, RuntimeError::RestAdapter(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fails_when_output_mapping_requires_missing_field() {
|
||||
let base_url = spawn_runtime_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_rest_operation(&base_url, false, true);
|
||||
|
||||
let error = executor
|
||||
.execute(&operation, &json!({ "email": "user@example.com" }))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, RuntimeError::Mapping(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn propagates_graphql_operation_errors() {
|
||||
let endpoint = spawn_graphql_server().await;
|
||||
let executor = RuntimeExecutor::new();
|
||||
let operation = test_graphql_operation(&endpoint, true);
|
||||
|
||||
let error = executor
|
||||
.execute(&operation, &json!({ "email": "fail@example.com" }))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(error, RuntimeError::GraphqlAdapter(_)));
|
||||
}
|
||||
|
||||
async fn spawn_runtime_server() -> String {
|
||||
let app = Router::new().route("/leads", post(create_lead));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn spawn_graphql_server() -> String {
|
||||
let app = Router::new().route("/", post(graphql_handler));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
format!("http://{}", address)
|
||||
}
|
||||
|
||||
async fn create_lead(Json(payload): Json<Value>) -> (axum::http::StatusCode, Json<Value>) {
|
||||
let should_fail = payload
|
||||
.get("fail")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_fail {
|
||||
return (
|
||||
axum::http::StatusCode::BAD_GATEWAY,
|
||||
Json(json!({ "error": "upstream failed" })),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
axum::http::StatusCode::OK,
|
||||
Json(json!({ "id": "lead_123", "email": payload["email"] })),
|
||||
)
|
||||
}
|
||||
|
||||
async fn graphql_handler(Json(payload): Json<Value>) -> Json<Value> {
|
||||
let email = payload
|
||||
.get("variables")
|
||||
.and_then(|variables| variables.get("email"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
|
||||
if email == "fail@example.com" {
|
||||
return Json(json!({
|
||||
"data": { "createLead": null },
|
||||
"errors": [{ "message": "lead creation failed" }]
|
||||
}));
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
"data": {
|
||||
"createLead": {
|
||||
"id": "lead_123",
|
||||
"status": "created"
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn test_rest_operation(
|
||||
base_url: &str,
|
||||
should_fail: bool,
|
||||
invalid_output: bool,
|
||||
) -> RuntimeOperation {
|
||||
let mut input_rules = vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.body.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}];
|
||||
|
||||
if should_fail {
|
||||
input_rules.push(MappingRule {
|
||||
source: "$.mcp.fail".to_owned(),
|
||||
target: "$.request.body.fail".to_owned(),
|
||||
required: false,
|
||||
default_value: Some(Value::Bool(true)),
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
});
|
||||
}
|
||||
|
||||
let output_source = if invalid_output {
|
||||
"$.response.body.missing"
|
||||
} else {
|
||||
"$.response.body.id"
|
||||
};
|
||||
|
||||
RuntimeOperation::from(Operation {
|
||||
id: OperationId::new("op_rest_runtime"),
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create Lead".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
status: OperationStatus::Published,
|
||||
version: 1,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: base_url.to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/leads".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: object_schema("email", SchemaKind::String),
|
||||
output_schema: object_schema("id", SchemaKind::String),
|
||||
input_mapping: MappingSet { rules: input_rules },
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: output_source.to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create Lead".to_owned(),
|
||||
description: "Creates a CRM lead".to_owned(),
|
||||
tags: vec!["crm".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({ "email": "user@example.com" }),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: Some(GeneratedDraft {
|
||||
status: GeneratedDraftStatus::Available,
|
||||
source_types: vec!["input_json".to_owned()],
|
||||
generated_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||
input_schema_generated: true,
|
||||
output_schema_generated: true,
|
||||
input_mapping_generated: true,
|
||||
output_mapping_generated: true,
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
config_export: None,
|
||||
created_at: "2026-03-25T20:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T20:00:00Z".to_owned(),
|
||||
published_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
fn test_graphql_operation(endpoint: &str, _should_fail: bool) -> RuntimeOperation {
|
||||
RuntimeOperation::from(Operation {
|
||||
id: OperationId::new("op_graphql_runtime"),
|
||||
name: "crm_create_lead_graphql".to_owned(),
|
||||
display_name: "Create Lead GraphQL".to_owned(),
|
||||
protocol: Protocol::Graphql,
|
||||
status: OperationStatus::Published,
|
||||
version: 1,
|
||||
target: Target::Graphql(GraphqlTarget {
|
||||
endpoint: endpoint.to_owned(),
|
||||
operation_type: GraphqlOperationType::Mutation,
|
||||
operation_name: "CreateLead".to_owned(),
|
||||
query_template:
|
||||
"mutation CreateLead($email: String!) { createLead(email: $email) { id status } }"
|
||||
.to_owned(),
|
||||
response_path: "$.response.body.data.createLead".to_owned(),
|
||||
}),
|
||||
input_schema: object_schema("email", SchemaKind::String),
|
||||
output_schema: object_schema("id", SchemaKind::String),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.variables.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.data.id".to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create Lead GraphQL".to_owned(),
|
||||
description: "Creates a CRM lead through GraphQL".to_owned(),
|
||||
tags: vec!["crm".to_owned(), "graphql".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({ "email": "user@example.com" }),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: Some(GeneratedDraft {
|
||||
status: GeneratedDraftStatus::Available,
|
||||
source_types: vec!["input_json".to_owned(), "output_json".to_owned()],
|
||||
generated_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||
input_schema_generated: true,
|
||||
output_schema_generated: true,
|
||||
input_mapping_generated: true,
|
||||
output_mapping_generated: true,
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
config_export: None,
|
||||
created_at: "2026-03-25T20:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T20:00:00Z".to_owned(),
|
||||
published_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
fn test_grpc_operation(server_addr: &str) -> RuntimeOperation {
|
||||
RuntimeOperation::from(Operation {
|
||||
id: OperationId::new("op_grpc_runtime"),
|
||||
name: "echo_unary_grpc".to_owned(),
|
||||
display_name: "Unary Echo gRPC".to_owned(),
|
||||
protocol: Protocol::Grpc,
|
||||
status: OperationStatus::Published,
|
||||
version: 1,
|
||||
target: Target::Grpc(GrpcTarget {
|
||||
server_addr: server_addr.to_owned(),
|
||||
package: "echo".to_owned(),
|
||||
service: "EchoService".to_owned(),
|
||||
method: "UnaryEcho".to_owned(),
|
||||
descriptor_ref: DescriptorId::new("desc_echo"),
|
||||
descriptor_set_b64: grpc_test_support::descriptor_set_b64(),
|
||||
}),
|
||||
input_schema: object_schema("message", SchemaKind::String),
|
||||
output_schema: object_schema("message", SchemaKind::String),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.message".to_owned(),
|
||||
target: "$.request.grpc.message".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.data.message".to_owned(),
|
||||
target: "$.output.message".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Unary Echo gRPC".to_owned(),
|
||||
description: "Echoes a unary gRPC payload".to_owned(),
|
||||
tags: vec!["grpc".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({ "message": "hello" }),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: Some(GeneratedDraft {
|
||||
status: GeneratedDraftStatus::Available,
|
||||
source_types: vec!["descriptor_set".to_owned()],
|
||||
generated_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||
input_schema_generated: true,
|
||||
output_schema_generated: true,
|
||||
input_mapping_generated: true,
|
||||
output_mapping_generated: true,
|
||||
warnings: Vec::new(),
|
||||
}),
|
||||
config_export: None,
|
||||
created_at: "2026-03-25T20:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T20:00:00Z".to_owned(),
|
||||
published_at: Some("2026-03-25T20:00:00Z".to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
fn object_schema(field_name: &str, kind: SchemaKind) -> Schema {
|
||||
Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::from([(
|
||||
field_name.to_owned(),
|
||||
Schema {
|
||||
kind,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user