community: trim premium backend surfaces

This commit is contained in:
a.tolmachev
2026-05-07 20:17:53 +00:00
parent a3d8040ccc
commit 796def4cfd
9 changed files with 27 additions and 803 deletions
Generated
-104
View File
@@ -10,11 +10,8 @@ dependencies = [
"axum",
"axum-extra",
"base64",
"crank-adapter-grpc",
"crank-adapter-soap",
"crank-core",
"crank-mapping",
"crank-proto",
"crank-registry",
"crank-runtime",
"crank-schema",
@@ -383,20 +380,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crank-adapter-graphql"
version = "0.1.0"
dependencies = [
"axum",
"crank-core",
"crank-mapping",
"reqwest",
"serde",
"serde_json",
"thiserror",
"tokio",
]
[[package]]
name = "crank-adapter-grpc"
version = "0.1.0"
@@ -432,35 +415,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "crank-adapter-soap"
version = "0.1.0"
dependencies = [
"axum",
"crank-core",
"crank-mapping",
"reqwest",
"roxmltree",
"serde",
"serde_json",
"thiserror",
"tokio",
]
[[package]]
name = "crank-adapter-websocket"
version = "0.1.0"
dependencies = [
"crank-core",
"futures-util",
"reqwest",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-tungstenite",
]
[[package]]
name = "crank-core"
version = "0.1.0"
@@ -521,11 +475,7 @@ dependencies = [
"async-trait",
"axum",
"base64",
"crank-adapter-graphql",
"crank-adapter-grpc",
"crank-adapter-rest",
"crank-adapter-soap",
"crank-adapter-websocket",
"crank-core",
"crank-mapping",
"crank-schema",
@@ -538,7 +488,6 @@ dependencies = [
"thiserror",
"time",
"tokio",
"tokio-tungstenite",
"tracing",
"tracing-subscriber",
]
@@ -604,12 +553,6 @@ dependencies = [
"cipher",
]
[[package]]
name = "data-encoding"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
[[package]]
name = "der"
version = "0.7.10"
@@ -2160,12 +2103,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rsa"
version = "0.9.10"
@@ -2894,22 +2831,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tungstenite",
"webpki-roots 0.26.11",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -3108,25 +3029,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand 0.9.2",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.19.0"
@@ -3206,12 +3108,6 @@ dependencies = [
"serde",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8_iter"
version = "1.0.4"
-4
View File
@@ -9,10 +9,6 @@ members = [
"crates/crank-registry",
"crates/crank-runtime",
"crates/crank-adapter-rest",
"crates/crank-adapter-graphql",
"crates/crank-adapter-grpc",
"crates/crank-adapter-websocket",
"crates/crank-adapter-soap",
]
resolver = "3"
-3
View File
@@ -10,11 +10,8 @@ argon2.workspace = true
axum.workspace = true
axum-extra.workspace = true
base64.workspace = true
crank-adapter-grpc = { path = "../../crates/crank-adapter-grpc", features = ["test-support"] }
crank-adapter-soap = { path = "../../crates/crank-adapter-soap" }
crank-core = { path = "../../crates/crank-core" }
crank-mapping = { path = "../../crates/crank-mapping" }
crank-proto = { path = "../../crates/crank-proto" }
crank-registry = { path = "../../crates/crank-registry" }
crank-runtime = { path = "../../crates/crank-runtime" }
crank-schema = { path = "../../crates/crank-schema" }
+2 -27
View File
@@ -29,9 +29,8 @@ use crate::{
operations::{
archive_operation, create_operation, create_version, delete_operation,
export_operation, generate_draft, get_operation, get_operation_version,
list_grpc_services, list_operations, list_soap_services, publish_operation, run_test,
update_operation, upload_descriptor_set, upload_input_json, upload_output_json,
upload_proto_descriptor, upload_wsdl_descriptor, upload_xsd_descriptor,
list_operations, publish_operation, run_test, update_operation, upload_input_json,
upload_output_json,
},
secrets::{create_secret, delete_secret, get_secret, list_secrets, rotate_secret},
streaming::{
@@ -75,30 +74,6 @@ pub fn build_app(state: AppState) -> Router {
"/operations/{operation_id}/samples/output-json",
post(upload_output_json),
)
.route(
"/operations/{operation_id}/descriptors/proto",
post(upload_proto_descriptor),
)
.route(
"/operations/{operation_id}/descriptors/descriptor-set",
post(upload_descriptor_set),
)
.route(
"/operations/{operation_id}/descriptors/wsdl",
post(upload_wsdl_descriptor),
)
.route(
"/operations/{operation_id}/descriptors/xsd",
post(upload_xsd_descriptor),
)
.route(
"/operations/{operation_id}/grpc/services",
get(list_grpc_services),
)
.route(
"/operations/{operation_id}/soap/services",
get(list_soap_services),
)
.route(
"/operations/{operation_id}/drafts/generate",
post(generate_draft),
-120
View File
@@ -1,6 +1,5 @@
use axum::{
Json,
body::Bytes,
extract::{Extension, Path, Query, State},
http::{HeaderMap, StatusCode, header},
response::IntoResponse,
@@ -232,118 +231,6 @@ pub async fn upload_output_json(
})))
}
pub async fn upload_proto_descriptor(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Result<Json<Value>, ApiError> {
let source_name = header_file_name(&headers);
let descriptor = state
.service
.upload_proto_file(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
source_name.as_deref(),
body.as_ref(),
)
.await?;
Ok(Json(json!(descriptor)))
}
pub async fn upload_descriptor_set(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Result<Json<Value>, ApiError> {
let source_name = header_file_name(&headers);
let descriptor = state
.service
.upload_descriptor_set(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
source_name.as_deref(),
body.as_ref(),
)
.await?;
Ok(Json(json!(descriptor)))
}
pub async fn upload_wsdl_descriptor(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Result<Json<Value>, ApiError> {
let source_name = header_file_name(&headers);
let descriptor = state
.service
.upload_wsdl_file(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
source_name.as_deref(),
body.as_ref(),
)
.await?;
Ok(Json(json!(descriptor)))
}
pub async fn upload_xsd_descriptor(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
body: Bytes,
) -> Result<Json<Value>, ApiError> {
let source_name = header_file_name(&headers);
let descriptor = state
.service
.upload_xsd_file(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
source_name.as_deref(),
body.as_ref(),
)
.await?;
Ok(Json(json!(descriptor)))
}
pub async fn list_grpc_services(
Path(path): Path<WorkspaceOperationPath>,
Query(query): Query<ExportQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let services = state
.service
.list_grpc_services(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
query.version,
)
.await?;
Ok(Json(json!({ "services": services })))
}
pub async fn list_soap_services(
Path(path): Path<WorkspaceOperationPath>,
Query(query): Query<ExportQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let services = state
.service
.list_soap_services(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
query.version,
)
.await?;
Ok(Json(json!({ "services": services })))
}
pub async fn generate_draft(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
@@ -394,10 +281,3 @@ pub async fn import_operation(
.await?;
Ok(Json(json!(imported)))
}
fn header_file_name(headers: &HeaderMap) -> Option<String> {
headers
.get("x-file-name")
.and_then(|value| value.to_str().ok())
.map(ToOwned::to_owned)
}
+16 -478
View File
@@ -2,7 +2,6 @@ use std::collections::BTreeMap;
use std::path::PathBuf;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use crank_adapter_soap::{SoapServiceSummary, inspect_wsdl};
use crank_core::{
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AggregationMode,
AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, AuthProfileId, ConfigExport,
@@ -17,7 +16,6 @@ use crank_core::{
Workspace, WorkspaceId, WorkspaceStatus,
};
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
use crank_proto::{ProtoService, services_from_descriptor_set_bytes};
use crank_registry::{
AgentSummary, AgentVersionRecord, AsyncJobFilter, CreateAgentRequest, CreateAsyncJobRequest,
CreateInvitationRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
@@ -26,10 +24,10 @@ use crank_registry::{
OperationAgentRef, OperationSampleMetadata, OperationSummary, OperationUsageSummary,
OperationVersionRecord, Page, PlatformApiKeyRecord, PostgresRegistry, PublishAgentRequest,
PublishRequest, RegistryError, RegistryOperation, RotateSecretRequest, SampleKind,
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
SaveSampleMetadataRequest, StreamSessionFilter, UpdateAsyncJobStatusRequest,
UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery,
UsageSummary, UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord,
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveSampleMetadataRequest,
StreamSessionFilter, UpdateAsyncJobStatusRequest, UpdateWorkspaceRequest,
UsageAgentBreakdown, UsageBucket, UsageOperationBreakdown, UsageQuery, UsageSummary,
UsageTimelinePoint, WorkspaceMembershipRecord, WorkspaceRecord,
};
use crank_runtime::{
PreparedRequest, ResolvedAuth, RuntimeError, RuntimeExecutor, RuntimeOperation,
@@ -696,8 +694,6 @@ pub struct GrpcMethodSummary {
pub output_schema: Schema,
}
pub type SoapServiceCatalog = Vec<SoapServiceSummary>;
fn default_operation_category() -> String {
"general".to_owned()
}
@@ -3265,332 +3261,6 @@ impl AdminService {
})
}
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("descriptor-set.bin")))]
pub async fn upload_descriptor_set(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
source_name: Option<&str>,
payload: &[u8],
) -> Result<DescriptorUploadResponse, ApiError> {
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Grpc {
return Err(ApiError::validation(
"descriptor upload is only allowed for grpc operations",
));
}
let services = services_from_descriptor_set_bytes(payload)
.map_err(|error| ApiError::validation(error.to_string()))?;
let descriptor_id = crank_core::DescriptorId::new(new_prefixed_id("desc"));
let created_at = OffsetDateTime::now_utc();
let storage_ref = self
.storage
.write_descriptor(
operation_id,
summary.current_draft_version,
crank_registry::DescriptorKind::DescriptorSet,
&descriptor_id,
source_name,
payload,
)
.await?;
self.registry
.save_descriptor_metadata(SaveDescriptorMetadataRequest {
descriptor: &crank_registry::DescriptorMetadata {
id: descriptor_id.clone(),
operation_id: Some(operation_id.clone()),
version: Some(summary.current_draft_version),
descriptor_kind: crank_registry::DescriptorKind::DescriptorSet,
storage_ref,
source_name: source_name.map(ToOwned::to_owned),
package_index: Some(
serde_json::to_value(&services)
.map_err(|error| ApiError::internal(error.to_string()))?,
),
created_at,
},
})
.await?;
info!(
operation_id = %operation_id.as_str(),
descriptor_id = %descriptor_id.as_str(),
version = summary.current_draft_version,
"descriptor set uploaded"
);
Ok(DescriptorUploadResponse {
descriptor_id: descriptor_id.as_str().to_owned(),
version: summary.current_draft_version,
})
}
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("schema.proto")))]
pub async fn upload_proto_file(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
source_name: Option<&str>,
payload: &[u8],
) -> Result<DescriptorUploadResponse, ApiError> {
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Grpc {
return Err(ApiError::validation(
"proto upload is only allowed for grpc operations",
));
}
let descriptor_id = crank_core::DescriptorId::new(new_prefixed_id("desc"));
let created_at = OffsetDateTime::now_utc();
let storage_ref = self
.storage
.write_descriptor(
operation_id,
summary.current_draft_version,
crank_registry::DescriptorKind::ProtoUpload,
&descriptor_id,
source_name,
payload,
)
.await?;
self.registry
.save_descriptor_metadata(SaveDescriptorMetadataRequest {
descriptor: &crank_registry::DescriptorMetadata {
id: descriptor_id.clone(),
operation_id: Some(operation_id.clone()),
version: Some(summary.current_draft_version),
descriptor_kind: crank_registry::DescriptorKind::ProtoUpload,
storage_ref,
source_name: source_name.map(ToOwned::to_owned),
package_index: None,
created_at,
},
})
.await?;
info!(
operation_id = %operation_id.as_str(),
descriptor_id = %descriptor_id.as_str(),
version = summary.current_draft_version,
"proto file uploaded"
);
Ok(DescriptorUploadResponse {
descriptor_id: descriptor_id.as_str().to_owned(),
version: summary.current_draft_version,
})
}
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version))]
pub async fn list_grpc_services(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
version: Option<u32>,
) -> Result<Vec<GrpcServiceSummary>, ApiError> {
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Grpc {
return Err(ApiError::validation(
"grpc services are only available for grpc operations",
));
}
let version = version.unwrap_or(summary.current_draft_version);
let descriptor = self
.registry
.list_descriptor_metadata(operation_id, version)
.await?
.into_iter()
.rev()
.find(|descriptor| {
descriptor.descriptor_kind == crank_registry::DescriptorKind::DescriptorSet
})
.ok_or_else(|| {
ApiError::not_found_with_context(
"descriptor set was not uploaded",
json!({
"operation_id": operation_id.as_str(),
"version": version,
"descriptor_kind": "descriptor_set",
}),
)
})?;
let bytes = self.storage.read_bytes(&descriptor.storage_ref).await?;
let services = services_from_descriptor_set_bytes(&bytes)
.map_err(|error| ApiError::validation(error.to_string()))?;
services
.into_iter()
.map(proto_service_summary)
.collect::<Result<Vec<_>, _>>()
}
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("service.wsdl")))]
pub async fn upload_wsdl_file(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
source_name: Option<&str>,
payload: &[u8],
) -> Result<DescriptorUploadResponse, ApiError> {
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Soap {
return Err(ApiError::validation(
"wsdl upload is only allowed for soap operations",
));
}
let services =
inspect_wsdl(payload).map_err(|error| ApiError::validation(error.to_string()))?;
let descriptor_id = crank_core::DescriptorId::new(new_prefixed_id("desc"));
let created_at = OffsetDateTime::now_utc();
let storage_ref = self
.storage
.write_descriptor(
operation_id,
summary.current_draft_version,
crank_registry::DescriptorKind::WsdlUpload,
&descriptor_id,
source_name,
payload,
)
.await?;
self.registry
.save_descriptor_metadata(SaveDescriptorMetadataRequest {
descriptor: &crank_registry::DescriptorMetadata {
id: descriptor_id.clone(),
operation_id: Some(operation_id.clone()),
version: Some(summary.current_draft_version),
descriptor_kind: crank_registry::DescriptorKind::WsdlUpload,
storage_ref,
source_name: source_name.map(ToOwned::to_owned),
package_index: Some(
serde_json::to_value(&services)
.map_err(|error| ApiError::internal(error.to_string()))?,
),
created_at,
},
})
.await?;
info!(
operation_id = %operation_id.as_str(),
descriptor_id = %descriptor_id.as_str(),
version = summary.current_draft_version,
"wsdl uploaded"
);
Ok(DescriptorUploadResponse {
descriptor_id: descriptor_id.as_str().to_owned(),
version: summary.current_draft_version,
})
}
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("schema.xsd")))]
pub async fn upload_xsd_file(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
source_name: Option<&str>,
payload: &[u8],
) -> Result<DescriptorUploadResponse, ApiError> {
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Soap {
return Err(ApiError::validation(
"xsd upload is only allowed for soap operations",
));
}
let descriptor_id = crank_core::DescriptorId::new(new_prefixed_id("desc"));
let created_at = OffsetDateTime::now_utc();
let storage_ref = self
.storage
.write_descriptor(
operation_id,
summary.current_draft_version,
crank_registry::DescriptorKind::XsdUpload,
&descriptor_id,
source_name,
payload,
)
.await?;
self.registry
.save_descriptor_metadata(SaveDescriptorMetadataRequest {
descriptor: &crank_registry::DescriptorMetadata {
id: descriptor_id.clone(),
operation_id: Some(operation_id.clone()),
version: Some(summary.current_draft_version),
descriptor_kind: crank_registry::DescriptorKind::XsdUpload,
storage_ref,
source_name: source_name.map(ToOwned::to_owned),
package_index: None,
created_at,
},
})
.await?;
info!(
operation_id = %operation_id.as_str(),
descriptor_id = %descriptor_id.as_str(),
version = summary.current_draft_version,
"xsd uploaded"
);
Ok(DescriptorUploadResponse {
descriptor_id: descriptor_id.as_str().to_owned(),
version: summary.current_draft_version,
})
}
#[instrument(skip(self), fields(operation_id = %operation_id.as_str(), version))]
pub async fn list_soap_services(
&self,
workspace_id: &WorkspaceId,
operation_id: &OperationId,
version: Option<u32>,
) -> Result<SoapServiceCatalog, ApiError> {
let summary = self.get_operation(workspace_id, operation_id).await?;
if summary.protocol != Protocol::Soap {
return Err(ApiError::validation(
"soap services are only available for soap operations",
));
}
let version = version.unwrap_or(summary.current_draft_version);
let descriptor = self
.registry
.list_descriptor_metadata(operation_id, version)
.await?
.into_iter()
.rev()
.find(|descriptor| {
descriptor.descriptor_kind == crank_registry::DescriptorKind::WsdlUpload
})
.ok_or_else(|| {
ApiError::not_found_with_context(
"wsdl was not uploaded",
json!({
"operation_id": operation_id.as_str(),
"version": version,
"descriptor_kind": "wsdl_upload",
}),
)
})?;
let package_index = descriptor.package_index.ok_or_else(|| {
ApiError::not_found_with_context(
"wsdl inspection metadata is not available",
json!({
"operation_id": operation_id.as_str(),
"version": version,
"descriptor_kind": "wsdl_upload",
}),
)
})?;
serde_json::from_value(package_index).map_err(|error| ApiError::internal(error.to_string()))
}
#[instrument(skip(self))]
pub async fn get_auth_profile(
&self,
@@ -4802,11 +4472,7 @@ fn protocol_capability_view(protocol: Protocol, edition: ProductEdition) -> Prot
.filter(|behavior| protocol.supports_transport_behavior(*behavior))
.collect()
};
let supports_upload_artifacts = match protocol {
Protocol::Rest | Protocol::Graphql | Protocol::Websocket => Vec::new(),
Protocol::Grpc => vec!["proto".to_owned(), "descriptor_set".to_owned()],
Protocol::Soap => vec!["wsdl".to_owned(), "xsd".to_owned()],
};
let supports_upload_artifacts = Vec::new();
ProtocolCapabilityView {
protocol,
@@ -5072,13 +4738,9 @@ fn validate_response_cache_policy(
match target {
Target::Rest(rest_target) if rest_target.method == crank_core::HttpMethod::Get => {}
Target::Graphql(graphql_target)
if graphql_target.operation_type == crank_core::GraphqlOperationType::Query => {}
Target::Grpc(grpc_target) if grpc_target.read_only => {}
_ => {
return Err(ApiError::validation_with_context(
"response cache is supported only for REST GET, GraphQL query, and read-only unary gRPC operations"
.to_owned(),
"response cache is supported only for REST GET operations".to_owned(),
json!({
"field": "execution_config.response_cache",
}),
@@ -5103,8 +4765,7 @@ mod tests {
use std::collections::BTreeMap;
use crank_core::{
ExecutionConfig, GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod,
ResponseCachePolicy, RestTarget, StreamingConfig, Target, TransportBehavior,
ExecutionConfig, HttpMethod, ResponseCachePolicy, RestTarget, Target,
};
use super::validate_response_cache_policy;
@@ -5121,44 +4782,6 @@ mod tests {
}
}
#[test]
fn accepts_response_cache_for_graphql_query() {
let target = Target::Graphql(GraphqlTarget {
endpoint: "http://example.invalid/graphql".to_owned(),
operation_type: GraphqlOperationType::Query,
operation_name: "LookupLead".to_owned(),
query_template:
"query LookupLead($email: String!) { lookupLead(email: $email) { id } }".to_owned(),
response_path: "$.response.body.data.lookupLead".to_owned(),
});
let result = validate_response_cache_policy(&target, &cacheable_execution_config());
assert!(result.is_ok());
}
#[test]
fn rejects_response_cache_for_graphql_mutation() {
let target = Target::Graphql(GraphqlTarget {
endpoint: "http://example.invalid/graphql".to_owned(),
operation_type: GraphqlOperationType::Mutation,
operation_name: "CreateLead".to_owned(),
query_template:
"mutation CreateLead($email: String!) { createLead(email: $email) { id } }"
.to_owned(),
response_path: "$.response.body.data.createLead".to_owned(),
});
let error =
validate_response_cache_policy(&target, &cacheable_execution_config()).unwrap_err();
assert!(matches!(error, crate::error::ApiError::Validation { .. }));
assert_eq!(
error.to_string(),
"response cache is supported only for REST GET, GraphQL query, and read-only unary gRPC operations"
);
}
#[test]
fn accepts_response_cache_for_rest_get() {
let target = Target::Rest(RestTarget {
@@ -5174,32 +4797,14 @@ mod tests {
}
#[test]
fn accepts_response_cache_for_read_only_unary_grpc() {
let target = Target::Grpc(GrpcTarget {
server_addr: "http://127.0.0.1:50051".to_owned(),
package: "echo".to_owned(),
service: "EchoService".to_owned(),
method: "UnaryEcho".to_owned(),
read_only: true,
descriptor_ref: "desc_echo".into(),
descriptor_set_b64: "ZmFrZQ==".to_owned(),
});
let result = validate_response_cache_policy(&target, &cacheable_execution_config());
assert!(result.is_ok());
}
#[test]
fn rejects_response_cache_for_non_read_only_grpc() {
let target = Target::Grpc(GrpcTarget {
server_addr: "http://127.0.0.1:50051".to_owned(),
package: "echo".to_owned(),
service: "EchoService".to_owned(),
method: "UnaryEcho".to_owned(),
read_only: false,
descriptor_ref: "desc_echo".into(),
descriptor_set_b64: "ZmFrZQ==".to_owned(),
fn rejects_response_cache_for_non_rest_target() {
let target = Target::Graphql(crank_core::GraphqlTarget {
endpoint: "http://example.invalid/graphql".to_owned(),
operation_type: crank_core::GraphqlOperationType::Query,
operation_name: "LookupLead".to_owned(),
query_template:
"query LookupLead($email: String!) { lookupLead(email: $email) { id } }".to_owned(),
response_path: "$.response.body.data.lookupLead".to_owned(),
});
let error =
@@ -5208,52 +4813,7 @@ mod tests {
assert!(matches!(error, crate::error::ApiError::Validation { .. }));
assert_eq!(
error.to_string(),
"response cache is supported only for REST GET, GraphQL query, and read-only unary gRPC operations"
);
}
#[test]
fn rejects_response_cache_for_streaming_grpc() {
let target = Target::Grpc(GrpcTarget {
server_addr: "http://127.0.0.1:50051".to_owned(),
package: "echo".to_owned(),
service: "EchoService".to_owned(),
method: "ServerEcho".to_owned(),
read_only: true,
descriptor_ref: "desc_echo".into(),
descriptor_set_b64: "ZmFrZQ==".to_owned(),
});
let mut execution_config = cacheable_execution_config();
execution_config.streaming = Some(StreamingConfig {
mode: crank_core::ExecutionMode::Window,
transport_behavior: TransportBehavior::ServerStream,
window_duration_ms: Some(1_000),
poll_interval_ms: None,
upstream_timeout_ms: Some(1_000),
idle_timeout_ms: None,
max_session_lifetime_ms: None,
max_items: Some(1),
max_bytes: None,
aggregation_mode: crank_core::AggregationMode::RawItems,
summary_path: None,
items_path: None,
cursor_path: None,
status_path: None,
done_path: None,
redacted_paths: Vec::new(),
truncate_item_fields: false,
max_field_length: None,
drop_duplicates: false,
sampling_rate: None,
tool_family: crank_core::ToolFamilyConfig::default(),
});
let error = validate_response_cache_policy(&target, &execution_config).unwrap_err();
assert!(matches!(error, crate::error::ApiError::Validation { .. }));
assert_eq!(
error.to_string(),
"response cache is supported only for unary operations"
"response cache is supported only for REST GET operations"
);
}
}
@@ -5283,25 +4843,3 @@ fn enrich_operation_summary(
agent_refs,
}
}
fn proto_service_summary(service: ProtoService) -> Result<GrpcServiceSummary, ApiError> {
let methods = service
.unary_methods()
.map(|method| {
Ok(GrpcMethodSummary {
name: method.name.clone(),
kind: "unary".to_owned(),
input_schema: crank_proto::message_to_schema(&method.input)
.map_err(|error| ApiError::validation(error.to_string()))?,
output_schema: crank_proto::message_to_schema(&method.output)
.map_err(|error| ApiError::validation(error.to_string()))?,
})
})
.collect::<Result<Vec<_>, ApiError>>()?;
Ok(GrpcServiceSummary {
package: service.package,
service: service.name,
methods,
})
}
+2 -45
View File
@@ -1,7 +1,7 @@
use std::path::{Path, PathBuf};
use crank_core::{DescriptorId, OperationId, SampleId};
use crank_registry::{DescriptorKind, SampleKind};
use crank_core::{OperationId, SampleId};
use crank_registry::SampleKind;
use serde_json::Value;
use thiserror::Error;
use tokio::fs;
@@ -58,40 +58,6 @@ impl LocalArtifactStorage {
Ok(serde_json::from_slice(&bytes)?)
}
pub async fn write_descriptor(
&self,
operation_id: &OperationId,
version: u32,
descriptor_kind: DescriptorKind,
descriptor_id: &DescriptorId,
source_name: Option<&str>,
payload: &[u8],
) -> Result<String, StorageError> {
let file_name = match descriptor_kind {
DescriptorKind::ProtoUpload => source_name.unwrap_or("schema.proto").to_owned(),
DescriptorKind::DescriptorSet => source_name.unwrap_or("descriptor-set.bin").to_owned(),
DescriptorKind::ReflectionSnapshot => {
source_name.unwrap_or("reflection.bin").to_owned()
}
DescriptorKind::WsdlUpload => source_name.unwrap_or("service.wsdl").to_owned(),
DescriptorKind::XsdUpload => source_name.unwrap_or("schema.xsd").to_owned(),
};
let path = self
.root
.join("descriptors")
.join(operation_id.as_str())
.join(format!("v{version}"))
.join(format!("{}_{}", descriptor_id.as_str(), file_name));
write_bytes(&path, payload).await?;
Ok(to_storage_ref(&path))
}
pub async fn read_bytes(&self, storage_ref: &str) -> Result<Vec<u8>, StorageError> {
let path = from_storage_ref(storage_ref)?;
Ok(fs::read(path).await?)
}
}
async fn write_json_file(path: &Path, payload: &Value) -> Result<(), StorageError> {
@@ -104,15 +70,6 @@ async fn write_json_file(path: &Path, payload: &Value) -> Result<(), StorageErro
Ok(())
}
async fn write_bytes(path: &Path, payload: &[u8]) -> Result<(), StorageError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(path, payload).await?;
Ok(())
}
fn to_storage_ref(path: &Path) -> String {
format!("file://{}", path.display())
}
+5 -16
View File
@@ -6,26 +6,17 @@ rust-version.workspace = true
version.workspace = true
[features]
default = [
"protocol-graphql",
"protocol-grpc",
"protocol-soap",
"protocol-websocket",
]
protocol-graphql = ["dep:crank-adapter-graphql"]
protocol-grpc = ["dep:crank-adapter-grpc"]
protocol-soap = ["dep:crank-adapter-soap"]
protocol-websocket = ["dep:crank-adapter-websocket"]
default = []
protocol-graphql = []
protocol-grpc = []
protocol-soap = []
protocol-websocket = []
[dependencies]
aes-gcm.workspace = true
async-trait = "0.1"
base64.workspace = true
crank-adapter-graphql = { path = "../crank-adapter-graphql", optional = true }
crank-adapter-grpc = { path = "../crank-adapter-grpc", optional = true }
crank-adapter-rest = { path = "../crank-adapter-rest" }
crank-adapter-soap = { path = "../crank-adapter-soap", optional = true }
crank-adapter-websocket = { path = "../crank-adapter-websocket", optional = true }
crank-core = { path = "../crank-core" }
crank-mapping = { path = "../crank-mapping" }
crank-schema = { path = "../crank-schema" }
@@ -40,8 +31,6 @@ tracing.workspace = true
[dev-dependencies]
axum.workspace = true
crank-adapter-grpc = { path = "../crank-adapter-grpc", features = ["test-support"] }
futures-util = "0.3"
time.workspace = true
tokio-tungstenite.workspace = true
tracing-subscriber.workspace = true
+2 -6
View File
@@ -824,12 +824,8 @@ fn response_cache_ttl(operation: &RuntimeOperation) -> Option<Duration> {
return None;
}
let is_cacheable_protocol = match &operation.target {
Target::Rest(target) => target.method == HttpMethod::Get,
Target::Graphql(target) => target.operation_type == crank_core::GraphqlOperationType::Query,
Target::Grpc(target) => target.read_only,
_ => false,
};
let is_cacheable_protocol =
matches!(&operation.target, Target::Rest(target) if target.method == HttpMethod::Get);
if !is_cacheable_protocol {
return None;
}