diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9a577bd7..1e9bfb5e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -23,9 +23,12 @@ dependencies = [ name = "adc-sdk" version = "0.1.0" dependencies = [ + "async-trait", + "semver", "serde", "serde_json", "sha1", + "thiserror", ] [[package]] @@ -62,6 +65,17 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1194,6 +1208,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 5bbdad46..cbd5e78e 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -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" diff --git a/rust/crates/adc-sdk/Cargo.toml b/rust/crates/adc-sdk/Cargo.toml index 665917e8..38f4e9ad 100644 --- a/rust/crates/adc-sdk/Cargo.toml +++ b/rust/crates/adc-sdk/Cargo.toml @@ -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 } diff --git a/rust/crates/adc-sdk/src/backend/error.rs b/rust/crates/adc-sdk/src/backend/error.rs new file mode 100644 index 00000000..c8950ba5 --- /dev/null +++ b/rust/crates/adc-sdk/src/backend/error.rs @@ -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), +} diff --git a/rust/crates/adc-sdk/src/backend/mod.rs b/rust/crates/adc-sdk/src/backend/mod.rs new file mode 100644 index 00000000..9a9857b0 --- /dev/null +++ b/rust/crates/adc-sdk/src/backend/mod.rs @@ -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, +} + +#[derive(Debug, Clone, Default)] +pub struct BackendSyncOptions { + pub concurrent: Option, + pub exit_on_failure: Option, +} + +#[derive(Debug)] +pub struct BackendSyncResult { + pub success: bool, + pub event: Event, + pub error: Option, + pub server: Option, +} + +#[derive(Debug, Clone)] +pub struct BackendValidationError { + pub resource_type: ResourceType, + pub resource_id: Option, + pub resource_name: Option, + pub index: usize, + pub error: String, + pub event: Option, +} + +#[derive(Debug, Clone, Default)] +pub struct BackendValidateResult { + pub success: bool, + pub error_message: Option, + pub errors: Vec, +} + +/// 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; + + async fn default_value(&self) -> Result; + + async fn dump(&self) -> Result; + + /// 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, opts: BackendSyncOptions) -> Vec; + + /// 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 { + Err(BackendError::Unsupported("validate".into())) + } + + async fn support_stream_route(&self) -> Result { + Ok(false) + } +} diff --git a/rust/crates/adc-sdk/src/default_value.rs b/rust/crates/adc-sdk/src/default_value.rs new file mode 100644 index 00000000..5cd647c2 --- /dev/null +++ b/rust/crates/adc-sdk/src/default_value.rs @@ -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, + pub plugins: HashMap, +} diff --git a/rust/crates/adc-sdk/src/lib.rs b/rust/crates/adc-sdk/src/lib.rs index cfb7f756..ea943612 100644 --- a/rust/crates/adc-sdk/src/lib.rs +++ b/rust/crates/adc-sdk/src/lib.rs @@ -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}; @@ -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; - -/// 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, - pub plugins: std::collections::HashMap, -}