Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ rust-version = "1.95"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sha1 = "0.10"
async-trait = "0.1"
semver = "1"
thiserror = "2"

[profile.release]
lto = "fat"
Expand Down
3 changes: 3 additions & 0 deletions rust/crates/adc-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ rust-version.workspace = true
serde = { workspace = true }
serde_json = { workspace = true }
sha1 = { workspace = true }
async-trait = { workspace = true }
semver = { workspace = true }
thiserror = { workspace = true }
36 changes: 36 additions & 0 deletions rust/crates/adc-sdk/src/backend/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::error::Error as StdError;

/// Failure modes shared by every `Backend` implementation. The TS codebase had
/// no equivalent taxonomy — call sites threw bare `Error`/`AxiosError` values
/// or stuffed an `Error` into a result struct's `error?` field — so this is a
/// new design surface, not a port.
///
/// Concrete backends (apisix, api7, apisix-standalone) map their own
/// transport/serialization errors into these variants; anything that doesn't
/// fit a specific variant goes through `Other`.
#[derive(Debug, thiserror::Error)]
pub enum BackendError {
#[error("network request failed: {0}")]
Transport(String),

#[error("authentication failed: {0}")]
Auth(String),

#[error("resource not found: {0}")]
NotFound(String),

/// The backend service reached us and responded, but rejected the
/// request or reported failure at the application level (as opposed to a
/// transport-level failure, which is `Transport`).
#[error("backend rejected the request (status {status}): {message}")]
Api { status: u16, message: String },

#[error("failed to (de)serialize backend payload: {0}")]
Serialization(String),

#[error("operation not supported by this backend: {0}")]
Unsupported(String),

#[error(transparent)]
Other(#[from] Box<dyn StdError + Send + Sync>),
}
102 changes: 102 additions & 0 deletions rust/crates/adc-sdk/src/backend/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! The `Backend` trait: the interface every gateway integration (apisix,
//! api7, apisix-standalone) implements, and the shared result/error types
//! that flow across it. `adc-sdk` only defines the contract — concrete
//! implementations live in their own crates and depend on this one.

mod error;

pub use error::BackendError;

use async_trait::async_trait;
use semver::Version;

use crate::{DefaultValue, Event, ResourceType, resources::Configuration};

/// Static, non-behavioral facts about a `Backend` implementation, used by the
/// CLI to scope log output (e.g. `[apisix]`) without the trait needing a
/// `name()`-shaped method per concern.
#[derive(Debug, Clone, Default)]
pub struct BackendMetadata {
pub log_scope: Vec<String>,
}

#[derive(Debug, Clone, Default)]
pub struct BackendSyncOptions {
pub concurrent: Option<usize>,
pub exit_on_failure: Option<bool>,
}

#[derive(Debug)]
pub struct BackendSyncResult {
pub success: bool,
pub event: Event,
pub error: Option<BackendError>,
pub server: Option<String>,
}

#[derive(Debug, Clone)]
pub struct BackendValidationError {
pub resource_type: ResourceType,
pub resource_id: Option<String>,
pub resource_name: Option<String>,
pub index: usize,
pub error: String,
pub event: Option<Event>,
}

#[derive(Debug, Clone, Default)]
pub struct BackendValidateResult {
pub success: bool,
pub error_message: Option<String>,
pub errors: Vec<BackendValidationError>,
}

/// A gateway integration. Implementations own their own connection state
/// (HTTP client, credentials, target server) — none of that is threaded
/// through trait methods here, since it varies by backend (e.g.
/// apisix-standalone has no notion of a remote server to `ping`, only a
/// local cache to read/write).
///
/// `dyn Backend` is the CLI's dispatch mechanism for "which backend did the
/// user configure", so methods stay object-safe (boxed futures via
/// `#[async_trait]`, no generics).
///
/// Not carried over from the TS `Backend` interface: `on(eventType, cb)`
/// event subscription for task-progress/debug-request events. That was a
/// hand-rolled pub-sub built to feed the CLI's listr2 progress renderer and
/// axios debug logging. Its two jobs map directly onto `tracing`
/// instrumentation instead — `TASK_START`/`TASK_DONE` become a `tracing`
/// span's enter/exit, `AXIOS_DEBUG` becomes a `tracing::debug!` call at the
/// request site — so implementations emit spans/events directly rather than
/// through a bespoke bus on this trait.
#[async_trait]
pub trait Backend: Send + Sync {
fn metadata(&self) -> BackendMetadata;

async fn ping(&self) -> Result<(), BackendError>;

async fn version(&self) -> Result<Version, BackendError>;

async fn default_value(&self) -> Result<DefaultValue, BackendError>;

async fn dump(&self) -> Result<Configuration, BackendError>;

/// Applies `events` and reports one result per event. The overall call
/// doesn't fail as a whole — a partial or total failure is expressed as
/// individual `BackendSyncResult`s with `success: false`, mirroring how
/// sync is inherently a batch of independent operations rather than one
/// atomic unit. Concurrency (per `opts.concurrent`) is an implementation
/// detail of each backend, not something the trait signature encodes.
async fn sync(&self, events: Vec<Event>, opts: BackendSyncOptions) -> Vec<BackendSyncResult>;

/// Not every backend can pre-validate events against the remote server
/// before applying them; the default rejects with `Unsupported`,
/// matching the TS interface's `validate?` being absent.
async fn validate(&self, _events: &[Event]) -> Result<BackendValidateResult, BackendError> {
Err(BackendError::Unsupported("validate".into()))
}

async fn support_stream_route(&self) -> Result<bool, BackendError> {
Ok(false)
}
}
15 changes: 15 additions & 0 deletions rust/crates/adc-sdk/src/default_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::collections::HashMap;

use serde_json::Value;

use crate::resource::ResourceType;

/// Per-resource-type and per-plugin default values, merged into local
/// configuration before diffing so that a value matching the backend's
/// default doesn't show up as a spurious change. Fetched from the backend
/// (`Backend::default_value`) and fed into the differ.
#[derive(Debug, Clone, Default)]
pub struct DefaultValue {
pub core: HashMap<ResourceType, Value>,
pub plugins: HashMap<String, Value>,
}
26 changes: 12 additions & 14 deletions rust/crates/adc-sdk/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
//! ADC's core data model: resource type definitions, the typed resource
//! layer for parsing declarative configuration (`resources` module), differ
//! event types shared with backend/CLI consumers, and a generic JSON
//! value-diff utility.
//! event types shared with backend/CLI consumers, a generic JSON value-diff
//! utility, and the `Backend` trait implemented by each gateway integration.
//!
//! Not yet here: semantic validation (cross-field rules, regex, min/max) on
//! top of the `resources` types, a `Backend` trait, and JSON Schema export.
//! The differ's own field-metadata table lives in `adc-differ` instead of
//! here, since nothing outside the differ consumes it.
//! top of the `resources` types, and JSON Schema export. The differ's own
//! field-metadata table lives in `adc-differ` instead of here, since nothing
//! outside the differ consumes it.

pub mod backend;
pub mod default_value;
pub mod event;
pub mod resource;
pub mod resources;
pub mod utils;
pub mod value_diff;

pub use backend::{
Backend, BackendError, BackendMetadata, BackendSyncOptions, BackendSyncResult, BackendValidateResult,
BackendValidationError,
};
pub use default_value::DefaultValue;
pub use event::{Event, EventKind, EventType};
pub use resource::{FieldListType, ResourceType};
pub use value_diff::{DiffPath, PathSegment, ValueDiff, diff_value};
Expand All @@ -29,12 +36,3 @@ use serde_json::{Map, Value};
/// Distinct from `resources::InternalConfiguration`, the typed counterpart
/// used to parse and validate declarative configuration.
pub type InternalConfiguration = Map<String, Value>;

/// Per-resource-type and per-plugin default values, merged into local
/// configuration before diffing so that a value matching the backend's
/// default doesn't show up as a spurious change.
#[derive(Debug, Clone, Default)]
pub struct DefaultValue {
pub core: std::collections::HashMap<ResourceType, Value>,
pub plugins: std::collections::HashMap<String, Value>,
}