feat(desktop): add redis gui foundation baseline

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Senior Frontend Engineer
2026-03-28 10:39:40 +00:00
commit 8d50243d36
56 changed files with 17423 additions and 0 deletions

7
crates/redis-core/Cargo.lock generated Normal file
View File

@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "redis-core"
version = "0.1.0"

View File

@@ -0,0 +1,9 @@
[package]
name = "redis-core"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
serde = { workspace = true }
thiserror = { workspace = true }

View File

@@ -0,0 +1,54 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BackendBootstrap {
pub app_name: String,
pub surface: EntrySurface,
pub persistence: PersistenceBoundary,
pub command_execution: String,
pub key_browsing: String,
pub next_backend_milestone: String,
}
impl BackendBootstrap {
pub fn for_desktop() -> Self {
Self {
app_name: "Redis GUI Foundation".to_string(),
surface: EntrySurface::Desktop,
persistence: PersistenceBoundary {
connection_catalog: "not_configured".to_string(),
secrets: SecretHandling::MemoryOnly.as_str().to_string(),
},
command_execution: "single_instance_enabled".to_string(),
key_browsing: "scan_metadata_enabled".to_string(),
next_backend_milestone: "value_surface_and_catalog_persistence".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EntrySurface {
Desktop,
Cli,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PersistenceBoundary {
pub connection_catalog: String,
pub secrets: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SecretHandling {
MemoryOnly,
}
impl SecretHandling {
pub fn as_str(self) -> &'static str {
match self {
Self::MemoryOnly => "memory_only",
}
}
}

View File

@@ -0,0 +1,238 @@
use serde::{Deserialize, Serialize};
use crate::BackendError;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ConnectionProfileDraft {
pub display_name: String,
pub target: ConnectionTarget,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ConnectionTarget {
pub host: String,
pub port: u16,
pub database: i64,
pub username: Option<String>,
pub tls_mode: TlsMode,
}
impl Default for ConnectionTarget {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 6379,
database: 0,
username: None,
tls_mode: TlsMode::Disabled,
}
}
}
impl ConnectionTarget {
pub fn validate(&self) -> Result<(), BackendError> {
if self.host.trim().is_empty() {
return Err(BackendError::invalid_connection_config(
"Redis host must not be empty.",
));
}
if self.database < 0 {
return Err(BackendError::invalid_connection_config(
"Redis database must be zero or a positive integer.",
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisConnectionRequest {
pub target: ConnectionTarget,
pub password: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ConnectionTestResult {
pub selected_database: i64,
pub authenticated_as: Option<String>,
pub round_trip_status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisCommandRequest {
pub connection: RedisConnectionRequest,
pub command: String,
#[serde(default)]
pub arguments: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CommandExecutionResult {
pub command: String,
pub arguments: Vec<String>,
pub database: i64,
pub response: RedisResponse,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisKeyBrowseRequest {
pub connection: RedisConnectionRequest,
#[serde(default = "default_scan_cursor")]
pub cursor: String,
#[serde(default)]
pub pattern: Option<String>,
#[serde(default = "default_scan_page_size")]
pub page_size: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisKeyBrowseResult {
pub database: i64,
pub submitted_cursor: String,
pub next_cursor: String,
pub has_more: bool,
pub page_size: u32,
pub pattern: Option<String>,
pub keys: Vec<RedisKeyMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisKeyMetadataRequest {
pub connection: RedisConnectionRequest,
pub key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisValueReadRequest {
pub connection: RedisConnectionRequest,
pub key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisValueWriteRequest {
pub connection: RedisConnectionRequest,
pub key: String,
pub value: RedisValueWriteInput,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisKeyTtlUpdateRequest {
pub connection: RedisConnectionRequest,
pub key: String,
pub operation: RedisKeyTtlUpdate,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisKeyMetadata {
pub name: String,
pub database: i64,
pub exists: bool,
pub key_type: String,
pub ttl: RedisKeyTtl,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RedisKeyTtl {
Persistent,
Missing,
ExpiresInMillis { value: i64 },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisValueRecord {
pub metadata: RedisKeyMetadata,
pub data: RedisValueData,
pub write_capability: RedisValueWriteCapability,
pub ttl_write_supported: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisKeyTtlUpdateResult {
pub metadata: RedisKeyMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisFieldValueEntry {
pub field: RedisValueContent,
pub value: RedisValueContent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisSortedSetEntry {
pub member: RedisValueContent,
pub score: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RedisStreamEntry {
pub id: String,
pub fields: Vec<RedisFieldValueEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RedisValueContent {
String { value: String },
Binary { bytes: Vec<u8> },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RedisValueData {
Missing,
String { value: RedisValueContent },
Hash { entries: Vec<RedisFieldValueEntry> },
List { items: Vec<RedisValueContent> },
Set { members: Vec<RedisValueContent> },
SortedSet { entries: Vec<RedisSortedSetEntry> },
Stream { entries: Vec<RedisStreamEntry> },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RedisValueWriteInput {
String { value: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RedisValueWriteCapability {
None,
ReplaceString,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RedisKeyTtlUpdate {
Persist,
ExpiresInMillis { value: u64 },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RedisResponse {
Null,
Integer { value: i64 },
String { value: String },
Binary { bytes: Vec<u8> },
Array { items: Vec<RedisResponse> },
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TlsMode {
Disabled,
Preferred,
Required,
}
fn default_scan_cursor() -> String {
"0".to_string()
}
fn default_scan_page_size() -> u32 {
100
}

View File

@@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BackendErrorCode {
InvalidConnectionConfig,
UnsupportedTlsMode,
UnsupportedOperation,
ConnectionFailed,
AuthenticationFailed,
CommandFailed,
Internal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Error)]
#[error("{message}")]
pub struct BackendError {
pub code: BackendErrorCode,
pub message: String,
pub detail: Option<String>,
}
impl BackendError {
pub fn invalid_connection_config(message: impl Into<String>) -> Self {
Self::new(BackendErrorCode::InvalidConnectionConfig, message, None)
}
pub fn unsupported_tls_mode(message: impl Into<String>) -> Self {
Self::new(BackendErrorCode::UnsupportedTlsMode, message, None)
}
pub fn connection_failed(message: impl Into<String>, detail: Option<String>) -> Self {
Self::new(BackendErrorCode::ConnectionFailed, message, detail)
}
pub fn unsupported_operation(message: impl Into<String>, detail: Option<String>) -> Self {
Self::new(BackendErrorCode::UnsupportedOperation, message, detail)
}
pub fn authentication_failed(message: impl Into<String>, detail: Option<String>) -> Self {
Self::new(BackendErrorCode::AuthenticationFailed, message, detail)
}
pub fn command_failed(message: impl Into<String>, detail: Option<String>) -> Self {
Self::new(BackendErrorCode::CommandFailed, message, detail)
}
pub fn internal(message: impl Into<String>, detail: Option<String>) -> Self {
Self::new(BackendErrorCode::Internal, message, detail)
}
fn new(code: BackendErrorCode, message: impl Into<String>, detail: Option<String>) -> Self {
Self {
code,
message: message.into(),
detail,
}
}
}

View File

@@ -0,0 +1,45 @@
mod bootstrap;
mod connection;
mod error;
mod service;
pub use bootstrap::{BackendBootstrap, EntrySurface, PersistenceBoundary, SecretHandling};
pub use connection::{
CommandExecutionResult, ConnectionProfileDraft, ConnectionTarget, ConnectionTestResult,
RedisCommandRequest, RedisConnectionRequest, RedisFieldValueEntry, RedisKeyBrowseRequest,
RedisKeyBrowseResult, RedisKeyMetadata, RedisKeyMetadataRequest, RedisKeyTtl,
RedisKeyTtlUpdate, RedisKeyTtlUpdateRequest, RedisKeyTtlUpdateResult, RedisResponse,
RedisSortedSetEntry, RedisStreamEntry, RedisValueContent, RedisValueData,
RedisValueReadRequest, RedisValueRecord, RedisValueWriteCapability, RedisValueWriteInput,
RedisValueWriteRequest, TlsMode,
};
pub use error::{BackendError, BackendErrorCode};
pub use service::{
browse_keys, execute_command, inspect_key, read_value, test_connection, update_key_ttl,
write_value,
};
#[cfg(test)]
mod tests {
use crate::{BackendBootstrap, ConnectionTarget, TlsMode};
#[test]
fn desktop_bootstrap_keeps_persistence_guardrails_stable() {
let bootstrap = BackendBootstrap::for_desktop();
assert_eq!(bootstrap.persistence.connection_catalog, "not_configured");
assert_eq!(bootstrap.persistence.secrets, "memory_only");
assert_eq!(bootstrap.command_execution, "single_instance_enabled");
assert_eq!(bootstrap.key_browsing, "scan_metadata_enabled");
}
#[test]
fn connection_target_defaults_match_local_redis_expectation() {
let target = ConnectionTarget::default();
assert_eq!(target.host, "127.0.0.1");
assert_eq!(target.port, 6379);
assert_eq!(target.database, 0);
assert_eq!(target.tls_mode, TlsMode::Disabled);
}
}

File diff suppressed because it is too large Load Diff