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
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
//! Mirrors `libs/sdk/src/core/differ.ts`'s `RESOURCE_DIFFER_META` table.
//! Per-resource-type metadata the differ needs: which `InternalConfiguration`
//! field holds the resource's collection, how to derive an id/name for a
//! local (not-yet-synced) item, and each field's merge/diff strategy.
//!
//! In TS, the per-field entries (`fields`) are derived at runtime from Zod
//! schema `.meta()` annotations via `readFieldMeta()`. This Rust port skips
//! reflecting over a schema layer (see the crate-level "staged build" note)
//! and instead hand-transcribes the same annotations directly from
//! `libs/sdk/src/core/schema.ts`, which remains the single source of truth —
//! if that file's `withDifferMeta(...)` calls change, this table must be
//! updated to match.
//! The per-field entries (`fields`) are hand-transcribed here rather than
//! derived by reflecting over `adc_sdk::resources`' struct definitions at
//! runtime — if a resource's shape changes there, this table must be updated
//! to match by hand.

use adc_sdk::ResourceType;
use adc_sdk::utils::generate_id;
use serde_json::Value;

use crate::field_meta::FieldMeta;
use crate::resource::ResourceType;
use crate::utils::generate_id;

fn str_field<'a>(item: &'a Value, key: &str) -> &'a str {
item.get(key).and_then(Value::as_str).unwrap_or_default()
Expand Down Expand Up @@ -43,7 +42,8 @@ pub struct ResourceDifferMeta {
/// Resolve which `ResourceType` to use for default-value lookup.
/// Only needed for `Service`, which may be a stream service.
pub resolve_default_type: Option<fn(&Value) -> ResourceType>,
/// Per-field merge strategies, hand-transcribed from `schema.ts` (see module docs).
/// Per-field merge strategies, hand-transcribed independently of
/// `adc_sdk::resources`' struct definitions (see module docs).
pub fields: &'static [(&'static str, FieldMeta)],
}

Expand Down
24 changes: 12 additions & 12 deletions rust/crates/adc-differ/src/differ_v4.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
//! Port of `libs/differ/src/differv4.ts`'s `DifferV4`. Only v4 is ported;
//! the TS side also has an older v3 algorithm, gated behind an env var and
//! kept only as a fallback, which this crate does not implement.
//! The resource-diffing algorithm: compares local (desired) and remote
//! (actual) configuration and produces an ordered list of create/update/
//! delete events describing how to reconcile them.

use std::collections::{HashMap, HashSet};

use adc_sdk::{
CollectionKind, DefaultValue, Event, EventType, FieldMeta, InternalConfiguration,
ResourceDifferMeta, ResourceType, diff_value, differ_meta, utils::generate_id,
};
use adc_sdk::{DefaultValue, Event, EventType, InternalConfiguration, ResourceType, diff_value, utils::generate_id};
use serde_json::{Map, Value, json};

/// (name, id, item) — mirrors differv4.ts's `ResourceTuple`.
use crate::differ_meta::{CollectionKind, ResourceDifferMeta, differ_meta};
use crate::field_meta::FieldMeta;

/// (name, id, item) extracted from one resource's raw JSON representation.
type ResourceTuple = (String, String, Value);

pub struct DifferV4 {
Expand All @@ -37,8 +37,8 @@ impl DifferV4 {
result.extend(differ.diff_resource(resource_type, &meta, local_tuples, remote_tuples));
}

// Unwrap one level of subEvents (mirrors differv4.ts's post-loop flatten) and
// drop ONLY_SUB_EVENTS placeholder events, which exist only to carry subEvents up.
// Unwrap one level of subEvents and drop ONLY_SUB_EVENTS placeholder
// events, which exist only to carry subEvents up to this point.
let mut unwrapped: Vec<Event> = Vec::new();
for mut event in result {
let subs = std::mem::take(&mut event.sub_events);
Expand Down Expand Up @@ -443,8 +443,8 @@ fn set_key(v: &mut Value, key: &str, value: Value) {
}

/// Event ordering table: deletions precede creates, SSL creates precede routes
/// (SSL may be referenced by upstream mTLS and must exist first).
/// Mirrors differv4.ts's `order` table. Missing combos sort last (JS `?? Infinity`).
/// (SSL may be referenced by upstream mTLS and must exist first). Combos not
/// listed here sort last.
fn order_priority(resource_type: ResourceType, event_type: EventType) -> u32 {
use EventType::*;
use ResourceType::*;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
/// Mirrors the `FieldMeta` union in `libs/sdk/src/core/field-registry.ts`.
/// Per-field merge/diff strategy: how the differ should compare and combine
/// a given resource field when computing an update event.
///
/// In TS this is attached to a Zod schema field via `.meta()`/`withDifferMeta()`
/// and later read back with `readFieldMeta()`. Since this Rust port skips the
/// full Zod-equivalent schema layer (see adc-sdk crate docs), the per-resource
/// field tables are hand-written directly in `differ_meta.rs` instead of being
/// derived from a schema at runtime — the source of truth (schema.ts) is still
/// the same, just read manually rather than reflected.
/// These strategies are hand-written into each resource's metadata table in
/// `differ_meta.rs`, independently of the field definitions in `adc_sdk::resources`
/// — the two must be kept in sync by hand when a resource's shape changes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FieldMeta {
Map {
Expand All @@ -17,8 +15,10 @@ pub enum FieldMeta {
config_key: Option<&'static str>,
},
ObjectMap,
// No resource currently declares an Atomic field — kept for completeness
// of the merge-strategy vocabulary, not because anything constructs it.
#[allow(dead_code)]
Atomic {
#[allow(dead_code)]
strip: bool,
},
Array {
Expand Down
2 changes: 2 additions & 0 deletions rust/crates/adc-differ/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod differ_meta;
pub mod differ_v4;
mod field_meta;

pub use differ_v4::DifferV4;
10 changes: 5 additions & 5 deletions rust/crates/adc-sdk/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde_json::Value;
use crate::resource::ResourceType;
use crate::value_diff::ValueDiff;

/// Mirrors `EventType` in `libs/sdk/src/core/differ.ts`.
/// The kind of change a differ event represents.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
Expand All @@ -15,12 +15,12 @@ pub enum EventType {
OnlySubEvents,
}

/// Mirrors `Event<T>` in `libs/sdk/src/core/differ.ts`.
/// A single detected change between local and remote configuration for one resource.
///
/// `sub_events` is populated while the differ is building nested events and is
/// always cleared (mirroring TS's `unset(event, 'subEvents')`) before the final
/// flattened list is returned from `DifferV4::diff`, so it is not part of the
/// crate's public "result" contract even though it stays on the struct.
/// always cleared before the final flattened list is returned from
/// `DifferV4::diff`, so it is not part of the crate's public "result" contract
/// even though it stays on the struct.
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Event {
pub resource_type: ResourceType,
Expand Down
40 changes: 19 additions & 21 deletions rust/crates/adc-sdk/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
//! Minimal Rust port of `@api7/adc-sdk`, scoped to exactly what `adc-differ`
//! needs: resource/event type definitions, the differ field-metadata table,
//! and a generic JSON value-diff utility (replacing the TS side's
//! `datum-diff` dependency).
//! 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.
//!
//! What's deliberately not here: input validation (a `validator`-crate
//! equivalent of `libs/sdk/src/core/schema.ts`'s Zod refinements), a
//! `Backend` trait, and `schemars` JSON Schema export. Resource bodies are
//! plain `serde_json::Value` rather than per-resource typed structs — see
//! the note on `InternalConfiguration` below for why.
//! 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.

pub mod differ_meta;
pub mod event;
pub mod field_meta;
pub mod resource;
pub mod resources;
pub mod utils;
pub mod value_diff;

pub use differ_meta::{CollectionKind, ResourceDifferMeta, differ_meta};
pub use event::{Event, EventType};
pub use field_meta::FieldMeta;
pub use resource::{FieldListType, ResourceType};
pub use value_diff::{DiffPath, PathSegment, ValueDiff, diff_value};

use serde_json::{Map, Value};

/// Mirrors `InternalConfiguration` in `libs/sdk/src/core/schema.ts`.
/// The differ's working representation of a full configuration: a plain
/// `Map<String, Value>` keyed by config field name (`services`, `routes`,
/// `global_rules`, ...). The differ algorithm treats resource bodies as
/// opaque structural values rather than strongly-typed ones, since diffing
/// is a generic structural operation independent of any one resource's shape.
///
/// Unlike the TS side, this is a plain `Map<String, Value>` keyed by config
/// field name (`services`, `routes`, `global_rules`, ...) rather than a
/// strongly-typed struct — see the crate-level docs for why: the differ
/// algorithm itself treats resource bodies as opaque structural values, so a
/// generic `Value`-based representation is both the lowest-risk and most
/// faithful translation of `differv4.ts`'s actual (dynamically-typed) behavior.
/// Distinct from `resources::InternalConfiguration`, the typed counterpart
/// used to parse and validate declarative configuration.
pub type InternalConfiguration = Map<String, Value>;

/// Mirrors `DefaultValue` in `libs/sdk/src/core/differ.ts`.
/// 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>,
Expand Down
2 changes: 1 addition & 1 deletion rust/crates/adc-sdk/src/resource.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// Mirrors `libs/sdk/src/core/resource.ts`.
/// The kinds of resources ADC manages.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ResourceType {
Route,
Expand Down
36 changes: 36 additions & 0 deletions rust/crates/adc-sdk/src/resources/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Types shared across multiple resource definitions.

use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;

/// A resource's labels: each value is a single string or a list of strings.
pub type Labels = HashMap<String, LabelValue>;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum LabelValue {
Single(String),
Multiple(Vec<String>),
}

/// An arbitrary, unvalidated plugin configuration object — its shape depends
/// entirely on which plugin it configures, so it's kept structurally open
/// (no `deny_unknown_fields`) rather than typed field-by-field.
pub type Plugin = serde_json::Map<String, Value>;

/// A plugin name to configuration map.
pub type Plugins = serde_json::Map<String, Value>;

/// An APISIX condition expression: an arbitrary nested array structure
/// evaluated by the gateway at request time.
pub type Expr = Vec<Value>;

/// Connect/send/read timeouts in seconds, shared by upstream and route configs.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Timeout {
pub connect: f64,
pub send: f64,
pub read: f64,
}
59 changes: 59 additions & 0 deletions rust/crates/adc-sdk/src/resources/consumer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! The `Consumer`, `ConsumerCredential` and `ConsumerGroup` resources.

use serde::{Deserialize, Serialize};

use super::common::{Labels, Plugin, Plugins};

/// A credential attached to a consumer (e.g. an API key or JWT secret).
/// `type` is kept as a plain string rather than a closed enum: the 4-value
/// restriction ("key-auth"/"basic-auth"/"jwt-auth"/"hmac-auth") is a semantic
/// rule for the validation layer, not encoded here.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConsumerCredential {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Labels>,

#[serde(rename = "type")]
pub r#type: String,
pub config: Plugin,
}

/// A consumer, identified by `username` rather than `name`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Consumer {
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Labels>,

#[serde(skip_serializing_if = "Option::is_none")]
pub plugins: Option<Plugins>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credentials: Option<Vec<ConsumerCredential>>,
}

/// A named group of consumers sharing plugin configuration.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConsumerGroup {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Labels>,

#[serde(skip_serializing_if = "Option::is_none")]
pub plugins: Option<Plugins>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consumers: Option<Vec<Consumer>>,
}
90 changes: 90 additions & 0 deletions rust/crates/adc-sdk/src/resources/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! The typed ADC resource model. This *is* the resource definition — the
//! shape the CLI parses local YAML/JSON declarative configuration into.
//!
//! Fields with a default value deserialize with that default already filled
//! in (see the doc comment on `upstream.rs` for the fields this applies to)
//! — parsing is active, not a passive shape description: a field that's
//! missing from input but has a default is still populated on the result.
//!
//! Not yet covered: semantic validation (cross-field rules, regex, min/max),
//! which is a separate, later pass on top of this same model.
//!
//! Naming note: distinct from `crate::resource` (the differ's `ResourceType`/
//! `FieldListType` metadata enums) and from `crate::InternalConfiguration`
//! (the `Map<String, Value>` alias `adc-differ` operates on) — different
//! modules, different concerns, no relation beyond sharing this crate.

pub mod common;
pub mod consumer;
pub mod route;
pub mod service;
pub mod ssl;
pub mod upstream;

pub use common::{Expr, Labels, LabelValue, Plugin, Plugins, Timeout};
pub use consumer::{Consumer, ConsumerCredential, ConsumerGroup};
pub use route::{HttpMethod, Route, StreamRoute};
pub use service::{Service, ServiceRoutes};
pub use ssl::{SSL, SSLCertificate, SslClient, SslProtocol, SslType};
pub use upstream::{
Upstream, UpstreamBalancer, UpstreamHealthCheck, UpstreamHealthCheckActive,
UpstreamHealthCheckActiveHealthy, UpstreamHealthCheckActiveUnhealthy, UpstreamHealthCheckPassive,
UpstreamHealthCheckPassiveHealthy, UpstreamHealthCheckPassiveUnhealthy, UpstreamHealthCheckType,
UpstreamKeepalivePool, UpstreamNode, UpstreamPassHost, UpstreamScheme, UpstreamTls,
};

use serde::{Deserialize, Serialize};

/// A global rule is just a plugin config map applied gateway-wide.
pub type GlobalRule = Plugins;
/// Metadata (shared config) for a plugin, keyed by plugin name.
pub type PluginMetadata = Plugins;

/// The external, user-facing declarative config file shape: nested
/// sub-resources embedded under their parent, no top-level
/// routes/upstreams/consumer_credentials.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Configuration {
#[serde(skip_serializing_if = "Option::is_none")]
pub services: Option<Vec<Service>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssls: Option<Vec<SSL>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consumers: Option<Vec<Consumer>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consumer_groups: Option<Vec<ConsumerGroup>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub global_rules: Option<GlobalRule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plugin_metadata: Option<PluginMetadata>,
}

/// The flattened internal representation: adds top-level
/// routes/stream_routes/consumer_credentials/upstreams alongside the nested
/// sub-resources, so every resource is also reachable directly by its own
/// collection field.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InternalConfiguration {
#[serde(skip_serializing_if = "Option::is_none")]
pub services: Option<Vec<Service>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssls: Option<Vec<SSL>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consumers: Option<Vec<Consumer>>,

#[serde(skip_serializing_if = "Option::is_none")]
pub global_rules: Option<GlobalRule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plugin_metadata: Option<PluginMetadata>,

#[serde(skip_serializing_if = "Option::is_none")]
pub routes: Option<Vec<Route>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_routes: Option<Vec<StreamRoute>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub consumer_credentials: Option<Vec<ConsumerCredential>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub upstreams: Option<Vec<Upstream>>,
}
Loading