mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 16:37:07 +00:00
feat(extension): add schema contracts (#3384)
This commit is contained in:
Generated
+9
@@ -9360,6 +9360,15 @@ dependencies = [
|
|||||||
"xxhash-rust",
|
"xxhash-rust",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustfs-extension-schema"
|
||||||
|
version = "1.0.0-beta.8"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustfs-filemeta"
|
name = "rustfs-filemeta"
|
||||||
version = "1.0.0-beta.8"
|
version = "1.0.0-beta.8"
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ members = [
|
|||||||
"crates/s3select-query", # S3 Select query engine
|
"crates/s3select-query", # S3 Select query engine
|
||||||
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
"crates/scanner", # Scanner for data integrity checks and health monitoring
|
||||||
"crates/security-governance", # Security governance contracts
|
"crates/security-governance", # Security governance contracts
|
||||||
|
"crates/extension-schema", # Extension schema contracts
|
||||||
"crates/signer", # client signer
|
"crates/signer", # client signer
|
||||||
"crates/storage-api", # Storage API contracts
|
"crates/storage-api", # Storage API contracts
|
||||||
"crates/targets", # Target-specific configurations and utilities
|
"crates/targets", # Target-specific configurations and utilities
|
||||||
@@ -113,6 +114,7 @@ rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-beta.8" }
|
|||||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-beta.8" }
|
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-beta.8" }
|
||||||
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-beta.8" }
|
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-beta.8" }
|
||||||
rustfs-security-governance = { path = "crates/security-governance", version = "1.0.0-beta.8" }
|
rustfs-security-governance = { path = "crates/security-governance", version = "1.0.0-beta.8" }
|
||||||
|
rustfs-extension-schema = { path = "crates/extension-schema", version = "1.0.0-beta.8" }
|
||||||
rustfs-signer = { path = "crates/signer", version = "1.0.0-beta.8" }
|
rustfs-signer = { path = "crates/signer", version = "1.0.0-beta.8" }
|
||||||
rustfs-storage-api = { path = "crates/storage-api", version = "1.0.0-beta.8" }
|
rustfs-storage-api = { path = "crates/storage-api", version = "1.0.0-beta.8" }
|
||||||
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-beta.8" }
|
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-beta.8" }
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "rustfs-extension-schema"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
description = "Extension schema contracts for RustFS."
|
||||||
|
keywords = ["extension", "schema", "contract", "rustfs"]
|
||||||
|
categories = ["web-programming", "development-tools"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub const EXTENSION_SCHEMA_VERSION: &str = "rustfs.extension-schema.v1";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ExtensionKind {
|
||||||
|
TargetPlugin,
|
||||||
|
S3Hook,
|
||||||
|
OpsDiagnostics,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ExtensionRuntimeBoundary {
|
||||||
|
Builtin,
|
||||||
|
Sidecar,
|
||||||
|
Wasm,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtensionRuntimeBoundary {
|
||||||
|
pub const fn requires_disabled_by_default(self) -> bool {
|
||||||
|
matches!(self, Self::Sidecar | Self::Wasm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ExtensionRuntimeContract {
|
||||||
|
pub api_version: String,
|
||||||
|
pub boundary: ExtensionRuntimeBoundary,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct ExtensionCapabilityRef(String);
|
||||||
|
|
||||||
|
impl ExtensionCapabilityRef {
|
||||||
|
pub fn new(capability: impl Into<String>) -> Self {
|
||||||
|
Self(capability.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ExtensionSchema {
|
||||||
|
pub schema_version: String,
|
||||||
|
pub extension_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub provider: String,
|
||||||
|
pub version: String,
|
||||||
|
pub kind: ExtensionKind,
|
||||||
|
pub runtime: ExtensionRuntimeContract,
|
||||||
|
pub capabilities: Vec<ExtensionCapabilityRef>,
|
||||||
|
pub disabled_by_default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, PartialEq, Eq)]
|
||||||
|
pub enum ExtensionSchemaError {
|
||||||
|
#[error("extension schema at index {index} has an empty extension id")]
|
||||||
|
EmptyExtensionId { index: usize },
|
||||||
|
|
||||||
|
#[error("extension schema for {extension_id} has an unsupported schema version {schema_version}")]
|
||||||
|
UnsupportedSchemaVersion { extension_id: String, schema_version: String },
|
||||||
|
|
||||||
|
#[error("extension schema for {extension_id} has an empty display name")]
|
||||||
|
EmptyDisplayName { extension_id: String },
|
||||||
|
|
||||||
|
#[error("extension schema for {extension_id} has an empty provider")]
|
||||||
|
EmptyProvider { extension_id: String },
|
||||||
|
|
||||||
|
#[error("extension schema for {extension_id} has an empty version")]
|
||||||
|
EmptyVersion { extension_id: String },
|
||||||
|
|
||||||
|
#[error("extension schema for {extension_id} has an empty runtime API version")]
|
||||||
|
EmptyRuntimeApiVersion { extension_id: String },
|
||||||
|
|
||||||
|
#[error("extension schema for {extension_id} must declare at least one capability")]
|
||||||
|
EmptyCapabilities { extension_id: String },
|
||||||
|
|
||||||
|
#[error("extension schema for {extension_id} has an empty capability")]
|
||||||
|
EmptyCapability { extension_id: String },
|
||||||
|
|
||||||
|
#[error("extension schema for {extension_id} duplicates capability {capability}")]
|
||||||
|
DuplicateCapability { extension_id: String, capability: String },
|
||||||
|
|
||||||
|
#[error("external extension schema for {extension_id} must be disabled by default")]
|
||||||
|
ExternalMustBeDisabledByDefault { extension_id: String },
|
||||||
|
|
||||||
|
#[error("duplicate extension schema for {extension_id}")]
|
||||||
|
DuplicateExtension { extension_id: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_extension_schemas(schemas: &[ExtensionSchema]) -> Result<(), ExtensionSchemaError> {
|
||||||
|
let mut extension_ids = BTreeSet::new();
|
||||||
|
|
||||||
|
for (index, schema) in schemas.iter().enumerate() {
|
||||||
|
let extension_id = schema.extension_id.trim();
|
||||||
|
if extension_id.is_empty() {
|
||||||
|
return Err(ExtensionSchemaError::EmptyExtensionId { index });
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.schema_version != EXTENSION_SCHEMA_VERSION {
|
||||||
|
return Err(ExtensionSchemaError::UnsupportedSchemaVersion {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
schema_version: schema.schema_version.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.display_name.trim().is_empty() {
|
||||||
|
return Err(ExtensionSchemaError::EmptyDisplayName {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.provider.trim().is_empty() {
|
||||||
|
return Err(ExtensionSchemaError::EmptyProvider {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.version.trim().is_empty() {
|
||||||
|
return Err(ExtensionSchemaError::EmptyVersion {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.runtime.api_version.trim().is_empty() {
|
||||||
|
return Err(ExtensionSchemaError::EmptyRuntimeApiVersion {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.capabilities.is_empty() {
|
||||||
|
return Err(ExtensionSchemaError::EmptyCapabilities {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut capabilities = BTreeSet::new();
|
||||||
|
for capability in &schema.capabilities {
|
||||||
|
if capability.as_str().trim().is_empty() {
|
||||||
|
return Err(ExtensionSchemaError::EmptyCapability {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !capabilities.insert(capability.as_str()) {
|
||||||
|
return Err(ExtensionSchemaError::DuplicateCapability {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
capability: capability.as_str().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema.runtime.boundary.requires_disabled_by_default() && !schema.disabled_by_default {
|
||||||
|
return Err(ExtensionSchemaError::ExternalMustBeDisabledByDefault {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !extension_ids.insert(schema.extension_id.as_str()) {
|
||||||
|
return Err(ExtensionSchemaError::DuplicateExtension {
|
||||||
|
extension_id: schema.extension_id.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
EXTENSION_SCHEMA_VERSION, ExtensionCapabilityRef, ExtensionKind, ExtensionRuntimeBoundary, ExtensionRuntimeContract,
|
||||||
|
ExtensionSchema, ExtensionSchemaError, validate_extension_schemas,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn target_schema(
|
||||||
|
extension_id: &str,
|
||||||
|
capability: &str,
|
||||||
|
boundary: ExtensionRuntimeBoundary,
|
||||||
|
disabled_by_default: bool,
|
||||||
|
) -> ExtensionSchema {
|
||||||
|
ExtensionSchema {
|
||||||
|
schema_version: EXTENSION_SCHEMA_VERSION.to_string(),
|
||||||
|
extension_id: extension_id.to_string(),
|
||||||
|
display_name: "Webhook Target".to_string(),
|
||||||
|
provider: "rustfs".to_string(),
|
||||||
|
version: "1.0.0".to_string(),
|
||||||
|
kind: ExtensionKind::TargetPlugin,
|
||||||
|
runtime: ExtensionRuntimeContract {
|
||||||
|
api_version: "rustfs.extension.v1".to_string(),
|
||||||
|
boundary,
|
||||||
|
},
|
||||||
|
capabilities: vec![ExtensionCapabilityRef::new(capability)],
|
||||||
|
disabled_by_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extension_schema_serializes_stable_json_shape() {
|
||||||
|
let schema = target_schema("rustfs.builtin.webhook", "target.notify.v1", ExtensionRuntimeBoundary::Builtin, false);
|
||||||
|
|
||||||
|
let value = serde_json::to_value(schema).expect("extension schema should serialize");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
value,
|
||||||
|
json!({
|
||||||
|
"schema_version": "rustfs.extension-schema.v1",
|
||||||
|
"extension_id": "rustfs.builtin.webhook",
|
||||||
|
"display_name": "Webhook Target",
|
||||||
|
"provider": "rustfs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"kind": "target_plugin",
|
||||||
|
"runtime": {
|
||||||
|
"api_version": "rustfs.extension.v1",
|
||||||
|
"boundary": "builtin"
|
||||||
|
},
|
||||||
|
"capabilities": ["target.notify.v1"],
|
||||||
|
"disabled_by_default": false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_extension_schema_contracts() {
|
||||||
|
let schemas = [ExtensionSchema {
|
||||||
|
schema_version: EXTENSION_SCHEMA_VERSION.to_string(),
|
||||||
|
extension_id: "rustfs.ops.diagnostics".to_string(),
|
||||||
|
display_name: "Ops Diagnostics".to_string(),
|
||||||
|
provider: "rustfs".to_string(),
|
||||||
|
version: "1.0.0".to_string(),
|
||||||
|
kind: ExtensionKind::OpsDiagnostics,
|
||||||
|
runtime: ExtensionRuntimeContract {
|
||||||
|
api_version: "rustfs.extension.v1".to_string(),
|
||||||
|
boundary: ExtensionRuntimeBoundary::Builtin,
|
||||||
|
},
|
||||||
|
capabilities: vec![ExtensionCapabilityRef::new("ops.diagnostics.v1")],
|
||||||
|
disabled_by_default: false,
|
||||||
|
}];
|
||||||
|
|
||||||
|
assert!(validate_extension_schemas(&schemas).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_external_extensions_enabled_by_default() {
|
||||||
|
let schemas = [target_schema(
|
||||||
|
"external.sidecar.audit",
|
||||||
|
"target.audit.v1",
|
||||||
|
ExtensionRuntimeBoundary::Sidecar,
|
||||||
|
false,
|
||||||
|
)];
|
||||||
|
|
||||||
|
let err = validate_extension_schemas(&schemas).expect_err("external sidecars must start disabled");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
ExtensionSchemaError::ExternalMustBeDisabledByDefault {
|
||||||
|
extension_id: "external.sidecar.audit".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_duplicate_extension_capabilities() {
|
||||||
|
let mut schema = target_schema("rustfs.builtin.webhook", "target.notify.v1", ExtensionRuntimeBoundary::Builtin, false);
|
||||||
|
schema.capabilities.push(ExtensionCapabilityRef::new("target.notify.v1"));
|
||||||
|
let schemas = [schema];
|
||||||
|
|
||||||
|
let err = validate_extension_schemas(&schemas).expect_err("duplicate capabilities should fail validation");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
ExtensionSchemaError::DuplicateCapability {
|
||||||
|
extension_id: "rustfs.builtin.webhook".to_string(),
|
||||||
|
capability: "target.notify.v1".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_duplicate_extension_ids() {
|
||||||
|
let schemas = [
|
||||||
|
target_schema("rustfs.builtin.webhook", "target.notify.v1", ExtensionRuntimeBoundary::Builtin, false),
|
||||||
|
target_schema("rustfs.builtin.webhook", "target.notify.v2", ExtensionRuntimeBoundary::Builtin, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
let err = validate_extension_schemas(&schemas).expect_err("duplicate extension ids should fail validation");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
ExtensionSchemaError::DuplicateExtension {
|
||||||
|
extension_id: "rustfs.builtin.webhook".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unknown_schema_version() {
|
||||||
|
let mut schema = target_schema("rustfs.builtin.webhook", "target.notify.v1", ExtensionRuntimeBoundary::Builtin, false);
|
||||||
|
schema.schema_version = "rustfs.extension-schema.v0".to_string();
|
||||||
|
let schemas = [schema];
|
||||||
|
|
||||||
|
let err = validate_extension_schemas(&schemas).expect_err("unknown schema version should fail validation");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
ExtensionSchemaError::UnsupportedSchemaVersion {
|
||||||
|
extension_id: "rustfs.builtin.webhook".to_string(),
|
||||||
|
schema_version: "rustfs.extension-schema.v0".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_empty_capabilities() {
|
||||||
|
let mut schema = target_schema("rustfs.builtin.webhook", "target.notify.v1", ExtensionRuntimeBoundary::Builtin, false);
|
||||||
|
schema.capabilities.clear();
|
||||||
|
let schemas = [schema];
|
||||||
|
|
||||||
|
let err = validate_extension_schemas(&schemas).expect_err("extension capabilities must be explicit");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
ExtensionSchemaError::EmptyCapabilities {
|
||||||
|
extension_id: "rustfs.builtin.webhook".to_string()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
CRATE_DIR="${ROOT_DIR}/crates/extension-schema"
|
||||||
|
CARGO_TOML="${CRATE_DIR}/Cargo.toml"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'Extension schema boundary check failed: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ -f "$CARGO_TOML" ]] || fail "crates/extension-schema/Cargo.toml is missing"
|
||||||
|
|
||||||
|
grep -qxF ' "crates/extension-schema", # Extension schema contracts' "${ROOT_DIR}/Cargo.toml" ||
|
||||||
|
fail "workspace members must include crates/extension-schema"
|
||||||
|
|
||||||
|
grep -Eq '^rustfs-extension-schema = \{ path = "crates/extension-schema", version = "[^"]+" \}$' "${ROOT_DIR}/Cargo.toml" ||
|
||||||
|
fail "workspace dependencies must include rustfs-extension-schema"
|
||||||
|
|
||||||
|
if grep -E '^(rustfs-|tokio|axum|hyper|reqwest|tower|s3s)[A-Za-z0-9_-]*[[:space:]]*=' "$CARGO_TOML" >/dev/null; then
|
||||||
|
fail "extension-schema must stay a lightweight contract crate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (cd "$ROOT_DIR" && rg -n 'rustfs_targets|rustfs_ecstore|rustfs::|tokio::|axum::|hyper::|reqwest::' crates/extension-schema/src >/dev/null); then
|
||||||
|
fail "extension-schema source must not depend on runtime or implementation crates"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Extension schema boundaries passed."
|
||||||
Reference in New Issue
Block a user