Initialize project scaffold and domain model
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "mcpaas-adapter-graphql"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-adapter-graphql"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "mcpaas-adapter-grpc"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-proto = { path = "../mcpaas-proto" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-adapter-grpc"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "mcpaas-adapter-rest"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-adapter-rest"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "mcpaas-core"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_yaml.workspace = true
|
||||
@@ -0,0 +1,59 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{ids::AuthProfileId, protocol::AuthKind};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SecretRef(pub String);
|
||||
|
||||
impl SecretRef {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BearerAuthConfig {
|
||||
pub header_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BasicAuthConfig {
|
||||
pub username_secret_ref: SecretRef,
|
||||
pub password_secret_ref: SecretRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ApiKeyHeaderAuthConfig {
|
||||
pub header_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ApiKeyQueryAuthConfig {
|
||||
pub param_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuthConfig {
|
||||
Bearer(BearerAuthConfig),
|
||||
Basic(BasicAuthConfig),
|
||||
ApiKeyHeader(ApiKeyHeaderAuthConfig),
|
||||
ApiKeyQuery(ApiKeyQueryAuthConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AuthProfile {
|
||||
pub id: AuthProfileId,
|
||||
pub name: String,
|
||||
pub kind: AuthKind,
|
||||
pub config: AuthConfig,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
macro_rules! define_id {
|
||||
($name:ident) => {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct $name(String);
|
||||
|
||||
impl $name {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for $name {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for $name {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for $name {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_id!(OperationId);
|
||||
define_id!(DescriptorId);
|
||||
define_id!(ToolId);
|
||||
define_id!(SampleId);
|
||||
define_id!(AuthProfileId);
|
||||
@@ -0,0 +1,16 @@
|
||||
pub mod auth;
|
||||
pub mod ids;
|
||||
pub mod operation;
|
||||
pub mod protocol;
|
||||
|
||||
pub use auth::{
|
||||
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
|
||||
BearerAuthConfig, SecretRef,
|
||||
};
|
||||
pub use ids::{AuthProfileId, DescriptorId, OperationId, SampleId, ToolId};
|
||||
pub use operation::{
|
||||
ConfigExport, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus, GraphqlTarget,
|
||||
GrpcProtocolOptions, GrpcTarget, Operation, OperationStatus, ProtocolOptions, RestTarget,
|
||||
RetryPolicy, Samples, Target, ToolDescription, ToolExample,
|
||||
};
|
||||
pub use protocol::{AuthKind, ExportMode, GraphqlOperationType, HttpMethod, Protocol};
|
||||
@@ -0,0 +1,363 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
ids::{AuthProfileId, DescriptorId, OperationId, SampleId},
|
||||
protocol::{ExportMode, GraphqlOperationType, HttpMethod, Protocol},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OperationStatus {
|
||||
Draft,
|
||||
Testing,
|
||||
Published,
|
||||
Archived,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RestTarget {
|
||||
pub base_url: String,
|
||||
pub method: HttpMethod,
|
||||
pub path_template: String,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub static_headers: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GraphqlTarget {
|
||||
pub endpoint: String,
|
||||
pub operation_type: GraphqlOperationType,
|
||||
pub operation_name: String,
|
||||
pub query_template: String,
|
||||
pub response_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GrpcTarget {
|
||||
pub server_addr: String,
|
||||
pub package: String,
|
||||
pub service: String,
|
||||
pub method: String,
|
||||
pub descriptor_ref: DescriptorId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum Target {
|
||||
Rest(RestTarget),
|
||||
Graphql(GraphqlTarget),
|
||||
Grpc(GrpcTarget),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct RetryPolicy {
|
||||
pub max_attempts: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct GrpcProtocolOptions {
|
||||
pub use_tls: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct ProtocolOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub grpc: Option<GrpcProtocolOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExecutionConfig {
|
||||
pub timeout_ms: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub retry_policy: Option<RetryPolicy>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auth_profile_ref: Option<AuthProfileId>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub protocol_options: Option<ProtocolOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ToolExample {
|
||||
pub input: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ToolDescription {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub examples: Vec<ToolExample>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct Samples {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_json_sample_ref: Option<SampleId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_json_sample_ref: Option<SampleId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub proto_file_ref: Option<SampleId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub descriptor_ref: Option<DescriptorId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GeneratedDraftStatus {
|
||||
None,
|
||||
Available,
|
||||
Stale,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GeneratedDraft {
|
||||
pub status: GeneratedDraftStatus,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub source_types: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generated_at: Option<String>,
|
||||
pub input_schema_generated: bool,
|
||||
pub output_schema_generated: bool,
|
||||
pub input_mapping_generated: bool,
|
||||
pub output_mapping_generated: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ConfigExport {
|
||||
pub format_version: String,
|
||||
pub export_mode: ExportMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Operation<TSchema, TMapping> {
|
||||
pub id: OperationId,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub protocol: Protocol,
|
||||
pub status: OperationStatus,
|
||||
pub version: u32,
|
||||
pub target: Target,
|
||||
pub input_schema: TSchema,
|
||||
pub output_schema: TSchema,
|
||||
pub input_mapping: TMapping,
|
||||
pub output_mapping: TMapping,
|
||||
pub execution_config: ExecutionConfig,
|
||||
pub tool_description: ToolDescription,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub samples: Option<Samples>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generated_draft: Option<GeneratedDraft>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub config_export: Option<ConfigExport>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub published_at: Option<String>,
|
||||
}
|
||||
|
||||
impl<TSchema, TMapping> Operation<TSchema, TMapping> {
|
||||
pub fn tool_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn is_draft(&self) -> bool {
|
||||
self.status == OperationStatus::Draft
|
||||
}
|
||||
|
||||
pub fn is_published(&self) -> bool {
|
||||
self.status == OperationStatus::Published
|
||||
}
|
||||
|
||||
pub fn protocol(&self) -> Protocol {
|
||||
self.protocol
|
||||
}
|
||||
|
||||
pub fn auth_profile_ref(&self) -> Option<&AuthProfileId> {
|
||||
self.execution_config.auth_profile_ref.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
auth::{AuthConfig, AuthProfile, BearerAuthConfig, SecretRef},
|
||||
ids::{AuthProfileId, OperationId},
|
||||
operation::{
|
||||
ConfigExport, ExecutionConfig, GraphqlTarget, Operation, OperationStatus,
|
||||
ProtocolOptions, RestTarget, Samples, Target, ToolDescription, ToolExample,
|
||||
},
|
||||
protocol::{AuthKind, ExportMode, GraphqlOperationType, HttpMethod, Protocol},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn rest_target_serializes_with_kind_tag() {
|
||||
let target = Target::Rest(RestTarget {
|
||||
base_url: "https://api.example.com".to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
});
|
||||
|
||||
let value = serde_json::to_value(target).unwrap();
|
||||
|
||||
assert_eq!(value["kind"], "rest");
|
||||
assert_eq!(value["method"], "POST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphql_target_serializes_response_path() {
|
||||
let target = Target::Graphql(GraphqlTarget {
|
||||
endpoint: "https://api.example.com/graphql".to_owned(),
|
||||
operation_type: GraphqlOperationType::Mutation,
|
||||
operation_name: "CreateLead".to_owned(),
|
||||
query_template: "mutation {}".to_owned(),
|
||||
response_path: "$.response.body.data.createLead".to_owned(),
|
||||
});
|
||||
|
||||
let value = serde_json::to_value(target).unwrap();
|
||||
|
||||
assert_eq!(value["kind"], "graphql");
|
||||
assert_eq!(value["operation_type"], "mutation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_exposes_local_domain_helpers() {
|
||||
let operation = Operation {
|
||||
id: OperationId::new("op_01"),
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create Lead".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
status: OperationStatus::Draft,
|
||||
version: 1,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: "https://api.example.com".to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: json!({"type":"object"}),
|
||||
output_schema: json!({"type":"object"}),
|
||||
input_mapping: json!({"rules":[]}),
|
||||
output_mapping: json!({"rules":[]}),
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 10_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: Some(AuthProfileId::new("auth_01")),
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: Some(ProtocolOptions::default()),
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create CRM lead".to_owned(),
|
||||
description: "Creates a new lead.".to_owned(),
|
||||
tags: Vec::new(),
|
||||
examples: Vec::new(),
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: None,
|
||||
config_export: None,
|
||||
created_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
published_at: None,
|
||||
};
|
||||
|
||||
assert_eq!(operation.tool_name(), "crm_create_lead");
|
||||
assert!(operation.is_draft());
|
||||
assert!(!operation.is_published());
|
||||
assert_eq!(operation.protocol(), Protocol::Rest);
|
||||
assert_eq!(
|
||||
operation.auth_profile_ref().map(|value| value.as_str()),
|
||||
Some("auth_01")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_profile_serializes_secret_refs_without_secret_values() {
|
||||
let profile = AuthProfile {
|
||||
id: AuthProfileId::new("auth_01"),
|
||||
name: "crm-prod-bearer".to_owned(),
|
||||
kind: AuthKind::Bearer,
|
||||
config: AuthConfig::Bearer(BearerAuthConfig {
|
||||
header_name: "Authorization".to_owned(),
|
||||
secret_ref: SecretRef::new("secret://auth/crm-prod-token"),
|
||||
}),
|
||||
created_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(profile).unwrap();
|
||||
|
||||
assert_eq!(value["kind"], "bearer");
|
||||
assert_eq!(
|
||||
value["config"]["bearer"]["secret_ref"],
|
||||
"secret://auth/crm-prod-token"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_roundtrips_through_yaml() {
|
||||
let operation = Operation {
|
||||
id: OperationId::new("op_01"),
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create Lead".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
status: OperationStatus::Published,
|
||||
version: 3,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: "https://api.example.com".to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::from([("X-App-Source".to_owned(), "mcpaas".to_owned())]),
|
||||
}),
|
||||
input_schema: json!({"type":"object","fields":{"email":{"type":"string","required":true}}}),
|
||||
output_schema: json!({"type":"object","fields":{"id":{"type":"string","required":true}}}),
|
||||
input_mapping: json!({"rules":[{"source":"$.mcp.email","target":"$.request.body.email"}]}),
|
||||
output_mapping: json!({"rules":[{"source":"$.response.body.id","target":"$.output.id"}]}),
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 10_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: Some(AuthProfileId::new("auth_01")),
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: Some(ProtocolOptions::default()),
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create CRM lead".to_owned(),
|
||||
description: "Creates a new lead.".to_owned(),
|
||||
tags: vec!["crm".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({"email":"user@example.com"}),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: None,
|
||||
config_export: Some(ConfigExport {
|
||||
format_version: "1".to_owned(),
|
||||
export_mode: ExportMode::Portable,
|
||||
}),
|
||||
created_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T08:10:00Z".to_owned(),
|
||||
published_at: Some("2026-03-25T08:15:00Z".to_owned()),
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&operation).unwrap();
|
||||
let restored: Operation<serde_json::Value, serde_json::Value> =
|
||||
serde_yaml::from_str(&yaml).unwrap();
|
||||
|
||||
assert!(yaml.contains("protocol: rest"));
|
||||
assert!(yaml.contains("export_mode: portable"));
|
||||
assert_eq!(restored, operation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Protocol {
|
||||
Rest,
|
||||
Graphql,
|
||||
Grpc,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum HttpMethod {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Patch,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GraphqlOperationType {
|
||||
Query,
|
||||
Mutation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuthKind {
|
||||
Bearer,
|
||||
Basic,
|
||||
ApiKeyHeader,
|
||||
ApiKeyQuery,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExportMode {
|
||||
Portable,
|
||||
Bundle,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "mcpaas-mapping"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_yaml.workspace = true
|
||||
@@ -0,0 +1,129 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MappingCondition {
|
||||
pub source: String,
|
||||
pub equals: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TransformKind {
|
||||
Identity,
|
||||
ToString,
|
||||
ToNumber,
|
||||
ToBoolean,
|
||||
Join,
|
||||
Split,
|
||||
WrapArray,
|
||||
UnwrapSingleton,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Transform {
|
||||
pub kind: TransformKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MappingRule {
|
||||
pub source: String,
|
||||
pub target: String,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_value: Option<Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub transform: Option<Transform>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub condition: Option<MappingCondition>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct MappingSet {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub rules: Vec<MappingRule>,
|
||||
}
|
||||
|
||||
impl MappingSet {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rules.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.rules.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{MappingRule, MappingSet, Transform, TransformKind};
|
||||
|
||||
#[test]
|
||||
fn mapping_set_reports_non_empty_rules() {
|
||||
let mapping = MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.body.contact.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: Some(Transform {
|
||||
kind: TransformKind::Identity,
|
||||
}),
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
};
|
||||
|
||||
assert!(!mapping.is_empty());
|
||||
assert_eq!(mapping.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_rule_serializes_jsonpath_fields() {
|
||||
let mapping = MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.body.id".to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: false,
|
||||
default_value: Some(json!("lead_123")),
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: Some("map identifier".to_owned()),
|
||||
}],
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(mapping).unwrap();
|
||||
|
||||
assert_eq!(value["rules"][0]["source"], "$.response.body.id");
|
||||
assert_eq!(value["rules"][0]["target"], "$.output.id");
|
||||
assert_eq!(value["rules"][0]["default_value"], "lead_123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_set_roundtrips_through_yaml() {
|
||||
let mapping = MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.tags".to_owned(),
|
||||
target: "$.request.body.tags".to_owned(),
|
||||
required: false,
|
||||
default_value: None,
|
||||
transform: Some(Transform {
|
||||
kind: TransformKind::WrapArray,
|
||||
}),
|
||||
condition: None,
|
||||
notes: Some("normalize tags".to_owned()),
|
||||
}],
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&mapping).unwrap();
|
||||
let restored: MappingSet = serde_yaml::from_str(&yaml).unwrap();
|
||||
|
||||
assert!(yaml.contains("source: $.mcp.tags"));
|
||||
assert_eq!(restored, mapping);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "mcpaas-proto"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-schema = { path = "../mcpaas-schema" }
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-proto"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "mcpaas-registry"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-mapping = { path = "../mcpaas-mapping" }
|
||||
mcpaas-schema = { path = "../mcpaas-schema" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-registry"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "mcpaas-runtime"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-adapter-graphql = { path = "../mcpaas-adapter-graphql" }
|
||||
mcpaas-adapter-grpc = { path = "../mcpaas-adapter-grpc" }
|
||||
mcpaas-adapter-rest = { path = "../mcpaas-adapter-rest" }
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-mapping = { path = "../mcpaas-mapping" }
|
||||
mcpaas-schema = { path = "../mcpaas-schema" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-runtime"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "mcpaas-schema"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_yaml.workspace = true
|
||||
@@ -0,0 +1,164 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SchemaKind {
|
||||
Object,
|
||||
Array,
|
||||
String,
|
||||
Integer,
|
||||
Number,
|
||||
Boolean,
|
||||
Enum,
|
||||
#[serde(rename = "null")]
|
||||
Null,
|
||||
Oneof,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Schema {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: SchemaKind,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(default)]
|
||||
pub nullable: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_value: Option<Value>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub fields: BTreeMap<String, Schema>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub items: Option<Box<Schema>>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub enum_values: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub variants: Vec<Schema>,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
pub fn is_object(&self) -> bool {
|
||||
self.kind == SchemaKind::Object
|
||||
}
|
||||
|
||||
pub fn field(&self, name: &str) -> Option<&Schema> {
|
||||
self.fields.get(name)
|
||||
}
|
||||
|
||||
pub fn has_required_fields(&self) -> bool {
|
||||
self.fields.values().any(|field| field.required)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::{Schema, SchemaKind};
|
||||
|
||||
#[test]
|
||||
fn object_schema_exposes_fields() {
|
||||
let mut fields = BTreeMap::new();
|
||||
fields.insert(
|
||||
"email".to_owned(),
|
||||
Schema {
|
||||
kind: SchemaKind::String,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let schema = Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: Some("User input".to_owned()),
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields,
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
};
|
||||
|
||||
assert!(schema.is_object());
|
||||
assert!(schema.has_required_fields());
|
||||
assert_eq!(
|
||||
schema.field("email").map(|field| field.required),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_serializes_type_field() {
|
||||
let schema = Schema {
|
||||
kind: SchemaKind::Array,
|
||||
description: None,
|
||||
required: false,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: Some(Box::new(Schema {
|
||||
kind: SchemaKind::String,
|
||||
description: None,
|
||||
required: false,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
})),
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(schema).unwrap();
|
||||
|
||||
assert_eq!(value["type"], "array");
|
||||
assert_eq!(value["items"]["type"], "string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_roundtrips_through_yaml() {
|
||||
let schema = Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: Some("Lead output".to_owned()),
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::from([(
|
||||
"id".to_owned(),
|
||||
Schema {
|
||||
kind: SchemaKind::String,
|
||||
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(),
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&schema).unwrap();
|
||||
let restored: Schema = serde_yaml::from_str(&yaml).unwrap();
|
||||
|
||||
assert!(yaml.contains("type: object"));
|
||||
assert_eq!(restored, schema);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user