Initialize project scaffold and domain model

This commit is contained in:
a.tolmachev
2026-03-25 12:20:42 +03:00
commit fb302b2a2c
51 changed files with 6815 additions and 0 deletions
+59
View File
@@ -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,
}
+42
View File
@@ -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);
+16
View File
@@ -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};
+363
View File
@@ -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);
}
}
+42
View File
@@ -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,
}