Compare commits
48 Commits
9ed558562b
...
monorepo-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ec1fa1761 | ||
|
|
11185282c4 | ||
|
|
492f1f1e55 | ||
|
|
241ab99584 | ||
|
|
8bd5b5c62f | ||
|
|
7e21258d2e | ||
|
|
49277cfdd4 | ||
|
|
1f6dc3cd75 | ||
|
|
7350b0985c | ||
|
|
73bc6dc99c | ||
|
|
095645a209 | ||
|
|
532977056d | ||
|
|
2435f58256 | ||
|
|
ceb560c658 | ||
|
|
d88c239bf6 | ||
|
|
01c4ff2e14 | ||
|
|
c2890e1f3d | ||
|
|
e1ea44c68c | ||
|
|
cec2361b00 | ||
|
|
9672b9949c | ||
|
|
e4e9594a9d | ||
|
|
6daa5202b1 | ||
|
|
cae47da5f2 | ||
|
|
85c7c89c28 | ||
|
|
0d80266e9b | ||
|
|
a604d62d44 | ||
|
|
2cbbfd21aa | ||
|
|
1c17d07497 | ||
|
|
ad15becd7a | ||
|
|
c2a6272413 | ||
|
|
c51af13fb1 | ||
|
|
d9d8562539 | ||
|
|
6891631b8d | ||
|
|
738d58b5f1 | ||
|
|
3081125716 | ||
|
|
6073c7ab43 | ||
|
|
8157dc7a60 | ||
|
|
3b130e9208 | ||
|
|
ab81434c4e | ||
|
|
62c54dc1eb | ||
|
|
347802b2a4 | ||
|
|
a5a8d98984 | ||
|
|
5b42da8290 | ||
|
|
4e041f36ce | ||
|
|
22926b7266 | ||
|
|
0a7f032028 | ||
|
|
4edec5e72d | ||
|
|
c7d524c76a |
125
Cargo.lock
generated
125
Cargo.lock
generated
@@ -595,8 +595,8 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures",
|
"futures",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-types 0.13.5",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rstest",
|
"rstest",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -637,9 +637,11 @@ dependencies = [
|
|||||||
name = "common"
|
name = "common"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-build 0.14.1",
|
||||||
|
"prost-types 0.13.5",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tantivy",
|
"tantivy",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-build",
|
"tonic-build",
|
||||||
@@ -1955,6 +1957,15 @@ version = "0.11.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
|
checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata 0.1.10",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@@ -2495,7 +2506,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"prost-derive",
|
"prost-derive 0.13.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"prost-derive 0.14.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2511,8 +2532,28 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-types 0.13.5",
|
||||||
|
"regex",
|
||||||
|
"syn 2.0.104",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-build"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"log",
|
||||||
|
"multimap",
|
||||||
|
"once_cell",
|
||||||
|
"petgraph",
|
||||||
|
"prettyplease",
|
||||||
|
"prost 0.14.1",
|
||||||
|
"prost-types 0.14.1",
|
||||||
"regex",
|
"regex",
|
||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@@ -2531,13 +2572,35 @@ dependencies = [
|
|||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-derive"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost-types"
|
name = "prost-types"
|
||||||
version = "0.13.5"
|
version = "0.13.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prost-types"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
|
||||||
|
dependencies = [
|
||||||
|
"prost 0.14.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2747,8 +2810,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata 0.4.9",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
dependencies = [
|
||||||
|
"regex-syntax 0.6.29",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2759,9 +2831,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -3026,7 +3104,7 @@ version = "0.5.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
@@ -3132,8 +3210,9 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-build 0.14.1",
|
||||||
|
"prost-types 0.13.5",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
"regex",
|
"regex",
|
||||||
"rstest",
|
"rstest",
|
||||||
@@ -3751,7 +3830,7 @@ dependencies = [
|
|||||||
"fnv",
|
"fnv",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"plist",
|
"plist",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3857,7 +3936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18"
|
checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.5",
|
||||||
"utf8-ranges",
|
"utf8-ranges",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4169,7 +4248,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"socket2 0.5.10",
|
"socket2 0.5.10",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
@@ -4187,8 +4266,8 @@ checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"prost-build",
|
"prost-build 0.13.5",
|
||||||
"prost-types",
|
"prost-types 0.13.5",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
@@ -4199,8 +4278,8 @@ version = "0.13.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9687bd5bfeafebdded2356950f278bba8226f0b32109537c4253406e09aafe1"
|
checksum = "f9687bd5bfeafebdded2356950f278bba8226f0b32109537c4253406e09aafe1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prost",
|
"prost 0.13.5",
|
||||||
"prost-types",
|
"prost-types 0.13.5",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tonic",
|
"tonic",
|
||||||
@@ -4287,10 +4366,14 @@ version = "0.3.19"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,6 +6,49 @@ use crate::validation::{CharacterLimits, PatternFilters, DisplayMask};
|
|||||||
use crate::validation::{CustomFormatter, FormattingResult, PositionMapper};
|
use crate::validation::{CustomFormatter, FormattingResult, PositionMapper};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Whitelist of allowed exact values for a field.
|
||||||
|
/// If configured, the field is valid when it is empty (by default) or when the
|
||||||
|
/// content exactly matches one of the allowed values. This does not block field
|
||||||
|
/// switching (unlike minimum length in CharacterLimits).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AllowedValues {
|
||||||
|
allowed: Vec<String>,
|
||||||
|
allow_empty: bool,
|
||||||
|
case_insensitive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AllowedValues {
|
||||||
|
pub fn new(allowed: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
allowed,
|
||||||
|
allow_empty: true,
|
||||||
|
case_insensitive: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allow or disallow empty value to be considered valid (default: true).
|
||||||
|
pub fn allow_empty(mut self, allow: bool) -> Self {
|
||||||
|
self.allow_empty = allow;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable/disable ASCII case-insensitive matching (default: false).
|
||||||
|
pub fn case_insensitive(mut self, ci: bool) -> Self {
|
||||||
|
self.case_insensitive = ci;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, text: &str) -> bool {
|
||||||
|
if self.case_insensitive {
|
||||||
|
self.allowed
|
||||||
|
.iter()
|
||||||
|
.any(|s| s.eq_ignore_ascii_case(text))
|
||||||
|
} else {
|
||||||
|
self.allowed.iter().any(|s| s == text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Main validation configuration for a field
|
/// Main validation configuration for a field
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct ValidationConfig {
|
pub struct ValidationConfig {
|
||||||
@@ -22,6 +65,9 @@ pub struct ValidationConfig {
|
|||||||
#[cfg(feature = "validation")]
|
#[cfg(feature = "validation")]
|
||||||
pub custom_formatter: Option<Arc<dyn CustomFormatter + Send + Sync>>,
|
pub custom_formatter: Option<Arc<dyn CustomFormatter + Send + Sync>>,
|
||||||
|
|
||||||
|
/// Optional: restrict the field to one of exact allowed values (or empty)
|
||||||
|
pub allowed_values: Option<AllowedValues>,
|
||||||
|
|
||||||
/// Enable external validation indicator UI (feature 5)
|
/// Enable external validation indicator UI (feature 5)
|
||||||
pub external_validation_enabled: bool,
|
pub external_validation_enabled: bool,
|
||||||
|
|
||||||
@@ -50,6 +96,7 @@ impl std::fmt::Debug for ValidationConfig {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
.field("allowed_values", &self.allowed_values)
|
||||||
.field("external_validation_enabled", &self.external_validation_enabled)
|
.field("external_validation_enabled", &self.external_validation_enabled)
|
||||||
.field("external_validation", &self.external_validation)
|
.field("external_validation", &self.external_validation)
|
||||||
.finish()
|
.finish()
|
||||||
@@ -167,6 +214,18 @@ impl ValidationConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allowed values (whitelist) validation
|
||||||
|
if let Some(ref allowed) = self.allowed_values {
|
||||||
|
// Empty value is allowed (default) or required (if allow_empty is false)
|
||||||
|
if text.is_empty() {
|
||||||
|
if !allowed.allow_empty {
|
||||||
|
return ValidationResult::warning("Value required");
|
||||||
|
}
|
||||||
|
} else if !allowed.matches(text) {
|
||||||
|
return ValidationResult::error("Value must be one of the allowed options");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Future: Add other validation types here
|
// Future: Add other validation types here
|
||||||
|
|
||||||
ValidationResult::Valid
|
ValidationResult::Valid
|
||||||
@@ -183,6 +242,12 @@ impl ValidationConfig {
|
|||||||
#[cfg(not(feature = "validation"))]
|
#[cfg(not(feature = "validation"))]
|
||||||
{ false }
|
{ false }
|
||||||
}
|
}
|
||||||
|
|| self.allowed_values.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if whitelist is configured
|
||||||
|
pub fn has_allowed_values(&self) -> bool {
|
||||||
|
self.allowed_values.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allows_field_switch(&self, text: &str) -> bool {
|
pub fn allows_field_switch(&self, text: &str) -> bool {
|
||||||
@@ -289,6 +354,41 @@ impl ValidationConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restrict content to one of the provided exact values (or empty).
|
||||||
|
/// - Empty is considered valid by default.
|
||||||
|
/// - Matching is case-sensitive by default.
|
||||||
|
pub fn with_allowed_values<S>(mut self, values: Vec<S>) -> Self
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let vals: Vec<String> = values.into_iter().map(Into::into).collect();
|
||||||
|
self.config.allowed_values = Some(AllowedValues::new(vals));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as with_allowed_values, but case-insensitive (ASCII).
|
||||||
|
pub fn with_allowed_values_ci<S>(mut self, values: Vec<S>) -> Self
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let vals: Vec<String> = values.into_iter().map(Into::into).collect();
|
||||||
|
self.config.allowed_values = Some(AllowedValues::new(vals).case_insensitive(true));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure whether empty value should be allowed when using AllowedValues.
|
||||||
|
pub fn with_allowed_values_allow_empty(mut self, allow_empty: bool) -> Self {
|
||||||
|
if let Some(av) = self.config.allowed_values.take() {
|
||||||
|
self.config.allowed_values = Some(AllowedValues {
|
||||||
|
allow_empty,
|
||||||
|
..av
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.config.allowed_values = Some(AllowedValues::new(vec![]).allow_empty(allow_empty));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Enable or disable external validation indicator UI (feature 5)
|
/// Enable or disable external validation indicator UI (feature 5)
|
||||||
pub fn with_external_validation_enabled(mut self, enabled: bool) -> Self {
|
pub fn with_external_validation_enabled(mut self, enabled: bool) -> Self {
|
||||||
self.config.external_validation_enabled = enabled;
|
self.config.external_validation_enabled = enabled;
|
||||||
@@ -391,6 +491,47 @@ mod tests {
|
|||||||
assert!(config.display_mask.is_some());
|
assert!(config.display_mask.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_allowed_values() {
|
||||||
|
let config = ValidationConfigBuilder::new()
|
||||||
|
.with_allowed_values(vec!["alpha", "beta", "gamma", "delta", "epsilon"])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Empty should be valid by default
|
||||||
|
let result = config.validate_content("");
|
||||||
|
assert!(result.is_acceptable());
|
||||||
|
|
||||||
|
// Exact allowed values are valid
|
||||||
|
assert!(config.validate_content("alpha").is_acceptable());
|
||||||
|
assert!(config.validate_content("beta").is_acceptable());
|
||||||
|
|
||||||
|
// Anything else is an error
|
||||||
|
let res = config.validate_content("alph");
|
||||||
|
assert!(res.is_error());
|
||||||
|
let res = config.validate_content("ALPHA");
|
||||||
|
assert!(res.is_error()); // case-sensitive by default
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_allowed_values_case_insensitive_and_required() {
|
||||||
|
let config = ValidationConfigBuilder::new()
|
||||||
|
.with_allowed_values_ci(vec!["Yes", "No"])
|
||||||
|
.with_allowed_values_allow_empty(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Empty is not allowed now (warning so it's still acceptable for typing)
|
||||||
|
let res = config.validate_content("");
|
||||||
|
assert!(res.is_acceptable());
|
||||||
|
|
||||||
|
// Case-insensitive matches
|
||||||
|
assert!(config.validate_content("yes").is_acceptable());
|
||||||
|
assert!(config.validate_content("NO").is_acceptable());
|
||||||
|
|
||||||
|
// Random text is an error
|
||||||
|
let res = config.validate_content("maybe");
|
||||||
|
assert!(res.is_error());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_validation_result() {
|
fn test_validation_result() {
|
||||||
let valid = ValidationResult::Valid;
|
let valid = ValidationResult::Valid;
|
||||||
|
|||||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
canvas_config.toml.txt
|
canvas_config.toml.txt
|
||||||
|
ui_debug.log
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ license.workspace = true
|
|||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
canvas = { path = "../canvas", features = ["gui", "suggestions", "cursor-style", "keymap"] }
|
canvas = { path = "../canvas", features = ["gui", "suggestions", "cursor-style", "keymap", "validation"] }
|
||||||
|
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
crossterm = { workspace = true }
|
crossterm = { workspace = true }
|
||||||
@@ -24,14 +24,15 @@ tokio = { version = "1.44.2", features = ["full", "macros"] }
|
|||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
tonic = "0.13.0"
|
tonic = "0.13.0"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = "0.3.19"
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
tui-textarea = { version = "0.7.0", features = ["crossterm", "ratatui", "search"] }
|
tui-textarea = { version = "0.7.0", features = ["crossterm", "ratatui", "search"] }
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
unicode-width.workspace = true
|
unicode-width.workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = ["validation"]
|
||||||
ui-debug = []
|
ui-debug = []
|
||||||
|
validation = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = "0.25.0"
|
rstest = "0.25.0"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ enter_command_mode = [":", "ctrl+;"]
|
|||||||
next_buffer = ["space+b+n"]
|
next_buffer = ["space+b+n"]
|
||||||
previous_buffer = ["space+b+p"]
|
previous_buffer = ["space+b+p"]
|
||||||
close_buffer = ["space+b+d"]
|
close_buffer = ["space+b+d"]
|
||||||
|
revert = ["space+b+r"]
|
||||||
|
|
||||||
[keybindings.general]
|
[keybindings.general]
|
||||||
up = ["k", "Up"]
|
up = ["k", "Up"]
|
||||||
@@ -27,7 +28,6 @@ move_up = ["Up"]
|
|||||||
move_down = ["Down"]
|
move_down = ["Down"]
|
||||||
toggle_sidebar = ["ctrl+t"]
|
toggle_sidebar = ["ctrl+t"]
|
||||||
toggle_buffer_list = ["ctrl+b"]
|
toggle_buffer_list = ["ctrl+b"]
|
||||||
revert = ["space+b+r"]
|
|
||||||
|
|
||||||
# MODE SPECIFIC
|
# MODE SPECIFIC
|
||||||
# READ ONLY MODE
|
# READ ONLY MODE
|
||||||
@@ -60,7 +60,7 @@ prev_field = ["Shift+Tab"]
|
|||||||
|
|
||||||
[keybindings.highlight]
|
[keybindings.highlight]
|
||||||
exit_highlight_mode = ["esc"]
|
exit_highlight_mode = ["esc"]
|
||||||
enter_highlight_mode_linewise = ["ctrl+v"]
|
enter_highlight_mode_linewise = ["shift+v"]
|
||||||
|
|
||||||
### AUTOGENERATED CANVAS CONFIG
|
### AUTOGENERATED CANVAS CONFIG
|
||||||
# Required
|
# Required
|
||||||
|
|||||||
@@ -250,28 +250,63 @@ impl Config {
|
|||||||
key: KeyCode,
|
key: KeyCode,
|
||||||
modifiers: KeyModifiers,
|
modifiers: KeyModifiers,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// Special handling for shift+character combinations
|
|
||||||
if binding.to_lowercase().starts_with("shift+") {
|
// Normalize binding once
|
||||||
let parts: Vec<&str> = binding.split('+').collect();
|
let binding_lc = binding.to_lowercase();
|
||||||
if parts.len() == 2 && parts[1].len() == 1 {
|
|
||||||
let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap();
|
// Robust handling for Shift+Tab
|
||||||
let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap();
|
// Accept either BackTab (with or without SHIFT flagged) or Tab+SHIFT
|
||||||
if let KeyCode::Char(actual_char) = key {
|
if binding_lc == "shift+tab" || binding_lc == "backtab" {
|
||||||
if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) {
|
return match key {
|
||||||
return true;
|
KeyCode::BackTab => true,
|
||||||
}
|
KeyCode::Tab => modifiers.contains(KeyModifiers::SHIFT),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If binding contains '+', distinguish between:
|
||||||
|
// - modifier combos (e.g., ctrl+shift+s) => single key + modifiers
|
||||||
|
// - multi-key sequences (e.g., space+b+r, g+g) => NOT a single key
|
||||||
|
if binding_lc.contains('+') {
|
||||||
|
let parts: Vec<&str> = binding_lc.split('+').collect();
|
||||||
|
let is_modifier = |t: &str| {
|
||||||
|
matches!(
|
||||||
|
t,
|
||||||
|
"ctrl" | "control" | "shift" | "alt" | "super" | "windows" | "cmd" | "hyper" | "meta"
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let non_modifier_count = parts.iter().filter(|p| !is_modifier(p)).count();
|
||||||
|
if non_modifier_count > 1 {
|
||||||
|
// This is a multi-key sequence (e.g., space+b+r, g+g), not a single keybind.
|
||||||
|
// It must be handled by the sequence engine, not here.
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Shift+Tab -> BackTab
|
// Robust handling for shift+<char> (letters)
|
||||||
if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() {
|
// Many terminals send uppercase Char without SHIFT bit.
|
||||||
|
if binding_lc.starts_with("shift+") {
|
||||||
|
let parts: Vec<&str> = binding.split('+').collect();
|
||||||
|
if parts.len() == 2 && parts[1].chars().count() == 1 {
|
||||||
|
let base = parts[1].chars().next().unwrap();
|
||||||
|
let upper = base.to_ascii_uppercase();
|
||||||
|
let lower = base.to_ascii_lowercase();
|
||||||
|
if let KeyCode::Char(actual) = key {
|
||||||
|
// Accept uppercase char regardless of SHIFT bit
|
||||||
|
if actual == upper {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Also accept lowercase char with SHIFT flagged (some terms do this)
|
||||||
|
if actual == lower && modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle multi-character bindings (all standard keys without modifiers)
|
// Handle multi-character bindings (all standard keys without modifiers)
|
||||||
if binding.len() > 1 && !binding.contains('+') {
|
if binding.len() > 1 && !binding.contains('+') {
|
||||||
return match binding.to_lowercase().as_str() {
|
return match binding_lc.as_str() {
|
||||||
// Navigation keys
|
// Navigation keys
|
||||||
"left" => key == KeyCode::Left,
|
"left" => key == KeyCode::Left,
|
||||||
"right" => key == KeyCode::Right,
|
"right" => key == KeyCode::Right,
|
||||||
@@ -371,6 +406,7 @@ impl Config {
|
|||||||
let mut expected_key = None;
|
let mut expected_key = None;
|
||||||
|
|
||||||
for part in parts {
|
for part in parts {
|
||||||
|
let part_lc = part.to_lowercase();
|
||||||
match part.to_lowercase().as_str() {
|
match part.to_lowercase().as_str() {
|
||||||
// Modifiers
|
// Modifiers
|
||||||
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
|
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
|
||||||
@@ -789,12 +825,43 @@ impl Config {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize bindings for canvas consumption:
|
||||||
|
// - "shift+<char>" -> also add "<CHAR>"
|
||||||
|
// - "shift+tab" -> also add "backtab"
|
||||||
|
// This keeps your config human-friendly while making the canvas happy.
|
||||||
|
fn normalize_for_canvas(
|
||||||
|
map: &HashMap<String, Vec<String>>,
|
||||||
|
) -> HashMap<String, Vec<String>> {
|
||||||
|
let mut out: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
for (action, bindings) in map {
|
||||||
|
let mut new_list: Vec<String> = Vec::new();
|
||||||
|
for b in bindings {
|
||||||
|
new_list.push(b.clone());
|
||||||
|
let blc = b.to_lowercase();
|
||||||
|
if blc.starts_with("shift+") {
|
||||||
|
let parts: Vec<&str> = b.split('+').collect();
|
||||||
|
if parts.len() == 2 && parts[1].chars().count() == 1 {
|
||||||
|
let ch = parts[1].chars().next().unwrap();
|
||||||
|
new_list.push(ch.to_ascii_uppercase().to_string());
|
||||||
|
}
|
||||||
|
if blc == "shift+tab" {
|
||||||
|
new_list.push("backtab".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if blc == "shift+tab" {
|
||||||
|
new_list.push("backtab".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.insert(action.clone(), new_list);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_canvas_keymap(&self) -> CanvasKeyMap {
|
pub fn build_canvas_keymap(&self) -> CanvasKeyMap {
|
||||||
CanvasKeyMap::from_mode_maps(
|
let ro = Self::normalize_for_canvas(&self.keybindings.read_only);
|
||||||
&self.keybindings.read_only,
|
let ed = Self::normalize_for_canvas(&self.keybindings.edit);
|
||||||
&self.keybindings.edit,
|
let hl = Self::normalize_for_canvas(&self.keybindings.highlight);
|
||||||
&self.keybindings.highlight,
|
CanvasKeyMap::from_mode_maps(&ro, &ed, &hl)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// client/src/config/key_sequences.rs
|
// client/src/config/key_sequences.rs
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct ParsedKey {
|
pub struct ParsedKey {
|
||||||
@@ -25,19 +26,21 @@ impl KeySequenceTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
|
info!("KeySequenceTracker.reset() from {:?}", self.current_sequence);
|
||||||
self.current_sequence.clear();
|
self.current_sequence.clear();
|
||||||
self.last_key_time = Instant::now();
|
self.last_key_time = Instant::now();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_key(&mut self, key: KeyCode) -> bool {
|
pub fn add_key(&mut self, key: KeyCode) -> bool {
|
||||||
// Check if timeout has expired
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now.duration_since(self.last_key_time) > self.timeout {
|
if now.duration_since(self.last_key_time) > self.timeout {
|
||||||
|
info!("KeySequenceTracker timeout — reset before adding {:?}", key);
|
||||||
self.reset();
|
self.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
self.current_sequence.push(key);
|
self.current_sequence.push(key);
|
||||||
self.last_key_time = now;
|
self.last_key_time = now;
|
||||||
|
info!("KeySequenceTracker state after add: {:?}", self.current_sequence);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +70,7 @@ impl KeySequenceTracker {
|
|||||||
// Helper function to convert any KeyCode to a string representation
|
// Helper function to convert any KeyCode to a string representation
|
||||||
pub fn key_to_string(key: &KeyCode) -> String {
|
pub fn key_to_string(key: &KeyCode) -> String {
|
||||||
match key {
|
match key {
|
||||||
|
KeyCode::Char(' ') => "space".to_string(),
|
||||||
KeyCode::Char(c) => c.to_string(),
|
KeyCode::Char(c) => c.to_string(),
|
||||||
KeyCode::Left => "left".to_string(),
|
KeyCode::Left => "left".to_string(),
|
||||||
KeyCode::Right => "right".to_string(),
|
KeyCode::Right => "right".to_string(),
|
||||||
@@ -90,6 +94,7 @@ pub fn key_to_string(key: &KeyCode) -> String {
|
|||||||
// Helper function to convert a string to a KeyCode
|
// Helper function to convert a string to a KeyCode
|
||||||
pub fn string_to_keycode(s: &str) -> Option<KeyCode> {
|
pub fn string_to_keycode(s: &str) -> Option<KeyCode> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
|
"space" => Some(KeyCode::Char(' ')),
|
||||||
"left" => Some(KeyCode::Left),
|
"left" => Some(KeyCode::Left),
|
||||||
"right" => Some(KeyCode::Right),
|
"right" => Some(KeyCode::Right),
|
||||||
"up" => Some(KeyCode::Up),
|
"up" => Some(KeyCode::Up),
|
||||||
@@ -113,26 +118,21 @@ pub fn string_to_keycode(s: &str) -> Option<KeyCode> {
|
|||||||
pub fn parse_binding(binding: &str) -> Vec<ParsedKey> {
|
pub fn parse_binding(binding: &str) -> Vec<ParsedKey> {
|
||||||
let mut sequence = Vec::new();
|
let mut sequence = Vec::new();
|
||||||
|
|
||||||
// Handle different binding formats
|
// Split into multi-key sequence:
|
||||||
let parts: Vec<String> = if binding.contains('+') {
|
// - If contains space → sequence split by space
|
||||||
// Format with explicit '+' separators like "g+left"
|
// - Else split by '+'
|
||||||
binding.split('+').map(|s| s.to_string()).collect()
|
let parts: Vec<&str> = if binding.contains(' ') {
|
||||||
} else if binding.contains(' ') {
|
binding.split(' ').collect()
|
||||||
// Format with spaces like "g left"
|
|
||||||
binding.split(' ').map(|s| s.to_string()).collect()
|
|
||||||
} else if is_compound_key(binding) {
|
|
||||||
// A single compound key like "left" or "enter"
|
|
||||||
vec![binding.to_string()]
|
|
||||||
} else {
|
} else {
|
||||||
// Simple character sequence like "gg"
|
binding.split('+').collect()
|
||||||
binding.chars().map(|c| c.to_string()).collect()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for part in &parts {
|
for part in parts {
|
||||||
if let Some(key) = parse_key_part(part) {
|
if let Some(parsed) = parse_key_part(part) {
|
||||||
sequence.push(key);
|
sequence.push(parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sequence
|
sequence
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ fn is_compound_key(part: &str) -> bool {
|
|||||||
matches!(part.to_lowercase().as_str(),
|
matches!(part.to_lowercase().as_str(),
|
||||||
"esc" | "up" | "down" | "left" | "right" | "enter" |
|
"esc" | "up" | "down" | "left" | "right" | "enter" |
|
||||||
"backspace" | "delete" | "tab" | "backtab" | "home" |
|
"backspace" | "delete" | "tab" | "backtab" | "home" |
|
||||||
"end" | "pageup" | "pagedown" | "insert"
|
"end" | "pageup" | "pagedown" | "insert" | "space"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ impl AppState {
|
|||||||
self.ui.dialog.purpose = Some(purpose);
|
self.ui.dialog.purpose = Some(purpose);
|
||||||
self.ui.dialog.is_loading = false;
|
self.ui.dialog.is_loading = false;
|
||||||
self.ui.dialog.dialog_show = true;
|
self.ui.dialog.dialog_show = true;
|
||||||
self.ui.focus_outside_canvas = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_loading_dialog(&mut self, title: &str, message: &str) {
|
pub fn show_loading_dialog(&mut self, title: &str, message: &str) {
|
||||||
@@ -30,7 +29,6 @@ impl AppState {
|
|||||||
self.ui.dialog.purpose = None;
|
self.ui.dialog.purpose = None;
|
||||||
self.ui.dialog.is_loading = true;
|
self.ui.dialog.is_loading = true;
|
||||||
self.ui.dialog.dialog_show = true;
|
self.ui.dialog.dialog_show = true;
|
||||||
self.ui.focus_outside_canvas = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_dialog_content(
|
pub fn update_dialog_content(
|
||||||
@@ -55,7 +53,6 @@ impl AppState {
|
|||||||
self.ui.dialog.dialog_buttons.clear();
|
self.ui.dialog.dialog_buttons.clear();
|
||||||
self.ui.dialog.dialog_active_button_index = 0;
|
self.ui.dialog.dialog_active_button_index = 0;
|
||||||
self.ui.dialog.purpose = None;
|
self.ui.dialog.purpose = None;
|
||||||
self.ui.focus_outside_canvas = false;
|
|
||||||
self.ui.dialog.is_loading = false;
|
self.ui.dialog.is_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,14 +154,13 @@ pub async fn handle_dialog_event(
|
|||||||
DialogPurpose::ConfirmDeleteColumns => match selected_index {
|
DialogPurpose::ConfirmDeleteColumns => match selected_index {
|
||||||
0 => {
|
0 => {
|
||||||
// "Confirm" button selected
|
// "Confirm" button selected
|
||||||
if let Page::Admin(state) = &mut router.current {
|
if let Page::AddTable(page) = &mut router.current {
|
||||||
let outcome_message =
|
let outcome_message = handle_delete_selected_columns(&mut page.state);
|
||||||
handle_delete_selected_columns(&mut state.add_table_state);
|
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
return Some(Ok(EventOutcome::Ok(outcome_message)));
|
return Some(Ok(EventOutcome::Ok(outcome_message)));
|
||||||
}
|
}
|
||||||
return Some(Ok(EventOutcome::Ok(
|
return Some(Ok(EventOutcome::Ok(
|
||||||
"Admin state not active".to_string(),
|
"AddTable page not active".to_string(),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
|
|||||||
41
client/src/input/action.rs
Normal file
41
client/src/input/action.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// src/input/action.rs
|
||||||
|
use crate::movement::MovementAction;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum BufferAction {
|
||||||
|
Next,
|
||||||
|
Previous,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CoreAction {
|
||||||
|
Save,
|
||||||
|
ForceQuit,
|
||||||
|
SaveAndQuit,
|
||||||
|
Revert,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum AppAction {
|
||||||
|
// Global/UI
|
||||||
|
ToggleSidebar,
|
||||||
|
ToggleBufferList,
|
||||||
|
OpenSearch,
|
||||||
|
FindFilePaletteToggle,
|
||||||
|
|
||||||
|
// Buffers
|
||||||
|
Buffer(BufferAction),
|
||||||
|
|
||||||
|
// Command mode
|
||||||
|
EnterCommandMode,
|
||||||
|
ExitCommandMode,
|
||||||
|
CommandExecute,
|
||||||
|
CommandBackspace,
|
||||||
|
|
||||||
|
// Navigation across UI
|
||||||
|
Navigate(MovementAction),
|
||||||
|
|
||||||
|
// Core actions
|
||||||
|
Core(CoreAction),
|
||||||
|
}
|
||||||
195
client/src/input/engine.rs
Normal file
195
client/src/input/engine.rs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// src/input/engine.rs
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||||
|
use crate::input::action::{AppAction, BufferAction, CoreAction};
|
||||||
|
use crate::movement::MovementAction;
|
||||||
|
use crate::modes::handlers::mode_manager::AppMode;
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
use crate::input::leader::{leader_has_any_start, leader_is_prefix, leader_match_action};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct InputContext {
|
||||||
|
pub app_mode: AppMode,
|
||||||
|
pub overlay_active: bool,
|
||||||
|
pub allow_navigation_capture: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum InputOutcome {
|
||||||
|
Action(AppAction),
|
||||||
|
Pending, // sequence in progress
|
||||||
|
PassThrough, // let page/canvas handle it
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InputEngine {
|
||||||
|
seq: KeySequenceTracker,
|
||||||
|
leader_seq: KeySequenceTracker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputEngine {
|
||||||
|
pub fn new(normal_timeout_ms: u64, leader_timeout_ms: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
seq: KeySequenceTracker::new(normal_timeout_ms),
|
||||||
|
leader_seq: KeySequenceTracker::new(leader_timeout_ms),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_sequence(&mut self) {
|
||||||
|
info!("InputEngine.reset_sequence() leader_seq_before={:?}", self.leader_seq.current_sequence);
|
||||||
|
self.seq.reset();
|
||||||
|
self.leader_seq.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_active_sequence(&self) -> bool {
|
||||||
|
!self.seq.current_sequence.is_empty()
|
||||||
|
|| !self.leader_seq.current_sequence.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_key(
|
||||||
|
&mut self,
|
||||||
|
key_event: KeyEvent,
|
||||||
|
ctx: &InputContext,
|
||||||
|
config: &Config,
|
||||||
|
) -> InputOutcome {
|
||||||
|
// Command mode keys are special (exit/execute/backspace) and typed chars
|
||||||
|
if ctx.app_mode == AppMode::Command {
|
||||||
|
if config.is_exit_command_mode(key_event.code, key_event.modifiers) {
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::Action(AppAction::ExitCommandMode);
|
||||||
|
}
|
||||||
|
if config.is_command_execute(key_event.code, key_event.modifiers) {
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::Action(AppAction::CommandExecute);
|
||||||
|
}
|
||||||
|
if config.is_command_backspace(key_event.code, key_event.modifiers) {
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::Action(AppAction::CommandBackspace);
|
||||||
|
}
|
||||||
|
// Let command-line collect characters and other keys pass through
|
||||||
|
self.seq.reset();
|
||||||
|
return InputOutcome::PassThrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If overlays are active, do not intercept (palette, navigation, etc.)
|
||||||
|
if ctx.overlay_active {
|
||||||
|
self.seq.reset();
|
||||||
|
// Also reset leader sequence to avoid leaving a stale "space" active
|
||||||
|
info!("Overlay active → reset leader_seq (was {:?})", self.leader_seq.current_sequence);
|
||||||
|
self.leader_seq.reset();
|
||||||
|
return InputOutcome::PassThrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space-led multi-key sequences (leader = space)
|
||||||
|
let space = KeyCode::Char(' ');
|
||||||
|
let leader_active = !self.leader_seq.current_sequence.is_empty()
|
||||||
|
&& self.leader_seq.current_sequence[0] == space;
|
||||||
|
|
||||||
|
// Keep collecting leader sequence even if allow_navigation_capture is false.
|
||||||
|
if leader_active {
|
||||||
|
self.leader_seq.add_key(key_event.code);
|
||||||
|
let sequence = self.leader_seq.get_sequence();
|
||||||
|
info!(
|
||||||
|
"Leader active updated: {:?} (added {:?})",
|
||||||
|
sequence, key_event.code
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(action_str) = leader_match_action(config, &sequence) {
|
||||||
|
info!("Leader matched '{}' with sequence {:?}", action_str, sequence);
|
||||||
|
if let Some(app_action) = map_action_string(action_str, ctx) {
|
||||||
|
self.leader_seq.reset();
|
||||||
|
return InputOutcome::Action(app_action);
|
||||||
|
}
|
||||||
|
self.leader_seq.reset();
|
||||||
|
return InputOutcome::PassThrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
if leader_is_prefix(config, &sequence) {
|
||||||
|
info!("Leader prefix continuing...");
|
||||||
|
return InputOutcome::Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Leader sequence reset (no match/prefix).");
|
||||||
|
self.leader_seq.reset();
|
||||||
|
// fall through to regular handling of this key
|
||||||
|
} else if ctx.allow_navigation_capture
|
||||||
|
&& key_event.code == space
|
||||||
|
&& leader_has_any_start(config)
|
||||||
|
{
|
||||||
|
// Start a leader sequence only if capturing is allowed
|
||||||
|
self.leader_seq.reset();
|
||||||
|
self.leader_seq.add_key(space);
|
||||||
|
info!("Leader started: {:?}", self.leader_seq.get_sequence());
|
||||||
|
return InputOutcome::Pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-key mapping: try general binds first (arrows, open_search, enter_command_mode)
|
||||||
|
if let Some(action_str) =
|
||||||
|
config.get_general_action(key_event.code, key_event.modifiers)
|
||||||
|
{
|
||||||
|
if let Some(app_action) = map_action_string(action_str, ctx) {
|
||||||
|
return InputOutcome::Action(app_action);
|
||||||
|
}
|
||||||
|
// Unknown to app layer (likely canvas movement etc.) → pass
|
||||||
|
return InputOutcome::PassThrough;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then app-level common/read-only/edit/highlight for UI toggles or core actions
|
||||||
|
if let Some(action_str) = config.get_app_action(key_event.code, key_event.modifiers) {
|
||||||
|
if let Some(app_action) = map_action_string(action_str, ctx) {
|
||||||
|
return InputOutcome::Action(app_action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputOutcome::PassThrough
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_to_movement(s: &str) -> Option<MovementAction> {
|
||||||
|
match s {
|
||||||
|
"up" => Some(MovementAction::Up),
|
||||||
|
"down" => Some(MovementAction::Down),
|
||||||
|
"left" => Some(MovementAction::Left),
|
||||||
|
"right" => Some(MovementAction::Right),
|
||||||
|
"next" => Some(MovementAction::Next),
|
||||||
|
"previous" => Some(MovementAction::Previous),
|
||||||
|
"select" => Some(MovementAction::Select),
|
||||||
|
"esc" => Some(MovementAction::Esc),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_action_string(action: &str, ctx: &InputContext) -> Option<AppAction> {
|
||||||
|
match action {
|
||||||
|
// Global/UI
|
||||||
|
"toggle_sidebar" => Some(AppAction::ToggleSidebar),
|
||||||
|
"toggle_buffer_list" => Some(AppAction::ToggleBufferList),
|
||||||
|
"open_search" => Some(AppAction::OpenSearch),
|
||||||
|
"find_file_palette_toggle" => Some(AppAction::FindFilePaletteToggle),
|
||||||
|
|
||||||
|
// Buffers
|
||||||
|
"next_buffer" => Some(AppAction::Buffer(BufferAction::Next)),
|
||||||
|
"previous_buffer" => Some(AppAction::Buffer(BufferAction::Previous)),
|
||||||
|
"close_buffer" => Some(AppAction::Buffer(BufferAction::Close)),
|
||||||
|
|
||||||
|
// Command mode
|
||||||
|
"enter_command_mode" => Some(AppAction::EnterCommandMode),
|
||||||
|
"exit_command_mode" => Some(AppAction::ExitCommandMode),
|
||||||
|
"command_execute" => Some(AppAction::CommandExecute),
|
||||||
|
"command_backspace" => Some(AppAction::CommandBackspace),
|
||||||
|
|
||||||
|
// Navigation across UI (only if allowed)
|
||||||
|
s if str_to_movement(s).is_some() && ctx.allow_navigation_capture => {
|
||||||
|
Some(AppAction::Navigate(str_to_movement(s).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core actions
|
||||||
|
"save" => Some(AppAction::Core(CoreAction::Save)),
|
||||||
|
"force_quit" => Some(AppAction::Core(CoreAction::ForceQuit)),
|
||||||
|
"save_and_quit" => Some(AppAction::Core(CoreAction::SaveAndQuit)),
|
||||||
|
"revert" => Some(AppAction::Core(CoreAction::Revert)),
|
||||||
|
|
||||||
|
// Unknown to app layer: ignore (canvas-specific actions, etc.)
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
74
client/src/input/leader.rs
Normal file
74
client/src/input/leader.rs
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// src/input/leader.rs
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::config::binds::key_sequences::parse_binding;
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
|
||||||
|
/// Collect leader (= space-prefixed) bindings from *all* binding maps
|
||||||
|
fn leader_bindings<'a>(config: &'a Config) -> Vec<(&'a str, Vec<KeyCode>)> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
// Include all keybinding maps, not just global
|
||||||
|
let all_modes: Vec<&std::collections::HashMap<String, Vec<String>>> = vec![
|
||||||
|
&config.keybindings.general,
|
||||||
|
&config.keybindings.read_only,
|
||||||
|
&config.keybindings.edit,
|
||||||
|
&config.keybindings.highlight,
|
||||||
|
&config.keybindings.command,
|
||||||
|
&config.keybindings.common,
|
||||||
|
&config.keybindings.global,
|
||||||
|
];
|
||||||
|
|
||||||
|
for mode in all_modes {
|
||||||
|
for (action, bindings) in mode {
|
||||||
|
for b in bindings {
|
||||||
|
let parsed = parse_binding(b);
|
||||||
|
if parsed.first().map(|pk| pk.code) == Some(KeyCode::Char(' ')) {
|
||||||
|
let codes =
|
||||||
|
parsed.into_iter().map(|pk| pk.code).collect::<Vec<_>>();
|
||||||
|
out.push((action.as_str(), codes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is there any leader binding configured at all?
|
||||||
|
pub fn leader_has_any_start(config: &Config) -> bool {
|
||||||
|
leader_bindings(config)
|
||||||
|
.iter()
|
||||||
|
.any(|(_, seq)| seq.first() == Some(&KeyCode::Char(' ')))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is `sequence` a prefix of any configured leader sequence?
|
||||||
|
pub fn leader_is_prefix(config: &Config, sequence: &[KeyCode]) -> bool {
|
||||||
|
if sequence.is_empty() || sequence[0] != KeyCode::Char(' ') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (_, full) in leader_bindings(config) {
|
||||||
|
if full.len() > sequence.len()
|
||||||
|
&& full.iter().zip(sequence.iter()).all(|(a, b)| a == b)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is `sequence` an exact leader match? If yes, return the action string.
|
||||||
|
pub fn leader_match_action<'a>(
|
||||||
|
config: &'a Config,
|
||||||
|
sequence: &[KeyCode],
|
||||||
|
) -> Option<&'a str> {
|
||||||
|
if sequence.is_empty() || sequence[0] != KeyCode::Char(' ') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for (action, full) in leader_bindings(config) {
|
||||||
|
if full.len() == sequence.len()
|
||||||
|
&& full.iter().zip(sequence.iter()).all(|(a, b)| a == b)
|
||||||
|
{
|
||||||
|
return Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
4
client/src/input/mod.rs
Normal file
4
client/src/input/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/input/mod.rs
|
||||||
|
pub mod action;
|
||||||
|
pub mod engine;
|
||||||
|
pub mod leader;
|
||||||
@@ -14,6 +14,7 @@ pub mod search;
|
|||||||
pub mod bottom_panel;
|
pub mod bottom_panel;
|
||||||
pub mod pages;
|
pub mod pages;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
|
pub mod input;
|
||||||
|
|
||||||
pub use ui::run_ui;
|
pub use ui::run_ui;
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,35 @@ use client::run_ui;
|
|||||||
use client::utils::debug_logger::UiDebugWriter;
|
use client::utils::debug_logger::UiDebugWriter;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tracing_subscriber;
|
use tracing_subscriber::EnvFilter;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
{
|
{
|
||||||
// If ui-debug is on, set up our custom writer.
|
use std::sync::Once;
|
||||||
|
static INIT_LOGGER: Once = Once::new();
|
||||||
|
|
||||||
|
INIT_LOGGER.call_once(|| {
|
||||||
let writer = UiDebugWriter::new();
|
let writer = UiDebugWriter::new();
|
||||||
tracing_subscriber::fmt()
|
let _ = tracing_subscriber::fmt()
|
||||||
.with_level(false) // Don't show INFO, ERROR, etc.
|
.with_max_level(tracing::Level::DEBUG)
|
||||||
.with_target(false) // Don't show the module path.
|
.with_target(false)
|
||||||
.without_time() // This is the correct and simpler method.
|
.without_time()
|
||||||
.with_writer(move || writer.clone())
|
.with_writer(move || writer.clone())
|
||||||
.init();
|
// Filter out noisy grpc/h2 internals
|
||||||
|
.with_env_filter("client=debug,tonic=info,h2=info,tower=info")
|
||||||
|
.try_init();
|
||||||
|
|
||||||
|
client::utils::debug_logger::spawn_file_logger();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "ui-debug"))]
|
#[cfg(not(feature = "ui-debug"))]
|
||||||
{
|
{
|
||||||
if env::var("ENABLE_TRACING").is_ok() {
|
if env::var("ENABLE_TRACING").is_ok() {
|
||||||
tracing_subscriber::fmt::init();
|
let _ = tracing_subscriber::fmt::try_init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ pub async fn handle_navigation_event(
|
|||||||
}
|
}
|
||||||
"select" => {
|
"select" => {
|
||||||
let (context, index) = match &router.current {
|
let (context, index) = match &router.current {
|
||||||
Page::Intro(state) => (UiContext::Intro, state.selected_option),
|
Page::Intro(state) => (UiContext::Intro, state.focused_button_index),
|
||||||
Page::Login(_) if app_state.ui.focus_outside_canvas => {
|
Page::Login(state) if state.focus_outside_canvas => {
|
||||||
(UiContext::Login, app_state.focused_button_index)
|
(UiContext::Login, state.focused_button_index)
|
||||||
}
|
}
|
||||||
Page::Register(_) if app_state.ui.focus_outside_canvas => {
|
Page::Register(state) if state.focus_outside_canvas => {
|
||||||
(UiContext::Register, app_state.focused_button_index)
|
(UiContext::Register, state.focused_button_index)
|
||||||
}
|
}
|
||||||
Page::Admin(state) => {
|
Page::Admin(state) => {
|
||||||
(UiContext::Admin, state.get_selected_index().unwrap_or(0))
|
(UiContext::Admin, state.get_selected_index().unwrap_or(0))
|
||||||
@@ -91,24 +91,24 @@ pub async fn handle_navigation_event(
|
|||||||
|
|
||||||
pub fn up(app_state: &mut AppState, router: &mut Router) {
|
pub fn up(app_state: &mut AppState, router: &mut Router) {
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
Page::Login(page) if app_state.ui.focus_outside_canvas => {
|
Page::Login(page) if page.focus_outside_canvas => {
|
||||||
if app_state.focused_button_index == 0 {
|
if page.focused_button_index == 0 {
|
||||||
app_state.ui.focus_outside_canvas = false;
|
page.focus_outside_canvas = false;
|
||||||
let last_field_index = page.state.field_count().saturating_sub(1);
|
let last_field_index = page.state.field_count().saturating_sub(1);
|
||||||
page.state.set_current_field(last_field_index);
|
page.state.set_current_field(last_field_index);
|
||||||
} else {
|
} else {
|
||||||
app_state.focused_button_index =
|
page.focused_button_index =
|
||||||
app_state.focused_button_index.saturating_sub(1);
|
page.focused_button_index.saturating_sub(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Page::Register(state) if app_state.ui.focus_outside_canvas => {
|
Page::Register(state) if state.focus_outside_canvas => {
|
||||||
if app_state.focused_button_index == 0 {
|
if state.focused_button_index == 0 {
|
||||||
app_state.ui.focus_outside_canvas = false;
|
state.focus_outside_canvas = false;
|
||||||
let last_field_index = state.state.field_count().saturating_sub(1);
|
let last_field_index = state.state.field_count().saturating_sub(1);
|
||||||
state.set_current_field(last_field_index);
|
state.set_current_field(last_field_index);
|
||||||
} else {
|
} else {
|
||||||
app_state.focused_button_index =
|
state.focused_button_index =
|
||||||
app_state.focused_button_index.saturating_sub(1);
|
state.focused_button_index.saturating_sub(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Page::Intro(state) => state.previous_option(),
|
Page::Intro(state) => state.previous_option(),
|
||||||
@@ -119,10 +119,16 @@ pub fn up(app_state: &mut AppState, router: &mut Router) {
|
|||||||
|
|
||||||
pub fn down(app_state: &mut AppState, router: &mut Router) {
|
pub fn down(app_state: &mut AppState, router: &mut Router) {
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
Page::Login(_) | Page::Register(_) if app_state.ui.focus_outside_canvas => {
|
Page::Login(state) if state.focus_outside_canvas => {
|
||||||
let num_general_elements = 2;
|
let num_general_elements = 2;
|
||||||
if app_state.focused_button_index < num_general_elements - 1 {
|
if state.focused_button_index < num_general_elements - 1 {
|
||||||
app_state.focused_button_index += 1;
|
state.focused_button_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Page::Register(state) if state.focus_outside_canvas => {
|
||||||
|
let num_general_elements = 2;
|
||||||
|
if state.focused_button_index < num_general_elements - 1 {
|
||||||
|
state.focused_button_index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Page::Intro(state) => state.next_option(),
|
Page::Intro(state) => state.next_option(),
|
||||||
@@ -134,11 +140,11 @@ pub fn down(app_state: &mut AppState, router: &mut Router) {
|
|||||||
pub fn next_option(app_state: &mut AppState, router: &mut Router) {
|
pub fn next_option(app_state: &mut AppState, router: &mut Router) {
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
Page::Intro(state) => state.next_option(),
|
Page::Intro(state) => state.next_option(),
|
||||||
Page::Admin(_) => {
|
Page::Admin(state) => {
|
||||||
let option_count = app_state.profile_tree.profiles.len();
|
let option_count = app_state.profile_tree.profiles.len();
|
||||||
if option_count > 0 {
|
if option_count > 0 {
|
||||||
app_state.focused_button_index =
|
state.focused_button_index =
|
||||||
(app_state.focused_button_index + 1) % option_count;
|
(state.focused_button_index + 1) % option_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -148,13 +154,13 @@ pub fn next_option(app_state: &mut AppState, router: &mut Router) {
|
|||||||
pub fn previous_option(app_state: &mut AppState, router: &mut Router) {
|
pub fn previous_option(app_state: &mut AppState, router: &mut Router) {
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
Page::Intro(state) => state.previous_option(),
|
Page::Intro(state) => state.previous_option(),
|
||||||
Page::Admin(_) => {
|
Page::Admin(state) => {
|
||||||
let option_count = app_state.profile_tree.profiles.len();
|
let option_count = app_state.profile_tree.profiles.len();
|
||||||
if option_count > 0 {
|
if option_count > 0 {
|
||||||
app_state.focused_button_index = if app_state.focused_button_index == 0 {
|
state.focused_button_index = if state.focused_button_index == 0 {
|
||||||
option_count.saturating_sub(1)
|
option_count.saturating_sub(1)
|
||||||
} else {
|
} else {
|
||||||
app_state.focused_button_index - 1
|
state.focused_button_index - 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// src/modes/handlers/event.rs
|
// src/modes/handlers/event.rs
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
use crate::input::engine::{InputContext, InputEngine, InputOutcome};
|
||||||
|
use crate::input::action::{AppAction, BufferAction, CoreAction};
|
||||||
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
|
use crate::buffer::{AppView, BufferState, switch_buffer, toggle_buffer_list};
|
||||||
use crate::sidebar::toggle_sidebar;
|
use crate::sidebar::toggle_sidebar;
|
||||||
use crate::search::event::handle_search_palette_event;
|
use crate::search::event::handle_search_palette_event;
|
||||||
@@ -51,6 +52,7 @@ use common::proto::komp_ac::search::search_response::Hit;
|
|||||||
use crossterm::event::{Event, KeyCode};
|
use crossterm::event::{Event, KeyCode};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum EventOutcome {
|
pub enum EventOutcome {
|
||||||
@@ -76,7 +78,7 @@ pub struct EventHandler {
|
|||||||
pub command_message: String,
|
pub command_message: String,
|
||||||
pub edit_mode_cooldown: bool,
|
pub edit_mode_cooldown: bool,
|
||||||
pub ideal_cursor_column: usize,
|
pub ideal_cursor_column: usize,
|
||||||
pub key_sequence_tracker: KeySequenceTracker,
|
pub input_engine: InputEngine,
|
||||||
pub auth_client: AuthClient,
|
pub auth_client: AuthClient,
|
||||||
pub grpc_client: GrpcClient,
|
pub grpc_client: GrpcClient,
|
||||||
pub login_result_sender: mpsc::Sender<LoginResult>,
|
pub login_result_sender: mpsc::Sender<LoginResult>,
|
||||||
@@ -106,8 +108,10 @@ impl EventHandler {
|
|||||||
command_message: String::new(),
|
command_message: String::new(),
|
||||||
edit_mode_cooldown: false,
|
edit_mode_cooldown: false,
|
||||||
ideal_cursor_column: 0,
|
ideal_cursor_column: 0,
|
||||||
key_sequence_tracker: KeySequenceTracker::new(400),
|
input_engine: InputEngine::new(400, 5000),
|
||||||
auth_client: AuthClient::new().await?,
|
auth_client: AuthClient::with_channel(
|
||||||
|
grpc_client.channel()
|
||||||
|
).await?,
|
||||||
grpc_client,
|
grpc_client,
|
||||||
login_result_sender,
|
login_result_sender,
|
||||||
register_result_sender,
|
register_result_sender,
|
||||||
@@ -226,6 +230,12 @@ impl EventHandler {
|
|||||||
) -> Result<EventOutcome> {
|
) -> Result<EventOutcome> {
|
||||||
if app_state.ui.show_search_palette {
|
if app_state.ui.show_search_palette {
|
||||||
if let Event::Key(key_event) = event {
|
if let Event::Key(key_event) = event {
|
||||||
|
info!(
|
||||||
|
"RAW KEY: code={:?} mods={:?} active_seq={} ",
|
||||||
|
key_event.code,
|
||||||
|
key_event.modifiers,
|
||||||
|
self.input_engine.has_active_sequence(),
|
||||||
|
);
|
||||||
if let Some(message) = handle_search_palette_event(
|
if let Some(message) = handle_search_palette_event(
|
||||||
key_event,
|
key_event,
|
||||||
app_state,
|
app_state,
|
||||||
@@ -296,17 +306,79 @@ impl EventHandler {
|
|||||||
if let Event::Key(key_event) = event {
|
if let Event::Key(key_event) = event {
|
||||||
let key_code = key_event.code;
|
let key_code = key_event.code;
|
||||||
let modifiers = key_event.modifiers;
|
let modifiers = key_event.modifiers;
|
||||||
|
info!(
|
||||||
|
"RAW KEY: code={:?} mods={:?} pre_active_seq={}",
|
||||||
|
key_code, modifiers, self.input_engine.has_active_sequence()
|
||||||
|
);
|
||||||
|
|
||||||
// LOGIN: canvas <-> buttons focus handoff
|
|
||||||
// Do not let Login canvas receive keys when overlays/palettes are active
|
|
||||||
let overlay_active = self.command_mode
|
let overlay_active = self.command_mode
|
||||||
|| app_state.ui.show_search_palette
|
|| app_state.ui.show_search_palette
|
||||||
|| self.navigation_state.active;
|
|| self.navigation_state.active;
|
||||||
|
|
||||||
|
// Determine if canvas is in edit mode (we avoid capturing navigation then)
|
||||||
|
let in_form_edit_mode = matches!(
|
||||||
|
&router.current,
|
||||||
|
Page::Form(path) if {
|
||||||
|
if let Some(editor) = app_state.editor_for_path_ref(path) {
|
||||||
|
editor.mode() == CanvasMode::Edit
|
||||||
|
} else { false }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Centralized key -> action resolution
|
||||||
|
let allow_nav = self.input_engine.has_active_sequence()
|
||||||
|
|| (!in_form_edit_mode && !overlay_active);
|
||||||
|
let input_ctx = InputContext {
|
||||||
|
app_mode: current_mode,
|
||||||
|
overlay_active,
|
||||||
|
allow_navigation_capture: allow_nav,
|
||||||
|
};
|
||||||
|
info!(
|
||||||
|
"InputContext: app_mode={:?}, overlay_active={}, in_form_edit_mode={}, allow_nav={}, has_active_seq={}",
|
||||||
|
current_mode, overlay_active, in_form_edit_mode, allow_nav, self.input_engine.has_active_sequence()
|
||||||
|
);
|
||||||
|
|
||||||
|
let outcome = self.input_engine.process_key(key_event, &input_ctx, config);
|
||||||
|
info!(
|
||||||
|
"ENGINE OUTCOME: {:?} post_active_seq={}",
|
||||||
|
outcome, self.input_engine.has_active_sequence()
|
||||||
|
);
|
||||||
|
match outcome {
|
||||||
|
InputOutcome::Action(action) => {
|
||||||
|
if let Some(outcome) = self
|
||||||
|
.handle_app_action(
|
||||||
|
action,
|
||||||
|
key_event, // pass original key
|
||||||
|
config,
|
||||||
|
terminal,
|
||||||
|
command_handler,
|
||||||
|
auth_state,
|
||||||
|
buffer_state,
|
||||||
|
app_state,
|
||||||
|
router,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
// No early return on None (e.g., Navigate) — fall through
|
||||||
|
}
|
||||||
|
InputOutcome::Pending => {
|
||||||
|
// waiting for more keys in a sequence
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
InputOutcome::PassThrough => {
|
||||||
|
// fall through to page/canvas handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOGIN: canvas <-> buttons focus handoff
|
||||||
|
// Do not let Login canvas receive keys when overlays/palettes are active
|
||||||
|
|
||||||
if !overlay_active {
|
if !overlay_active {
|
||||||
if let Page::Login(login_page) = &mut router.current {
|
if let Page::Login(login_page) = &mut router.current {
|
||||||
let outcome =
|
let outcome =
|
||||||
login::event::handle_login_event(event, app_state, login_page)?;
|
login::event::handle_login_event(event.clone(), app_state, login_page)?;
|
||||||
// Only return if the login page actually consumed the key
|
// Only return if the login page actually consumed the key
|
||||||
if !outcome.get_message_if_ok().is_empty() {
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
return Ok(outcome);
|
return Ok(outcome);
|
||||||
@@ -321,14 +393,125 @@ impl EventHandler {
|
|||||||
if !outcome.get_message_if_ok().is_empty() {
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
return Ok(outcome);
|
return Ok(outcome);
|
||||||
}
|
}
|
||||||
} else if let Page::Form(path) = &router.current {
|
} else if let Page::Form(path_str) = &router.current {
|
||||||
|
let path = path_str.clone();
|
||||||
|
|
||||||
|
if let Event::Key(_key_event) = event {
|
||||||
|
// Do NOT call the input engine here again. The top-level
|
||||||
|
// process_key call above already ran for this key.
|
||||||
|
// If we are waiting for more leader keys, swallow the key.
|
||||||
|
info!("Form branch: has_active_seq={}", self.input_engine.has_active_sequence());
|
||||||
|
if self.input_engine.has_active_sequence() {
|
||||||
|
info!("Form branch suppressing key {:?}, leader in progress", key_event.code);
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, forward to the form editor/canvas.
|
||||||
let outcome = forms::event::handle_form_event(
|
let outcome = forms::event::handle_form_event(
|
||||||
event,
|
event,
|
||||||
app_state,
|
app_state,
|
||||||
path,
|
&path,
|
||||||
&mut self.ideal_cursor_column,
|
&mut self.ideal_cursor_column,
|
||||||
)?;
|
)?;
|
||||||
// Only return if the form page actually consumed the key
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Page::AddLogic(add_logic_page) = &mut router.current {
|
||||||
|
// Allow ":" (enter_command_mode) even when inside AddLogic canvas
|
||||||
|
if let Some(action) =
|
||||||
|
config.get_general_action(key_event.code, key_event.modifiers)
|
||||||
|
{
|
||||||
|
if action == "enter_command_mode"
|
||||||
|
&& !self.command_mode
|
||||||
|
&& !app_state.ui.show_search_palette
|
||||||
|
&& !self.navigation_state.active
|
||||||
|
{
|
||||||
|
self.command_mode = true;
|
||||||
|
self.command_input.clear();
|
||||||
|
self.command_message.clear();
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
self.set_focus_outside(router, true);
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let movement_action_early = if let Some(act) =
|
||||||
|
config.get_general_action(key_event.code, key_event.modifiers)
|
||||||
|
{
|
||||||
|
match act {
|
||||||
|
"up" => Some(MovementAction::Up),
|
||||||
|
"down" => Some(MovementAction::Down),
|
||||||
|
"left" => Some(MovementAction::Left),
|
||||||
|
"right" => Some(MovementAction::Right),
|
||||||
|
"next" => Some(MovementAction::Next),
|
||||||
|
"previous" => Some(MovementAction::Previous),
|
||||||
|
"select" => Some(MovementAction::Select),
|
||||||
|
"esc" => Some(MovementAction::Esc),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else { None };
|
||||||
|
|
||||||
|
let outcome = add_logic::event::handle_add_logic_event(
|
||||||
|
key_event,
|
||||||
|
movement_action_early,
|
||||||
|
config,
|
||||||
|
app_state,
|
||||||
|
add_logic_page,
|
||||||
|
self.grpc_client.clone(),
|
||||||
|
self.save_logic_result_sender.clone(),
|
||||||
|
)?;
|
||||||
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
} else if let Page::AddTable(add_table_page) = &mut router.current {
|
||||||
|
// Allow ":" (enter_command_mode) even when inside AddTable canvas
|
||||||
|
if let Some(action) =
|
||||||
|
config.get_general_action(key_event.code, key_event.modifiers)
|
||||||
|
{
|
||||||
|
if action == "enter_command_mode"
|
||||||
|
&& !self.command_mode
|
||||||
|
&& !app_state.ui.show_search_palette
|
||||||
|
&& !self.navigation_state.active
|
||||||
|
{
|
||||||
|
self.command_mode = true;
|
||||||
|
self.command_input.clear();
|
||||||
|
self.command_message.clear();
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
self.set_focus_outside(router, true);
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle AddTable before global actions so canvas gets first shot at keys.
|
||||||
|
// Map keys to MovementAction (same as AddLogic early handler)
|
||||||
|
let movement_action_early = if let Some(act) =
|
||||||
|
config.get_general_action(key_event.code, key_event.modifiers)
|
||||||
|
{
|
||||||
|
match act {
|
||||||
|
"up" => Some(MovementAction::Up),
|
||||||
|
"down" => Some(MovementAction::Down),
|
||||||
|
"left" => Some(MovementAction::Left),
|
||||||
|
"right" => Some(MovementAction::Right),
|
||||||
|
"next" => Some(MovementAction::Next),
|
||||||
|
"previous" => Some(MovementAction::Previous),
|
||||||
|
"select" => Some(MovementAction::Select),
|
||||||
|
"esc" => Some(MovementAction::Esc),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let outcome = add_table::event::handle_add_table_event(
|
||||||
|
key_event,
|
||||||
|
movement_action_early,
|
||||||
|
config,
|
||||||
|
app_state,
|
||||||
|
add_table_page,
|
||||||
|
self.grpc_client.clone(),
|
||||||
|
self.save_table_result_sender.clone(),
|
||||||
|
)?;
|
||||||
|
// Only stop if the page consumed the key; else let global handling proceed.
|
||||||
if !outcome.get_message_if_ok().is_empty() {
|
if !outcome.get_message_if_ok().is_empty() {
|
||||||
return Ok(outcome);
|
return Ok(outcome);
|
||||||
}
|
}
|
||||||
@@ -339,8 +522,8 @@ impl EventHandler {
|
|||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
admin_state,
|
|
||||||
buffer_state,
|
buffer_state,
|
||||||
|
router,
|
||||||
&mut self.command_message,
|
&mut self.command_message,
|
||||||
)? {
|
)? {
|
||||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||||
@@ -349,90 +532,11 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if toggle_sidebar(
|
|
||||||
&mut app_state.ui,
|
// Sidebar/buffer toggles now handled via AppAction in the engine
|
||||||
config,
|
|
||||||
key_code,
|
|
||||||
modifiers,
|
|
||||||
) {
|
|
||||||
let message = format!(
|
|
||||||
"Sidebar {}",
|
|
||||||
if app_state.ui.show_sidebar {
|
|
||||||
"shown"
|
|
||||||
} else {
|
|
||||||
"hidden"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return Ok(EventOutcome::Ok(message));
|
|
||||||
}
|
|
||||||
if toggle_buffer_list(
|
|
||||||
&mut app_state.ui,
|
|
||||||
config,
|
|
||||||
key_code,
|
|
||||||
modifiers,
|
|
||||||
) {
|
|
||||||
let message = format!(
|
|
||||||
"Buffer {}",
|
|
||||||
if app_state.ui.show_buffer_list {
|
|
||||||
"shown"
|
|
||||||
} else {
|
|
||||||
"hidden"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return Ok(EventOutcome::Ok(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_mode == AppMode::General {
|
if current_mode == AppMode::General {
|
||||||
if let Some(action) = config.get_action_for_key_in_mode(
|
// General mode specific key mapping now handled via AppAction
|
||||||
&config.keybindings.global,
|
|
||||||
key_code,
|
|
||||||
modifiers,
|
|
||||||
) {
|
|
||||||
match action {
|
|
||||||
"next_buffer" => {
|
|
||||||
if switch_buffer(buffer_state, true) {
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
"Switched to next buffer".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"previous_buffer" => {
|
|
||||||
if switch_buffer(buffer_state, false) {
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
"Switched to previous buffer".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"close_buffer" => {
|
|
||||||
let current_table_name =
|
|
||||||
app_state.current_view_table_name.as_deref();
|
|
||||||
let message = buffer_state
|
|
||||||
.close_buffer_with_intro_fallback(
|
|
||||||
current_table_name,
|
|
||||||
);
|
|
||||||
return Ok(EventOutcome::Ok(message));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(action) = config.get_general_action(key_code, modifiers) {
|
|
||||||
if action == "open_search" {
|
|
||||||
if let Page::Form(_) = &router.current {
|
|
||||||
if let Some(table_name) =
|
|
||||||
app_state.current_view_table_name.clone()
|
|
||||||
{
|
|
||||||
app_state.ui.show_search_palette = true;
|
|
||||||
app_state.search_state =
|
|
||||||
Some(SearchState::new(table_name));
|
|
||||||
app_state.ui.focus_outside_canvas = true;
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
"Search palette opened".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
@@ -460,22 +564,6 @@ impl EventHandler {
|
|||||||
// Let the current page handle decoupled movement first
|
// Let the current page handle decoupled movement first
|
||||||
if let Some(ma) = movement_action {
|
if let Some(ma) = movement_action {
|
||||||
match &mut router.current {
|
match &mut router.current {
|
||||||
// LOGIN: From buttons (general) back into the canvas with 'k' (Up),
|
|
||||||
// but ONLY from the left-most "Login" button.
|
|
||||||
Page::AddTable(state) => {
|
|
||||||
if state.handle_movement(ma) {
|
|
||||||
// Keep UI focus consistent with inputs vs. outer elements
|
|
||||||
use crate::pages::admin_panel::add_table::state::AddTableFocus;
|
|
||||||
let is_canvas_input = matches!(
|
|
||||||
state.current_focus,
|
|
||||||
AddTableFocus::InputTableName
|
|
||||||
| AddTableFocus::InputColumnName
|
|
||||||
| AddTableFocus::InputColumnType
|
|
||||||
);
|
|
||||||
app_state.ui.focus_outside_canvas = !is_canvas_input;
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Page::Intro(state) => {
|
Page::Intro(state) => {
|
||||||
if state.handle_movement(ma) {
|
if state.handle_movement(ma) {
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
@@ -485,42 +573,6 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional page-specific handlers (non-movement or rich actions)
|
|
||||||
let client_clone = self.grpc_client.clone();
|
|
||||||
let sender_clone = self.save_logic_result_sender.clone();
|
|
||||||
if add_logic::nav::handle_add_logic_navigation(
|
|
||||||
key_event,
|
|
||||||
config,
|
|
||||||
app_state,
|
|
||||||
buffer_state,
|
|
||||||
client_clone,
|
|
||||||
sender_clone,
|
|
||||||
&mut self.command_message,
|
|
||||||
router,
|
|
||||||
) {
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
self.command_message.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Page::AddTable(add_table_state) = &mut router.current {
|
|
||||||
let client_clone = self.grpc_client.clone();
|
|
||||||
let sender_clone = self.save_table_result_sender.clone();
|
|
||||||
if add_table::nav::handle_add_table_navigation(
|
|
||||||
key_event,
|
|
||||||
config,
|
|
||||||
app_state,
|
|
||||||
add_table_state,
|
|
||||||
client_clone,
|
|
||||||
sender_clone,
|
|
||||||
&mut self.command_message,
|
|
||||||
) {
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
self.command_message.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic navigation for the rest (Intro/Login/Register/Form)
|
// Generic navigation for the rest (Intro/Login/Register/Form)
|
||||||
let nav_outcome = if matches!(&router.current, Page::AddTable(_) | Page::AddLogic(_)) {
|
let nav_outcome = if matches!(&router.current, Page::AddTable(_) | Page::AddLogic(_)) {
|
||||||
// Skip generic navigation for AddTable/AddLogic (they have their own handlers)
|
// Skip generic navigation for AddTable/AddLogic (they have their own handlers)
|
||||||
@@ -616,146 +668,20 @@ impl EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppMode::Command => {
|
AppMode::Command => {
|
||||||
if config.is_exit_command_mode(key_code, modifiers) {
|
// Command-mode keys already handled by the engine.
|
||||||
self.command_input.clear();
|
// Collect characters not handled (typed command input).
|
||||||
self.command_message.clear();
|
match key_code {
|
||||||
self.command_mode = false;
|
KeyCode::Char(c) => {
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
if let Page::Form(path) = &router.current {
|
|
||||||
if let Some(editor) = app_state.editor_for_path(path) {
|
|
||||||
editor.set_mode(CanvasMode::ReadOnly);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
"Exited command mode".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.is_command_execute(key_code, modifiers) {
|
|
||||||
let (mut current_position, total_count) =
|
|
||||||
if let Page::Form(path) = &router.current {
|
|
||||||
if let Some(fs) =
|
|
||||||
app_state.form_state_for_path_ref(path)
|
|
||||||
{
|
|
||||||
(fs.current_position, fs.total_count)
|
|
||||||
} else {
|
|
||||||
(1, 0)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(1, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
let outcome = command_mode::handle_command_event(
|
|
||||||
key_event,
|
|
||||||
config,
|
|
||||||
app_state,
|
|
||||||
router,
|
|
||||||
&mut self.command_input,
|
|
||||||
&mut self.command_message,
|
|
||||||
&mut self.grpc_client,
|
|
||||||
command_handler,
|
|
||||||
terminal,
|
|
||||||
&mut current_position,
|
|
||||||
total_count,
|
|
||||||
).await?;
|
|
||||||
if let Page::Form(path) = &router.current {
|
|
||||||
if let Some(fs) = app_state.form_state_for_path(path) {
|
|
||||||
fs.current_position = current_position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.command_mode = false;
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
let new_mode = ModeManager::derive_mode(
|
|
||||||
app_state,
|
|
||||||
self,
|
|
||||||
router,
|
|
||||||
);
|
|
||||||
app_state.update_mode(new_mode);
|
|
||||||
return Ok(outcome);
|
|
||||||
}
|
|
||||||
|
|
||||||
if key_code == KeyCode::Backspace {
|
|
||||||
self.command_input.pop();
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let KeyCode::Char(c) = key_code {
|
|
||||||
if c == 'f' {
|
|
||||||
self.key_sequence_tracker.add_key(key_code);
|
|
||||||
let sequence =
|
|
||||||
self.key_sequence_tracker.get_sequence();
|
|
||||||
|
|
||||||
if config.matches_key_sequence_generalized(
|
|
||||||
&sequence,
|
|
||||||
) == Some("find_file_palette_toggle")
|
|
||||||
{
|
|
||||||
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
|
|
||||||
let mut all_table_paths: Vec<String> =
|
|
||||||
app_state
|
|
||||||
.profile_tree
|
|
||||||
.profiles
|
|
||||||
.iter()
|
|
||||||
.flat_map(|profile| {
|
|
||||||
profile.tables.iter().map(
|
|
||||||
move |table| {
|
|
||||||
format!(
|
|
||||||
"{}/{}",
|
|
||||||
profile.name,
|
|
||||||
table.name
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
all_table_paths.sort();
|
|
||||||
|
|
||||||
self.navigation_state
|
|
||||||
.activate_find_file(all_table_paths);
|
|
||||||
|
|
||||||
self.command_mode = false;
|
|
||||||
self.command_input.clear();
|
|
||||||
self.command_message.clear();
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
"Table selection palette activated"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
self.command_input.push('f');
|
|
||||||
if sequence.len() > 1
|
|
||||||
&& sequence[0] == KeyCode::Char('f')
|
|
||||||
{
|
|
||||||
self.command_input.push('f');
|
|
||||||
}
|
|
||||||
self.command_message = "Find File not available in this view."
|
|
||||||
.to_string();
|
|
||||||
return Ok(EventOutcome::Ok(
|
|
||||||
self.command_message.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.is_key_sequence_prefix(&sequence) {
|
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c != 'f'
|
|
||||||
&& !self.key_sequence_tracker.current_sequence.is_empty()
|
|
||||||
{
|
|
||||||
self.key_sequence_tracker.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.command_input.push(c);
|
self.command_input.push(c);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
|
_ => {
|
||||||
self.key_sequence_tracker.reset();
|
self.input_engine.reset_sequence();
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if let Event::Resize(_, _) = event {
|
} else if let Event::Resize(_, _) = event {
|
||||||
return Ok(EventOutcome::Ok("Resized".to_string()));
|
return Ok(EventOutcome::Ok("Resized".to_string()));
|
||||||
}
|
}
|
||||||
@@ -893,4 +819,275 @@ impl EventHandler {
|
|||||||
"find_file_palette_toggle"
|
"find_file_palette_toggle"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_focus_outside(&mut self, router: &mut Router, outside: bool) {
|
||||||
|
match &mut router.current {
|
||||||
|
Page::Login(state) => state.focus_outside_canvas = outside,
|
||||||
|
Page::Register(state) => state.focus_outside_canvas = outside,
|
||||||
|
Page::Intro(state) => state.focus_outside_canvas = outside,
|
||||||
|
Page::Admin(state) => state.focus_outside_canvas = outside,
|
||||||
|
Page::AddLogic(state) => state.focus_outside_canvas = outside,
|
||||||
|
Page::AddTable(state) => state.focus_outside_canvas = outside,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_focused_button(&mut self, router: &mut Router, index: usize) {
|
||||||
|
match &mut router.current {
|
||||||
|
Page::Login(state) => state.focused_button_index = index,
|
||||||
|
Page::Register(state) => state.focused_button_index = index,
|
||||||
|
Page::Intro(state) => state.focused_button_index = index,
|
||||||
|
Page::Admin(state) => state.focused_button_index = index,
|
||||||
|
Page::AddLogic(state) => state.focused_button_index = index,
|
||||||
|
Page::AddTable(state) => state.focused_button_index = index,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_focus_outside(&self, router: &Router) -> bool {
|
||||||
|
match &router.current {
|
||||||
|
Page::Login(state) => state.focus_outside_canvas,
|
||||||
|
Page::Register(state) => state.focus_outside_canvas,
|
||||||
|
Page::Intro(state) => state.focus_outside_canvas,
|
||||||
|
Page::Admin(state) => state.focus_outside_canvas,
|
||||||
|
Page::AddLogic(state) => state.focus_outside_canvas,
|
||||||
|
Page::AddTable(state) => state.focus_outside_canvas,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focused_button(&self, router: &Router) -> usize {
|
||||||
|
match &router.current {
|
||||||
|
Page::Login(state) => state.focused_button_index,
|
||||||
|
Page::Register(state) => state.focused_button_index,
|
||||||
|
Page::Intro(state) => state.focused_button_index,
|
||||||
|
Page::Admin(state) => state.focused_button_index,
|
||||||
|
Page::AddLogic(state) => state.focused_button_index,
|
||||||
|
Page::AddTable(state) => state.focused_button_index,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_command(
|
||||||
|
&mut self,
|
||||||
|
key_event: crossterm::event::KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
command_handler: &mut CommandHandler,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
router: &mut Router,
|
||||||
|
) -> Result<EventOutcome> {
|
||||||
|
let (mut current_position, total_count) = if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(fs) = app_state.form_state_for_path_ref(path) {
|
||||||
|
(fs.current_position, fs.total_count)
|
||||||
|
} else {
|
||||||
|
(1, 0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(1, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let outcome = command_mode::handle_command_event(
|
||||||
|
key_event,
|
||||||
|
config,
|
||||||
|
app_state,
|
||||||
|
router,
|
||||||
|
&mut self.command_input,
|
||||||
|
&mut self.command_message,
|
||||||
|
&mut self.grpc_client,
|
||||||
|
command_handler,
|
||||||
|
terminal,
|
||||||
|
&mut current_position,
|
||||||
|
total_count,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
|
fs.current_position = current_position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.command_mode = false;
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
let new_mode = ModeManager::derive_mode(app_state, self, router);
|
||||||
|
app_state.update_mode(new_mode);
|
||||||
|
Ok(outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn handle_app_action(
|
||||||
|
&mut self,
|
||||||
|
action: AppAction,
|
||||||
|
key_event: crossterm::event::KeyEvent,
|
||||||
|
config: &Config,
|
||||||
|
terminal: &mut TerminalCore,
|
||||||
|
command_handler: &mut CommandHandler,
|
||||||
|
auth_state: &mut AuthState,
|
||||||
|
buffer_state: &mut BufferState,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
router: &mut Router,
|
||||||
|
) -> Result<Option<EventOutcome>> {
|
||||||
|
match action {
|
||||||
|
AppAction::ToggleSidebar => {
|
||||||
|
app_state.ui.show_sidebar = !app_state.ui.show_sidebar;
|
||||||
|
let message = format!(
|
||||||
|
"Sidebar {}",
|
||||||
|
if app_state.ui.show_sidebar {
|
||||||
|
"shown"
|
||||||
|
} else {
|
||||||
|
"hidden"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(Some(EventOutcome::Ok(message)))
|
||||||
|
}
|
||||||
|
AppAction::ToggleBufferList => {
|
||||||
|
app_state.ui.show_buffer_list = !app_state.ui.show_buffer_list;
|
||||||
|
let message = format!(
|
||||||
|
"Buffer {}",
|
||||||
|
if app_state.ui.show_buffer_list {
|
||||||
|
"shown"
|
||||||
|
} else {
|
||||||
|
"hidden"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(Some(EventOutcome::Ok(message)))
|
||||||
|
}
|
||||||
|
AppAction::Buffer(BufferAction::Next) => {
|
||||||
|
if switch_buffer(buffer_state, true) {
|
||||||
|
return Ok(Some(EventOutcome::Ok(
|
||||||
|
"Switched to next buffer".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::Buffer(BufferAction::Previous) => {
|
||||||
|
if switch_buffer(buffer_state, false) {
|
||||||
|
return Ok(Some(EventOutcome::Ok(
|
||||||
|
"Switched to previous buffer".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::Buffer(BufferAction::Close) => {
|
||||||
|
let current_table_name = app_state.current_view_table_name.as_deref();
|
||||||
|
let message = buffer_state
|
||||||
|
.close_buffer_with_intro_fallback(current_table_name);
|
||||||
|
Ok(Some(EventOutcome::Ok(message)))
|
||||||
|
}
|
||||||
|
AppAction::OpenSearch => {
|
||||||
|
if let Page::Form(_) = &router.current {
|
||||||
|
if let Some(table_name) = app_state.current_view_table_name.clone() {
|
||||||
|
app_state.ui.show_search_palette = true;
|
||||||
|
app_state.search_state = Some(SearchState::new(table_name));
|
||||||
|
self.set_focus_outside(router, true);
|
||||||
|
return Ok(Some(EventOutcome::Ok(
|
||||||
|
"Search palette opened".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::FindFilePaletteToggle => {
|
||||||
|
if matches!(&router.current, Page::Form(_) | Page::Intro(_)) {
|
||||||
|
let mut all_table_paths: Vec<String> = app_state
|
||||||
|
.profile_tree
|
||||||
|
.profiles
|
||||||
|
.iter()
|
||||||
|
.flat_map(|profile| {
|
||||||
|
profile.tables.iter().map(move |table| {
|
||||||
|
format!("{}/{}", profile.name, table.name)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
all_table_paths.sort();
|
||||||
|
|
||||||
|
self.navigation_state.activate_find_file(all_table_paths);
|
||||||
|
self.command_mode = false;
|
||||||
|
self.command_input.clear();
|
||||||
|
self.command_message.clear();
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
return Ok(Some(EventOutcome::Ok(
|
||||||
|
"Table selection palette activated".to_string(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::EnterCommandMode => {
|
||||||
|
if !self.is_in_form_edit_mode(router, app_state)
|
||||||
|
&& !self.command_mode
|
||||||
|
&& !app_state.ui.show_search_palette
|
||||||
|
&& !self.navigation_state.active
|
||||||
|
{
|
||||||
|
self.command_mode = true;
|
||||||
|
self.command_input.clear();
|
||||||
|
self.command_message.clear();
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
|
||||||
|
// Keep focus outside so canvas won’t consume keystrokes
|
||||||
|
self.set_focus_outside(router, true);
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::ExitCommandMode => {
|
||||||
|
self.command_input.clear();
|
||||||
|
self.command_message.clear();
|
||||||
|
self.command_mode = false;
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
|
editor.set_mode(CanvasMode::ReadOnly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(EventOutcome::Ok(
|
||||||
|
"Exited command mode".to_string(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
AppAction::CommandExecute => {
|
||||||
|
// Execute using the actual configured key that triggered the action
|
||||||
|
let out = self
|
||||||
|
.execute_command(
|
||||||
|
key_event,
|
||||||
|
config,
|
||||||
|
terminal,
|
||||||
|
command_handler,
|
||||||
|
app_state,
|
||||||
|
router,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Some(out))
|
||||||
|
}
|
||||||
|
AppAction::CommandBackspace => {
|
||||||
|
self.command_input.pop();
|
||||||
|
self.input_engine.reset_sequence();
|
||||||
|
Ok(Some(EventOutcome::Ok(String::new())))
|
||||||
|
}
|
||||||
|
AppAction::Core(core) => {
|
||||||
|
let s = match core {
|
||||||
|
CoreAction::Save => "save",
|
||||||
|
CoreAction::ForceQuit => "force_quit",
|
||||||
|
CoreAction::SaveAndQuit => "save_and_quit",
|
||||||
|
CoreAction::Revert => "revert",
|
||||||
|
};
|
||||||
|
let out = self
|
||||||
|
.handle_core_action(s, auth_state, terminal, app_state, router)
|
||||||
|
.await?;
|
||||||
|
Ok(Some(out))
|
||||||
|
}
|
||||||
|
AppAction::Navigate(_ma) => {
|
||||||
|
// Movement is still handled by page/nav code paths that
|
||||||
|
// follow after PassThrough. We return None here to keep flow.
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_in_form_edit_mode(&self, router: &Router, app_state: &AppState) -> bool {
|
||||||
|
if let Page::Form(path) = &router.current {
|
||||||
|
if let Some(editor) = app_state.editor_for_path_ref(path) {
|
||||||
|
return editor.mode() == CanvasMode::Edit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,14 +40,11 @@ impl ModeManager {
|
|||||||
// If focus is inside a canvas, we don't duplicate canvas modes here.
|
// If focus is inside a canvas, we don't duplicate canvas modes here.
|
||||||
// Canvas crate owns ReadOnly/Edit/Highlight internally.
|
// Canvas crate owns ReadOnly/Edit/Highlight internally.
|
||||||
match &router.current {
|
match &router.current {
|
||||||
Page::Form(_)
|
Page::Form(_) => AppMode::General, // Form always has its own canvas
|
||||||
| Page::Login(_)
|
Page::Login(state) if !state.focus_outside_canvas => AppMode::General,
|
||||||
| Page::Register(_)
|
Page::Register(state) if !state.focus_outside_canvas => AppMode::General,
|
||||||
| Page::AddTable(_)
|
Page::AddTable(state) if !state.focus_outside_canvas => AppMode::General,
|
||||||
| Page::AddLogic(_) if !app_state.ui.focus_outside_canvas => {
|
Page::AddLogic(state) if !state.focus_outside_canvas => AppMode::General,
|
||||||
// Canvas active → let canvas handle its own AppMode
|
|
||||||
AppMode::General
|
|
||||||
}
|
|
||||||
_ => AppMode::General,
|
_ => AppMode::General,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ use crossterm::event::KeyEvent;
|
|||||||
|
|
||||||
use crate::buffer::state::BufferState;
|
use crate::buffer::state::BufferState;
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::pages::admin::AdminState;
|
|
||||||
use crate::pages::admin::main::logic::handle_admin_navigation;
|
use crate::pages::admin::main::logic::handle_admin_navigation;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::pages::routing::{Router, Page};
|
||||||
|
|
||||||
/// Handle all Admin page-specific key events (movement + actions).
|
/// Handle all Admin page-specific key events (movement + actions).
|
||||||
/// Returns true if the key was handled (so the caller should stop propagation).
|
/// Returns true if the key was handled (so the caller should stop propagation).
|
||||||
@@ -14,10 +14,11 @@ pub fn handle_admin_event(
|
|||||||
key_event: KeyEvent,
|
key_event: KeyEvent,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
admin_state: &mut AdminState,
|
|
||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
|
router: &mut Router,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
|
if let Page::Admin(admin_state) = &mut router.current {
|
||||||
// 1) Map general action to MovementAction (same mapping used in event.rs)
|
// 1) Map general action to MovementAction (same mapping used in event.rs)
|
||||||
let movement_action = if let Some(act) =
|
let movement_action = if let Some(act) =
|
||||||
config.get_general_action(key_event.code, key_event.modifiers)
|
config.get_general_action(key_event.code, key_event.modifiers)
|
||||||
@@ -49,12 +50,16 @@ pub fn handle_admin_event(
|
|||||||
key_event,
|
key_event,
|
||||||
config,
|
config,
|
||||||
app_state,
|
app_state,
|
||||||
admin_state,
|
|
||||||
buffer_state,
|
buffer_state,
|
||||||
|
router,
|
||||||
command_message,
|
command_message,
|
||||||
) {
|
) {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we reached here, nothing was handled
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// src/pages/admin/admin/state.rs
|
// src/pages/admin/admin/state.rs
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use crate::pages::admin_panel::add_table::state::AddTableState;
|
|
||||||
use crate::pages::admin_panel::add_logic::state::AddLogicState;
|
|
||||||
use crate::movement::{move_focus, MovementAction};
|
use crate::movement::{move_focus, MovementAction};
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
|
|
||||||
@@ -27,8 +25,8 @@ pub struct AdminState {
|
|||||||
pub selected_profile_index: Option<usize>,
|
pub selected_profile_index: Option<usize>,
|
||||||
pub selected_table_index: Option<usize>,
|
pub selected_table_index: Option<usize>,
|
||||||
pub current_focus: AdminFocus,
|
pub current_focus: AdminFocus,
|
||||||
pub add_table_state: AddTableState,
|
pub focus_outside_canvas: bool,
|
||||||
pub add_logic_state: AddLogicState,
|
pub focused_button_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AdminState {
|
impl AdminState {
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ use crate::pages::admin::{AdminFocus, AdminState};
|
|||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::config::binds::config::Config;
|
use crate::config::binds::config::Config;
|
||||||
use crate::buffer::state::{BufferState, AppView};
|
use crate::buffer::state::{BufferState, AppView};
|
||||||
use crate::pages::admin_panel::add_table::state::{AddTableState, LinkDefinition};
|
|
||||||
use ratatui::widgets::ListState;
|
use ratatui::widgets::ListState;
|
||||||
use crate::pages::admin_panel::add_logic::state::{AddLogicState, AddLogicFocus};
|
use crate::pages::admin_panel::add_table::state::{AddTableFormState, LinkDefinition};
|
||||||
|
use crate::pages::admin_panel::add_logic::state::{AddLogicState, AddLogicFocus, AddLogicFormState};
|
||||||
|
use crate::pages::routing::{Page, Router};
|
||||||
|
|
||||||
// Helper functions list_select_next and list_select_previous remain the same
|
// Helper functions list_select_next and list_select_previous remain the same
|
||||||
fn list_select_next(list_state: &mut ListState, item_count: usize) {
|
fn list_select_next(list_state: &mut ListState, item_count: usize) {
|
||||||
@@ -36,17 +37,35 @@ pub fn handle_admin_navigation(
|
|||||||
key: crossterm::event::KeyEvent,
|
key: crossterm::event::KeyEvent,
|
||||||
config: &Config,
|
config: &Config,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
admin_state: &mut AdminState,
|
|
||||||
buffer_state: &mut BufferState,
|
buffer_state: &mut BufferState,
|
||||||
|
router: &mut Router,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let action = config.get_general_action(key.code, key.modifiers).map(String::from);
|
let action = config.get_general_action(key.code, key.modifiers).map(String::from);
|
||||||
let current_focus = admin_state.current_focus;
|
|
||||||
|
// Check if we're in admin page, but don't borrow mutably yet
|
||||||
|
let is_admin = matches!(&router.current, Page::Admin(_));
|
||||||
|
if !is_admin {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current focus without borrowing mutably
|
||||||
|
let current_focus = if let Page::Admin(admin_state) = &router.current {
|
||||||
|
admin_state.current_focus
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
let profile_count = app_state.profile_tree.profiles.len();
|
let profile_count = app_state.profile_tree.profiles.len();
|
||||||
let mut handled = false;
|
let mut handled = false;
|
||||||
|
|
||||||
match current_focus {
|
match current_focus {
|
||||||
AdminFocus::ProfilesPane => {
|
AdminFocus::ProfilesPane => {
|
||||||
|
// Now we can borrow mutably since we're not reassigning router.current
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
admin_state.current_focus = AdminFocus::InsideProfilesList;
|
admin_state.current_focus = AdminFocus::InsideProfilesList;
|
||||||
@@ -64,7 +83,6 @@ pub fn handle_admin_navigation(
|
|||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
Some("previous_option") | Some("move_up") => {
|
Some("previous_option") | Some("move_up") => {
|
||||||
// No wrap-around: Stay on ProfilesPane if trying to go "before" it
|
|
||||||
*command_message = "At first focusable pane.".to_string();
|
*command_message = "At first focusable pane.".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@@ -73,6 +91,10 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
AdminFocus::InsideProfilesList => {
|
AdminFocus::InsideProfilesList => {
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("move_up") => {
|
Some("move_up") => {
|
||||||
if profile_count > 0 {
|
if profile_count > 0 {
|
||||||
@@ -90,11 +112,11 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
admin_state.selected_profile_index = admin_state.profile_list_state.selected();
|
admin_state.selected_profile_index = admin_state.profile_list_state.selected();
|
||||||
admin_state.selected_table_index = None; // Deselect table when profile changes
|
admin_state.selected_table_index = None;
|
||||||
if let Some(profile_idx) = admin_state.selected_profile_index {
|
if let Some(profile_idx) = admin_state.selected_profile_index {
|
||||||
if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) {
|
if let Some(profile) = app_state.profile_tree.profiles.get(profile_idx) {
|
||||||
if !profile.tables.is_empty() {
|
if !profile.tables.is_empty() {
|
||||||
admin_state.table_list_state.select(Some(0)); // Auto-select first table for nav
|
admin_state.table_list_state.select(Some(0));
|
||||||
} else {
|
} else {
|
||||||
admin_state.table_list_state.select(None);
|
admin_state.table_list_state.select(None);
|
||||||
}
|
}
|
||||||
@@ -118,6 +140,10 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
AdminFocus::Tables => {
|
AdminFocus::Tables => {
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
admin_state.current_focus = AdminFocus::InsideTablesList;
|
admin_state.current_focus = AdminFocus::InsideTablesList;
|
||||||
@@ -147,7 +173,7 @@ pub fn handle_admin_navigation(
|
|||||||
} else {
|
} else {
|
||||||
*command_message = "No tables in selected profile.".to_string();
|
*command_message = "No tables in selected profile.".to_string();
|
||||||
}
|
}
|
||||||
admin_state.current_focus = AdminFocus::Tables; // Stay in Tables pane if no tables to enter
|
admin_state.current_focus = AdminFocus::Tables;
|
||||||
}
|
}
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@@ -166,6 +192,10 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
AdminFocus::InsideTablesList => {
|
AdminFocus::InsideTablesList => {
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("move_up") => {
|
Some("move_up") => {
|
||||||
let current_profile_idx = admin_state.selected_profile_index
|
let current_profile_idx = admin_state.selected_profile_index
|
||||||
@@ -205,7 +235,7 @@ pub fn handle_admin_navigation(
|
|||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("select") => { // This is for persistently selecting a table with [*]
|
Some("select") => {
|
||||||
admin_state.selected_table_index = admin_state.table_list_state.selected();
|
admin_state.selected_table_index = admin_state.table_list_state.selected();
|
||||||
let table_name = admin_state.selected_profile_index
|
let table_name = admin_state.selected_profile_index
|
||||||
.and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx))
|
.and_then(|p_idx| app_state.profile_tree.profiles.get(p_idx))
|
||||||
@@ -225,29 +255,36 @@ pub fn handle_admin_navigation(
|
|||||||
|
|
||||||
AdminFocus::Button1 => { // Add Logic Button
|
AdminFocus::Button1 => { // Add Logic Button
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("select") => { // Typically "Enter" key
|
Some("select") => {
|
||||||
if let Some(p_idx) = admin_state.selected_profile_index {
|
// Extract needed data first, before any router reassignment
|
||||||
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
|
let (selected_profile_idx, selected_table_idx) = if let Page::Admin(admin_state) = &router.current {
|
||||||
if let Some(t_idx) = admin_state.selected_table_index {
|
(admin_state.selected_profile_index, admin_state.selected_table_index)
|
||||||
if let Some(table) = profile.tables.get(t_idx) {
|
} else {
|
||||||
// Both profile and table are selected, proceed
|
return false;
|
||||||
admin_state.add_logic_state = AddLogicState {
|
|
||||||
profile_name: profile.name.clone(),
|
|
||||||
selected_table_name: Some(table.name.clone()),
|
|
||||||
selected_table_id: Some(table.id), // If you have table IDs
|
|
||||||
editor_keybinding_mode: config.editor.keybinding_mode.clone(),
|
|
||||||
current_focus: AddLogicFocus::default(),
|
|
||||||
..AddLogicState::default()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(p_idx) = selected_profile_idx {
|
||||||
|
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
|
||||||
|
if let Some(t_idx) = selected_table_idx {
|
||||||
|
if let Some(table) = profile.tables.get(t_idx) {
|
||||||
|
// Create AddLogic page with selected profile & table
|
||||||
|
let add_logic_form = AddLogicFormState::new_with_table(
|
||||||
|
&config.editor,
|
||||||
|
profile.name.clone(),
|
||||||
|
Some(table.id),
|
||||||
|
table.name.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
// Store table info for later fetching
|
// Store table info for later fetching
|
||||||
app_state.pending_table_structure_fetch = Some((
|
app_state.pending_table_structure_fetch = Some((
|
||||||
profile.name.clone(),
|
profile.name.clone(),
|
||||||
table.name.clone()
|
table.name.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Now it's safe to reassign router.current
|
||||||
|
router.current = Page::AddLogic(add_logic_form);
|
||||||
buffer_state.update_history(AppView::AddLogic);
|
buffer_state.update_history(AppView::AddLogic);
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
*command_message = format!(
|
*command_message = format!(
|
||||||
"Opening Add Logic for table '{}' in profile '{}'...",
|
"Opening Add Logic for table '{}' in profile '{}'...",
|
||||||
table.name, profile.name
|
table.name, profile.name
|
||||||
@@ -267,11 +304,17 @@ pub fn handle_admin_navigation(
|
|||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
Some("previous_option") | Some("move_up") => {
|
Some("previous_option") | Some("move_up") => {
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
admin_state.current_focus = AdminFocus::Tables;
|
admin_state.current_focus = AdminFocus::Tables;
|
||||||
*command_message = "Focus: Tables Pane".to_string();
|
*command_message = "Focus: Tables Pane".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
Some("next_option") | Some("move_down") => {
|
Some("next_option") | Some("move_down") => {
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
admin_state.current_focus = AdminFocus::Button2;
|
admin_state.current_focus = AdminFocus::Button2;
|
||||||
*command_message = "Focus: Add Table Button".to_string();
|
*command_message = "Focus: Add Table Button".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
@@ -283,25 +326,36 @@ pub fn handle_admin_navigation(
|
|||||||
AdminFocus::Button2 => { // Add Table Button
|
AdminFocus::Button2 => { // Add Table Button
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
if let Some(p_idx) = admin_state.selected_profile_index {
|
// Extract needed data first
|
||||||
|
let selected_profile_idx = if let Page::Admin(admin_state) = &router.current {
|
||||||
|
admin_state.selected_profile_index
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(p_idx) = selected_profile_idx {
|
||||||
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
|
if let Some(profile) = app_state.profile_tree.profiles.get(p_idx) {
|
||||||
let selected_profile_name = profile.name.clone();
|
let selected_profile_name = profile.name.clone();
|
||||||
// Prepare links from the selected profile's existing tables
|
// Prepare links from the selected profile's existing tables
|
||||||
let available_links: Vec<LinkDefinition> = profile.tables.iter()
|
let available_links: Vec<LinkDefinition> = profile.tables.iter()
|
||||||
.map(|table| LinkDefinition {
|
.map(|table| LinkDefinition {
|
||||||
linked_table_name: table.name.clone(),
|
linked_table_name: table.name.clone(),
|
||||||
is_required: false, // Default, can be changed in AddTable screen
|
is_required: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
admin_state.add_table_state = AddTableState {
|
// Build decoupled AddTable page and route into it
|
||||||
profile_name: selected_profile_name,
|
let mut page = AddTableFormState::new(selected_profile_name.clone());
|
||||||
links: available_links,
|
page.state.links = available_links;
|
||||||
..AddTableState::default() // Reset other fields
|
|
||||||
};
|
// Now safe to reassign router.current
|
||||||
|
router.current = Page::AddTable(page);
|
||||||
buffer_state.update_history(AppView::AddTable);
|
buffer_state.update_history(AppView::AddTable);
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
*command_message = format!("Opening Add Table for profile '{}'...", admin_state.add_table_state.profile_name);
|
*command_message = format!(
|
||||||
|
"Opening Add Table for profile '{}'...",
|
||||||
|
selected_profile_name
|
||||||
|
);
|
||||||
handled = true;
|
handled = true;
|
||||||
} else {
|
} else {
|
||||||
*command_message = "Error: Selected profile index out of bounds.".to_string();
|
*command_message = "Error: Selected profile index out of bounds.".to_string();
|
||||||
@@ -313,11 +367,17 @@ pub fn handle_admin_navigation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("previous_option") | Some("move_up") => {
|
Some("previous_option") | Some("move_up") => {
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
admin_state.current_focus = AdminFocus::Button1;
|
admin_state.current_focus = AdminFocus::Button1;
|
||||||
*command_message = "Focus: Add Logic Button".to_string();
|
*command_message = "Focus: Add Logic Button".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
Some("next_option") | Some("move_down") => {
|
Some("next_option") | Some("move_down") => {
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
admin_state.current_focus = AdminFocus::Button3;
|
admin_state.current_focus = AdminFocus::Button3;
|
||||||
*command_message = "Focus: Change Table Button".to_string();
|
*command_message = "Focus: Change Table Button".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
@@ -329,17 +389,18 @@ pub fn handle_admin_navigation(
|
|||||||
AdminFocus::Button3 => { // Change Table Button
|
AdminFocus::Button3 => { // Change Table Button
|
||||||
match action.as_deref() {
|
match action.as_deref() {
|
||||||
Some("select") => {
|
Some("select") => {
|
||||||
// Future: Logic to load selected table into AddTableState for editing
|
|
||||||
*command_message = "Action: Change Table (Not Implemented)".to_string();
|
*command_message = "Action: Change Table (Not Implemented)".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
Some("previous_option") | Some("move_up") => {
|
Some("previous_option") | Some("move_up") => {
|
||||||
|
let Page::Admin(admin_state) = &mut router.current else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
admin_state.current_focus = AdminFocus::Button2;
|
admin_state.current_focus = AdminFocus::Button2;
|
||||||
*command_message = "Focus: Add Table Button".to_string();
|
*command_message = "Focus: Add Table Button".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
Some("next_option") | Some("move_down") => {
|
Some("next_option") | Some("move_down") => {
|
||||||
// No wrap-around: Stay on Button3 if trying to go "after" it
|
|
||||||
*command_message = "At last focusable button.".to_string();
|
*command_message = "At last focusable button.".to_string();
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|||||||
159
client/src/pages/admin_panel/add_logic/event.rs
Normal file
159
client/src/pages/admin_panel/add_logic/event.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
// src/pages/admin_panel/add_logic/event.rs
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::movement::{move_focus, MovementAction};
|
||||||
|
use crate::pages::admin_panel::add_logic::nav::SaveLogicResultSender;
|
||||||
|
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicFormState};
|
||||||
|
use crate::components::common::text_editor::TextEditor;
|
||||||
|
use crate::services::grpc_client::GrpcClient;
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
|
use canvas::{AppMode as CanvasMode, DataProvider};
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
|
||||||
|
/// Focus traversal order for non-canvas navigation
|
||||||
|
const ADD_LOGIC_FOCUS_ORDER: [AddLogicFocus; 6] = [
|
||||||
|
AddLogicFocus::InputLogicName,
|
||||||
|
AddLogicFocus::InputTargetColumn,
|
||||||
|
AddLogicFocus::InputDescription,
|
||||||
|
AddLogicFocus::ScriptContentPreview,
|
||||||
|
AddLogicFocus::SaveButton,
|
||||||
|
AddLogicFocus::CancelButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Handles all AddLogic page-specific events.
|
||||||
|
/// Return a non-empty Ok(message) only when the page actually consumed the key,
|
||||||
|
/// otherwise return Ok("") to let global handling proceed.
|
||||||
|
pub fn handle_add_logic_event(
|
||||||
|
key_event: KeyEvent,
|
||||||
|
movement: Option<MovementAction>,
|
||||||
|
config: &Config,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
add_logic_page: &mut AddLogicFormState,
|
||||||
|
grpc_client: GrpcClient,
|
||||||
|
save_logic_sender: SaveLogicResultSender,
|
||||||
|
) -> Result<EventOutcome> {
|
||||||
|
// 1) Script editor fullscreen mode
|
||||||
|
if add_logic_page.state.current_focus == AddLogicFocus::InsideScriptContent {
|
||||||
|
match key_event.code {
|
||||||
|
crossterm::event::KeyCode::Esc => {
|
||||||
|
add_logic_page.state.current_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
|
add_logic_page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok("Exited script editing.".to_string()));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let changed = {
|
||||||
|
let mut editor_borrow =
|
||||||
|
add_logic_page.state.script_content_editor.borrow_mut();
|
||||||
|
TextEditor::handle_input(
|
||||||
|
&mut editor_borrow,
|
||||||
|
key_event,
|
||||||
|
&add_logic_page.state.editor_keybinding_mode,
|
||||||
|
&mut add_logic_page.state.vim_state,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if changed {
|
||||||
|
add_logic_page.state.has_unsaved_changes = true;
|
||||||
|
return Ok(EventOutcome::Ok("Script updated".to_string()));
|
||||||
|
}
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Inside canvas: forward to FormEditor
|
||||||
|
let inside_canvas_inputs = matches!(
|
||||||
|
add_logic_page.state.current_focus,
|
||||||
|
AddLogicFocus::InputLogicName
|
||||||
|
| AddLogicFocus::InputTargetColumn
|
||||||
|
| AddLogicFocus::InputDescription
|
||||||
|
);
|
||||||
|
|
||||||
|
if inside_canvas_inputs {
|
||||||
|
// Only allow leaving the canvas with Down/Next when the form editor
|
||||||
|
// is in ReadOnly mode. In Edit mode, keep focus inside the canvas.
|
||||||
|
let in_edit_mode = add_logic_page.editor.mode() == CanvasMode::Edit;
|
||||||
|
if !in_edit_mode {
|
||||||
|
if let Some(ma) = movement {
|
||||||
|
let last_idx = add_logic_page
|
||||||
|
.editor
|
||||||
|
.data_provider()
|
||||||
|
.field_count()
|
||||||
|
.saturating_sub(1);
|
||||||
|
let at_last = add_logic_page.editor.current_field() >= last_idx;
|
||||||
|
if at_last && matches!(ma, MovementAction::Down | MovementAction::Next) {
|
||||||
|
add_logic_page.state.last_canvas_field = last_idx;
|
||||||
|
add_logic_page.state.current_focus = AddLogicFocus::ScriptContentPreview;
|
||||||
|
add_logic_page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok("Moved to Script Preview".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match add_logic_page.handle_key_event(key_event) {
|
||||||
|
canvas::keymap::KeyEventOutcome::Consumed(Some(msg)) => {
|
||||||
|
add_logic_page.sync_from_editor();
|
||||||
|
return Ok(EventOutcome::Ok(msg));
|
||||||
|
}
|
||||||
|
canvas::keymap::KeyEventOutcome::Consumed(None) => {
|
||||||
|
add_logic_page.sync_from_editor();
|
||||||
|
return Ok(EventOutcome::Ok("Input updated".into()));
|
||||||
|
}
|
||||||
|
canvas::keymap::KeyEventOutcome::Pending => {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
canvas::keymap::KeyEventOutcome::NotMatched => {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Outside canvas
|
||||||
|
if let Some(ma) = movement {
|
||||||
|
let mut current = add_logic_page.state.current_focus;
|
||||||
|
if move_focus(&ADD_LOGIC_FOCUS_ORDER, &mut current, ma) {
|
||||||
|
add_logic_page.state.current_focus = current;
|
||||||
|
add_logic_page.focus_outside_canvas = !matches!(
|
||||||
|
add_logic_page.state.current_focus,
|
||||||
|
AddLogicFocus::InputLogicName
|
||||||
|
| AddLogicFocus::InputTargetColumn
|
||||||
|
| AddLogicFocus::InputDescription
|
||||||
|
);
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
match ma {
|
||||||
|
MovementAction::Select => match add_logic_page.state.current_focus {
|
||||||
|
AddLogicFocus::ScriptContentPreview => {
|
||||||
|
add_logic_page.state.current_focus = AddLogicFocus::InsideScriptContent;
|
||||||
|
add_logic_page.focus_outside_canvas = false;
|
||||||
|
return Ok(EventOutcome::Ok(
|
||||||
|
"Fullscreen script editing. Esc to exit.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
AddLogicFocus::SaveButton => {
|
||||||
|
if let Some(msg) = add_logic_page.state.save_logic() {
|
||||||
|
return Ok(EventOutcome::Ok(msg));
|
||||||
|
} else {
|
||||||
|
return Ok(EventOutcome::Ok("Saved (no changes)".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddLogicFocus::CancelButton => {
|
||||||
|
return Ok(EventOutcome::Ok("Cancelled Add Logic".to_string()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
MovementAction::Esc => {
|
||||||
|
if add_logic_page.state.current_focus == AddLogicFocus::ScriptContentPreview {
|
||||||
|
add_logic_page.state.current_focus = AddLogicFocus::InputDescription;
|
||||||
|
add_logic_page.focus_outside_canvas = false;
|
||||||
|
return Ok(EventOutcome::Ok("Back to Description".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
|
}
|
||||||
115
client/src/pages/admin_panel/add_logic/loader.rs
Normal file
115
client/src/pages/admin_panel/add_logic/loader.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
// src/pages/admin_panel/add_logic/loader.rs
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
use crate::pages::admin_panel::add_logic::state::AddLogicFormState;
|
||||||
|
use crate::pages::routing::{Page, Router};
|
||||||
|
use crate::services::grpc_client::GrpcClient;
|
||||||
|
use crate::services::ui_service::UiService;
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
|
||||||
|
/// Process pending table structure fetch for AddLogic page.
|
||||||
|
/// Returns true if UI needs a redraw.
|
||||||
|
pub async fn process_pending_table_structure_fetch(
|
||||||
|
app_state: &mut AppState,
|
||||||
|
router: &mut Router,
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
command_message: &mut String,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let mut needs_redraw = false;
|
||||||
|
|
||||||
|
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
||||||
|
if let Page::AddLogic(page) = &mut router.current {
|
||||||
|
if page.profile_name() == profile_name
|
||||||
|
&& page.selected_table_name().map(|s| s.as_str()) == Some(table_name.as_str())
|
||||||
|
{
|
||||||
|
info!(
|
||||||
|
"Fetching table structure for {}.{}",
|
||||||
|
profile_name, table_name
|
||||||
|
);
|
||||||
|
|
||||||
|
let fetch_message = UiService::initialize_add_logic_table_data(
|
||||||
|
grpc_client,
|
||||||
|
&mut page.state, // keep state here, UiService expects AddLogicState
|
||||||
|
&app_state.profile_tree,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
error!(
|
||||||
|
"Error initializing add_logic_table_data for {}.{}: {}",
|
||||||
|
profile_name, table_name, e
|
||||||
|
);
|
||||||
|
format!("Error fetching table structure: {}", e)
|
||||||
|
});
|
||||||
|
|
||||||
|
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
|
||||||
|
info!("{}", fetch_message);
|
||||||
|
} else {
|
||||||
|
*command_message = fetch_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔑 Rebuild FormEditor with updated state (so suggestions work)
|
||||||
|
page.editor = canvas::FormEditor::new(page.state.clone());
|
||||||
|
|
||||||
|
needs_redraw = true;
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, \
|
||||||
|
but AddLogic state is for {}.{:?}",
|
||||||
|
profile_name,
|
||||||
|
table_name,
|
||||||
|
page.profile_name(),
|
||||||
|
page.selected_table_name()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"Pending table structure fetch for {}.{} but AddLogic view is not active. Ignored.",
|
||||||
|
profile_name, table_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(needs_redraw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the AddLogic page is awaiting columns for a selected table in the script editor,
|
||||||
|
/// fetch them and update the state. Returns true if UI needs a redraw.
|
||||||
|
pub async fn maybe_fetch_columns_for_awaiting_table(
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
page: &mut AddLogicFormState,
|
||||||
|
command_message: &mut String,
|
||||||
|
) -> Result<bool> {
|
||||||
|
if let Some(table_name) = page
|
||||||
|
.state
|
||||||
|
.script_editor_awaiting_column_autocomplete
|
||||||
|
.clone()
|
||||||
|
{
|
||||||
|
let profile_name = page.state.profile_name.clone();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Fetching columns for table selection: {}.{}",
|
||||||
|
profile_name, table_name
|
||||||
|
);
|
||||||
|
match UiService::fetch_columns_for_table(grpc_client, &profile_name, &table_name).await {
|
||||||
|
Ok(columns) => {
|
||||||
|
page.state.set_columns_for_table_autocomplete(columns.clone());
|
||||||
|
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
|
||||||
|
*command_message =
|
||||||
|
format!("Columns for '{}' loaded. Select a column.", table_name);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Failed to fetch columns for {}.{}: {}",
|
||||||
|
profile_name, table_name, e
|
||||||
|
);
|
||||||
|
page.state.script_editor_awaiting_column_autocomplete = None;
|
||||||
|
page.state.deactivate_script_editor_autocomplete();
|
||||||
|
*command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
@@ -3,3 +3,5 @@
|
|||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod nav;
|
pub mod nav;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
pub mod loader;
|
||||||
|
pub mod event;
|
||||||
|
|||||||
@@ -1,531 +1,6 @@
|
|||||||
// src/pages/admin_panel/add_logic/nav.rs
|
// src/pages/admin_panel/add_logic/nav.rs
|
||||||
|
|
||||||
use crate::config::binds::config::{Config, EditorKeybindingMode};
|
|
||||||
use crate::state::app::state::AppState;
|
|
||||||
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState};
|
|
||||||
use crate::buffer::{AppView, BufferState};
|
|
||||||
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
|
|
||||||
use crate::services::GrpcClient;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crate::components::common::text_editor::TextEditor;
|
use tokio::sync::mpsc;
|
||||||
use crate::services::ui_service::UiService;
|
|
||||||
use tui_textarea::CursorMove;
|
|
||||||
use crate::pages::admin::AdminState;
|
|
||||||
use crate::pages::routing::{Router, Page};
|
|
||||||
|
|
||||||
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
pub type SaveLogicResultSender = mpsc::Sender<Result<String>>;
|
||||||
|
|
||||||
pub fn handle_add_logic_navigation(
|
|
||||||
key_event: KeyEvent,
|
|
||||||
config: &Config,
|
|
||||||
app_state: &mut AppState,
|
|
||||||
buffer_state: &mut BufferState,
|
|
||||||
grpc_client: GrpcClient,
|
|
||||||
save_logic_sender: SaveLogicResultSender,
|
|
||||||
command_message: &mut String,
|
|
||||||
router: &mut Router,
|
|
||||||
) -> bool {
|
|
||||||
if let Page::AddLogic(add_logic_state) = &mut router.current {
|
|
||||||
// === FULLSCREEN SCRIPT EDITING ===
|
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
|
||||||
// === AUTOCOMPLETE HANDLING ===
|
|
||||||
if add_logic_state.script_editor_autocomplete_active {
|
|
||||||
match key_event.code {
|
|
||||||
KeyCode::Char(c) if c.is_alphanumeric() || c == '_' => {
|
|
||||||
add_logic_state.script_editor_filter_text.push(c);
|
|
||||||
add_logic_state.update_script_editor_suggestions();
|
|
||||||
{
|
|
||||||
let mut editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*command_message =
|
|
||||||
format!("Filtering: @{}", add_logic_state.script_editor_filter_text);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
if !add_logic_state.script_editor_filter_text.is_empty() {
|
|
||||||
add_logic_state.script_editor_filter_text.pop();
|
|
||||||
add_logic_state.update_script_editor_suggestions();
|
|
||||||
{
|
|
||||||
let mut editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*command_message =
|
|
||||||
if add_logic_state.script_editor_filter_text.is_empty() {
|
|
||||||
"Autocomplete: @".to_string()
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"Filtering: @{}",
|
|
||||||
add_logic_state.script_editor_filter_text
|
|
||||||
)
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
let should_deactivate =
|
|
||||||
if let Some((trigger_line, trigger_col)) =
|
|
||||||
add_logic_state.script_editor_trigger_position
|
|
||||||
{
|
|
||||||
let current_cursor = {
|
|
||||||
let editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow();
|
|
||||||
editor_borrow.cursor()
|
|
||||||
};
|
|
||||||
current_cursor.0 == trigger_line
|
|
||||||
&& current_cursor.1 == trigger_col + 1
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
if should_deactivate {
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
*command_message = "Autocomplete cancelled".to_string();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let mut editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Tab | KeyCode::Down => {
|
|
||||||
if !add_logic_state.script_editor_suggestions.is_empty() {
|
|
||||||
let current = add_logic_state
|
|
||||||
.script_editor_selected_suggestion_index
|
|
||||||
.unwrap_or(0);
|
|
||||||
let next =
|
|
||||||
(current + 1) % add_logic_state.script_editor_suggestions.len();
|
|
||||||
add_logic_state.script_editor_selected_suggestion_index = Some(next);
|
|
||||||
*command_message = format!(
|
|
||||||
"Selected: {}",
|
|
||||||
add_logic_state.script_editor_suggestions[next]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Up => {
|
|
||||||
if !add_logic_state.script_editor_suggestions.is_empty() {
|
|
||||||
let current = add_logic_state
|
|
||||||
.script_editor_selected_suggestion_index
|
|
||||||
.unwrap_or(0);
|
|
||||||
let prev = if current == 0 {
|
|
||||||
add_logic_state.script_editor_suggestions.len() - 1
|
|
||||||
} else {
|
|
||||||
current - 1
|
|
||||||
};
|
|
||||||
add_logic_state.script_editor_selected_suggestion_index = Some(prev);
|
|
||||||
*command_message = format!(
|
|
||||||
"Selected: {}",
|
|
||||||
add_logic_state.script_editor_suggestions[prev]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
if let Some(selected_idx) =
|
|
||||||
add_logic_state.script_editor_selected_suggestion_index
|
|
||||||
{
|
|
||||||
if let Some(suggestion) = add_logic_state
|
|
||||||
.script_editor_suggestions
|
|
||||||
.get(selected_idx)
|
|
||||||
.cloned()
|
|
||||||
{
|
|
||||||
let trigger_pos =
|
|
||||||
add_logic_state.script_editor_trigger_position;
|
|
||||||
let filter_len =
|
|
||||||
add_logic_state.script_editor_filter_text.len();
|
|
||||||
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
add_logic_state.has_unsaved_changes = true;
|
|
||||||
|
|
||||||
if let Some(pos) = trigger_pos {
|
|
||||||
let mut editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
|
|
||||||
if suggestion == "sql" {
|
|
||||||
replace_autocomplete_text(
|
|
||||||
&mut editor_borrow,
|
|
||||||
pos,
|
|
||||||
filter_len,
|
|
||||||
"sql",
|
|
||||||
);
|
|
||||||
editor_borrow.insert_str("('')");
|
|
||||||
editor_borrow.move_cursor(CursorMove::Back);
|
|
||||||
editor_borrow.move_cursor(CursorMove::Back);
|
|
||||||
*command_message = "Inserted: @sql('')".to_string();
|
|
||||||
} else {
|
|
||||||
let is_table_selection =
|
|
||||||
add_logic_state.is_table_name_suggestion(&suggestion);
|
|
||||||
replace_autocomplete_text(
|
|
||||||
&mut editor_borrow,
|
|
||||||
pos,
|
|
||||||
filter_len,
|
|
||||||
&suggestion,
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_table_selection {
|
|
||||||
editor_borrow.insert_str(".");
|
|
||||||
let new_cursor = editor_borrow.cursor();
|
|
||||||
drop(editor_borrow);
|
|
||||||
|
|
||||||
add_logic_state.script_editor_trigger_position =
|
|
||||||
Some(new_cursor);
|
|
||||||
add_logic_state.script_editor_autocomplete_active = true;
|
|
||||||
add_logic_state.script_editor_filter_text.clear();
|
|
||||||
add_logic_state
|
|
||||||
.trigger_column_autocomplete_for_table(
|
|
||||||
suggestion.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let profile_name =
|
|
||||||
add_logic_state.profile_name.clone();
|
|
||||||
let table_name_for_fetch = suggestion.clone();
|
|
||||||
let mut client_clone = grpc_client.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(e) = UiService::fetch_columns_for_table(
|
|
||||||
&mut client_clone,
|
|
||||||
&profile_name,
|
|
||||||
&table_name_for_fetch,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to fetch columns for {}.{}: {}",
|
|
||||||
profile_name,
|
|
||||||
table_name_for_fetch,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*command_message = format!(
|
|
||||||
"Selected table '{}', fetching columns...",
|
|
||||||
suggestion
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
*command_message =
|
|
||||||
format!("Inserted: {}", suggestion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
{
|
|
||||||
let mut editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
*command_message = "Autocomplete cancelled".to_string();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
add_logic_state.deactivate_script_editor_autocomplete();
|
|
||||||
*command_message = "Autocomplete cancelled".to_string();
|
|
||||||
{
|
|
||||||
let mut editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger autocomplete with '@'
|
|
||||||
if key_event.code == KeyCode::Char('@') && key_event.modifiers == KeyModifiers::NONE {
|
|
||||||
let should_trigger = match add_logic_state.editor_keybinding_mode {
|
|
||||||
EditorKeybindingMode::Vim => {
|
|
||||||
TextEditor::is_vim_insert_mode(&add_logic_state.vim_state)
|
|
||||||
}
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
if should_trigger {
|
|
||||||
let cursor_before = {
|
|
||||||
let editor_borrow = add_logic_state.script_content_editor.borrow();
|
|
||||||
editor_borrow.cursor()
|
|
||||||
};
|
|
||||||
{
|
|
||||||
let mut editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
add_logic_state.script_editor_trigger_position = Some(cursor_before);
|
|
||||||
add_logic_state.script_editor_autocomplete_active = true;
|
|
||||||
add_logic_state.script_editor_filter_text.clear();
|
|
||||||
add_logic_state.update_script_editor_suggestions();
|
|
||||||
add_logic_state.has_unsaved_changes = true;
|
|
||||||
*command_message = "Autocomplete: @ (Tab/↑↓ to navigate, Enter to select, Esc to cancel)".to_string();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Esc handling
|
|
||||||
if key_event.code == KeyCode::Esc && key_event.modifiers == KeyModifiers::NONE {
|
|
||||||
match add_logic_state.editor_keybinding_mode {
|
|
||||||
EditorKeybindingMode::Vim => {
|
|
||||||
let was_insert =
|
|
||||||
TextEditor::is_vim_insert_mode(&add_logic_state.vim_state);
|
|
||||||
{
|
|
||||||
let mut editor_borrow =
|
|
||||||
add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if was_insert {
|
|
||||||
*command_message =
|
|
||||||
"VIM: Normal Mode. Esc again to exit script.".to_string();
|
|
||||||
} else {
|
|
||||||
add_logic_state.current_focus =
|
|
||||||
AddLogicFocus::ScriptContentPreview;
|
|
||||||
app_state.ui.focus_outside_canvas = true;
|
|
||||||
*command_message = "Exited script editing.".to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
add_logic_state.current_focus = AddLogicFocus::ScriptContentPreview;
|
|
||||||
app_state.ui.focus_outside_canvas = true;
|
|
||||||
*command_message = "Exited script editing.".to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal text input
|
|
||||||
let changed = {
|
|
||||||
let mut editor_borrow = add_logic_state.script_content_editor.borrow_mut();
|
|
||||||
TextEditor::handle_input(
|
|
||||||
&mut editor_borrow,
|
|
||||||
key_event,
|
|
||||||
&add_logic_state.editor_keybinding_mode,
|
|
||||||
&mut add_logic_state.vim_state,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if changed {
|
|
||||||
add_logic_state.has_unsaved_changes = true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === NON-FULLSCREEN NAVIGATION ===
|
|
||||||
let action = config.get_general_action(key_event.code, key_event.modifiers);
|
|
||||||
let current_focus = add_logic_state.current_focus;
|
|
||||||
let mut handled = true;
|
|
||||||
let mut new_focus = current_focus;
|
|
||||||
|
|
||||||
match action.as_deref() {
|
|
||||||
Some("exit_table_scroll") => {
|
|
||||||
handled = false;
|
|
||||||
}
|
|
||||||
Some("move_up") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName => {}
|
|
||||||
AddLogicFocus::InputTargetColumn => new_focus = AddLogicFocus::InputLogicName,
|
|
||||||
AddLogicFocus::InputDescription => {
|
|
||||||
new_focus = AddLogicFocus::InputTargetColumn
|
|
||||||
}
|
|
||||||
AddLogicFocus::ScriptContentPreview => {
|
|
||||||
new_focus = AddLogicFocus::InputDescription
|
|
||||||
}
|
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::ScriptContentPreview,
|
|
||||||
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("move_down") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName => {
|
|
||||||
new_focus = AddLogicFocus::InputTargetColumn
|
|
||||||
}
|
|
||||||
AddLogicFocus::InputTargetColumn => {
|
|
||||||
new_focus = AddLogicFocus::InputDescription
|
|
||||||
}
|
|
||||||
AddLogicFocus::InputDescription => {
|
|
||||||
add_logic_state.last_canvas_field = 2;
|
|
||||||
new_focus = AddLogicFocus::ScriptContentPreview;
|
|
||||||
}
|
|
||||||
AddLogicFocus::ScriptContentPreview => {
|
|
||||||
new_focus = AddLogicFocus::SaveButton
|
|
||||||
}
|
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
|
||||||
AddLogicFocus::CancelButton => {}
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("next_option") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName
|
|
||||||
| AddLogicFocus::InputTargetColumn
|
|
||||||
| AddLogicFocus::InputDescription => {
|
|
||||||
new_focus = AddLogicFocus::ScriptContentPreview
|
|
||||||
}
|
|
||||||
AddLogicFocus::ScriptContentPreview => {
|
|
||||||
new_focus = AddLogicFocus::SaveButton
|
|
||||||
}
|
|
||||||
AddLogicFocus::SaveButton => new_focus = AddLogicFocus::CancelButton,
|
|
||||||
AddLogicFocus::CancelButton => {}
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("previous_option") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName
|
|
||||||
| AddLogicFocus::InputTargetColumn
|
|
||||||
| AddLogicFocus::InputDescription => {}
|
|
||||||
AddLogicFocus::ScriptContentPreview => {
|
|
||||||
new_focus = AddLogicFocus::InputDescription
|
|
||||||
}
|
|
||||||
AddLogicFocus::SaveButton => {
|
|
||||||
new_focus = AddLogicFocus::ScriptContentPreview
|
|
||||||
}
|
|
||||||
AddLogicFocus::CancelButton => new_focus = AddLogicFocus::SaveButton,
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("next_field") => {
|
|
||||||
new_focus = match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName => AddLogicFocus::InputTargetColumn,
|
|
||||||
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputDescription,
|
|
||||||
AddLogicFocus::InputDescription => AddLogicFocus::ScriptContentPreview,
|
|
||||||
AddLogicFocus::ScriptContentPreview => AddLogicFocus::SaveButton,
|
|
||||||
AddLogicFocus::SaveButton => AddLogicFocus::CancelButton,
|
|
||||||
AddLogicFocus::CancelButton => AddLogicFocus::InputLogicName,
|
|
||||||
_ => current_focus,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some("prev_field") => {
|
|
||||||
new_focus = match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName => AddLogicFocus::CancelButton,
|
|
||||||
AddLogicFocus::InputTargetColumn => AddLogicFocus::InputLogicName,
|
|
||||||
AddLogicFocus::InputDescription => AddLogicFocus::InputTargetColumn,
|
|
||||||
AddLogicFocus::ScriptContentPreview => AddLogicFocus::InputDescription,
|
|
||||||
AddLogicFocus::SaveButton => AddLogicFocus::ScriptContentPreview,
|
|
||||||
AddLogicFocus::CancelButton => AddLogicFocus::SaveButton,
|
|
||||||
_ => current_focus,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some("select") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::ScriptContentPreview => {
|
|
||||||
new_focus = AddLogicFocus::InsideScriptContent;
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
let mode_hint = match add_logic_state.editor_keybinding_mode {
|
|
||||||
EditorKeybindingMode::Vim => {
|
|
||||||
"VIM mode - 'i'/'a'/'o' to edit"
|
|
||||||
}
|
|
||||||
_ => "Enter/Ctrl+E to edit",
|
|
||||||
};
|
|
||||||
*command_message = format!(
|
|
||||||
"Fullscreen script editing. {} or Esc to exit.",
|
|
||||||
mode_hint
|
|
||||||
);
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
AddLogicFocus::SaveButton => {
|
|
||||||
*command_message = "Save logic action".to_string();
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
AddLogicFocus::CancelButton => {
|
|
||||||
buffer_state.update_history(AppView::Admin);
|
|
||||||
*command_message = "Cancelled Add Logic".to_string();
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
AddLogicFocus::InputLogicName
|
|
||||||
| AddLogicFocus::InputTargetColumn
|
|
||||||
| AddLogicFocus::InputDescription => {
|
|
||||||
// Focus canvas inputs; let canvas keymap handle editing
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
handled = false; // forward to canvas
|
|
||||||
}
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("toggle_edit_mode") => {
|
|
||||||
match current_focus {
|
|
||||||
AddLogicFocus::InputLogicName
|
|
||||||
| AddLogicFocus::InputTargetColumn
|
|
||||||
| AddLogicFocus::InputDescription => {
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
*command_message =
|
|
||||||
"Focus moved to input. Use i/a (Vim) or type to edit.".to_string();
|
|
||||||
handled = true;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
*command_message = "Cannot toggle edit mode here.".to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
|
|
||||||
if handled && current_focus != new_focus {
|
|
||||||
add_logic_state.current_focus = new_focus;
|
|
||||||
let new_is_canvas_input_focus = matches!(
|
|
||||||
new_focus,
|
|
||||||
AddLogicFocus::InputLogicName
|
|
||||||
| AddLogicFocus::InputTargetColumn
|
|
||||||
| AddLogicFocus::InputDescription
|
|
||||||
);
|
|
||||||
if new_is_canvas_input_focus {
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
} else {
|
|
||||||
app_state.ui.focus_outside_canvas = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handled
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_autocomplete_text(
|
|
||||||
editor: &mut tui_textarea::TextArea,
|
|
||||||
trigger_pos: (usize, usize),
|
|
||||||
filter_len: usize,
|
|
||||||
replacement: &str,
|
|
||||||
) {
|
|
||||||
let filter_start_pos = (trigger_pos.0, trigger_pos.1 + 1);
|
|
||||||
editor.move_cursor(CursorMove::Jump(filter_start_pos.0 as u16, filter_start_pos.1 as u16));
|
|
||||||
for _ in 0..filter_len {
|
|
||||||
editor.delete_next_char();
|
|
||||||
}
|
|
||||||
editor.insert_str(replacement);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// src/pages/admin_panel/add_logic/state.rs
|
// src/pages/admin_panel/add_logic/state.rs
|
||||||
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
use crate::config::binds::config::{EditorConfig, EditorKeybindingMode};
|
||||||
use crate::components::common::text_editor::{TextEditor, VimState};
|
use crate::components::common::text_editor::{TextEditor, VimState};
|
||||||
use canvas::{DataProvider, AppMode};
|
use canvas::{DataProvider, AppMode, FormEditor, SuggestionItem};
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
@@ -98,6 +99,19 @@ impl AddLogicState {
|
|||||||
|
|
||||||
pub const INPUT_FIELD_COUNT: usize = 3;
|
pub const INPUT_FIELD_COUNT: usize = 3;
|
||||||
|
|
||||||
|
/// Build canvas SuggestionItem list for target column
|
||||||
|
pub fn column_suggestions_sync(&self, query: &str) -> Vec<SuggestionItem> {
|
||||||
|
let q = query.to_lowercase();
|
||||||
|
self.table_columns_for_suggestions
|
||||||
|
.iter()
|
||||||
|
.filter(|c| q.is_empty() || c.to_lowercase().contains(&q))
|
||||||
|
.map(|c| SuggestionItem {
|
||||||
|
display_text: c.clone(),
|
||||||
|
value_to_store: c.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates the target_column_suggestions based on current input.
|
/// Updates the target_column_suggestions based on current input.
|
||||||
pub fn update_target_column_suggestions(&mut self) {
|
pub fn update_target_column_suggestions(&mut self) {
|
||||||
let current_input = self.target_column_input.to_lowercase();
|
let current_input = self.target_column_input.to_lowercase();
|
||||||
@@ -315,3 +329,242 @@ impl DataProvider for AddLogicState {
|
|||||||
field_index == 1
|
field_index == 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper that owns both the raw state and its FormEditor (like LoginFormState)
|
||||||
|
pub struct AddLogicFormState {
|
||||||
|
pub state: AddLogicState,
|
||||||
|
pub editor: FormEditor<AddLogicState>,
|
||||||
|
pub focus_outside_canvas: bool,
|
||||||
|
pub focused_button_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
// manual Debug because FormEditor may not implement Debug
|
||||||
|
impl std::fmt::Debug for AddLogicFormState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("AddLogicFormState")
|
||||||
|
.field("state", &self.state)
|
||||||
|
.field("focus_outside_canvas", &self.focus_outside_canvas)
|
||||||
|
.field("focused_button_index", &self.focused_button_index)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddLogicFormState {
|
||||||
|
pub fn new(editor_config: &EditorConfig) -> Self {
|
||||||
|
let state = AddLogicState::new(editor_config);
|
||||||
|
let editor = FormEditor::new(state.clone());
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
editor,
|
||||||
|
focus_outside_canvas: false,
|
||||||
|
focused_button_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_table(
|
||||||
|
editor_config: &EditorConfig,
|
||||||
|
profile_name: String,
|
||||||
|
table_id: Option<i64>,
|
||||||
|
table_name: String,
|
||||||
|
) -> Self {
|
||||||
|
let mut state = AddLogicState::new(editor_config);
|
||||||
|
state.profile_name = profile_name;
|
||||||
|
state.selected_table_id = table_id;
|
||||||
|
state.selected_table_name = Some(table_name);
|
||||||
|
let editor = FormEditor::new(state.clone());
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
editor,
|
||||||
|
focus_outside_canvas: false,
|
||||||
|
focused_button_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_state(state: AddLogicState) -> Self {
|
||||||
|
let editor = FormEditor::new(state.clone());
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
editor,
|
||||||
|
focus_outside_canvas: false,
|
||||||
|
focused_button_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync state from editor's data provider snapshot
|
||||||
|
pub fn sync_from_editor(&mut self) {
|
||||||
|
self.state = self.editor.data_provider().clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Delegates to AddLogicState fields ===
|
||||||
|
|
||||||
|
pub fn current_focus(&self) -> AddLogicFocus {
|
||||||
|
self.state.current_focus
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_focus(&mut self, focus: AddLogicFocus) {
|
||||||
|
self.state.current_focus = focus;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_unsaved_changes(&self) -> bool {
|
||||||
|
self.state.has_unsaved_changes
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||||
|
self.state.has_unsaved_changes = changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn profile_name(&self) -> &str {
|
||||||
|
&self.state.profile_name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_table_name(&self) -> Option<&String> {
|
||||||
|
self.state.selected_table_name.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_table_id(&self) -> Option<i64> {
|
||||||
|
self.state.selected_table_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn script_content_editor(&self) -> &Rc<RefCell<TextArea<'static>>> {
|
||||||
|
&self.state.script_content_editor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn script_content_editor_mut(&mut self) -> &mut Rc<RefCell<TextArea<'static>>> {
|
||||||
|
&mut self.state.script_content_editor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vim_state(&self) -> &VimState {
|
||||||
|
&self.state.vim_state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vim_state_mut(&mut self) -> &mut VimState {
|
||||||
|
&mut self.state.vim_state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editor_keybinding_mode(&self) -> &EditorKeybindingMode {
|
||||||
|
&self.state.editor_keybinding_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn script_editor_autocomplete_active(&self) -> bool {
|
||||||
|
self.state.script_editor_autocomplete_active
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn script_editor_suggestions(&self) -> &Vec<String> {
|
||||||
|
&self.state.script_editor_suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn script_editor_selected_suggestion_index(&self) -> Option<usize> {
|
||||||
|
self.state.script_editor_selected_suggestion_index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn target_column_suggestions(&self) -> &Vec<String> {
|
||||||
|
&self.state.target_column_suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_target_column_suggestion_index(&self) -> Option<usize> {
|
||||||
|
self.state.selected_target_column_suggestion_index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn in_target_column_suggestion_mode(&self) -> bool {
|
||||||
|
self.state.in_target_column_suggestion_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_target_column_suggestions(&self) -> bool {
|
||||||
|
self.state.show_target_column_suggestions
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Delegates to FormEditor ===
|
||||||
|
|
||||||
|
pub fn mode(&self) -> AppMode {
|
||||||
|
self.editor.mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor_position(&self) -> usize {
|
||||||
|
self.editor.cursor_position()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_key_event(
|
||||||
|
&mut self,
|
||||||
|
key_event: crossterm::event::KeyEvent,
|
||||||
|
) -> canvas::keymap::KeyEventOutcome {
|
||||||
|
// Customize behavior for Target Column (field index 1) in Edit mode,
|
||||||
|
// mirroring how Register page does suggestions for Role.
|
||||||
|
let in_target_col_field = self.editor.current_field() == 1;
|
||||||
|
let in_edit_mode = self.editor.mode() == canvas::AppMode::Edit;
|
||||||
|
|
||||||
|
if in_target_col_field && in_edit_mode {
|
||||||
|
match key_event.code {
|
||||||
|
// Tab: open suggestions if inactive; otherwise cycle next
|
||||||
|
KeyCode::Tab => {
|
||||||
|
if !self.editor.is_suggestions_active() {
|
||||||
|
if let Some(query) = self.editor.start_suggestions(1) {
|
||||||
|
let items = self.state.column_suggestions_sync(&query);
|
||||||
|
let applied =
|
||||||
|
self.editor.apply_suggestions_result(1, &query, items);
|
||||||
|
if applied {
|
||||||
|
self.editor.update_inline_completion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.editor.suggestions_next();
|
||||||
|
}
|
||||||
|
return canvas::keymap::KeyEventOutcome::Consumed(None);
|
||||||
|
}
|
||||||
|
// Shift+Tab: cycle suggestions too (fallback to next)
|
||||||
|
KeyCode::BackTab => {
|
||||||
|
if self.editor.is_suggestions_active() {
|
||||||
|
self.editor.suggestions_next();
|
||||||
|
return canvas::keymap::KeyEventOutcome::Consumed(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Enter: apply selected suggestion (if active)
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if self.editor.is_suggestions_active() {
|
||||||
|
let _ = self.editor.apply_suggestion();
|
||||||
|
return canvas::keymap::KeyEventOutcome::Consumed(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Esc: close suggestions if active
|
||||||
|
KeyCode::Esc => {
|
||||||
|
if self.editor.is_suggestions_active() {
|
||||||
|
self.editor.close_suggestions();
|
||||||
|
return canvas::keymap::KeyEventOutcome::Consumed(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Character input: mutate then refresh suggestions if active
|
||||||
|
KeyCode::Char(_) => {
|
||||||
|
let outcome = self.editor.handle_key_event(key_event);
|
||||||
|
if self.editor.is_suggestions_active() {
|
||||||
|
if let Some(query) = self.editor.start_suggestions(1) {
|
||||||
|
let items = self.state.column_suggestions_sync(&query);
|
||||||
|
let applied =
|
||||||
|
self.editor.apply_suggestions_result(1, &query, items);
|
||||||
|
if applied {
|
||||||
|
self.editor.update_inline_completion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
// Backspace/Delete: mutate then refresh suggestions if active
|
||||||
|
KeyCode::Backspace | KeyCode::Delete => {
|
||||||
|
let outcome = self.editor.handle_key_event(key_event);
|
||||||
|
if self.editor.is_suggestions_active() {
|
||||||
|
if let Some(query) = self.editor.start_suggestions(1) {
|
||||||
|
let items = self.state.column_suggestions_sync(&query);
|
||||||
|
let applied =
|
||||||
|
self.editor.apply_suggestions_result(1, &query, items);
|
||||||
|
if applied {
|
||||||
|
self.editor.update_inline_completion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
_ => { /* fall through */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default: let canvas handle it
|
||||||
|
self.editor.handle_key_event(key_event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// src/pages/admin_panel/add_logic/ui.rs
|
// src/pages/admin_panel/add_logic/ui.rs
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState};
|
use crate::pages::admin_panel::add_logic::state::{AddLogicFocus, AddLogicState, AddLogicFormState};
|
||||||
use canvas::{render_canvas, FormEditor};
|
use canvas::{render_canvas, render_suggestions_dropdown, DefaultCanvasTheme, FormEditor};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
@@ -19,7 +19,7 @@ pub fn render_add_logic(
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
add_logic_state: &mut AddLogicState,
|
add_logic_state: &mut AddLogicFormState,
|
||||||
) {
|
) {
|
||||||
let main_block = Block::default()
|
let main_block = Block::default()
|
||||||
.title(" Add New Logic Script ")
|
.title(" Add New Logic Script ")
|
||||||
@@ -32,9 +32,13 @@ pub fn render_add_logic(
|
|||||||
f.render_widget(main_block, area);
|
f.render_widget(main_block, area);
|
||||||
|
|
||||||
// Handle full-screen script editing
|
// Handle full-screen script editing
|
||||||
if add_logic_state.current_focus == AddLogicFocus::InsideScriptContent {
|
if add_logic_state.current_focus() == AddLogicFocus::InsideScriptContent {
|
||||||
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_ref = add_logic_state
|
||||||
let border_style_color = if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) {
|
.state
|
||||||
|
.script_content_editor
|
||||||
|
.borrow_mut();
|
||||||
|
|
||||||
|
let border_style_color = if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(add_logic_state.vim_state()) {
|
||||||
theme.highlight
|
theme.highlight
|
||||||
} else {
|
} else {
|
||||||
theme.secondary
|
theme.secondary
|
||||||
@@ -44,13 +48,13 @@ pub fn render_add_logic(
|
|||||||
editor_ref.set_cursor_line_style(Style::default());
|
editor_ref.set_cursor_line_style(Style::default());
|
||||||
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||||
|
|
||||||
let script_title_hint = match add_logic_state.editor_keybinding_mode {
|
let script_title_hint = match add_logic_state.editor_keybinding_mode() {
|
||||||
EditorKeybindingMode::Vim => {
|
EditorKeybindingMode::Vim => {
|
||||||
let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(&add_logic_state.vim_state);
|
let vim_mode_status = crate::components::common::text_editor::TextEditor::get_vim_mode_status(add_logic_state.vim_state());
|
||||||
format!("Script {}", vim_mode_status)
|
format!("Script {}", vim_mode_status)
|
||||||
}
|
}
|
||||||
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
|
EditorKeybindingMode::Emacs | EditorKeybindingMode::Default => {
|
||||||
if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(&add_logic_state.vim_state) {
|
if crate::components::common::text_editor::TextEditor::is_vim_insert_mode(add_logic_state.vim_state()) {
|
||||||
"Script (Editing)".to_string()
|
"Script (Editing)".to_string()
|
||||||
} else {
|
} else {
|
||||||
"Script".to_string()
|
"Script".to_string()
|
||||||
@@ -72,10 +76,10 @@ pub fn render_add_logic(
|
|||||||
drop(editor_ref);
|
drop(editor_ref);
|
||||||
|
|
||||||
// === SCRIPT EDITOR AUTOCOMPLETE RENDERING ===
|
// === SCRIPT EDITOR AUTOCOMPLETE RENDERING ===
|
||||||
if add_logic_state.script_editor_autocomplete_active && !add_logic_state.script_editor_suggestions.is_empty() {
|
if add_logic_state.script_editor_autocomplete_active() && !add_logic_state.script_editor_suggestions().is_empty() {
|
||||||
// Get the current cursor position from textarea
|
// Get the current cursor position from textarea
|
||||||
let current_cursor = {
|
let current_cursor = {
|
||||||
let editor_borrow = add_logic_state.script_content_editor.borrow();
|
let editor_borrow = add_logic_state.script_content_editor().borrow();
|
||||||
editor_borrow.cursor() // Returns (row, col) as (usize, usize)
|
editor_borrow.cursor() // Returns (row, col) as (usize, usize)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,8 +107,8 @@ pub fn render_add_logic(
|
|||||||
input_rect,
|
input_rect,
|
||||||
f.area(), // Full frame area for clamping
|
f.area(), // Full frame area for clamping
|
||||||
theme,
|
theme,
|
||||||
&add_logic_state.script_editor_suggestions,
|
add_logic_state.script_editor_suggestions(),
|
||||||
add_logic_state.script_editor_selected_suggestion_index,
|
add_logic_state.script_editor_selected_suggestion_index(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,21 +132,21 @@ pub fn render_add_logic(
|
|||||||
let buttons_area = main_chunks[3];
|
let buttons_area = main_chunks[3];
|
||||||
|
|
||||||
// Top info
|
// Top info
|
||||||
|
let table_label = if let Some(name) = add_logic_state.selected_table_name() {
|
||||||
|
name.clone()
|
||||||
|
} else if let Some(id) = add_logic_state.selected_table_id() {
|
||||||
|
format!("ID {}", id)
|
||||||
|
} else {
|
||||||
|
"Global (Not Selected)".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let profile_text = Paragraph::new(vec![
|
let profile_text = Paragraph::new(vec![
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!("Profile: {}", add_logic_state.profile_name),
|
format!("Profile: {}", add_logic_state.profile_name()),
|
||||||
Style::default().fg(theme.fg),
|
Style::default().fg(theme.fg),
|
||||||
)),
|
)),
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!(
|
format!("Table: {}", table_label),
|
||||||
"Table: {}",
|
|
||||||
add_logic_state
|
|
||||||
.selected_table_name
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| add_logic_state.selected_table_id
|
|
||||||
.map(|id| format!("ID {}", id))
|
|
||||||
.unwrap_or_else(|| "Global (Not Selected)".to_string()))
|
|
||||||
),
|
|
||||||
Style::default().fg(theme.fg),
|
Style::default().fg(theme.fg),
|
||||||
)),
|
)),
|
||||||
])
|
])
|
||||||
@@ -155,39 +159,34 @@ pub fn render_add_logic(
|
|||||||
|
|
||||||
// Canvas - USING CANVAS LIBRARY
|
// Canvas - USING CANVAS LIBRARY
|
||||||
let focus_on_canvas_inputs = matches!(
|
let focus_on_canvas_inputs = matches!(
|
||||||
add_logic_state.current_focus,
|
add_logic_state.current_focus(),
|
||||||
AddLogicFocus::InputLogicName
|
AddLogicFocus::InputLogicName
|
||||||
| AddLogicFocus::InputTargetColumn
|
| AddLogicFocus::InputTargetColumn
|
||||||
| AddLogicFocus::InputDescription
|
| AddLogicFocus::InputDescription
|
||||||
);
|
);
|
||||||
|
|
||||||
let editor = FormEditor::new(add_logic_state.clone());
|
let editor = &add_logic_state.editor;
|
||||||
let active_field_rect = render_canvas(f, canvas_area, &editor, theme);
|
let active_field_rect = render_canvas(f, canvas_area, editor, theme);
|
||||||
|
|
||||||
// --- Render Autocomplete for Target Column ---
|
// --- Canvas suggestions dropdown (Target Column, etc.) ---
|
||||||
if editor.mode() == canvas::AppMode::Edit && editor.current_field() == 1 { // Target Column field
|
if editor.mode() == canvas::AppMode::Edit {
|
||||||
if add_logic_state.in_target_column_suggestion_mode && add_logic_state.show_target_column_suggestions {
|
|
||||||
if !add_logic_state.target_column_suggestions.is_empty() {
|
|
||||||
if let Some(input_rect) = active_field_rect {
|
if let Some(input_rect) = active_field_rect {
|
||||||
autocomplete::render_autocomplete_dropdown(
|
render_suggestions_dropdown(
|
||||||
f,
|
f,
|
||||||
|
f.area(),
|
||||||
input_rect,
|
input_rect,
|
||||||
f.area(), // Full frame area for clamping
|
&DefaultCanvasTheme,
|
||||||
theme,
|
editor,
|
||||||
&add_logic_state.target_column_suggestions,
|
|
||||||
add_logic_state.selected_target_column_suggestion_index,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Script content preview
|
// Script content preview
|
||||||
{
|
{
|
||||||
let mut editor_ref = add_logic_state.script_content_editor.borrow_mut();
|
let mut editor_ref = add_logic_state.script_content_editor().borrow_mut();
|
||||||
editor_ref.set_cursor_line_style(Style::default());
|
editor_ref.set_cursor_line_style(Style::default());
|
||||||
|
|
||||||
let is_script_preview_focused = add_logic_state.current_focus == AddLogicFocus::ScriptContentPreview;
|
let is_script_preview_focused = add_logic_state.current_focus() == AddLogicFocus::ScriptContentPreview;
|
||||||
|
|
||||||
if is_script_preview_focused {
|
if is_script_preview_focused {
|
||||||
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
editor_ref.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||||
@@ -256,7 +255,7 @@ pub fn render_add_logic(
|
|||||||
let save_button = Paragraph::new(" Save Logic ")
|
let save_button = Paragraph::new(" Save Logic ")
|
||||||
.style(get_button_style(
|
.style(get_button_style(
|
||||||
AddLogicFocus::SaveButton,
|
AddLogicFocus::SaveButton,
|
||||||
add_logic_state.current_focus,
|
add_logic_state.current_focus(),
|
||||||
))
|
))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
@@ -264,7 +263,7 @@ pub fn render_add_logic(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(get_button_border_style(
|
.border_style(get_button_border_style(
|
||||||
add_logic_state.current_focus == AddLogicFocus::SaveButton,
|
add_logic_state.current_focus() == AddLogicFocus::SaveButton,
|
||||||
theme,
|
theme,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
@@ -273,7 +272,7 @@ pub fn render_add_logic(
|
|||||||
let cancel_button = Paragraph::new(" Cancel ")
|
let cancel_button = Paragraph::new(" Cancel ")
|
||||||
.style(get_button_style(
|
.style(get_button_style(
|
||||||
AddLogicFocus::CancelButton,
|
AddLogicFocus::CancelButton,
|
||||||
add_logic_state.current_focus,
|
add_logic_state.current_focus(),
|
||||||
))
|
))
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
@@ -281,7 +280,7 @@ pub fn render_add_logic(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(get_button_border_style(
|
.border_style(get_button_border_style(
|
||||||
add_logic_state.current_focus == AddLogicFocus::CancelButton,
|
add_logic_state.current_focus() == AddLogicFocus::CancelButton,
|
||||||
theme,
|
theme,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|||||||
287
client/src/pages/admin_panel/add_table/event.rs
Normal file
287
client/src/pages/admin_panel/add_table/event.rs
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
// src/pages/admin_panel/add_table/event.rs
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use crate::config::binds::config::Config;
|
||||||
|
use crate::movement::{move_focus, MovementAction};
|
||||||
|
use crate::pages::admin_panel::add_table::logic::{
|
||||||
|
handle_add_column_action, handle_delete_selected_columns,
|
||||||
|
};
|
||||||
|
use crate::pages::admin_panel::add_table::loader::handle_save_table_action;
|
||||||
|
use crate::pages::admin_panel::add_table::nav::SaveTableResultSender;
|
||||||
|
use crate::pages::admin_panel::add_table::state::{AddTableFocus, AddTableFormState};
|
||||||
|
use crate::services::grpc_client::GrpcClient;
|
||||||
|
use crate::state::app::state::AppState;
|
||||||
|
use crate::modes::handlers::event::EventOutcome;
|
||||||
|
use canvas::{AppMode as CanvasMode, DataProvider};
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
|
||||||
|
/// Focus traversal order for AddTable (outside canvas)
|
||||||
|
const ADD_TABLE_FOCUS_ORDER: [AddTableFocus; 10] = [
|
||||||
|
AddTableFocus::InputTableName,
|
||||||
|
AddTableFocus::InputColumnName,
|
||||||
|
AddTableFocus::InputColumnType,
|
||||||
|
AddTableFocus::AddColumnButton,
|
||||||
|
AddTableFocus::ColumnsTable,
|
||||||
|
AddTableFocus::IndexesTable,
|
||||||
|
AddTableFocus::LinksTable,
|
||||||
|
AddTableFocus::SaveButton,
|
||||||
|
AddTableFocus::DeleteSelectedButton,
|
||||||
|
AddTableFocus::CancelButton,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Handles all AddTable page-specific events.
|
||||||
|
/// Return a non-empty Ok(message) only when the page actually consumed the key,
|
||||||
|
/// otherwise return Ok("") to let global handling proceed.
|
||||||
|
pub fn handle_add_table_event(
|
||||||
|
key_event: KeyEvent,
|
||||||
|
movement: Option<MovementAction>,
|
||||||
|
config: &Config,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
page: &mut AddTableFormState,
|
||||||
|
mut grpc_client: GrpcClient,
|
||||||
|
save_result_sender: SaveTableResultSender,
|
||||||
|
) -> Result<EventOutcome> {
|
||||||
|
// 1) Inside canvas (FormEditor)
|
||||||
|
let inside_canvas_inputs = matches!(
|
||||||
|
page.current_focus(),
|
||||||
|
AddTableFocus::InputTableName
|
||||||
|
| AddTableFocus::InputColumnName
|
||||||
|
| AddTableFocus::InputColumnType
|
||||||
|
);
|
||||||
|
|
||||||
|
if inside_canvas_inputs {
|
||||||
|
// Disable global shortcuts while typing
|
||||||
|
page.focus_outside_canvas = false;
|
||||||
|
|
||||||
|
// Only allow leaving the canvas with Down/Next when in ReadOnly mode
|
||||||
|
let in_edit_mode = page.editor.mode() == CanvasMode::Edit;
|
||||||
|
if !in_edit_mode {
|
||||||
|
if let Some(ma) = movement {
|
||||||
|
let last_idx = page.editor.data_provider().field_count().saturating_sub(1);
|
||||||
|
let at_last = page.editor.current_field() >= last_idx;
|
||||||
|
if at_last && matches!(ma, MovementAction::Down | MovementAction::Next) {
|
||||||
|
page.state.last_canvas_field = last_idx;
|
||||||
|
page.set_current_focus(AddTableFocus::AddColumnButton);
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok("Moved to Add button".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the FormEditor handle typing
|
||||||
|
match page.editor.handle_key_event(key_event) {
|
||||||
|
canvas::keymap::KeyEventOutcome::Consumed(Some(msg)) => {
|
||||||
|
page.sync_from_editor();
|
||||||
|
return Ok(EventOutcome::Ok(msg));
|
||||||
|
}
|
||||||
|
canvas::keymap::KeyEventOutcome::Consumed(None) => {
|
||||||
|
page.sync_from_editor();
|
||||||
|
return Ok(EventOutcome::Ok("Input updated".into()));
|
||||||
|
}
|
||||||
|
canvas::keymap::KeyEventOutcome::Pending => {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
canvas::keymap::KeyEventOutcome::NotMatched => {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Outside canvas
|
||||||
|
if let Some(ma) = movement {
|
||||||
|
// Block outer moves when "inside" any table and handle locally
|
||||||
|
match page.current_focus() {
|
||||||
|
AddTableFocus::InsideColumnsTable => {
|
||||||
|
match ma {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = page.state.column_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
page.state.column_table_state.select(Some(next));
|
||||||
|
} else if !page.state.columns.is_empty() {
|
||||||
|
page.state.column_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = page.state.column_table_state.selected() {
|
||||||
|
let last = page.state.columns.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
page.state.column_table_state.select(Some(next));
|
||||||
|
} else if !page.state.columns.is_empty() {
|
||||||
|
page.state.column_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = page.state.column_table_state.selected() {
|
||||||
|
if let Some(col) = page.state.columns.get_mut(i) {
|
||||||
|
col.selected = !col.selected;
|
||||||
|
page.state.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
page.state.column_table_state.select(None);
|
||||||
|
page.set_current_focus(AddTableFocus::ColumnsTable);
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => {
|
||||||
|
// Block outer movement while inside
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddTableFocus::InsideIndexesTable => {
|
||||||
|
match ma {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = page.state.index_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
page.state.index_table_state.select(Some(next));
|
||||||
|
} else if !page.state.indexes.is_empty() {
|
||||||
|
page.state.index_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = page.state.index_table_state.selected() {
|
||||||
|
let last = page.state.indexes.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
page.state.index_table_state.select(Some(next));
|
||||||
|
} else if !page.state.indexes.is_empty() {
|
||||||
|
page.state.index_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = page.state.index_table_state.selected() {
|
||||||
|
if let Some(ix) = page.state.indexes.get_mut(i) {
|
||||||
|
ix.selected = !ix.selected;
|
||||||
|
page.state.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
page.state.index_table_state.select(None);
|
||||||
|
page.set_current_focus(AddTableFocus::IndexesTable);
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddTableFocus::InsideLinksTable => {
|
||||||
|
match ma {
|
||||||
|
MovementAction::Up => {
|
||||||
|
if let Some(i) = page.state.link_table_state.selected() {
|
||||||
|
let next = i.saturating_sub(1);
|
||||||
|
page.state.link_table_state.select(Some(next));
|
||||||
|
} else if !page.state.links.is_empty() {
|
||||||
|
page.state.link_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Down => {
|
||||||
|
if let Some(i) = page.state.link_table_state.selected() {
|
||||||
|
let last = page.state.links.len().saturating_sub(1);
|
||||||
|
let next = if i < last { i + 1 } else { i };
|
||||||
|
page.state.link_table_state.select(Some(next));
|
||||||
|
} else if !page.state.links.is_empty() {
|
||||||
|
page.state.link_table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Select => {
|
||||||
|
if let Some(i) = page.state.link_table_state.selected() {
|
||||||
|
if let Some(link) = page.state.links.get_mut(i) {
|
||||||
|
link.selected = !link.selected;
|
||||||
|
page.state.has_unsaved_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Esc => {
|
||||||
|
page.state.link_table_state.select(None);
|
||||||
|
page.set_current_focus(AddTableFocus::LinksTable);
|
||||||
|
page.focus_outside_canvas = true;
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
MovementAction::Next | MovementAction::Previous => {
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut current = page.current_focus();
|
||||||
|
if move_focus(&ADD_TABLE_FOCUS_ORDER, &mut current, ma) {
|
||||||
|
page.set_current_focus(current);
|
||||||
|
page.focus_outside_canvas = !matches!(
|
||||||
|
page.current_focus(),
|
||||||
|
AddTableFocus::InputTableName
|
||||||
|
| AddTableFocus::InputColumnName
|
||||||
|
| AddTableFocus::InputColumnType
|
||||||
|
);
|
||||||
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Rich actions
|
||||||
|
match ma {
|
||||||
|
MovementAction::Select => match page.current_focus() {
|
||||||
|
AddTableFocus::AddColumnButton => {
|
||||||
|
if let Some(msg) = page.state.add_column_from_inputs() {
|
||||||
|
// Focus is set by the state method; just bubble message
|
||||||
|
return Ok(EventOutcome::Ok(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddTableFocus::SaveButton => {
|
||||||
|
if page.state.table_name.is_empty() {
|
||||||
|
return Ok(EventOutcome::Ok("Cannot save: Table name is empty".into()));
|
||||||
|
}
|
||||||
|
if page.state.columns.is_empty() {
|
||||||
|
return Ok(EventOutcome::Ok("Cannot save: No columns defined".into()));
|
||||||
|
}
|
||||||
|
app_state.show_loading_dialog("Saving", "Please wait...");
|
||||||
|
let state_clone = page.state.clone();
|
||||||
|
let sender_clone = save_result_sender.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result = handle_save_table_action(&mut grpc_client, &state_clone).await;
|
||||||
|
let _ = sender_clone.send(result).await;
|
||||||
|
});
|
||||||
|
return Ok(EventOutcome::Ok("Saving table...".into()));
|
||||||
|
}
|
||||||
|
AddTableFocus::DeleteSelectedButton => {
|
||||||
|
let msg = page
|
||||||
|
.state
|
||||||
|
.delete_selected_items()
|
||||||
|
.unwrap_or_else(|| "No items selected for deletion".to_string());
|
||||||
|
return Ok(EventOutcome::Ok(msg));
|
||||||
|
}
|
||||||
|
AddTableFocus::CancelButton => {
|
||||||
|
return Ok(EventOutcome::Ok("Cancelled Add Table".to_string()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EventOutcome::Ok(String::new()))
|
||||||
|
}
|
||||||
78
client/src/pages/admin_panel/add_table/loader.rs
Normal file
78
client/src/pages/admin_panel/add_table/loader.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// src/pages/admin_panel/add_table/loader.rs
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::pages::admin_panel::add_table::state::AddTableState;
|
||||||
|
use crate::services::grpc_client::GrpcClient;
|
||||||
|
use common::proto::komp_ac::table_definition::{
|
||||||
|
ColumnDefinition as ProtoColumnDefinition, PostTableDefinitionRequest, TableLink as ProtoTableLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Prepares and sends the request to save the new table definition via gRPC.
|
||||||
|
pub async fn handle_save_table_action(
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
add_table_state: &AddTableState,
|
||||||
|
) -> Result<String> {
|
||||||
|
if add_table_state.table_name.is_empty() {
|
||||||
|
return Err(anyhow!("Table name cannot be empty."));
|
||||||
|
}
|
||||||
|
if add_table_state.columns.is_empty() {
|
||||||
|
return Err(anyhow!("Table must have at least one column."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let proto_columns: Vec<ProtoColumnDefinition> = add_table_state
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.map(|col| ProtoColumnDefinition {
|
||||||
|
name: col.name.clone(),
|
||||||
|
field_type: col.data_type.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let proto_indexes: Vec<String> = add_table_state
|
||||||
|
.indexes
|
||||||
|
.iter()
|
||||||
|
.filter(|idx| idx.selected)
|
||||||
|
.map(|idx| idx.name.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let proto_links: Vec<ProtoTableLink> = add_table_state
|
||||||
|
.links
|
||||||
|
.iter()
|
||||||
|
.filter(|link| link.selected)
|
||||||
|
.map(|link| ProtoTableLink {
|
||||||
|
linked_table_name: link.linked_table_name.clone(),
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let request = PostTableDefinitionRequest {
|
||||||
|
table_name: add_table_state.table_name.clone(),
|
||||||
|
columns: proto_columns,
|
||||||
|
indexes: proto_indexes,
|
||||||
|
links: proto_links,
|
||||||
|
profile_name: add_table_state.profile_name.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Sending PostTableDefinitionRequest: {:?}", request);
|
||||||
|
|
||||||
|
match grpc_client.post_table_definition(request).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if response.success {
|
||||||
|
Ok(format!(
|
||||||
|
"Table '{}' saved successfully.",
|
||||||
|
add_table_state.table_name
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let error_message = if !response.sql.is_empty() {
|
||||||
|
format!("Server failed to save table: {}", response.sql)
|
||||||
|
} else {
|
||||||
|
"Server failed to save table (unknown reason).".to_string()
|
||||||
|
};
|
||||||
|
Err(anyhow!(error_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(anyhow!("gRPC call failed: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,197 +1,24 @@
|
|||||||
// src/pages/admin_panel/add_table/logic.rs
|
// src/pages/admin_panel/add_table/logic.rs
|
||||||
use crate::pages::admin_panel::add_table::state;
|
|
||||||
use crate::pages::admin_panel::add_table::state::{AddTableState, AddTableFocus, IndexDefinition, ColumnDefinition};
|
|
||||||
use crate::services::GrpcClient;
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use common::proto::komp_ac::table_definition::{
|
|
||||||
PostTableDefinitionRequest,
|
|
||||||
ColumnDefinition as ProtoColumnDefinition,
|
|
||||||
TableLink as ProtoTableLink,
|
|
||||||
};
|
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
/// Handles the logic for adding a column when the "Add" button is activated.
|
use crate::pages::admin_panel::add_table::state::{AddTableState, AddTableFocus};
|
||||||
///
|
|
||||||
/// Takes the mutable state and command message string.
|
/// Thin wrapper around AddTableState::add_column_from_inputs
|
||||||
/// Returns `Some(AddTableFocus)` indicating the desired focus state after a successful add,
|
/// Returns Some(AddTableFocus) for compatibility with old call sites.
|
||||||
/// or `None` if the action failed (e.g., validation error).
|
|
||||||
pub fn handle_add_column_action(
|
pub fn handle_add_column_action(
|
||||||
add_table_state: &mut AddTableState,
|
add_table_state: &mut AddTableState,
|
||||||
command_message: &mut String,
|
command_message: &mut String,
|
||||||
) -> Option<AddTableFocus> {
|
) -> Option<AddTableFocus> {
|
||||||
|
if let Some(msg) = add_table_state.add_column_from_inputs() {
|
||||||
// Trim and create owned Strings from inputs
|
|
||||||
let table_name_in = add_table_state.table_name_input.trim();
|
|
||||||
let column_name_in = add_table_state.column_name_input.trim();
|
|
||||||
let column_type_in = add_table_state.column_type_input.trim();
|
|
||||||
|
|
||||||
// Validate all inputs needed for this combined action
|
|
||||||
let has_table_name = !table_name_in.is_empty();
|
|
||||||
let has_column_name = !column_name_in.is_empty();
|
|
||||||
let has_column_type = !column_type_in.is_empty();
|
|
||||||
|
|
||||||
match (has_table_name, has_column_name, has_column_type) {
|
|
||||||
// Case 1: Both column fields have input (Table name is optional here)
|
|
||||||
(_, true, true) => {
|
|
||||||
let mut msg = String::new();
|
|
||||||
// Optionally update table name if provided
|
|
||||||
if has_table_name {
|
|
||||||
add_table_state.table_name = table_name_in.to_string();
|
|
||||||
msg.push_str(&format!("Table name set to '{}'. ", add_table_state.table_name));
|
|
||||||
}
|
|
||||||
// Add the column
|
|
||||||
let new_column = ColumnDefinition {
|
|
||||||
name: column_name_in.to_string(),
|
|
||||||
data_type: column_type_in.to_string(),
|
|
||||||
selected: false,
|
|
||||||
};
|
|
||||||
add_table_state.columns.push(new_column.clone()); // Clone for msg
|
|
||||||
msg.push_str(&format!("Column '{}' added.", new_column.name));
|
|
||||||
|
|
||||||
// Add corresponding index definition (initially unselected)
|
|
||||||
let new_index = IndexDefinition {
|
|
||||||
name: column_name_in.to_string(),
|
|
||||||
selected: false,
|
|
||||||
};
|
|
||||||
add_table_state.indexes.push(new_index);
|
|
||||||
*command_message = msg;
|
*command_message = msg;
|
||||||
|
// State sets focus internally; return it explicitly for old call sites
|
||||||
// Clear all inputs and reset cursors
|
return Some(add_table_state.current_focus);
|
||||||
add_table_state.table_name_input.clear();
|
|
||||||
add_table_state.column_name_input.clear();
|
|
||||||
add_table_state.column_type_input.clear();
|
|
||||||
add_table_state.table_name_cursor_pos = 0;
|
|
||||||
add_table_state.column_name_cursor_pos = 0;
|
|
||||||
add_table_state.column_type_cursor_pos = 0;
|
|
||||||
add_table_state.has_unsaved_changes = true;
|
|
||||||
Some(AddTableFocus::InputColumnName) // Focus for next column
|
|
||||||
}
|
}
|
||||||
// Case 2: Only one column field has input (Error)
|
|
||||||
(_, true, false) | (_, false, true) => {
|
|
||||||
*command_message = "Both Column Name and Type are required to add a column.".to_string();
|
|
||||||
None // Indicate validation failure
|
|
||||||
}
|
|
||||||
// Case 3: Only Table name has input (No column input)
|
|
||||||
(true, false, false) => {
|
|
||||||
add_table_state.table_name = table_name_in.to_string();
|
|
||||||
*command_message = format!("Table name set to '{}'.", add_table_state.table_name);
|
|
||||||
// Clear only table name input
|
|
||||||
add_table_state.table_name_input.clear();
|
|
||||||
add_table_state.table_name_cursor_pos = 0;
|
|
||||||
add_table_state.has_unsaved_changes = true;
|
|
||||||
Some(AddTableFocus::InputTableName) // Keep focus here
|
|
||||||
}
|
|
||||||
// Case 4: All fields are empty
|
|
||||||
(false, false, false) => {
|
|
||||||
*command_message = "No input provided.".to_string();
|
|
||||||
None
|
None
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles deleting columns marked as selected in the AddTableState.
|
/// Thin wrapper around AddTableState::delete_selected_items
|
||||||
pub fn handle_delete_selected_columns(
|
pub fn handle_delete_selected_columns(add_table_state: &mut AddTableState) -> String {
|
||||||
add_table_state: &mut AddTableState,
|
add_table_state
|
||||||
) -> String {
|
.delete_selected_items()
|
||||||
let initial_count = add_table_state.columns.len();
|
.unwrap_or_else(|| "No items selected for deletion".to_string())
|
||||||
// Keep only the columns that are NOT selected
|
|
||||||
let initial_selected_indices: std::collections::HashSet<String> = add_table_state
|
|
||||||
.columns
|
|
||||||
.iter()
|
|
||||||
.filter(|col| col.selected)
|
|
||||||
.map(|col| col.name.clone())
|
|
||||||
.collect();
|
|
||||||
add_table_state.columns.retain(|col| !col.selected);
|
|
||||||
let deleted_count = initial_count - add_table_state.columns.len();
|
|
||||||
|
|
||||||
if deleted_count > 0 {
|
|
||||||
add_table_state.indexes.retain(|index| !initial_selected_indices.contains(&index.name));
|
|
||||||
add_table_state.has_unsaved_changes = true;
|
|
||||||
// Reset selection highlight as indices have changed
|
|
||||||
add_table_state.column_table_state.select(None);
|
|
||||||
// Optionally, select the first item if the list is not empty
|
|
||||||
// if !add_table_state.columns.is_empty() {
|
|
||||||
// add_table_state.column_table_state.select(Some(0));
|
|
||||||
// }
|
|
||||||
add_table_state.index_table_state.select(None);
|
|
||||||
format!("Deleted {} selected column(s).", deleted_count)
|
|
||||||
} else {
|
|
||||||
"No columns marked for deletion.".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepares and sends the request to save the new table definition via gRPC.
|
|
||||||
pub async fn handle_save_table_action(
|
|
||||||
grpc_client: &mut GrpcClient,
|
|
||||||
add_table_state: &AddTableState,
|
|
||||||
) -> Result<String> {
|
|
||||||
// --- Basic Validation ---
|
|
||||||
if add_table_state.table_name.is_empty() {
|
|
||||||
return Err(anyhow!("Table name cannot be empty."));
|
|
||||||
}
|
|
||||||
if add_table_state.columns.is_empty() {
|
|
||||||
return Err(anyhow!("Table must have at least one column."));
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Prepare Proto Data ---
|
|
||||||
let proto_columns: Vec<ProtoColumnDefinition> = add_table_state
|
|
||||||
.columns
|
|
||||||
.iter()
|
|
||||||
.map(|col| ProtoColumnDefinition {
|
|
||||||
name: col.name.clone(),
|
|
||||||
field_type: col.data_type.clone(), // Assuming data_type maps directly
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let proto_indexes: Vec<String> = add_table_state
|
|
||||||
.indexes
|
|
||||||
.iter()
|
|
||||||
.filter(|idx| idx.selected) // Only include selected indexes
|
|
||||||
.map(|idx| idx.name.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let proto_links: Vec<ProtoTableLink> = add_table_state
|
|
||||||
.links
|
|
||||||
.iter()
|
|
||||||
.filter(|link| link.selected) // Only include selected links
|
|
||||||
.map(|link| ProtoTableLink {
|
|
||||||
linked_table_name: link.linked_table_name.clone(),
|
|
||||||
// Assuming 'required' maps directly, adjust if needed
|
|
||||||
// For now, the proto only seems to use linked_table_name based on example
|
|
||||||
// If your proto evolves, map link.is_required here.
|
|
||||||
required: false, // Set based on your proto definition/needs
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// --- Create Request ---
|
|
||||||
let request = PostTableDefinitionRequest {
|
|
||||||
table_name: add_table_state.table_name.clone(),
|
|
||||||
columns: proto_columns,
|
|
||||||
indexes: proto_indexes,
|
|
||||||
links: proto_links,
|
|
||||||
profile_name: add_table_state.profile_name.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("Sending PostTableDefinitionRequest: {:?}", request);
|
|
||||||
|
|
||||||
// --- Call gRPC Service ---
|
|
||||||
match grpc_client.post_table_definition(request).await {
|
|
||||||
Ok(response) => {
|
|
||||||
if response.success {
|
|
||||||
Ok(format!(
|
|
||||||
"Table '{}' saved successfully.",
|
|
||||||
add_table_state.table_name
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
// Use the SQL message from the response if available, otherwise generic error
|
|
||||||
let error_message = if !response.sql.is_empty() {
|
|
||||||
format!("Server failed to save table: {}", response.sql)
|
|
||||||
} else {
|
|
||||||
"Server failed to save table (unknown reason).".to_string()
|
|
||||||
};
|
|
||||||
Err(anyhow!(error_message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Err(anyhow!("gRPC call failed: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ pub mod ui;
|
|||||||
pub mod nav;
|
pub mod nav;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod logic;
|
pub mod logic;
|
||||||
|
pub mod event;
|
||||||
|
pub mod loader;
|
||||||
|
|||||||
@@ -1,206 +1,6 @@
|
|||||||
// src/pages/admin_panel/add_table/nav.rs
|
// src/pages/admin_panel/add_table/nav.rs
|
||||||
|
|
||||||
use crate::config::binds::config::Config;
|
|
||||||
use crate::state::{
|
|
||||||
app::state::AppState,
|
|
||||||
};
|
|
||||||
use crate::pages::admin_panel::add_table::state::{AddTableFocus, AddTableState};
|
|
||||||
use crossterm::event::{KeyEvent};
|
|
||||||
use ratatui::widgets::TableState;
|
|
||||||
use crate::pages::admin_panel::add_table::logic::{handle_add_column_action, handle_save_table_action};
|
|
||||||
use crate::ui::handlers::context::DialogPurpose;
|
|
||||||
use crate::services::GrpcClient;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
pub type SaveTableResultSender = mpsc::Sender<Result<String>>;
|
pub type SaveTableResultSender = mpsc::Sender<Result<String>>;
|
||||||
|
|
||||||
fn navigate_table_up(table_state: &mut TableState, item_count: usize) -> bool {
|
|
||||||
if item_count == 0 { return false; }
|
|
||||||
let current_selection = table_state.selected();
|
|
||||||
match current_selection {
|
|
||||||
Some(index) => {
|
|
||||||
if index > 0 { table_state.select(Some(index - 1)); true }
|
|
||||||
else { false }
|
|
||||||
}
|
|
||||||
None => { table_state.select(Some(0)); true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn navigate_table_down(table_state: &mut TableState, item_count: usize) -> bool {
|
|
||||||
if item_count == 0 { return false; }
|
|
||||||
let current_selection = table_state.selected();
|
|
||||||
match current_selection {
|
|
||||||
Some(index) => {
|
|
||||||
if index < item_count - 1 { table_state.select(Some(index + 1)); true }
|
|
||||||
else { false }
|
|
||||||
}
|
|
||||||
None => { table_state.select(Some(0)); true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_add_table_navigation(
|
|
||||||
key: KeyEvent,
|
|
||||||
config: &Config,
|
|
||||||
app_state: &mut AppState,
|
|
||||||
add_table_state: &mut AddTableState,
|
|
||||||
grpc_client: GrpcClient,
|
|
||||||
save_result_sender: SaveTableResultSender,
|
|
||||||
command_message: &mut String,
|
|
||||||
) -> bool {
|
|
||||||
let action = config.get_general_action(key.code, key.modifiers);
|
|
||||||
let current_focus = add_table_state.current_focus;
|
|
||||||
let mut handled = true;
|
|
||||||
let mut new_focus = current_focus;
|
|
||||||
|
|
||||||
if matches!(current_focus, AddTableFocus::InsideColumnsTable | AddTableFocus::InsideIndexesTable | AddTableFocus::InsideLinksTable) {
|
|
||||||
if matches!(action.as_deref(), Some("next_option") | Some("previous_option")) {
|
|
||||||
*command_message = "Press Esc to exit table item navigation first.".to_string();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match action.as_deref() {
|
|
||||||
Some("exit_table_scroll") => {
|
|
||||||
match current_focus {
|
|
||||||
AddTableFocus::InsideColumnsTable => {
|
|
||||||
add_table_state.column_table_state.select(None);
|
|
||||||
new_focus = AddTableFocus::ColumnsTable;
|
|
||||||
// *command_message = "Exited Columns Table".to_string(); // Minimal change: remove message
|
|
||||||
}
|
|
||||||
AddTableFocus::InsideIndexesTable => {
|
|
||||||
add_table_state.index_table_state.select(None);
|
|
||||||
new_focus = AddTableFocus::IndexesTable;
|
|
||||||
// *command_message = "Exited Indexes Table".to_string();
|
|
||||||
}
|
|
||||||
AddTableFocus::InsideLinksTable => {
|
|
||||||
add_table_state.link_table_state.select(None);
|
|
||||||
new_focus = AddTableFocus::LinksTable;
|
|
||||||
// *command_message = "Exited Links Table".to_string();
|
|
||||||
}
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("move_up") => {
|
|
||||||
match current_focus {
|
|
||||||
AddTableFocus::InputTableName => {
|
|
||||||
// MINIMAL CHANGE: Do nothing, new_focus remains current_focus
|
|
||||||
// *command_message = "At top of form.".to_string(); // Remove message
|
|
||||||
}
|
|
||||||
AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputTableName,
|
|
||||||
AddTableFocus::InputColumnType => new_focus = AddTableFocus::InputColumnName,
|
|
||||||
AddTableFocus::AddColumnButton => new_focus = AddTableFocus::InputColumnType,
|
|
||||||
AddTableFocus::ColumnsTable => new_focus = AddTableFocus::AddColumnButton,
|
|
||||||
AddTableFocus::IndexesTable => new_focus = AddTableFocus::ColumnsTable,
|
|
||||||
AddTableFocus::LinksTable => new_focus = AddTableFocus::IndexesTable,
|
|
||||||
AddTableFocus::InsideColumnsTable => { navigate_table_up(&mut add_table_state.column_table_state, add_table_state.columns.len()); }
|
|
||||||
AddTableFocus::InsideIndexesTable => { navigate_table_up(&mut add_table_state.index_table_state, add_table_state.indexes.len()); }
|
|
||||||
AddTableFocus::InsideLinksTable => { navigate_table_up(&mut add_table_state.link_table_state, add_table_state.links.len()); }
|
|
||||||
AddTableFocus::SaveButton => new_focus = AddTableFocus::LinksTable,
|
|
||||||
AddTableFocus::DeleteSelectedButton => new_focus = AddTableFocus::SaveButton,
|
|
||||||
AddTableFocus::CancelButton => new_focus = AddTableFocus::DeleteSelectedButton,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("move_down") => {
|
|
||||||
match current_focus {
|
|
||||||
AddTableFocus::InputTableName => new_focus = AddTableFocus::InputColumnName,
|
|
||||||
AddTableFocus::InputColumnName => new_focus = AddTableFocus::InputColumnType,
|
|
||||||
AddTableFocus::InputColumnType => {
|
|
||||||
add_table_state.last_canvas_field = 2;
|
|
||||||
new_focus = AddTableFocus::AddColumnButton;
|
|
||||||
},
|
|
||||||
AddTableFocus::AddColumnButton => new_focus = AddTableFocus::ColumnsTable,
|
|
||||||
AddTableFocus::ColumnsTable => new_focus = AddTableFocus::IndexesTable,
|
|
||||||
AddTableFocus::IndexesTable => new_focus = AddTableFocus::LinksTable,
|
|
||||||
AddTableFocus::LinksTable => new_focus = AddTableFocus::SaveButton,
|
|
||||||
AddTableFocus::InsideColumnsTable => { navigate_table_down(&mut add_table_state.column_table_state, add_table_state.columns.len()); }
|
|
||||||
AddTableFocus::InsideIndexesTable => { navigate_table_down(&mut add_table_state.index_table_state, add_table_state.indexes.len()); }
|
|
||||||
AddTableFocus::InsideLinksTable => { navigate_table_down(&mut add_table_state.link_table_state, add_table_state.links.len()); }
|
|
||||||
AddTableFocus::SaveButton => new_focus = AddTableFocus::DeleteSelectedButton,
|
|
||||||
AddTableFocus::DeleteSelectedButton => new_focus = AddTableFocus::CancelButton,
|
|
||||||
AddTableFocus::CancelButton => {
|
|
||||||
// MINIMAL CHANGE: Do nothing, new_focus remains current_focus
|
|
||||||
// *command_message = "At bottom of form.".to_string(); // Remove message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("next_option") => { // This logic should already be non-wrapping
|
|
||||||
match current_focus {
|
|
||||||
AddTableFocus::InputTableName | AddTableFocus::InputColumnName | AddTableFocus::InputColumnType =>
|
|
||||||
{ new_focus = AddTableFocus::AddColumnButton; }
|
|
||||||
AddTableFocus::AddColumnButton => new_focus = AddTableFocus::ColumnsTable,
|
|
||||||
AddTableFocus::ColumnsTable => new_focus = AddTableFocus::IndexesTable,
|
|
||||||
AddTableFocus::IndexesTable => new_focus = AddTableFocus::LinksTable,
|
|
||||||
AddTableFocus::LinksTable => new_focus = AddTableFocus::SaveButton,
|
|
||||||
AddTableFocus::SaveButton => new_focus = AddTableFocus::DeleteSelectedButton,
|
|
||||||
AddTableFocus::DeleteSelectedButton => new_focus = AddTableFocus::CancelButton,
|
|
||||||
AddTableFocus::CancelButton => { /* *command_message = "At last focusable area.".to_string(); */ } // No change in focus
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("previous_option") => { // This logic should already be non-wrapping
|
|
||||||
match current_focus {
|
|
||||||
AddTableFocus::InputTableName | AddTableFocus::InputColumnName | AddTableFocus::InputColumnType =>
|
|
||||||
{ /* *command_message = "At first focusable area.".to_string(); */ } // No change in focus
|
|
||||||
AddTableFocus::AddColumnButton => new_focus = AddTableFocus::InputColumnType,
|
|
||||||
AddTableFocus::ColumnsTable => new_focus = AddTableFocus::AddColumnButton,
|
|
||||||
AddTableFocus::IndexesTable => new_focus = AddTableFocus::ColumnsTable,
|
|
||||||
AddTableFocus::LinksTable => new_focus = AddTableFocus::IndexesTable,
|
|
||||||
AddTableFocus::SaveButton => new_focus = AddTableFocus::LinksTable,
|
|
||||||
AddTableFocus::DeleteSelectedButton => new_focus = AddTableFocus::SaveButton,
|
|
||||||
AddTableFocus::CancelButton => new_focus = AddTableFocus::DeleteSelectedButton,
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some("next_field") => {
|
|
||||||
new_focus = match current_focus {
|
|
||||||
AddTableFocus::InputTableName => AddTableFocus::InputColumnName, AddTableFocus::InputColumnName => AddTableFocus::InputColumnType, AddTableFocus::InputColumnType => AddTableFocus::AddColumnButton, AddTableFocus::AddColumnButton => AddTableFocus::ColumnsTable,
|
|
||||||
AddTableFocus::ColumnsTable | AddTableFocus::InsideColumnsTable => AddTableFocus::IndexesTable, AddTableFocus::IndexesTable | AddTableFocus::InsideIndexesTable => AddTableFocus::LinksTable, AddTableFocus::LinksTable | AddTableFocus::InsideLinksTable => AddTableFocus::SaveButton,
|
|
||||||
AddTableFocus::SaveButton => AddTableFocus::DeleteSelectedButton, AddTableFocus::DeleteSelectedButton => AddTableFocus::CancelButton, AddTableFocus::CancelButton => AddTableFocus::InputTableName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some("prev_field") => {
|
|
||||||
new_focus = match current_focus {
|
|
||||||
AddTableFocus::InputTableName => AddTableFocus::CancelButton, AddTableFocus::InputColumnName => AddTableFocus::InputTableName, AddTableFocus::InputColumnType => AddTableFocus::InputColumnName, AddTableFocus::AddColumnButton => AddTableFocus::InputColumnType,
|
|
||||||
AddTableFocus::ColumnsTable | AddTableFocus::InsideColumnsTable => AddTableFocus::AddColumnButton, AddTableFocus::IndexesTable | AddTableFocus::InsideIndexesTable => AddTableFocus::ColumnsTable, AddTableFocus::LinksTable | AddTableFocus::InsideLinksTable => AddTableFocus::IndexesTable,
|
|
||||||
AddTableFocus::SaveButton => AddTableFocus::LinksTable, AddTableFocus::DeleteSelectedButton => AddTableFocus::SaveButton, AddTableFocus::CancelButton => AddTableFocus::DeleteSelectedButton,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some("select") => {
|
|
||||||
match current_focus {
|
|
||||||
AddTableFocus::ColumnsTable => { new_focus = AddTableFocus::InsideColumnsTable; if add_table_state.column_table_state.selected().is_none() && !add_table_state.columns.is_empty() { add_table_state.column_table_state.select(Some(0)); } /* Message removed */ }
|
|
||||||
AddTableFocus::IndexesTable => { new_focus = AddTableFocus::InsideIndexesTable; if add_table_state.index_table_state.selected().is_none() && !add_table_state.indexes.is_empty() { add_table_state.index_table_state.select(Some(0)); } /* Message removed */ }
|
|
||||||
AddTableFocus::LinksTable => { new_focus = AddTableFocus::InsideLinksTable; if add_table_state.link_table_state.selected().is_none() && !add_table_state.links.is_empty() { add_table_state.link_table_state.select(Some(0)); } /* Message removed */ }
|
|
||||||
AddTableFocus::InsideColumnsTable => { if let Some(index) = add_table_state.column_table_state.selected() { if let Some(col) = add_table_state.columns.get_mut(index) { col.selected = !col.selected; add_table_state.has_unsaved_changes = true; /* Message removed */ }} /* else { Message removed } */ }
|
|
||||||
AddTableFocus::InsideIndexesTable => { if let Some(index) = add_table_state.index_table_state.selected() { if let Some(idx_def) = add_table_state.indexes.get_mut(index) { idx_def.selected = !idx_def.selected; add_table_state.has_unsaved_changes = true; /* Message removed */ }} /* else { Message removed } */ }
|
|
||||||
AddTableFocus::InsideLinksTable => { if let Some(index) = add_table_state.link_table_state.selected() { if let Some(link) = add_table_state.links.get_mut(index) { link.selected = !link.selected; add_table_state.has_unsaved_changes = true; /* Message removed */ }} /* else { Message removed } */ }
|
|
||||||
AddTableFocus::AddColumnButton => { if let Some(focus_after_add) = handle_add_column_action(add_table_state, command_message) { new_focus = focus_after_add; } else { /* Message already set by handle_add_column_action */ }}
|
|
||||||
AddTableFocus::SaveButton => { if add_table_state.table_name.is_empty() { *command_message = "Cannot save: Table name is empty.".to_string(); } else if add_table_state.columns.is_empty() { *command_message = "Cannot save: No columns defined.".to_string(); } else { *command_message = "Saving table...".to_string(); app_state.show_loading_dialog("Saving", "Please wait..."); let mut client_clone = grpc_client.clone(); let state_clone = add_table_state.clone(); let sender_clone = save_result_sender.clone(); tokio::spawn(async move { let result = handle_save_table_action(&mut client_clone, &state_clone).await; let _ = sender_clone.send(result).await; }); }}
|
|
||||||
AddTableFocus::DeleteSelectedButton => { let columns_to_delete: Vec<(usize, String, String)> = add_table_state.columns.iter().enumerate().filter(|(_, col)| col.selected).map(|(index, col)| (index, col.name.clone(), col.data_type.clone())).collect(); if columns_to_delete.is_empty() { *command_message = "No columns selected for deletion.".to_string(); } else { let column_details: String = columns_to_delete.iter().map(|(index, name, dtype)| format!("{}. {} ({})", index + 1, name, dtype)).collect::<Vec<String>>().join("\n"); let message = format!("Delete the following columns?\n\n{}", column_details); app_state.show_dialog("Confirm Deletion", &message, vec!["Confirm".to_string(), "Cancel".to_string()], DialogPurpose::ConfirmDeleteColumns); }}
|
|
||||||
AddTableFocus::CancelButton => { *command_message = "Action: Cancel Add Table (Not Implemented)".to_string(); }
|
|
||||||
_ => { handled = false; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => handled = false,
|
|
||||||
}
|
|
||||||
|
|
||||||
if handled && current_focus != new_focus {
|
|
||||||
add_table_state.current_focus = new_focus;
|
|
||||||
// Minimal change: Command message update logic can be simplified or removed if not desired
|
|
||||||
// For now, let's keep it minimal and only update if it was truly a focus change,
|
|
||||||
// and not a boundary message.
|
|
||||||
if !command_message.starts_with("At ") && current_focus != new_focus { // Avoid overwriting boundary messages
|
|
||||||
// *command_message = format!("Focus: {:?}", add_table_state.current_focus); // Optional: restore if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let new_is_canvas_input_focus = matches!(new_focus,
|
|
||||||
AddTableFocus::InputTableName | AddTableFocus::InputColumnName | AddTableFocus::InputColumnType
|
|
||||||
);
|
|
||||||
app_state.ui.focus_outside_canvas = !new_is_canvas_input_focus;
|
|
||||||
}
|
|
||||||
// If not handled, command_message remains as it was (e.g., from a deeper function call or previous event)
|
|
||||||
// or can be cleared if that's the desired default. For minimal change, we leave it.
|
|
||||||
|
|
||||||
handled
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// src/pages/admin_panel/add_table/state.rs
|
// src/pages/admin_panel/add_table/state.rs
|
||||||
|
|
||||||
use canvas::{DataProvider, AppMode};
|
use canvas::{DataProvider, AppMode};
|
||||||
|
use canvas::FormEditor;
|
||||||
use ratatui::widgets::TableState;
|
use ratatui::widgets::TableState;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::movement::{move_focus, MovementAction};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ColumnDefinition {
|
pub struct ColumnDefinition {
|
||||||
@@ -98,23 +98,48 @@ impl AddTableState {
|
|||||||
|
|
||||||
/// Helper method to add a column from current inputs
|
/// Helper method to add a column from current inputs
|
||||||
pub fn add_column_from_inputs(&mut self) -> Option<String> {
|
pub fn add_column_from_inputs(&mut self) -> Option<String> {
|
||||||
if self.column_name_input.trim().is_empty() || self.column_type_input.trim().is_empty() {
|
let table_name_in = self.table_name_input.trim().to_string();
|
||||||
return Some("Both column name and type are required".to_string());
|
let column_name_in = self.column_name_input.trim().to_string();
|
||||||
|
let column_type_in = self.column_type_input.trim().to_string();
|
||||||
|
|
||||||
|
// Case: "only table name" provided → set it and stay on TableName
|
||||||
|
if !table_name_in.is_empty() && column_name_in.is_empty() && column_type_in.is_empty() {
|
||||||
|
self.table_name = table_name_in;
|
||||||
|
self.table_name_input.clear();
|
||||||
|
self.table_name_cursor_pos = 0;
|
||||||
|
self.current_focus = AddTableFocus::InputTableName;
|
||||||
|
self.has_unsaved_changes = true;
|
||||||
|
return Some(format!("Table name set to '{}'.", self.table_name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate column names
|
// Column validation
|
||||||
if self.columns.iter().any(|col| col.name == self.column_name_input.trim()) {
|
if column_name_in.is_empty() || column_type_in.is_empty() {
|
||||||
|
return Some("Both column name and type are required".to_string());
|
||||||
|
}
|
||||||
|
if self.columns.iter().any(|col| col.name == column_name_in) {
|
||||||
return Some("Column name already exists".to_string());
|
return Some("Column name already exists".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If table_name input present while adding first column, apply it too
|
||||||
|
if !table_name_in.is_empty() {
|
||||||
|
self.table_name = table_name_in;
|
||||||
|
self.table_name_input.clear();
|
||||||
|
self.table_name_cursor_pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Add the column
|
// Add the column
|
||||||
self.columns.push(ColumnDefinition {
|
self.columns.push(ColumnDefinition {
|
||||||
name: self.column_name_input.trim().to_string(),
|
name: column_name_in.clone(),
|
||||||
data_type: self.column_type_input.trim().to_string(),
|
data_type: column_type_in.clone(),
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
// Add a corresponding (unselected) index with the same name
|
||||||
|
self.indexes.push(IndexDefinition {
|
||||||
|
name: column_name_in.clone(),
|
||||||
selected: false,
|
selected: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear inputs and reset focus to column name for next entry
|
// Clear column inputs and set focus for next entry
|
||||||
self.column_name_input.clear();
|
self.column_name_input.clear();
|
||||||
self.column_type_input.clear();
|
self.column_type_input.clear();
|
||||||
self.column_name_cursor_pos = 0;
|
self.column_name_cursor_pos = 0;
|
||||||
@@ -123,23 +148,33 @@ impl AddTableState {
|
|||||||
self.last_canvas_field = 1;
|
self.last_canvas_field = 1;
|
||||||
self.has_unsaved_changes = true;
|
self.has_unsaved_changes = true;
|
||||||
|
|
||||||
Some(format!("Column '{}' added successfully", self.columns.last().unwrap().name))
|
Some(format!("Column '{}' added successfully", column_name_in))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method to delete selected items
|
/// Helper method to delete selected items
|
||||||
pub fn delete_selected_items(&mut self) -> Option<String> {
|
pub fn delete_selected_items(&mut self) -> Option<String> {
|
||||||
let mut deleted_items = Vec::new();
|
let mut deleted_items: Vec<String> = Vec::new();
|
||||||
|
|
||||||
// Remove selected columns
|
// Remove selected columns
|
||||||
let initial_column_count = self.columns.len();
|
let selected_col_names: std::collections::HashSet<String> = self
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.selected)
|
||||||
|
.map(|c| c.name.clone())
|
||||||
|
.collect();
|
||||||
|
if !selected_col_names.is_empty() {
|
||||||
self.columns.retain(|col| {
|
self.columns.retain(|col| {
|
||||||
if col.selected {
|
if selected_col_names.contains(&col.name) {
|
||||||
deleted_items.push(format!("column '{}'", col.name));
|
deleted_items.push(format!("column '{}'", col.name));
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Also purge indexes for deleted columns
|
||||||
|
self.indexes
|
||||||
|
.retain(|idx| !selected_col_names.contains(&idx.name));
|
||||||
|
}
|
||||||
|
|
||||||
// Remove selected indexes
|
// Remove selected indexes
|
||||||
let initial_index_count = self.indexes.len();
|
let initial_index_count = self.indexes.len();
|
||||||
@@ -167,6 +202,8 @@ impl AddTableState {
|
|||||||
Some("No items selected for deletion".to_string())
|
Some("No items selected for deletion".to_string())
|
||||||
} else {
|
} else {
|
||||||
self.has_unsaved_changes = true;
|
self.has_unsaved_changes = true;
|
||||||
|
self.column_table_state.select(None);
|
||||||
|
self.index_table_state.select(None);
|
||||||
Some(format!("Deleted: {}", deleted_items.join(", ")))
|
Some(format!("Deleted: {}", deleted_items.join(", ")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,174 +247,86 @@ impl DataProvider for AddTableState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AddTableFormState {
|
||||||
|
pub state: AddTableState,
|
||||||
|
pub editor: FormEditor<AddTableState>,
|
||||||
|
pub focus_outside_canvas: bool,
|
||||||
|
pub focused_button_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl AddTableState {
|
impl std::fmt::Debug for AddTableFormState {
|
||||||
pub fn handle_movement(&mut self, action: MovementAction) -> bool {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
use AddTableFocus::*;
|
f.debug_struct("AddTableFormState")
|
||||||
|
.field("state", &self.state)
|
||||||
// Linear outer focus order
|
.field("focus_outside_canvas", &self.focus_outside_canvas)
|
||||||
const ORDER: [AddTableFocus; 10] = [
|
.field("focused_button_index", &self.focused_button_index)
|
||||||
InputTableName,
|
.finish()
|
||||||
InputColumnName,
|
}
|
||||||
InputColumnType,
|
}
|
||||||
AddColumnButton,
|
|
||||||
ColumnsTable,
|
impl AddTableFormState {
|
||||||
IndexesTable,
|
pub fn new(profile_name: String) -> Self {
|
||||||
LinksTable,
|
let mut state = AddTableState::default();
|
||||||
SaveButton,
|
state.profile_name = profile_name;
|
||||||
DeleteSelectedButton,
|
let editor = FormEditor::new(state.clone());
|
||||||
CancelButton,
|
Self {
|
||||||
];
|
state,
|
||||||
|
editor,
|
||||||
// Enter "inside" on Select from outer panes
|
focus_outside_canvas: false,
|
||||||
match (self.current_focus, action) {
|
focused_button_index: 0,
|
||||||
(ColumnsTable, MovementAction::Select) => {
|
}
|
||||||
if !self.columns.is_empty() && self.column_table_state.selected().is_none() {
|
}
|
||||||
self.column_table_state.select(Some(0));
|
|
||||||
}
|
pub fn from_state(state: AddTableState) -> Self {
|
||||||
self.current_focus = InsideColumnsTable;
|
let editor = FormEditor::new(state.clone());
|
||||||
return true;
|
Self {
|
||||||
}
|
state,
|
||||||
(IndexesTable, MovementAction::Select) => {
|
editor,
|
||||||
if !self.indexes.is_empty() && self.index_table_state.selected().is_none() {
|
focus_outside_canvas: false,
|
||||||
self.index_table_state.select(Some(0));
|
focused_button_index: 0,
|
||||||
}
|
}
|
||||||
self.current_focus = InsideIndexesTable;
|
}
|
||||||
return true;
|
|
||||||
}
|
/// Sync state from editor’s snapshot
|
||||||
(LinksTable, MovementAction::Select) => {
|
pub fn sync_from_editor(&mut self) {
|
||||||
if !self.links.is_empty() && self.link_table_state.selected().is_none() {
|
self.state = self.editor.data_provider().clone();
|
||||||
self.link_table_state.select(Some(0));
|
}
|
||||||
}
|
|
||||||
self.current_focus = InsideLinksTable;
|
// === Delegates to AddTableState fields ===
|
||||||
return true;
|
pub fn current_focus(&self) -> AddTableFocus {
|
||||||
}
|
self.state.current_focus
|
||||||
_ => {}
|
}
|
||||||
}
|
pub fn set_current_focus(&mut self, focus: AddTableFocus) {
|
||||||
|
self.state.current_focus = focus;
|
||||||
// Handle "inside" states: Up/Down/Select/Esc; block outer movement keys
|
}
|
||||||
match self.current_focus {
|
pub fn profile_name(&self) -> &str {
|
||||||
InsideColumnsTable => {
|
&self.state.profile_name
|
||||||
match action {
|
}
|
||||||
MovementAction::Up => {
|
pub fn table_name(&self) -> &str {
|
||||||
if let Some(i) = self.column_table_state.selected() {
|
&self.state.table_name
|
||||||
let next = i.saturating_sub(1);
|
}
|
||||||
self.column_table_state.select(Some(next));
|
pub fn columns(&self) -> &Vec<ColumnDefinition> {
|
||||||
} else if !self.columns.is_empty() {
|
&self.state.columns
|
||||||
self.column_table_state.select(Some(0));
|
}
|
||||||
}
|
pub fn indexes(&self) -> &Vec<IndexDefinition> {
|
||||||
return true;
|
&self.state.indexes
|
||||||
}
|
}
|
||||||
MovementAction::Down => {
|
pub fn links(&self) -> &Vec<LinkDefinition> {
|
||||||
if let Some(i) = self.column_table_state.selected() {
|
&self.state.links
|
||||||
let last = self.columns.len().saturating_sub(1);
|
}
|
||||||
let next = if i < last { i + 1 } else { i };
|
pub fn column_table_state(&mut self) -> &mut TableState {
|
||||||
self.column_table_state.select(Some(next));
|
&mut self.state.column_table_state
|
||||||
} else if !self.columns.is_empty() {
|
}
|
||||||
self.column_table_state.select(Some(0));
|
pub fn index_table_state(&mut self) -> &mut TableState {
|
||||||
}
|
&mut self.state.index_table_state
|
||||||
return true;
|
}
|
||||||
}
|
pub fn link_table_state(&mut self) -> &mut TableState {
|
||||||
MovementAction::Select => {
|
&mut self.state.link_table_state
|
||||||
if let Some(i) = self.column_table_state.selected() {
|
}
|
||||||
if let Some(col) = self.columns.get_mut(i) {
|
pub fn set_focused_button(&mut self, index: usize) {
|
||||||
col.selected = !col.selected;
|
self.focused_button_index = index;
|
||||||
self.has_unsaved_changes = true;
|
}
|
||||||
}
|
pub fn focused_button(&self) -> usize {
|
||||||
}
|
self.focused_button_index
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Esc => {
|
|
||||||
self.column_table_state.select(None);
|
|
||||||
self.current_focus = ColumnsTable;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InsideIndexesTable => {
|
|
||||||
match action {
|
|
||||||
MovementAction::Up => {
|
|
||||||
if let Some(i) = self.index_table_state.selected() {
|
|
||||||
let next = i.saturating_sub(1);
|
|
||||||
self.index_table_state.select(Some(next));
|
|
||||||
} else if !self.indexes.is_empty() {
|
|
||||||
self.index_table_state.select(Some(0));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Down => {
|
|
||||||
if let Some(i) = self.index_table_state.selected() {
|
|
||||||
let last = self.indexes.len().saturating_sub(1);
|
|
||||||
let next = if i < last { i + 1 } else { i };
|
|
||||||
self.index_table_state.select(Some(next));
|
|
||||||
} else if !self.indexes.is_empty() {
|
|
||||||
self.index_table_state.select(Some(0));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Select => {
|
|
||||||
if let Some(i) = self.index_table_state.selected() {
|
|
||||||
if let Some(ix) = self.indexes.get_mut(i) {
|
|
||||||
ix.selected = !ix.selected;
|
|
||||||
self.has_unsaved_changes = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Esc => {
|
|
||||||
self.index_table_state.select(None);
|
|
||||||
self.current_focus = IndexesTable;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InsideLinksTable => {
|
|
||||||
match action {
|
|
||||||
MovementAction::Up => {
|
|
||||||
if let Some(i) = self.link_table_state.selected() {
|
|
||||||
let next = i.saturating_sub(1);
|
|
||||||
self.link_table_state.select(Some(next));
|
|
||||||
} else if !self.links.is_empty() {
|
|
||||||
self.link_table_state.select(Some(0));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Down => {
|
|
||||||
if let Some(i) = self.link_table_state.selected() {
|
|
||||||
let last = self.links.len().saturating_sub(1);
|
|
||||||
let next = if i < last { i + 1 } else { i };
|
|
||||||
self.link_table_state.select(Some(next));
|
|
||||||
} else if !self.links.is_empty() {
|
|
||||||
self.link_table_state.select(Some(0));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Select => {
|
|
||||||
if let Some(i) = self.link_table_state.selected() {
|
|
||||||
if let Some(link) = self.links.get_mut(i) {
|
|
||||||
link.selected = !link.selected;
|
|
||||||
self.has_unsaved_changes = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Esc => {
|
|
||||||
self.link_table_state.select(None);
|
|
||||||
self.current_focus = LinksTable;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
MovementAction::Next | MovementAction::Previous => return true, // block outer moves
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: outer navigation via helper
|
|
||||||
move_focus(&ORDER, &mut self.current_focus, action)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// src/pages/admin_panel/add_table/ui.rs
|
// src/pages/admin_panel/add_table/ui.rs
|
||||||
use crate::config::colors::themes::Theme;
|
use crate::config::colors::themes::Theme;
|
||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::pages::admin_panel::add_table::state::{AddTableFocus, AddTableState};
|
use crate::pages::admin_panel::add_table::state::{AddTableFocus, AddTableFormState};
|
||||||
use canvas::{render_canvas, FormEditor};
|
use canvas::render_canvas;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
style::{Modifier, Style},
|
style::{Modifier, Style},
|
||||||
@@ -19,7 +19,7 @@ pub fn render_add_table(
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
app_state: &AppState,
|
app_state: &AppState,
|
||||||
add_table_state: &mut AddTableState,
|
add_table_state: &mut AddTableFormState,
|
||||||
) {
|
) {
|
||||||
// --- Configuration ---
|
// --- Configuration ---
|
||||||
// Threshold width to switch between wide and narrow layouts
|
// Threshold width to switch between wide and narrow layouts
|
||||||
@@ -27,7 +27,7 @@ pub fn render_add_table(
|
|||||||
|
|
||||||
// --- State Checks ---
|
// --- State Checks ---
|
||||||
let focus_on_canvas_inputs = matches!(
|
let focus_on_canvas_inputs = matches!(
|
||||||
add_table_state.current_focus,
|
add_table_state.current_focus(),
|
||||||
AddTableFocus::InputTableName
|
AddTableFocus::InputTableName
|
||||||
| AddTableFocus::InputColumnName
|
| AddTableFocus::InputColumnName
|
||||||
| AddTableFocus::InputColumnType
|
| AddTableFocus::InputColumnType
|
||||||
@@ -45,11 +45,11 @@ pub fn render_add_table(
|
|||||||
f.render_widget(main_block, area);
|
f.render_widget(main_block, area);
|
||||||
|
|
||||||
// --- Fullscreen Columns Table Check (Narrow Screens Only) ---
|
// --- Fullscreen Columns Table Check (Narrow Screens Only) ---
|
||||||
if area.width < NARROW_LAYOUT_THRESHOLD && add_table_state.current_focus == AddTableFocus::InsideColumnsTable {
|
if area.width < NARROW_LAYOUT_THRESHOLD && add_table_state.current_focus() == AddTableFocus::InsideColumnsTable {
|
||||||
// Render ONLY the columns table taking the full inner area
|
// Render ONLY the columns table taking the full inner area
|
||||||
let columns_border_style = Style::default().fg(theme.highlight); // Always highlighted when fullscreen
|
let columns_border_style = Style::default().fg(theme.highlight); // Always highlighted when fullscreen
|
||||||
let column_rows: Vec<Row<'_>> = add_table_state
|
let column_rows: Vec<Row<'_>> = add_table_state
|
||||||
.columns
|
.columns()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|col_def| {
|
.map(|col_def| {
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
@@ -80,16 +80,16 @@ pub fn render_add_table(
|
|||||||
.fg(theme.highlight),
|
.fg(theme.highlight),
|
||||||
)
|
)
|
||||||
.highlight_symbol(" > "); // Use the inside symbol
|
.highlight_symbol(" > "); // Use the inside symbol
|
||||||
f.render_stateful_widget(columns_table, inner_area, &mut add_table_state.column_table_state);
|
f.render_stateful_widget(columns_table, inner_area, add_table_state.column_table_state());
|
||||||
return; // IMPORTANT: Stop rendering here for fullscreen mode
|
return; // IMPORTANT: Stop rendering here for fullscreen mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fullscreen Indexes Table Check ---
|
// --- Fullscreen Indexes Table Check ---
|
||||||
if add_table_state.current_focus == AddTableFocus::InsideIndexesTable { // Remove width check
|
if add_table_state.current_focus() == AddTableFocus::InsideIndexesTable { // Remove width check
|
||||||
// Render ONLY the indexes table taking the full inner area
|
// Render ONLY the indexes table taking the full inner area
|
||||||
let indexes_border_style = Style::default().fg(theme.highlight); // Always highlighted when fullscreen
|
let indexes_border_style = Style::default().fg(theme.highlight); // Always highlighted when fullscreen
|
||||||
let index_rows: Vec<Row<'_>> = add_table_state
|
let index_rows: Vec<Row<'_>> = add_table_state
|
||||||
.indexes
|
.indexes()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|index_def| {
|
.map(|index_def| {
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
@@ -115,16 +115,16 @@ pub fn render_add_table(
|
|||||||
)
|
)
|
||||||
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED).fg(theme.highlight))
|
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED).fg(theme.highlight))
|
||||||
.highlight_symbol(" > "); // Use the inside symbol
|
.highlight_symbol(" > "); // Use the inside symbol
|
||||||
f.render_stateful_widget(indexes_table, inner_area, &mut add_table_state.index_table_state);
|
f.render_stateful_widget(indexes_table, inner_area, &mut add_table_state.index_table_state());
|
||||||
return; // IMPORTANT: Stop rendering here for fullscreen mode
|
return; // IMPORTANT: Stop rendering here for fullscreen mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fullscreen Links Table Check ---
|
// --- Fullscreen Links Table Check ---
|
||||||
if add_table_state.current_focus == AddTableFocus::InsideLinksTable {
|
if add_table_state.current_focus() == AddTableFocus::InsideLinksTable {
|
||||||
// Render ONLY the links table taking the full inner area
|
// Render ONLY the links table taking the full inner area
|
||||||
let links_border_style = Style::default().fg(theme.highlight); // Always highlighted when fullscreen
|
let links_border_style = Style::default().fg(theme.highlight); // Always highlighted when fullscreen
|
||||||
let link_rows: Vec<Row<'_>> = add_table_state
|
let link_rows: Vec<Row<'_>> = add_table_state
|
||||||
.links
|
.links()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|link_def| {
|
.map(|link_def| {
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
@@ -151,7 +151,7 @@ pub fn render_add_table(
|
|||||||
)
|
)
|
||||||
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED).fg(theme.highlight))
|
.row_highlight_style(Style::default().add_modifier(Modifier::REVERSED).fg(theme.highlight))
|
||||||
.highlight_symbol(" > "); // Use the inside symbol
|
.highlight_symbol(" > "); // Use the inside symbol
|
||||||
f.render_stateful_widget(links_table, inner_area, &mut add_table_state.link_table_state);
|
f.render_stateful_widget(links_table, inner_area, &mut add_table_state.link_table_state());
|
||||||
return; // IMPORTANT: Stop rendering here for fullscreen mode
|
return; // IMPORTANT: Stop rendering here for fullscreen mode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,11 +220,11 @@ pub fn render_add_table(
|
|||||||
// --- Top Info Rendering (Wide - 2 lines) ---
|
// --- Top Info Rendering (Wide - 2 lines) ---
|
||||||
let profile_text = Paragraph::new(vec![
|
let profile_text = Paragraph::new(vec![
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!("Profile: {}", add_table_state.profile_name),
|
format!("Profile: {}", add_table_state.profile_name()),
|
||||||
theme.fg,
|
theme.fg,
|
||||||
)),
|
)),
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!("Table name: {}", add_table_state.table_name),
|
format!("Table name: {}", add_table_state.table_name()),
|
||||||
theme.fg,
|
theme.fg,
|
||||||
)),
|
)),
|
||||||
])
|
])
|
||||||
@@ -276,14 +276,14 @@ pub fn render_add_table(
|
|||||||
.split(top_info_area);
|
.split(top_info_area);
|
||||||
|
|
||||||
let profile_text = Paragraph::new(Span::styled(
|
let profile_text = Paragraph::new(Span::styled(
|
||||||
format!("Profile: {}", add_table_state.profile_name),
|
format!("Profile: {}", add_table_state.profile_name()),
|
||||||
theme.fg,
|
theme.fg,
|
||||||
))
|
))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
f.render_widget(profile_text, top_info_chunks[0]);
|
f.render_widget(profile_text, top_info_chunks[0]);
|
||||||
|
|
||||||
let table_name_text = Paragraph::new(Span::styled(
|
let table_name_text = Paragraph::new(Span::styled(
|
||||||
format!("Table: {}", add_table_state.table_name),
|
format!("Table: {}", add_table_state.table_name()),
|
||||||
theme.fg,
|
theme.fg,
|
||||||
))
|
))
|
||||||
.alignment(Alignment::Left);
|
.alignment(Alignment::Left);
|
||||||
@@ -293,14 +293,14 @@ pub fn render_add_table(
|
|||||||
// --- Common Widget Rendering (Uses calculated areas) ---
|
// --- Common Widget Rendering (Uses calculated areas) ---
|
||||||
|
|
||||||
// --- Columns Table Rendering ---
|
// --- Columns Table Rendering ---
|
||||||
let columns_focused = matches!(add_table_state.current_focus, AddTableFocus::ColumnsTable | AddTableFocus::InsideColumnsTable);
|
let columns_focused = matches!(add_table_state.current_focus(), AddTableFocus::ColumnsTable | AddTableFocus::InsideColumnsTable);
|
||||||
let columns_border_style = if columns_focused {
|
let columns_border_style = if columns_focused {
|
||||||
Style::default().fg(theme.highlight)
|
Style::default().fg(theme.highlight)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme.secondary)
|
Style::default().fg(theme.secondary)
|
||||||
};
|
};
|
||||||
let column_rows: Vec<Row<'_>> = add_table_state
|
let column_rows: Vec<Row<'_>> = add_table_state
|
||||||
.columns
|
.columns()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|col_def| {
|
.map(|col_def| {
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
@@ -341,12 +341,11 @@ pub fn render_add_table(
|
|||||||
f.render_stateful_widget(
|
f.render_stateful_widget(
|
||||||
columns_table,
|
columns_table,
|
||||||
columns_area,
|
columns_area,
|
||||||
&mut add_table_state.column_table_state,
|
&mut add_table_state.column_table_state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Canvas Rendering (Column Definition Input) - USING CANVAS LIBRARY ---
|
// --- Canvas Rendering (Column Definition Input) - USING CANVAS LIBRARY ---
|
||||||
let editor = FormEditor::new(add_table_state.clone());
|
let _active_field_rect = render_canvas(f, canvas_area, &add_table_state.editor, theme);
|
||||||
let _active_field_rect = render_canvas(f, canvas_area, &editor, theme);
|
|
||||||
|
|
||||||
// --- Button Style Helpers ---
|
// --- Button Style Helpers ---
|
||||||
let get_button_style = |button_focus: AddTableFocus, current_focus| {
|
let get_button_style = |button_focus: AddTableFocus, current_focus| {
|
||||||
@@ -374,11 +373,11 @@ pub fn render_add_table(
|
|||||||
|
|
||||||
// --- Add Button Rendering ---
|
// --- Add Button Rendering ---
|
||||||
// Determine if the add button is focused
|
// Determine if the add button is focused
|
||||||
let is_add_button_focused = add_table_state.current_focus == AddTableFocus::AddColumnButton;
|
let is_add_button_focused = add_table_state.current_focus() == AddTableFocus::AddColumnButton;
|
||||||
|
|
||||||
// Create the Add button Paragraph widget
|
// Create the Add button Paragraph widget
|
||||||
let add_button = Paragraph::new(" Add ")
|
let add_button = Paragraph::new(" Add ")
|
||||||
.style(get_button_style(AddTableFocus::AddColumnButton, add_table_state.current_focus)) // Use existing closure
|
.style(get_button_style(AddTableFocus::AddColumnButton, add_table_state.current_focus())) // Use existing closure
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
@@ -391,14 +390,14 @@ pub fn render_add_table(
|
|||||||
f.render_widget(add_button, add_button_area);
|
f.render_widget(add_button, add_button_area);
|
||||||
|
|
||||||
// --- Indexes Table Rendering ---
|
// --- Indexes Table Rendering ---
|
||||||
let indexes_focused = matches!(add_table_state.current_focus, AddTableFocus::IndexesTable | AddTableFocus::InsideIndexesTable);
|
let indexes_focused = matches!(add_table_state.current_focus(), AddTableFocus::IndexesTable | AddTableFocus::InsideIndexesTable);
|
||||||
let indexes_border_style = if indexes_focused {
|
let indexes_border_style = if indexes_focused {
|
||||||
Style::default().fg(theme.highlight)
|
Style::default().fg(theme.highlight)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme.secondary)
|
Style::default().fg(theme.secondary)
|
||||||
};
|
};
|
||||||
let index_rows: Vec<Row<'_>> = add_table_state
|
let index_rows: Vec<Row<'_>> = add_table_state
|
||||||
.indexes
|
.indexes()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|index_def| { // Use index_def now
|
.map(|index_def| { // Use index_def now
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
@@ -432,18 +431,18 @@ pub fn render_add_table(
|
|||||||
f.render_stateful_widget(
|
f.render_stateful_widget(
|
||||||
indexes_table,
|
indexes_table,
|
||||||
indexes_area,
|
indexes_area,
|
||||||
&mut add_table_state.index_table_state,
|
&mut add_table_state.index_table_state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Links Table Rendering ---
|
// --- Links Table Rendering ---
|
||||||
let links_focused = matches!(add_table_state.current_focus, AddTableFocus::LinksTable | AddTableFocus::InsideLinksTable);
|
let links_focused = matches!(add_table_state.current_focus(), AddTableFocus::LinksTable | AddTableFocus::InsideLinksTable);
|
||||||
let links_border_style = if links_focused {
|
let links_border_style = if links_focused {
|
||||||
Style::default().fg(theme.highlight)
|
Style::default().fg(theme.highlight)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(theme.secondary)
|
Style::default().fg(theme.secondary)
|
||||||
};
|
};
|
||||||
let link_rows: Vec<Row<'_>> = add_table_state
|
let link_rows: Vec<Row<'_>> = add_table_state
|
||||||
.links
|
.links()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|link_def| {
|
.map(|link_def| {
|
||||||
Row::new(vec![
|
Row::new(vec![
|
||||||
@@ -477,7 +476,7 @@ pub fn render_add_table(
|
|||||||
f.render_stateful_widget(
|
f.render_stateful_widget(
|
||||||
links_table,
|
links_table,
|
||||||
links_area,
|
links_area,
|
||||||
&mut add_table_state.link_table_state,
|
&mut add_table_state.link_table_state(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Save/Cancel Buttons Rendering ---
|
// --- Save/Cancel Buttons Rendering ---
|
||||||
@@ -491,51 +490,54 @@ pub fn render_add_table(
|
|||||||
.split(bottom_buttons_area);
|
.split(bottom_buttons_area);
|
||||||
|
|
||||||
let save_button = Paragraph::new(" Save table ")
|
let save_button = Paragraph::new(" Save table ")
|
||||||
.style(get_button_style(
|
.style(if add_table_state.current_focus() == AddTableFocus::SaveButton {
|
||||||
AddTableFocus::SaveButton,
|
Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD)
|
||||||
add_table_state.current_focus,
|
} else {
|
||||||
))
|
Style::default().fg(theme.secondary)
|
||||||
|
})
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(get_button_border_style(
|
.border_style(get_button_border_style(
|
||||||
add_table_state.current_focus == AddTableFocus::SaveButton, // Pass bool
|
add_table_state.current_focus() == AddTableFocus::SaveButton, // Pass bool
|
||||||
theme,
|
theme,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
f.render_widget(save_button, bottom_button_chunks[0]);
|
f.render_widget(save_button, bottom_button_chunks[0]);
|
||||||
|
|
||||||
let delete_button = Paragraph::new(" Delete Selected ")
|
let delete_button = Paragraph::new(" Delete Selected ")
|
||||||
.style(get_button_style(
|
.style(if add_table_state.current_focus() == AddTableFocus::DeleteSelectedButton {
|
||||||
AddTableFocus::DeleteSelectedButton,
|
Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD)
|
||||||
add_table_state.current_focus,
|
} else {
|
||||||
))
|
Style::default().fg(theme.secondary)
|
||||||
|
})
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(get_button_border_style(
|
.border_style(get_button_border_style(
|
||||||
add_table_state.current_focus == AddTableFocus::DeleteSelectedButton, // Pass bool
|
add_table_state.current_focus() == AddTableFocus::DeleteSelectedButton,
|
||||||
theme,
|
theme,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
f.render_widget(delete_button, bottom_button_chunks[1]);
|
f.render_widget(delete_button, bottom_button_chunks[1]);
|
||||||
|
|
||||||
let cancel_button = Paragraph::new(" Cancel ")
|
let cancel_button = Paragraph::new(" Cancel ")
|
||||||
.style(get_button_style(
|
.style(if add_table_state.current_focus() == AddTableFocus::CancelButton {
|
||||||
AddTableFocus::CancelButton,
|
Style::default().fg(theme.highlight).add_modifier(Modifier::BOLD)
|
||||||
add_table_state.current_focus,
|
} else {
|
||||||
))
|
Style::default().fg(theme.secondary)
|
||||||
|
})
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.border_style(get_button_border_style(
|
.border_style(get_button_border_style(
|
||||||
add_table_state.current_focus == AddTableFocus::CancelButton, // Pass bool
|
add_table_state.current_focus() == AddTableFocus::CancelButton,
|
||||||
theme,
|
theme,
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// src/pages/forms/state.rs
|
// src/pages/forms/state.rs
|
||||||
|
|
||||||
use canvas::{DataProvider, AppMode};
|
use canvas::{DataProvider, AppMode};
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
use canvas::{CharacterLimits, ValidationConfig, ValidationConfigBuilder};
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
use canvas::validation::limits::CountMode;
|
||||||
use common::proto::komp_ac::search::search_response::Hit;
|
use common::proto::komp_ac::search::search_response::Hit;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -39,6 +43,18 @@ pub struct FormState {
|
|||||||
pub autocomplete_loading: bool,
|
pub autocomplete_loading: bool,
|
||||||
pub link_display_map: HashMap<usize, String>,
|
pub link_display_map: HashMap<usize, String>,
|
||||||
pub app_mode: AppMode,
|
pub app_mode: AppMode,
|
||||||
|
// Validation 1 (character limits) per field. None = no validation for that field.
|
||||||
|
// Leave room for future rules (patterns, masks, etc.).
|
||||||
|
pub char_limits: Vec<Option<CharLimitsRule>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CharLimitsRule {
|
||||||
|
pub min: Option<usize>,
|
||||||
|
pub max: Option<usize>,
|
||||||
|
pub warn_at: Option<usize>,
|
||||||
|
pub count_mode: CountMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FormState {
|
impl FormState {
|
||||||
@@ -56,6 +72,7 @@ impl FormState {
|
|||||||
fields: Vec<FieldDefinition>,
|
fields: Vec<FieldDefinition>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let values = vec![String::new(); fields.len()];
|
let values = vec![String::new(); fields.len()];
|
||||||
|
let len = values.len();
|
||||||
FormState {
|
FormState {
|
||||||
id: 0,
|
id: 0,
|
||||||
profile_name,
|
profile_name,
|
||||||
@@ -73,6 +90,7 @@ impl FormState {
|
|||||||
autocomplete_loading: false,
|
autocomplete_loading: false,
|
||||||
link_display_map: HashMap::new(),
|
link_display_map: HashMap::new(),
|
||||||
app_mode: canvas::AppMode::Edit,
|
app_mode: canvas::AppMode::Edit,
|
||||||
|
char_limits: vec![None; len],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +274,24 @@ impl FormState {
|
|||||||
pub fn set_current_cursor_pos(&mut self, pos: usize) {
|
pub fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||||
self.current_cursor_pos = pos;
|
self.current_cursor_pos = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
pub fn set_character_limits_rules(
|
||||||
|
&mut self,
|
||||||
|
rules: Vec<Option<CharLimitsRule>>,
|
||||||
|
) {
|
||||||
|
if rules.len() == self.fields.len() {
|
||||||
|
self.char_limits = rules;
|
||||||
|
} else {
|
||||||
|
tracing::warn!(
|
||||||
|
"Character limits count {} != field count {} for {}.{}",
|
||||||
|
rules.len(),
|
||||||
|
self.fields.len(),
|
||||||
|
self.profile_name,
|
||||||
|
self.table_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Implement DataProvider for FormState
|
// Step 2: Implement DataProvider for FormState
|
||||||
@@ -282,4 +318,26 @@ impl DataProvider for FormState {
|
|||||||
fn supports_suggestions(&self, field_index: usize) -> bool {
|
fn supports_suggestions(&self, field_index: usize) -> bool {
|
||||||
self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false)
|
self.fields.get(field_index).map(|f| f.is_link).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validation 1: Provide character-limit-based validation to canvas
|
||||||
|
// Only compiled when the "validation" feature is enabled on canvas.
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
fn validation_config(&self, index: usize) -> Option<ValidationConfig> {
|
||||||
|
let rule = self.char_limits.get(index)?.as_ref()?;
|
||||||
|
let mut limits = match (rule.min, rule.max) {
|
||||||
|
(Some(min), Some(max)) => CharacterLimits::new_range(min, max),
|
||||||
|
(None, Some(max)) => CharacterLimits::new(max),
|
||||||
|
(Some(min), None) => CharacterLimits::new_range(min, usize::MAX),
|
||||||
|
(None, None) => CharacterLimits::new(usize::MAX),
|
||||||
|
};
|
||||||
|
limits = limits.with_count_mode(rule.count_mode);
|
||||||
|
if let Some(warn) = rule.warn_at {
|
||||||
|
limits = limits.with_warning_threshold(warn);
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
ValidationConfigBuilder::new()
|
||||||
|
.with_character_limits(limits)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ pub fn handle_intro_selection(
|
|||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
buffer_state.update_history(AppView::Register);
|
buffer_state.update_history(AppView::Register);
|
||||||
// Register view requires focus reset
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
app_state.focused_button_index = 0;
|
|
||||||
}
|
}
|
||||||
_ => return,
|
_ => return,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,27 @@ use crate::movement::MovementAction;
|
|||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct IntroState {
|
pub struct IntroState {
|
||||||
pub selected_option: usize,
|
pub focus_outside_canvas: bool,
|
||||||
|
pub focused_button_index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntroState {
|
impl IntroState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self {
|
||||||
|
focus_outside_canvas: true,
|
||||||
|
focused_button_index: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_option(&mut self) {
|
pub fn next_option(&mut self) {
|
||||||
if self.selected_option < 3 {
|
if self.focused_button_index < 3 {
|
||||||
self.selected_option += 1;
|
self.focused_button_index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_option(&mut self) {
|
pub fn previous_option(&mut self) {
|
||||||
if self.selected_option > 0 {
|
if self.focused_button_index > 0 {
|
||||||
self.selected_option -= 1
|
self.focused_button_index -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ pub fn render_intro(f: &mut Frame, intro_state: &IntroState, area: Rect, theme:
|
|||||||
|
|
||||||
let buttons = ["Continue", "Admin", "Login", "Register"];
|
let buttons = ["Continue", "Admin", "Login", "Register"];
|
||||||
for (i, &text) in buttons.iter().enumerate() {
|
for (i, &text) in buttons.iter().enumerate() {
|
||||||
render_button(f, button_area[i], text, intro_state.selected_option == i, theme);
|
let active = intro_state.focused_button_index == i;
|
||||||
|
render_button(f, button_area[i], text, active, theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ pub fn handle_login_event(
|
|||||||
&& modifiers.is_empty()
|
&& modifiers.is_empty()
|
||||||
{
|
{
|
||||||
login_page.focus_outside_canvas = false;
|
login_page.focus_outside_canvas = false;
|
||||||
app_state.ui.focus_outside_canvas = false; // 🔑 keep global in sync
|
|
||||||
login_page.editor.set_mode(CanvasMode::ReadOnly);
|
login_page.editor.set_mode(CanvasMode::ReadOnly);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
@@ -35,16 +34,15 @@ pub fn handle_login_event(
|
|||||||
if !login_page.focus_outside_canvas {
|
if !login_page.focus_outside_canvas {
|
||||||
let last_idx = login_page.editor.data_provider().field_count().saturating_sub(1);
|
let last_idx = login_page.editor.data_provider().field_count().saturating_sub(1);
|
||||||
let at_last = login_page.editor.current_field() >= last_idx;
|
let at_last = login_page.editor.current_field() >= last_idx;
|
||||||
if at_last
|
if login_page.editor.mode() == CanvasMode::ReadOnly
|
||||||
|
&& at_last
|
||||||
&& matches!(
|
&& matches!(
|
||||||
(key_code, modifiers),
|
(key_code, modifiers),
|
||||||
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
|
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
login_page.focus_outside_canvas = true;
|
login_page.focus_outside_canvas = true;
|
||||||
login_page.focused_button_index = 0; // focus "Login" button
|
login_page.focused_button_index = 0;
|
||||||
app_state.ui.focus_outside_canvas = true;
|
|
||||||
app_state.focused_button_index = 0;
|
|
||||||
login_page.editor.set_mode(CanvasMode::ReadOnly);
|
login_page.editor.set_mode(CanvasMode::ReadOnly);
|
||||||
return Ok(EventOutcome::Ok("Focus moved to buttons".into()));
|
return Ok(EventOutcome::Ok("Focus moved to buttons".into()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::ui::handlers::context::DialogPurpose;
|
|||||||
use common::proto::komp_ac::auth::LoginResponse;
|
use common::proto::komp_ac::auth::LoginResponse;
|
||||||
use crate::pages::login::LoginFormState;
|
use crate::pages::login::LoginFormState;
|
||||||
use crate::state::pages::auth::UserRole;
|
use crate::state::pages::auth::UserRole;
|
||||||
|
use canvas::DataProvider;
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
@@ -108,7 +109,19 @@ pub async fn revert(
|
|||||||
login_state: &mut LoginFormState,
|
login_state: &mut LoginFormState,
|
||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
// Clear the underlying state
|
||||||
login_state.clear();
|
login_state.clear();
|
||||||
|
|
||||||
|
// Also clear values inside the editor’s data provider
|
||||||
|
{
|
||||||
|
let dp = login_state.editor.data_provider_mut();
|
||||||
|
dp.set_field_value(0, "".to_string());
|
||||||
|
dp.set_field_value(1, "".to_string());
|
||||||
|
dp.set_current_field(0);
|
||||||
|
dp.set_current_cursor_pos(0);
|
||||||
|
dp.set_has_unsaved_changes(false);
|
||||||
|
}
|
||||||
|
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
"Login reverted".to_string()
|
"Login reverted".to_string()
|
||||||
}
|
}
|
||||||
@@ -125,9 +138,6 @@ pub async fn back_to_main(
|
|||||||
buffer_state.close_active_buffer();
|
buffer_state.close_active_buffer();
|
||||||
buffer_state.update_history(AppView::Intro);
|
buffer_state.update_history(AppView::Intro);
|
||||||
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
app_state.focused_button_index = 0;
|
|
||||||
|
|
||||||
"Returned to main menu".to_string()
|
"Returned to main menu".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,11 +80,8 @@ pub fn render_login(
|
|||||||
|
|
||||||
// Login Button
|
// Login Button
|
||||||
let login_button_index = 0;
|
let login_button_index = 0;
|
||||||
let login_active = if login_page.focus_outside_canvas {
|
let login_active = login_page.focus_outside_canvas
|
||||||
app_state.focused_button_index == login_button_index
|
&& login_page.focused_button_index == login_button_index;
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
let mut login_style = Style::default().fg(theme.fg);
|
let mut login_style = Style::default().fg(theme.fg);
|
||||||
let mut login_border = Style::default().fg(theme.border);
|
let mut login_border = Style::default().fg(theme.border);
|
||||||
if login_active {
|
if login_active {
|
||||||
@@ -107,11 +104,8 @@ pub fn render_login(
|
|||||||
|
|
||||||
// Return Button
|
// Return Button
|
||||||
let return_button_index = 1;
|
let return_button_index = 1;
|
||||||
let return_active = if app_state.ui.focus_outside_canvas {
|
let return_active = login_page.focus_outside_canvas
|
||||||
app_state.focused_button_index == return_button_index
|
&& login_page.focused_button_index == return_button_index;
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
let mut return_style = Style::default().fg(theme.fg);
|
let mut return_style = Style::default().fg(theme.fg);
|
||||||
let mut return_border = Style::default().fg(theme.border);
|
let mut return_border = Style::default().fg(theme.border);
|
||||||
if return_active {
|
if return_active {
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ pub fn handle_register_event(
|
|||||||
&& modifiers.is_empty()
|
&& modifiers.is_empty()
|
||||||
{
|
{
|
||||||
register_page.focus_outside_canvas = false;
|
register_page.focus_outside_canvas = false;
|
||||||
// Keep global in sync for now (cursor styling elsewhere still reads it)
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
register_page.editor.set_mode(CanvasMode::ReadOnly);
|
register_page.editor.set_mode(CanvasMode::ReadOnly);
|
||||||
return Ok(EventOutcome::Ok(String::new()));
|
return Ok(EventOutcome::Ok(String::new()));
|
||||||
}
|
}
|
||||||
@@ -38,7 +36,8 @@ pub fn handle_register_event(
|
|||||||
if !register_page.focus_outside_canvas {
|
if !register_page.focus_outside_canvas {
|
||||||
let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1);
|
let last_idx = register_page.editor.data_provider().field_count().saturating_sub(1);
|
||||||
let at_last = register_page.editor.current_field() >= last_idx;
|
let at_last = register_page.editor.current_field() >= last_idx;
|
||||||
if at_last
|
if register_page.editor.mode() == CanvasMode::ReadOnly
|
||||||
|
&& at_last
|
||||||
&& matches!(
|
&& matches!(
|
||||||
(key_code, modifiers),
|
(key_code, modifiers),
|
||||||
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
|
(KeyCode::Char('j'), KeyModifiers::NONE) | (KeyCode::Down, _)
|
||||||
@@ -46,9 +45,6 @@ pub fn handle_register_event(
|
|||||||
{
|
{
|
||||||
register_page.focus_outside_canvas = true;
|
register_page.focus_outside_canvas = true;
|
||||||
register_page.focused_button_index = 0; // focus "Register" button
|
register_page.focused_button_index = 0; // focus "Register" button
|
||||||
// Keep global in sync for now
|
|
||||||
app_state.ui.focus_outside_canvas = true;
|
|
||||||
app_state.focused_button_index = 0;
|
|
||||||
register_page.editor.set_mode(CanvasMode::ReadOnly);
|
register_page.editor.set_mode(CanvasMode::ReadOnly);
|
||||||
return Ok(EventOutcome::Ok("Focus moved to buttons".into()));
|
return Ok(EventOutcome::Ok("Focus moved to buttons".into()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ pub async fn back_to_login(
|
|||||||
// Reset focus state
|
// Reset focus state
|
||||||
register_state.focus_outside_canvas = false;
|
register_state.focus_outside_canvas = false;
|
||||||
register_state.focused_button_index = 0;
|
register_state.focused_button_index = 0;
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
app_state.focused_button_index = 0;
|
|
||||||
|
|
||||||
"Returned to main menu".to_string()
|
"Returned to main menu".to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// src/pages/routing/router.rs
|
// src/pages/routing/router.rs
|
||||||
use crate::state::pages::auth::AuthState;
|
use crate::state::pages::auth::AuthState;
|
||||||
use crate::pages::admin_panel::add_logic::state::AddLogicState;
|
use crate::pages::admin_panel::add_logic::state::AddLogicFormState;
|
||||||
use crate::pages::admin_panel::add_table::state::AddTableState;
|
use crate::pages::admin_panel::add_table::state::AddTableFormState;
|
||||||
use crate::pages::admin::AdminState;
|
use crate::pages::admin::AdminState;
|
||||||
use crate::pages::forms::FormState;
|
use crate::pages::forms::FormState;
|
||||||
use crate::pages::login::LoginFormState;
|
use crate::pages::login::LoginFormState;
|
||||||
@@ -14,8 +14,8 @@ pub enum Page {
|
|||||||
Login(LoginFormState),
|
Login(LoginFormState),
|
||||||
Register(RegisterFormState),
|
Register(RegisterFormState),
|
||||||
Admin(AdminState),
|
Admin(AdminState),
|
||||||
AddLogic(AddLogicState),
|
AddLogic(AddLogicFormState),
|
||||||
AddTable(AddTableState),
|
AddTable(AddTableFormState),
|
||||||
Form(String),
|
Form(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ pub async fn handle_search_palette_event(
|
|||||||
if should_close {
|
if should_close {
|
||||||
app_state.search_state = None;
|
app_state.search_state = None;
|
||||||
app_state.ui.show_search_palette = false;
|
app_state.ui.show_search_palette = false;
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(outcome_message)
|
Ok(outcome_message)
|
||||||
|
|||||||
@@ -14,12 +14,20 @@ pub struct AuthClient {
|
|||||||
|
|
||||||
impl AuthClient {
|
impl AuthClient {
|
||||||
pub async fn new() -> Result<Self> {
|
pub async fn new() -> Result<Self> {
|
||||||
|
// Kept for backward compatibility; opens a new connection.
|
||||||
let client = AuthServiceClient::connect("http://[::1]:50051")
|
let client = AuthServiceClient::connect("http://[::1]:50051")
|
||||||
.await
|
.await
|
||||||
.context("Failed to connect to auth service")?;
|
.context("Failed to connect to auth service")?;
|
||||||
Ok(Self { client })
|
Ok(Self { client })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Preferred: reuse an existing Channel (from GrpcClient).
|
||||||
|
pub async fn with_channel(channel: Channel) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
client: AuthServiceClient::new(channel),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Login user via gRPC.
|
/// Login user via gRPC.
|
||||||
pub async fn login(&mut self, identifier: String, password: String) -> Result<LoginResponse> {
|
pub async fn login(&mut self, identifier: String, password: String) -> Result<LoginResponse> {
|
||||||
let request = tonic::Request::new(LoginRequest { identifier, password });
|
let request = tonic::Request::new(LoginRequest { identifier, password });
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
use common::proto::komp_ac::common::Empty;
|
use common::proto::komp_ac::common::Empty;
|
||||||
use common::proto::komp_ac::table_structure::table_structure_service_client::TableStructureServiceClient;
|
use common::proto::komp_ac::table_structure::table_structure_service_client::TableStructureServiceClient;
|
||||||
use common::proto::komp_ac::table_structure::{GetTableStructureRequest, TableStructureResponse};
|
use common::proto::komp_ac::table_structure::{
|
||||||
|
GetTableStructureRequest, TableStructureResponse,
|
||||||
|
};
|
||||||
use common::proto::komp_ac::table_definition::{
|
use common::proto::komp_ac::table_definition::{
|
||||||
table_definition_client::TableDefinitionClient,
|
table_definition_client::TableDefinitionClient,
|
||||||
PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse,
|
PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse,
|
||||||
@@ -24,23 +26,41 @@ use common::proto::komp_ac::tables_data::{
|
|||||||
};
|
};
|
||||||
use crate::search::SearchGrpc;
|
use crate::search::SearchGrpc;
|
||||||
use common::proto::komp_ac::search::SearchResponse;
|
use common::proto::komp_ac::search::SearchResponse;
|
||||||
|
use common::proto::komp_ac::table_validation::{
|
||||||
|
table_validation_service_client::TableValidationServiceClient,
|
||||||
|
GetTableValidationRequest,
|
||||||
|
TableValidationResponse,
|
||||||
|
CountMode as PbCountMode,
|
||||||
|
FieldValidation as PbFieldValidation,
|
||||||
|
CharacterLimits as PbCharacterLimits,
|
||||||
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tonic::transport::Channel;
|
use tonic::transport::{Channel, Endpoint};
|
||||||
use prost_types::Value;
|
use prost_types::Value;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GrpcClient {
|
pub struct GrpcClient {
|
||||||
|
channel: Channel,
|
||||||
table_structure_client: TableStructureServiceClient<Channel>,
|
table_structure_client: TableStructureServiceClient<Channel>,
|
||||||
table_definition_client: TableDefinitionClient<Channel>,
|
table_definition_client: TableDefinitionClient<Channel>,
|
||||||
table_script_client: TableScriptClient<Channel>,
|
table_script_client: TableScriptClient<Channel>,
|
||||||
tables_data_client: TablesDataClient<Channel>,
|
tables_data_client: TablesDataClient<Channel>,
|
||||||
search_client: SearchGrpc,
|
search_client: SearchGrpc,
|
||||||
|
table_validation_client: TableValidationServiceClient<Channel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GrpcClient {
|
impl GrpcClient {
|
||||||
pub async fn new() -> Result<Self> {
|
pub async fn new() -> Result<Self> {
|
||||||
let channel = Channel::from_static("http://[::1]:50051")
|
let endpoint = Endpoint::from_static("http://[::1]:50051")
|
||||||
|
.connect_timeout(Duration::from_secs(5))
|
||||||
|
.tcp_keepalive(Some(Duration::from_secs(30)))
|
||||||
|
.keep_alive_while_idle(true)
|
||||||
|
.http2_keep_alive_interval(Duration::from_secs(15))
|
||||||
|
.keep_alive_timeout(Duration::from_secs(5));
|
||||||
|
|
||||||
|
let channel = endpoint
|
||||||
.connect()
|
.connect()
|
||||||
.await
|
.await
|
||||||
.context("Failed to create gRPC channel")?;
|
.context("Failed to create gRPC channel")?;
|
||||||
@@ -52,16 +72,43 @@ impl GrpcClient {
|
|||||||
let table_script_client = TableScriptClient::new(channel.clone());
|
let table_script_client = TableScriptClient::new(channel.clone());
|
||||||
let tables_data_client = TablesDataClient::new(channel.clone());
|
let tables_data_client = TablesDataClient::new(channel.clone());
|
||||||
let search_client = SearchGrpc::new(channel.clone());
|
let search_client = SearchGrpc::new(channel.clone());
|
||||||
|
let table_validation_client =
|
||||||
|
TableValidationServiceClient::new(channel.clone());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
channel,
|
||||||
table_structure_client,
|
table_structure_client,
|
||||||
table_definition_client,
|
table_definition_client,
|
||||||
table_script_client,
|
table_script_client,
|
||||||
tables_data_client,
|
tables_data_client,
|
||||||
search_client,
|
search_client,
|
||||||
|
table_validation_client,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose the shared channel so other typed clients can reuse it.
|
||||||
|
pub fn channel(&self) -> Channel {
|
||||||
|
self.channel.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch validation rules for a table. Absence of a field in response = no validation.
|
||||||
|
pub async fn get_table_validation(
|
||||||
|
&mut self,
|
||||||
|
profile_name: String,
|
||||||
|
table_name: String,
|
||||||
|
) -> Result<TableValidationResponse> {
|
||||||
|
let req = GetTableValidationRequest {
|
||||||
|
profile_name,
|
||||||
|
table_name,
|
||||||
|
};
|
||||||
|
let resp = self
|
||||||
|
.table_validation_client
|
||||||
|
.get_table_validation(tonic::Request::new(req))
|
||||||
|
.await
|
||||||
|
.context("gRPC GetTableValidation call failed")?;
|
||||||
|
Ok(resp.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_table_structure(
|
pub async fn get_table_structure(
|
||||||
&mut self,
|
&mut self,
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use crate::pages::admin_panel::add_logic::state::AddLogicState;
|
|||||||
use crate::pages::forms::logic::SaveOutcome;
|
use crate::pages::forms::logic::SaveOutcome;
|
||||||
use crate::utils::columns::filter_user_columns;
|
use crate::utils::columns::filter_user_columns;
|
||||||
use crate::pages::forms::{FieldDefinition, FormState};
|
use crate::pages::forms::{FieldDefinition, FormState};
|
||||||
|
use common::proto::komp_ac::table_validation::CountMode as PbCountMode;
|
||||||
|
use canvas::validation::limits::CountMode;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -314,4 +316,60 @@ impl UiService {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch and apply "Validation 1" (character limits) rules for this form.
|
||||||
|
pub async fn apply_validation1_for_form(
|
||||||
|
grpc_client: &mut GrpcClient,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
path: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (profile, table) = path
|
||||||
|
.split_once('/')
|
||||||
|
.context("Invalid form path for validation")?;
|
||||||
|
|
||||||
|
let resp = grpc_client
|
||||||
|
.get_table_validation(profile.to_string(), table.to_string())
|
||||||
|
.await
|
||||||
|
.context("Failed to fetch table validation")?;
|
||||||
|
|
||||||
|
if let Some(fs) = app_state.form_state_for_path(path) {
|
||||||
|
let mut rules: Vec<Option<crate::pages::forms::state::CharLimitsRule>> =
|
||||||
|
vec![None; fs.fields.len()];
|
||||||
|
|
||||||
|
for f in resp.fields {
|
||||||
|
if let Some(idx) = fs.fields.iter().position(|fd| fd.data_key == f.data_key) {
|
||||||
|
if let Some(limits) = f.limits {
|
||||||
|
let has_any =
|
||||||
|
limits.min != 0 || limits.max != 0 || limits.warn_at.is_some();
|
||||||
|
if has_any {
|
||||||
|
let cm = match PbCountMode::from_i32(limits.count_mode) {
|
||||||
|
Some(PbCountMode::Unspecified) | None => CountMode::Characters, // protobuf default → fallback
|
||||||
|
Some(PbCountMode::Chars) => CountMode::Characters,
|
||||||
|
Some(PbCountMode::Bytes) => CountMode::Bytes,
|
||||||
|
Some(PbCountMode::DisplayWidth) => CountMode::DisplayWidth,
|
||||||
|
};
|
||||||
|
|
||||||
|
let min = if limits.min == 0 { None } else { Some(limits.min as usize) };
|
||||||
|
let max = if limits.max == 0 { None } else { Some(limits.max as usize) };
|
||||||
|
let warn_at = limits.warn_at.map(|w| w as usize);
|
||||||
|
|
||||||
|
rules[idx] = Some(crate::pages::forms::state::CharLimitsRule {
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
warn_at,
|
||||||
|
count_mode: cm,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.set_character_limits_rules(rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
|
editor.set_validation_enabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ pub struct UiState {
|
|||||||
pub show_login: bool,
|
pub show_login: bool,
|
||||||
pub show_register: bool,
|
pub show_register: bool,
|
||||||
pub show_search_palette: bool,
|
pub show_search_palette: bool,
|
||||||
pub focus_outside_canvas: bool,
|
|
||||||
pub dialog: DialogState,
|
pub dialog: DialogState,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +51,6 @@ pub struct AppState {
|
|||||||
// NEW: The "Rulebook" cache. We use Arc for efficient sharing.
|
// NEW: The "Rulebook" cache. We use Arc for efficient sharing.
|
||||||
pub schema_cache: HashMap<String, Arc<TableStructureResponse>>,
|
pub schema_cache: HashMap<String, Arc<TableStructureResponse>>,
|
||||||
|
|
||||||
pub focused_button_index: usize,
|
|
||||||
pub pending_table_structure_fetch: Option<(String, String)>,
|
pub pending_table_structure_fetch: Option<(String, String)>,
|
||||||
|
|
||||||
pub search_state: Option<SearchState>,
|
pub search_state: Option<SearchState>,
|
||||||
@@ -61,7 +59,6 @@ pub struct AppState {
|
|||||||
pub ui: UiState,
|
pub ui: UiState,
|
||||||
|
|
||||||
pub form_editor: HashMap<String, FormEditor<FormState>>, // key = "profile/table"
|
pub form_editor: HashMap<String, FormEditor<FormState>>, // key = "profile/table"
|
||||||
|
|
||||||
#[cfg(feature = "ui-debug")]
|
#[cfg(feature = "ui-debug")]
|
||||||
pub debug_state: Option<DebugState>,
|
pub debug_state: Option<DebugState>,
|
||||||
}
|
}
|
||||||
@@ -77,7 +74,6 @@ impl AppState {
|
|||||||
current_view_table_name: None,
|
current_view_table_name: None,
|
||||||
current_mode: AppMode::General,
|
current_mode: AppMode::General,
|
||||||
schema_cache: HashMap::new(), // NEW: Initialize the cache
|
schema_cache: HashMap::new(), // NEW: Initialize the cache
|
||||||
focused_button_index: 0,
|
|
||||||
pending_table_structure_fetch: None,
|
pending_table_structure_fetch: None,
|
||||||
search_state: None,
|
search_state: None,
|
||||||
ui: UiState::default(),
|
ui: UiState::default(),
|
||||||
@@ -166,8 +162,7 @@ impl Default for UiState {
|
|||||||
show_login: false,
|
show_login: false,
|
||||||
show_register: false,
|
show_register: false,
|
||||||
show_buffer_list: true,
|
show_buffer_list: true,
|
||||||
show_search_palette: false, // ADDED
|
show_search_palette: false,
|
||||||
focus_outside_canvas: false,
|
|
||||||
dialog: DialogState::default(),
|
dialog: DialogState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,17 +20,12 @@ pub fn logout(
|
|||||||
// Delete stored auth data
|
// Delete stored auth data
|
||||||
if let Err(e) = delete_auth_data() {
|
if let Err(e) = delete_auth_data() {
|
||||||
error!("Failed to delete stored auth data: {}", e);
|
error!("Failed to delete stored auth data: {}", e);
|
||||||
// Continue anyway - user is logged out in memory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to intro screen
|
// Navigate to intro screen
|
||||||
buffer_state.history = vec![AppView::Intro];
|
buffer_state.history = vec![AppView::Intro];
|
||||||
buffer_state.active_index = 0;
|
buffer_state.active_index = 0;
|
||||||
|
|
||||||
// Reset UI state
|
|
||||||
app_state.ui.focus_outside_canvas = false;
|
|
||||||
app_state.focused_button_index = 0;
|
|
||||||
|
|
||||||
// Hide any open dialogs
|
// Hide any open dialogs
|
||||||
app_state.hide_dialog();
|
app_state.hide_dialog();
|
||||||
|
|
||||||
@@ -39,7 +34,7 @@ pub fn logout(
|
|||||||
"Logged Out",
|
"Logged Out",
|
||||||
"You have been successfully logged out.",
|
"You have been successfully logged out.",
|
||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::LoginSuccess, // Reuse or create a new purpose
|
DialogPurpose::LoginSuccess,
|
||||||
);
|
);
|
||||||
|
|
||||||
info!("User logged out successfully.");
|
info!("User logged out successfully.");
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ use crate::state::pages::auth::AuthState;
|
|||||||
use crate::state::pages::auth::UserRole;
|
use crate::state::pages::auth::UserRole;
|
||||||
use crate::pages::login::LoginFormState;
|
use crate::pages::login::LoginFormState;
|
||||||
use crate::pages::register::RegisterFormState;
|
use crate::pages::register::RegisterFormState;
|
||||||
|
use crate::pages::admin_panel::add_table;
|
||||||
|
use crate::pages::admin_panel::add_logic;
|
||||||
use crate::pages::admin::AdminState;
|
use crate::pages::admin::AdminState;
|
||||||
use crate::pages::admin::AdminFocus;
|
use crate::pages::admin::AdminFocus;
|
||||||
use crate::pages::admin::admin;
|
use crate::pages::admin::admin;
|
||||||
@@ -24,6 +26,7 @@ use crate::buffer::state::AppView;
|
|||||||
use crate::state::app::state::AppState;
|
use crate::state::app::state::AppState;
|
||||||
use crate::tui::terminal::{EventReader, TerminalCore};
|
use crate::tui::terminal::{EventReader, TerminalCore};
|
||||||
use crate::ui::handlers::render::render_ui;
|
use crate::ui::handlers::render::render_ui;
|
||||||
|
use crate::input::leader::leader_has_any_start;
|
||||||
use crate::pages::login;
|
use crate::pages::login;
|
||||||
use crate::pages::register;
|
use crate::pages::register;
|
||||||
use crate::pages::login::LoginResult;
|
use crate::pages::login::LoginResult;
|
||||||
@@ -120,6 +123,10 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
app_state.ensure_form_editor(&path, &config, || {
|
app_state.ensure_form_editor(&path, &config, || {
|
||||||
FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs)
|
FormState::new(initial_profile.clone(), initial_table.clone(), initial_field_defs)
|
||||||
});
|
});
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
UiService::apply_validation1_for_form(&mut grpc_client, &mut app_state, &path)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
buffer_state.update_history(AppView::Form(path.clone()));
|
buffer_state.update_history(AppView::Form(path.clone()));
|
||||||
router.navigate(Page::Form(path.clone()));
|
router.navigate(Page::Form(path.clone()));
|
||||||
|
|
||||||
@@ -225,8 +232,38 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
|| app_state.ui.show_search_palette
|
|| app_state.ui.show_search_palette
|
||||||
|| event_handler.navigation_state.active;
|
|| event_handler.navigation_state.active;
|
||||||
if !overlay_active {
|
if !overlay_active {
|
||||||
|
let inside_canvas = match &router.current {
|
||||||
|
Page::Form(_) => true,
|
||||||
|
Page::Login(state) => !state.focus_outside_canvas,
|
||||||
|
Page::Register(state) => !state.focus_outside_canvas,
|
||||||
|
Page::AddTable(state) => !state.focus_outside_canvas,
|
||||||
|
Page::AddLogic(state) => !state.focus_outside_canvas,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if inside_canvas {
|
||||||
|
// Do NOT forward to canvas while a leader is active or about to start.
|
||||||
|
// This prevents the canvas from stealing the second/third key (b/d/r).
|
||||||
|
let leader_in_progress = event_handler.input_engine.has_active_sequence();
|
||||||
|
let is_space = matches!(key_event.code, crossterm_event::KeyCode::Char(' '));
|
||||||
|
let can_start_leader = leader_has_any_start(&config);
|
||||||
|
let form_in_edit_mode = match &router.current {
|
||||||
|
Page::Form(path) => app_state
|
||||||
|
.editor_for_path_ref(path)
|
||||||
|
.map(|e| e.mode() == canvas::AppMode::Edit)
|
||||||
|
.unwrap_or(false),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let defer_to_engine_for_leader = leader_in_progress
|
||||||
|
|| (is_space && can_start_leader && !form_in_edit_mode);
|
||||||
|
|
||||||
|
if defer_to_engine_for_leader {
|
||||||
|
info!(
|
||||||
|
"Skipping canvas pre-handle: leader sequence active or starting"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
if let Page::Form(path) = &router.current {
|
if let Page::Form(path) = &router.current {
|
||||||
if !app_state.ui.focus_outside_canvas {
|
|
||||||
if let Some(editor) = app_state.editor_for_path(path) {
|
if let Some(editor) = app_state.editor_for_path(path) {
|
||||||
match editor.handle_key_event(*key_event) {
|
match editor.handle_key_event(*key_event) {
|
||||||
KeyEventOutcome::Consumed(Some(msg)) => {
|
KeyEventOutcome::Consumed(Some(msg)) => {
|
||||||
@@ -251,6 +288,7 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Call handle_event directly
|
// Call handle_event directly
|
||||||
let event_outcome_result = event_handler.handle_event(
|
let event_outcome_result = event_handler.handle_event(
|
||||||
@@ -364,7 +402,9 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
vec!["OK".to_string()],
|
vec!["OK".to_string()],
|
||||||
DialogPurpose::SaveTableSuccess,
|
DialogPurpose::SaveTableSuccess,
|
||||||
);
|
);
|
||||||
admin_state.add_table_state.has_unsaved_changes = false;
|
if let Page::AddTable(page) = &mut router.current {
|
||||||
|
page.state.has_unsaved_changes = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
event_handler.command_message = format!("Save failed: {}", e);
|
event_handler.command_message = format!("Save failed: {}", e);
|
||||||
@@ -423,8 +463,20 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
|
|
||||||
router.navigate(Page::Admin(admin_state.clone()));
|
router.navigate(Page::Admin(admin_state.clone()));
|
||||||
}
|
}
|
||||||
AppView::AddTable => router.navigate(Page::AddTable(admin_state.add_table_state.clone())),
|
AppView::AddTable => {
|
||||||
AppView::AddLogic => router.navigate(Page::AddLogic(admin_state.add_logic_state.clone())),
|
if let Page::AddTable(page) = &mut router.current {
|
||||||
|
// Ensure keymap is set once (same as AddLogic)
|
||||||
|
page.editor.set_keymap(config.build_canvas_keymap());
|
||||||
|
} else {
|
||||||
|
// Page is created by admin navigation (Button2). No-op here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppView::AddLogic => {
|
||||||
|
if let Page::AddLogic(page) = &mut router.current {
|
||||||
|
// Ensure keymap is set once
|
||||||
|
page.editor.set_keymap(config.build_canvas_keymap());
|
||||||
|
}
|
||||||
|
}
|
||||||
AppView::Form(path) => {
|
AppView::Form(path) => {
|
||||||
// Keep current_view_* consistent with the active buffer path
|
// Keep current_view_* consistent with the active buffer path
|
||||||
if let Some((profile, table)) = path.split_once('/') {
|
if let Some((profile, table)) = path.split_once('/') {
|
||||||
@@ -468,6 +520,21 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
prev_view_profile_name = current_view_profile;
|
prev_view_profile_name = current_view_profile;
|
||||||
prev_view_table_name = current_view_table;
|
prev_view_table_name = current_view_table;
|
||||||
table_just_switched = true;
|
table_just_switched = true;
|
||||||
|
// Apply character-limit validation for the new form
|
||||||
|
#[cfg(feature = "validation")]
|
||||||
|
if let (Some(prof), Some(tbl)) = (
|
||||||
|
app_state.current_view_profile_name.as_ref(),
|
||||||
|
app_state.current_view_table_name.as_ref(),
|
||||||
|
) {
|
||||||
|
let p = format!("{}/{}", prof, tbl);
|
||||||
|
UiService::apply_validation1_for_form(
|
||||||
|
&mut grpc_client,
|
||||||
|
&mut app_state,
|
||||||
|
&p,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app_state.update_dialog_content(
|
app_state.update_dialog_content(
|
||||||
@@ -485,67 +552,25 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with the rest of the positioning logic...
|
let needs_redraw_from_fetch = add_logic::loader::process_pending_table_structure_fetch(
|
||||||
// Now we can use CanvasState methods like get_current_input(), current_field(), etc.
|
&mut app_state,
|
||||||
|
&mut router,
|
||||||
|
&mut grpc_client,
|
||||||
|
&mut event_handler.command_message,
|
||||||
|
).await.unwrap_or(false);
|
||||||
|
|
||||||
|
if needs_redraw_from_fetch {
|
||||||
|
needs_redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((profile_name, table_name)) = app_state.pending_table_structure_fetch.take() {
|
|
||||||
if let Page::AddLogic(state) = &mut router.current {
|
if let Page::AddLogic(state) = &mut router.current {
|
||||||
if state.profile_name == profile_name
|
let needs_redraw_from_columns = add_logic::loader::maybe_fetch_columns_for_awaiting_table(
|
||||||
&& state.selected_table_name.as_deref() == Some(table_name.as_str())
|
|
||||||
{
|
|
||||||
info!("Fetching table structure for {}.{}", profile_name, table_name);
|
|
||||||
let fetch_message = UiService::initialize_add_logic_table_data(
|
|
||||||
&mut grpc_client,
|
&mut grpc_client,
|
||||||
state,
|
state,
|
||||||
&app_state.profile_tree,
|
&mut event_handler.command_message,
|
||||||
)
|
).await.unwrap_or(false);
|
||||||
.await
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
error!("Error initializing add_logic_table_data: {}", e);
|
|
||||||
format!("Error fetching table structure: {}", e)
|
|
||||||
});
|
|
||||||
|
|
||||||
if !fetch_message.contains("Error") && !fetch_message.contains("Warning") {
|
if needs_redraw_from_columns {
|
||||||
info!("{}", fetch_message);
|
|
||||||
} else {
|
|
||||||
event_handler.command_message = fetch_message;
|
|
||||||
}
|
|
||||||
needs_redraw = true;
|
|
||||||
} else {
|
|
||||||
error!(
|
|
||||||
"Mismatch in pending_table_structure_fetch: app_state wants {}.{}, but AddLogic state is for {}.{:?}",
|
|
||||||
profile_name,
|
|
||||||
table_name,
|
|
||||||
state.profile_name,
|
|
||||||
state.selected_table_name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"Pending table structure fetch for {}.{} but AddLogic view is not active. Fetch ignored.",
|
|
||||||
profile_name, table_name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Page::AddLogic(state) = &mut router.current {
|
|
||||||
if let Some(table_name) = state.script_editor_awaiting_column_autocomplete.clone() {
|
|
||||||
let profile_name = state.profile_name.clone();
|
|
||||||
|
|
||||||
info!("Fetching columns for table selection: {}.{}", profile_name, table_name);
|
|
||||||
match UiService::fetch_columns_for_table(&mut grpc_client, &profile_name, &table_name).await {
|
|
||||||
Ok(columns) => {
|
|
||||||
state.set_columns_for_table_autocomplete(columns.clone());
|
|
||||||
info!("Loaded {} columns for table '{}'", columns.len(), table_name);
|
|
||||||
event_handler.command_message = format!("Columns for '{}' loaded. Select a column.", table_name);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e);
|
|
||||||
state.script_editor_awaiting_column_autocomplete = None;
|
|
||||||
state.deactivate_script_editor_autocomplete();
|
|
||||||
event_handler.command_message = format!("Error loading columns for '{}': {}", table_name, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,7 +684,15 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
|
|
||||||
match current_mode {
|
match current_mode {
|
||||||
AppMode::General => {
|
AppMode::General => {
|
||||||
if app_state.ui.focus_outside_canvas {
|
let outside_canvas = match &router.current {
|
||||||
|
Page::Login(state) => state.focus_outside_canvas,
|
||||||
|
Page::Register(state) => state.focus_outside_canvas,
|
||||||
|
Page::AddTable(state) => state.focus_outside_canvas,
|
||||||
|
Page::AddLogic(state) => state.focus_outside_canvas,
|
||||||
|
_ => false, // Form and Admin don’t use this flag
|
||||||
|
};
|
||||||
|
|
||||||
|
if outside_canvas {
|
||||||
// Outside canvas → app decides
|
// Outside canvas → app decides
|
||||||
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
|
terminal.set_cursor_style(SetCursorStyle::SteadyUnderScore)?;
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
@@ -676,6 +709,12 @@ pub async fn run_ui() -> Result<()> {
|
|||||||
if let Page::Register(page) = &router.current {
|
if let Page::Register(page) = &router.current {
|
||||||
let _ = CursorManager::update_for_mode(page.editor.mode());
|
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||||||
}
|
}
|
||||||
|
if let Page::AddTable(page) = &router.current {
|
||||||
|
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||||||
|
}
|
||||||
|
if let Page::AddLogic(page) = &router.current {
|
||||||
|
let _ = CursorManager::update_for_mode(page.editor.mode());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppMode::Command => {
|
AppMode::Command => {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
// client/src/utils/debug_logger.rs
|
// client/src/utils/debug_logger.rs
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use std::collections::VecDeque; // <-- FIX: Import VecDeque
|
use std::collections::VecDeque; // <-- FIX: Import VecDeque
|
||||||
use std::io;
|
use std::io::{self, Write};
|
||||||
use std::sync::{Arc, Mutex}; // <-- FIX: Import Mutex
|
use std::sync::{Arc, Mutex}; // <-- FIX: Import Mutex
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref UI_DEBUG_BUFFER: Arc<Mutex<VecDeque<(String, bool)>>> =
|
static ref UI_DEBUG_BUFFER: Arc<Mutex<VecDeque<(String, bool)>>> =
|
||||||
@@ -27,11 +30,21 @@ impl UiDebugWriter {
|
|||||||
impl io::Write for UiDebugWriter {
|
impl io::Write for UiDebugWriter {
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
let mut buffer = UI_DEBUG_BUFFER.lock().unwrap();
|
let mut buffer = UI_DEBUG_BUFFER.lock().unwrap();
|
||||||
let message = String::from_utf8_lossy(buf);
|
let message = String::from_utf8_lossy(buf).trim().to_string();
|
||||||
let trimmed_message = message.trim().to_string();
|
let is_error = message.starts_with("ERROR");
|
||||||
let is_error = trimmed_message.starts_with("ERROR");
|
|
||||||
// Add the new message to the back of the queue
|
// Keep in memory for UI
|
||||||
buffer.push_back((trimmed_message, is_error));
|
buffer.push_back((message.clone(), is_error));
|
||||||
|
|
||||||
|
// ALSO log directly to file (non-blocking best effort)
|
||||||
|
if let Ok(mut file) = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open("ui_debug.log")
|
||||||
|
{
|
||||||
|
let _ = writeln!(file, "{message}");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(buf.len())
|
Ok(buf.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,3 +57,22 @@ impl io::Write for UiDebugWriter {
|
|||||||
pub fn pop_next_debug_message() -> Option<(String, bool)> {
|
pub fn pop_next_debug_message() -> Option<(String, bool)> {
|
||||||
UI_DEBUG_BUFFER.lock().unwrap().pop_front()
|
UI_DEBUG_BUFFER.lock().unwrap().pop_front()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// spawn a background thread that keeps draining UI_DEBUG_BUFFER
|
||||||
|
/// and writes messages into ui_debug.log continuously
|
||||||
|
pub fn spawn_file_logger() {
|
||||||
|
thread::spawn(|| loop {
|
||||||
|
// pop one message if present
|
||||||
|
if let Some((msg, _)) = pop_next_debug_message() {
|
||||||
|
if let Ok(mut file) = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open("ui_debug.log")
|
||||||
|
{
|
||||||
|
let _ = writeln!(file, "{msg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// small sleep to avoid burning CPU
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
39
client/tests/input/engine_leader_e2e.rs
Normal file
39
client/tests/input/engine_leader_e2e.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use client::config::binds::config::Config;
|
||||||
|
use client::input::engine::{InputEngine, InputContext, InputOutcome};
|
||||||
|
use client::modes::handlers::mode_manager::AppMode;
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
fn ctx() -> InputContext {
|
||||||
|
InputContext {
|
||||||
|
app_mode: AppMode::General,
|
||||||
|
overlay_active: false,
|
||||||
|
allow_navigation_capture: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(c: char) -> KeyEvent {
|
||||||
|
KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn engine_collects_space_b_r() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[keybindings]
|
||||||
|
revert = ["space+b+r"]
|
||||||
|
"#;
|
||||||
|
let config: Config = toml::from_str(toml_str).unwrap();
|
||||||
|
|
||||||
|
let mut eng = InputEngine::new(400, 5_000);
|
||||||
|
|
||||||
|
// space -> Pending (leader started)
|
||||||
|
let out1 = eng.process_key(key(' '), &ctx(), &config);
|
||||||
|
assert!(matches!(out1, InputOutcome::Pending));
|
||||||
|
|
||||||
|
// b -> Pending (prefix)
|
||||||
|
let out2 = eng.process_key(key('b'), &ctx(), &config);
|
||||||
|
assert!(matches!(out2, InputOutcome::Pending));
|
||||||
|
|
||||||
|
// r -> Action(revert)
|
||||||
|
let out3 = eng.process_key(key('r'), &ctx(), &config);
|
||||||
|
assert!(matches!(out3, InputOutcome::Action(_)));
|
||||||
|
}
|
||||||
25
client/tests/input/leader_sequences.rs
Normal file
25
client/tests/input/leader_sequences.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use client::config::binds::config::Config;
|
||||||
|
use client::input::leader::leader_match_action;
|
||||||
|
use client::config::binds::key_sequences::parse_binding;
|
||||||
|
use crossterm::event::KeyCode;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_space_b_d_binding() {
|
||||||
|
// Minimal fake config TOML
|
||||||
|
let toml_str = r#"
|
||||||
|
[keybindings]
|
||||||
|
close_buffer = ["space+b+d"]
|
||||||
|
"#;
|
||||||
|
let config: Config = toml::from_str(toml_str).unwrap();
|
||||||
|
|
||||||
|
let seq = vec![KeyCode::Char(' '), KeyCode::Char('b'), KeyCode::Char('d')];
|
||||||
|
let action = leader_match_action(&config, &seq);
|
||||||
|
assert_eq!(action, Some("close_buffer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_space_b_r() {
|
||||||
|
let seq = parse_binding("space+b+r");
|
||||||
|
let codes: Vec<KeyCode> = seq.iter().map(|p| p.code).collect();
|
||||||
|
assert_eq!(codes, vec![KeyCode::Char(' '), KeyCode::Char('b'), KeyCode::Char('r')]);
|
||||||
|
}
|
||||||
4
client/tests/input/mod.rs
Normal file
4
client/tests/input/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// tests/input/mod.rs
|
||||||
|
|
||||||
|
pub mod engine_leader_e2e;
|
||||||
|
pub mod leader_sequences;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
// tests/mod.rs
|
// tests/mod.rs
|
||||||
|
|
||||||
pub mod form;
|
// pub mod form;
|
||||||
|
pub mod input;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ serde = { version = "1.0.219", features = ["derive"] }
|
|||||||
|
|
||||||
# Search
|
# Search
|
||||||
tantivy = { workspace = true }
|
tantivy = { workspace = true }
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-build = "0.13.0"
|
tonic-build = { version = "0.13.0", features = ["prost-build"] }
|
||||||
|
prost-build = "0.14.1"
|
||||||
|
|||||||
@@ -1,21 +1,75 @@
|
|||||||
// common/build.rs
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tonic_build::configure()
|
tonic_build::configure()
|
||||||
.build_server(true)
|
.build_server(true)
|
||||||
.out_dir("src/proto")
|
|
||||||
.file_descriptor_set_path("src/proto/descriptor.bin")
|
.file_descriptor_set_path("src/proto/descriptor.bin")
|
||||||
.compile_protos( // Changed from .compile()
|
.out_dir("src/proto")
|
||||||
|
// Derive serde for the messages
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_validation.FieldValidation",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_validation.CharacterLimits",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_validation.DisplayMask",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_validation.TableValidationResponse",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_validation.UpdateFieldValidationRequest",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_validation.UpdateFieldValidationResponse",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
// Enum -> readable strings in JSON ("BYTES", "DISPLAY_WIDTH")
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_validation.CountMode",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_definition.ColumnDefinition",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_definition.TableLink",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]"
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_definition.PostTableDefinitionRequest",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_definition.TableDefinitionResponse",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]"
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_script.PostTableScriptRequest",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.type_attribute(
|
||||||
|
".komp_ac.table_script.TableScriptResponse",
|
||||||
|
"#[derive(serde::Serialize, serde::Deserialize)]",
|
||||||
|
)
|
||||||
|
.compile_protos(
|
||||||
&[
|
&[
|
||||||
"proto/common.proto",
|
"proto/common.proto",
|
||||||
"proto/adresar.proto",
|
"proto/adresar.proto",
|
||||||
"proto/auth.proto",
|
"proto/auth.proto",
|
||||||
"proto/uctovnictvo.proto",
|
|
||||||
"proto/table_structure.proto",
|
|
||||||
"proto/table_definition.proto",
|
|
||||||
"proto/tables_data.proto",
|
|
||||||
"proto/table_script.proto",
|
|
||||||
"proto/search.proto",
|
"proto/search.proto",
|
||||||
"proto/search2.proto",
|
"proto/search2.proto",
|
||||||
|
"proto/table_definition.proto",
|
||||||
|
"proto/table_script.proto",
|
||||||
|
"proto/table_structure.proto",
|
||||||
|
"proto/table_validation.proto",
|
||||||
|
"proto/tables_data.proto",
|
||||||
|
"proto/uctovnictvo.proto",
|
||||||
],
|
],
|
||||||
&["proto"],
|
&["proto"],
|
||||||
)?;
|
)?;
|
||||||
|
|||||||
79
common/proto/table_validation.proto
Normal file
79
common/proto/table_validation.proto
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
// common/proto/table_validation.proto
|
||||||
|
syntax = "proto3";
|
||||||
|
package komp_ac.table_validation;
|
||||||
|
|
||||||
|
// Request validation rules for a table
|
||||||
|
message GetTableValidationRequest {
|
||||||
|
string profileName = 1;
|
||||||
|
string tableName = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response with field-level validations; if a field is omitted,
|
||||||
|
// no validation is applied (default unspecified).
|
||||||
|
message TableValidationResponse {
|
||||||
|
repeated FieldValidation fields = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field-level validation (extensible for future kinds)
|
||||||
|
message FieldValidation {
|
||||||
|
// MUST match your frontend FormState.dataKey for the column
|
||||||
|
string dataKey = 1;
|
||||||
|
|
||||||
|
// Current: only CharacterLimits. More rules can be added later.
|
||||||
|
CharacterLimits limits = 10;
|
||||||
|
// Future expansion:
|
||||||
|
// PatternRules pattern = 11;
|
||||||
|
DisplayMask mask = 3;
|
||||||
|
// ExternalValidation external = 13;
|
||||||
|
// CustomFormatter formatter = 14;
|
||||||
|
bool required = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character length counting mode
|
||||||
|
enum CountMode {
|
||||||
|
COUNT_MODE_UNSPECIFIED = 0; // default: same as CHARS
|
||||||
|
CHARS = 1;
|
||||||
|
BYTES = 2;
|
||||||
|
DISPLAY_WIDTH = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character limit validation (Validation 1)
|
||||||
|
message CharacterLimits {
|
||||||
|
// When zero, the field is considered "not set". If both min/max are zero,
|
||||||
|
// the server should avoid sending this FieldValidation (no validation).
|
||||||
|
uint32 min = 1;
|
||||||
|
uint32 max = 2;
|
||||||
|
|
||||||
|
// Optional warning threshold; when unset, no warning threshold is applied.
|
||||||
|
optional uint32 warnAt = 3;
|
||||||
|
|
||||||
|
CountMode countMode = 4; // defaults to CHARS if unspecified
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mask for pretty display
|
||||||
|
message DisplayMask {
|
||||||
|
string pattern = 1; // e.g., "(###) ###-####" or "####-##-##"
|
||||||
|
string input_char = 2; // e.g., "#"
|
||||||
|
optional string template_char = 3; // e.g., "_"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service to fetch validations for a table
|
||||||
|
service TableValidationService {
|
||||||
|
rpc GetTableValidation(GetTableValidationRequest)
|
||||||
|
returns (TableValidationResponse);
|
||||||
|
|
||||||
|
rpc UpdateFieldValidation(UpdateFieldValidationRequest)
|
||||||
|
returns (UpdateFieldValidationResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateFieldValidationRequest {
|
||||||
|
string profileName = 1;
|
||||||
|
string tableName = 2;
|
||||||
|
string dataKey = 3;
|
||||||
|
FieldValidation validation = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateFieldValidationResponse {
|
||||||
|
bool success = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
@@ -34,6 +34,9 @@ pub mod proto {
|
|||||||
pub mod search2 {
|
pub mod search2 {
|
||||||
include!("proto/komp_ac.search2.rs");
|
include!("proto/komp_ac.search2.rs");
|
||||||
}
|
}
|
||||||
|
pub mod table_validation {
|
||||||
|
include!("proto/komp_ac.table_validation.rs");
|
||||||
|
}
|
||||||
pub const FILE_DESCRIPTOR_SET: &[u8] =
|
pub const FILE_DESCRIPTOR_SET: &[u8] =
|
||||||
include_bytes!("proto/descriptor.bin");
|
include_bytes!("proto/descriptor.bin");
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
// This file is @generated by prost-build.
|
// This file is @generated by prost-build.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct TableLink {
|
pub struct TableLink {
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
@@ -6,6 +7,7 @@ pub struct TableLink {
|
|||||||
#[prost(bool, tag = "2")]
|
#[prost(bool, tag = "2")]
|
||||||
pub required: bool,
|
pub required: bool,
|
||||||
}
|
}
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct PostTableDefinitionRequest {
|
pub struct PostTableDefinitionRequest {
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
@@ -19,6 +21,7 @@ pub struct PostTableDefinitionRequest {
|
|||||||
#[prost(string, tag = "5")]
|
#[prost(string, tag = "5")]
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
pub profile_name: ::prost::alloc::string::String,
|
||||||
}
|
}
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct ColumnDefinition {
|
pub struct ColumnDefinition {
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
@@ -26,6 +29,7 @@ pub struct ColumnDefinition {
|
|||||||
#[prost(string, tag = "2")]
|
#[prost(string, tag = "2")]
|
||||||
pub field_type: ::prost::alloc::string::String,
|
pub field_type: ::prost::alloc::string::String,
|
||||||
}
|
}
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct TableDefinitionResponse {
|
pub struct TableDefinitionResponse {
|
||||||
#[prost(bool, tag = "1")]
|
#[prost(bool, tag = "1")]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// This file is @generated by prost-build.
|
// This file is @generated by prost-build.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct PostTableScriptRequest {
|
pub struct PostTableScriptRequest {
|
||||||
#[prost(int64, tag = "1")]
|
#[prost(int64, tag = "1")]
|
||||||
@@ -10,6 +11,7 @@ pub struct PostTableScriptRequest {
|
|||||||
#[prost(string, tag = "4")]
|
#[prost(string, tag = "4")]
|
||||||
pub description: ::prost::alloc::string::String,
|
pub description: ::prost::alloc::string::String,
|
||||||
}
|
}
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct TableScriptResponse {
|
pub struct TableScriptResponse {
|
||||||
#[prost(int64, tag = "1")]
|
#[prost(int64, tag = "1")]
|
||||||
|
|||||||
@@ -1,78 +1,128 @@
|
|||||||
// This file is @generated by prost-build.
|
// This file is @generated by prost-build.
|
||||||
|
/// Request validation rules for a table
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct TableLink {
|
pub struct GetTableValidationRequest {
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
pub linked_table_name: ::prost::alloc::string::String,
|
pub profile_name: ::prost::alloc::string::String,
|
||||||
#[prost(bool, tag = "2")]
|
#[prost(string, tag = "2")]
|
||||||
|
pub table_name: ::prost::alloc::string::String,
|
||||||
|
}
|
||||||
|
/// Response with field-level validations; if a field is omitted,
|
||||||
|
/// no validation is applied (default unspecified).
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct TableValidationResponse {
|
||||||
|
#[prost(message, repeated, tag = "1")]
|
||||||
|
pub fields: ::prost::alloc::vec::Vec<FieldValidation>,
|
||||||
|
}
|
||||||
|
/// Field-level validation (extensible for future kinds)
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
|
pub struct FieldValidation {
|
||||||
|
/// MUST match your frontend FormState.dataKey for the column
|
||||||
|
#[prost(string, tag = "1")]
|
||||||
|
pub data_key: ::prost::alloc::string::String,
|
||||||
|
/// Current: only CharacterLimits. More rules can be added later.
|
||||||
|
#[prost(message, optional, tag = "10")]
|
||||||
|
pub limits: ::core::option::Option<CharacterLimits>,
|
||||||
|
/// Future expansion:
|
||||||
|
/// PatternRules pattern = 11;
|
||||||
|
#[prost(message, optional, tag = "3")]
|
||||||
|
pub mask: ::core::option::Option<DisplayMask>,
|
||||||
|
/// ExternalValidation external = 13;
|
||||||
|
/// CustomFormatter formatter = 14;
|
||||||
|
#[prost(bool, tag = "4")]
|
||||||
pub required: bool,
|
pub required: bool,
|
||||||
}
|
}
|
||||||
|
/// Character limit validation (Validation 1)
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
||||||
|
pub struct CharacterLimits {
|
||||||
|
/// When zero, the field is considered "not set". If both min/max are zero,
|
||||||
|
/// the server should avoid sending this FieldValidation (no validation).
|
||||||
|
#[prost(uint32, tag = "1")]
|
||||||
|
pub min: u32,
|
||||||
|
#[prost(uint32, tag = "2")]
|
||||||
|
pub max: u32,
|
||||||
|
/// Optional warning threshold; when unset, no warning threshold is applied.
|
||||||
|
#[prost(uint32, optional, tag = "3")]
|
||||||
|
pub warn_at: ::core::option::Option<u32>,
|
||||||
|
/// defaults to CHARS if unspecified
|
||||||
|
#[prost(enumeration = "CountMode", tag = "4")]
|
||||||
|
pub count_mode: i32,
|
||||||
|
}
|
||||||
|
/// Mask for pretty display
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct PostTableDefinitionRequest {
|
pub struct DisplayMask {
|
||||||
|
/// e.g., "(###) ###-####" or "####-##-##"
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
pub table_name: ::prost::alloc::string::String,
|
pub pattern: ::prost::alloc::string::String,
|
||||||
#[prost(message, repeated, tag = "2")]
|
/// e.g., "#"
|
||||||
pub links: ::prost::alloc::vec::Vec<TableLink>,
|
|
||||||
#[prost(message, repeated, tag = "3")]
|
|
||||||
pub columns: ::prost::alloc::vec::Vec<ColumnDefinition>,
|
|
||||||
#[prost(string, repeated, tag = "4")]
|
|
||||||
pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct ColumnDefinition {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
#[prost(string, tag = "2")]
|
||||||
pub field_type: ::prost::alloc::string::String,
|
pub input_char: ::prost::alloc::string::String,
|
||||||
|
/// e.g., "_"
|
||||||
|
#[prost(string, optional, tag = "3")]
|
||||||
|
pub template_char: ::core::option::Option<::prost::alloc::string::String>,
|
||||||
}
|
}
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct TableDefinitionResponse {
|
pub struct UpdateFieldValidationRequest {
|
||||||
#[prost(bool, tag = "1")]
|
|
||||||
pub success: bool,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub sql: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct ProfileTreeResponse {
|
|
||||||
#[prost(message, repeated, tag = "1")]
|
|
||||||
pub profiles: ::prost::alloc::vec::Vec<profile_tree_response::Profile>,
|
|
||||||
}
|
|
||||||
/// Nested message and enum types in `ProfileTreeResponse`.
|
|
||||||
pub mod profile_tree_response {
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Table {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, repeated, tag = "3")]
|
|
||||||
pub depends_on: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Profile {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub name: ::prost::alloc::string::String,
|
|
||||||
#[prost(message, repeated, tag = "2")]
|
|
||||||
pub tables: ::prost::alloc::vec::Vec<Table>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct DeleteTableRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
#[prost(string, tag = "1")]
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
pub profile_name: ::prost::alloc::string::String,
|
||||||
#[prost(string, tag = "2")]
|
#[prost(string, tag = "2")]
|
||||||
pub table_name: ::prost::alloc::string::String,
|
pub table_name: ::prost::alloc::string::String,
|
||||||
|
#[prost(string, tag = "3")]
|
||||||
|
pub data_key: ::prost::alloc::string::String,
|
||||||
|
#[prost(message, optional, tag = "4")]
|
||||||
|
pub validation: ::core::option::Option<FieldValidation>,
|
||||||
}
|
}
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||||
pub struct DeleteTableResponse {
|
pub struct UpdateFieldValidationResponse {
|
||||||
#[prost(bool, tag = "1")]
|
#[prost(bool, tag = "1")]
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
#[prost(string, tag = "2")]
|
#[prost(string, tag = "2")]
|
||||||
pub message: ::prost::alloc::string::String,
|
pub message: ::prost::alloc::string::String,
|
||||||
}
|
}
|
||||||
|
/// Character length counting mode
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum CountMode {
|
||||||
|
/// default: same as CHARS
|
||||||
|
Unspecified = 0,
|
||||||
|
Chars = 1,
|
||||||
|
Bytes = 2,
|
||||||
|
DisplayWidth = 3,
|
||||||
|
}
|
||||||
|
impl CountMode {
|
||||||
|
/// String value of the enum field names used in the ProtoBuf definition.
|
||||||
|
///
|
||||||
|
/// The values are not transformed in any way and thus are considered stable
|
||||||
|
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||||
|
pub fn as_str_name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Unspecified => "COUNT_MODE_UNSPECIFIED",
|
||||||
|
Self::Chars => "CHARS",
|
||||||
|
Self::Bytes => "BYTES",
|
||||||
|
Self::DisplayWidth => "DISPLAY_WIDTH",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
|
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||||
|
match value {
|
||||||
|
"COUNT_MODE_UNSPECIFIED" => Some(Self::Unspecified),
|
||||||
|
"CHARS" => Some(Self::Chars),
|
||||||
|
"BYTES" => Some(Self::Bytes),
|
||||||
|
"DISPLAY_WIDTH" => Some(Self::DisplayWidth),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Generated client implementations.
|
/// Generated client implementations.
|
||||||
pub mod table_definition_client {
|
pub mod table_validation_service_client {
|
||||||
#![allow(
|
#![allow(
|
||||||
unused_variables,
|
unused_variables,
|
||||||
dead_code,
|
dead_code,
|
||||||
@@ -82,11 +132,12 @@ pub mod table_definition_client {
|
|||||||
)]
|
)]
|
||||||
use tonic::codegen::*;
|
use tonic::codegen::*;
|
||||||
use tonic::codegen::http::Uri;
|
use tonic::codegen::http::Uri;
|
||||||
|
/// Service to fetch validations for a table
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TableDefinitionClient<T> {
|
pub struct TableValidationServiceClient<T> {
|
||||||
inner: tonic::client::Grpc<T>,
|
inner: tonic::client::Grpc<T>,
|
||||||
}
|
}
|
||||||
impl TableDefinitionClient<tonic::transport::Channel> {
|
impl TableValidationServiceClient<tonic::transport::Channel> {
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
/// Attempt to create a new client by connecting to a given endpoint.
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||||
where
|
where
|
||||||
@@ -97,7 +148,7 @@ pub mod table_definition_client {
|
|||||||
Ok(Self::new(conn))
|
Ok(Self::new(conn))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<T> TableDefinitionClient<T>
|
impl<T> TableValidationServiceClient<T>
|
||||||
where
|
where
|
||||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
T: tonic::client::GrpcService<tonic::body::Body>,
|
||||||
T::Error: Into<StdError>,
|
T::Error: Into<StdError>,
|
||||||
@@ -115,7 +166,7 @@ pub mod table_definition_client {
|
|||||||
pub fn with_interceptor<F>(
|
pub fn with_interceptor<F>(
|
||||||
inner: T,
|
inner: T,
|
||||||
interceptor: F,
|
interceptor: F,
|
||||||
) -> TableDefinitionClient<InterceptedService<T, F>>
|
) -> TableValidationServiceClient<InterceptedService<T, F>>
|
||||||
where
|
where
|
||||||
F: tonic::service::Interceptor,
|
F: tonic::service::Interceptor,
|
||||||
T::ResponseBody: Default,
|
T::ResponseBody: Default,
|
||||||
@@ -129,7 +180,9 @@ pub mod table_definition_client {
|
|||||||
http::Request<tonic::body::Body>,
|
http::Request<tonic::body::Body>,
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||||
{
|
{
|
||||||
TableDefinitionClient::new(InterceptedService::new(inner, interceptor))
|
TableValidationServiceClient::new(
|
||||||
|
InterceptedService::new(inner, interceptor),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/// Compress requests with the given encoding.
|
/// Compress requests with the given encoding.
|
||||||
///
|
///
|
||||||
@@ -162,11 +215,11 @@ pub mod table_definition_client {
|
|||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
self.inner = self.inner.max_encoding_message_size(limit);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub async fn post_table_definition(
|
pub async fn get_table_validation(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: impl tonic::IntoRequest<super::PostTableDefinitionRequest>,
|
request: impl tonic::IntoRequest<super::GetTableValidationRequest>,
|
||||||
) -> std::result::Result<
|
) -> std::result::Result<
|
||||||
tonic::Response<super::TableDefinitionResponse>,
|
tonic::Response<super::TableValidationResponse>,
|
||||||
tonic::Status,
|
tonic::Status,
|
||||||
> {
|
> {
|
||||||
self.inner
|
self.inner
|
||||||
@@ -179,23 +232,23 @@ pub mod table_definition_client {
|
|||||||
})?;
|
})?;
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
"/komp_ac.table_definition.TableDefinition/PostTableDefinition",
|
"/komp_ac.table_validation.TableValidationService/GetTableValidation",
|
||||||
);
|
);
|
||||||
let mut req = request.into_request();
|
let mut req = request.into_request();
|
||||||
req.extensions_mut()
|
req.extensions_mut()
|
||||||
.insert(
|
.insert(
|
||||||
GrpcMethod::new(
|
GrpcMethod::new(
|
||||||
"komp_ac.table_definition.TableDefinition",
|
"komp_ac.table_validation.TableValidationService",
|
||||||
"PostTableDefinition",
|
"GetTableValidation",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
self.inner.unary(req, path, codec).await
|
self.inner.unary(req, path, codec).await
|
||||||
}
|
}
|
||||||
pub async fn get_profile_tree(
|
pub async fn update_field_validation(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: impl tonic::IntoRequest<super::super::common::Empty>,
|
request: impl tonic::IntoRequest<super::UpdateFieldValidationRequest>,
|
||||||
) -> std::result::Result<
|
) -> std::result::Result<
|
||||||
tonic::Response<super::ProfileTreeResponse>,
|
tonic::Response<super::UpdateFieldValidationResponse>,
|
||||||
tonic::Status,
|
tonic::Status,
|
||||||
> {
|
> {
|
||||||
self.inner
|
self.inner
|
||||||
@@ -208,43 +261,14 @@ pub mod table_definition_client {
|
|||||||
})?;
|
})?;
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
let path = http::uri::PathAndQuery::from_static(
|
||||||
"/komp_ac.table_definition.TableDefinition/GetProfileTree",
|
"/komp_ac.table_validation.TableValidationService/UpdateFieldValidation",
|
||||||
);
|
);
|
||||||
let mut req = request.into_request();
|
let mut req = request.into_request();
|
||||||
req.extensions_mut()
|
req.extensions_mut()
|
||||||
.insert(
|
.insert(
|
||||||
GrpcMethod::new(
|
GrpcMethod::new(
|
||||||
"komp_ac.table_definition.TableDefinition",
|
"komp_ac.table_validation.TableValidationService",
|
||||||
"GetProfileTree",
|
"UpdateFieldValidation",
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn delete_table(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::DeleteTableRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::DeleteTableResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.table_definition.TableDefinition/DeleteTable",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.table_definition.TableDefinition",
|
|
||||||
"DeleteTable",
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
self.inner.unary(req, path, codec).await
|
self.inner.unary(req, path, codec).await
|
||||||
@@ -252,7 +276,7 @@ pub mod table_definition_client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Generated server implementations.
|
/// Generated server implementations.
|
||||||
pub mod table_definition_server {
|
pub mod table_validation_service_server {
|
||||||
#![allow(
|
#![allow(
|
||||||
unused_variables,
|
unused_variables,
|
||||||
dead_code,
|
dead_code,
|
||||||
@@ -261,40 +285,34 @@ pub mod table_definition_server {
|
|||||||
clippy::let_unit_value,
|
clippy::let_unit_value,
|
||||||
)]
|
)]
|
||||||
use tonic::codegen::*;
|
use tonic::codegen::*;
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with TableDefinitionServer.
|
/// Generated trait containing gRPC methods that should be implemented for use with TableValidationServiceServer.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait TableDefinition: std::marker::Send + std::marker::Sync + 'static {
|
pub trait TableValidationService: std::marker::Send + std::marker::Sync + 'static {
|
||||||
async fn post_table_definition(
|
async fn get_table_validation(
|
||||||
&self,
|
&self,
|
||||||
request: tonic::Request<super::PostTableDefinitionRequest>,
|
request: tonic::Request<super::GetTableValidationRequest>,
|
||||||
) -> std::result::Result<
|
) -> std::result::Result<
|
||||||
tonic::Response<super::TableDefinitionResponse>,
|
tonic::Response<super::TableValidationResponse>,
|
||||||
tonic::Status,
|
tonic::Status,
|
||||||
>;
|
>;
|
||||||
async fn get_profile_tree(
|
async fn update_field_validation(
|
||||||
&self,
|
&self,
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
request: tonic::Request<super::UpdateFieldValidationRequest>,
|
||||||
) -> std::result::Result<
|
) -> std::result::Result<
|
||||||
tonic::Response<super::ProfileTreeResponse>,
|
tonic::Response<super::UpdateFieldValidationResponse>,
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn delete_table(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::DeleteTableRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::DeleteTableResponse>,
|
|
||||||
tonic::Status,
|
tonic::Status,
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
/// Service to fetch validations for a table
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TableDefinitionServer<T> {
|
pub struct TableValidationServiceServer<T> {
|
||||||
inner: Arc<T>,
|
inner: Arc<T>,
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
accept_compression_encodings: EnabledCompressionEncodings,
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
send_compression_encodings: EnabledCompressionEncodings,
|
||||||
max_decoding_message_size: Option<usize>,
|
max_decoding_message_size: Option<usize>,
|
||||||
max_encoding_message_size: Option<usize>,
|
max_encoding_message_size: Option<usize>,
|
||||||
}
|
}
|
||||||
impl<T> TableDefinitionServer<T> {
|
impl<T> TableValidationServiceServer<T> {
|
||||||
pub fn new(inner: T) -> Self {
|
pub fn new(inner: T) -> Self {
|
||||||
Self::from_arc(Arc::new(inner))
|
Self::from_arc(Arc::new(inner))
|
||||||
}
|
}
|
||||||
@@ -345,9 +363,10 @@ pub mod table_definition_server {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for TableDefinitionServer<T>
|
impl<T, B> tonic::codegen::Service<http::Request<B>>
|
||||||
|
for TableValidationServiceServer<T>
|
||||||
where
|
where
|
||||||
T: TableDefinition,
|
T: TableValidationService,
|
||||||
B: Body + std::marker::Send + 'static,
|
B: Body + std::marker::Send + 'static,
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||||
{
|
{
|
||||||
@@ -362,25 +381,25 @@ pub mod table_definition_server {
|
|||||||
}
|
}
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||||
match req.uri().path() {
|
match req.uri().path() {
|
||||||
"/komp_ac.table_definition.TableDefinition/PostTableDefinition" => {
|
"/komp_ac.table_validation.TableValidationService/GetTableValidation" => {
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
struct PostTableDefinitionSvc<T: TableDefinition>(pub Arc<T>);
|
struct GetTableValidationSvc<T: TableValidationService>(pub Arc<T>);
|
||||||
impl<
|
impl<
|
||||||
T: TableDefinition,
|
T: TableValidationService,
|
||||||
> tonic::server::UnaryService<super::PostTableDefinitionRequest>
|
> tonic::server::UnaryService<super::GetTableValidationRequest>
|
||||||
for PostTableDefinitionSvc<T> {
|
for GetTableValidationSvc<T> {
|
||||||
type Response = super::TableDefinitionResponse;
|
type Response = super::TableValidationResponse;
|
||||||
type Future = BoxFuture<
|
type Future = BoxFuture<
|
||||||
tonic::Response<Self::Response>,
|
tonic::Response<Self::Response>,
|
||||||
tonic::Status,
|
tonic::Status,
|
||||||
>;
|
>;
|
||||||
fn call(
|
fn call(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: tonic::Request<super::PostTableDefinitionRequest>,
|
request: tonic::Request<super::GetTableValidationRequest>,
|
||||||
) -> Self::Future {
|
) -> Self::Future {
|
||||||
let inner = Arc::clone(&self.0);
|
let inner = Arc::clone(&self.0);
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
<T as TableDefinition>::post_table_definition(
|
<T as TableValidationService>::get_table_validation(
|
||||||
&inner,
|
&inner,
|
||||||
request,
|
request,
|
||||||
)
|
)
|
||||||
@@ -395,7 +414,7 @@ pub mod table_definition_server {
|
|||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
let max_encoding_message_size = self.max_encoding_message_size;
|
||||||
let inner = self.inner.clone();
|
let inner = self.inner.clone();
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
let method = PostTableDefinitionSvc(inner);
|
let method = GetTableValidationSvc(inner);
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
let mut grpc = tonic::server::Grpc::new(codec)
|
||||||
.apply_compression_config(
|
.apply_compression_config(
|
||||||
@@ -411,25 +430,30 @@ pub mod table_definition_server {
|
|||||||
};
|
};
|
||||||
Box::pin(fut)
|
Box::pin(fut)
|
||||||
}
|
}
|
||||||
"/komp_ac.table_definition.TableDefinition/GetProfileTree" => {
|
"/komp_ac.table_validation.TableValidationService/UpdateFieldValidation" => {
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
struct GetProfileTreeSvc<T: TableDefinition>(pub Arc<T>);
|
struct UpdateFieldValidationSvc<T: TableValidationService>(
|
||||||
|
pub Arc<T>,
|
||||||
|
);
|
||||||
impl<
|
impl<
|
||||||
T: TableDefinition,
|
T: TableValidationService,
|
||||||
> tonic::server::UnaryService<super::super::common::Empty>
|
> tonic::server::UnaryService<super::UpdateFieldValidationRequest>
|
||||||
for GetProfileTreeSvc<T> {
|
for UpdateFieldValidationSvc<T> {
|
||||||
type Response = super::ProfileTreeResponse;
|
type Response = super::UpdateFieldValidationResponse;
|
||||||
type Future = BoxFuture<
|
type Future = BoxFuture<
|
||||||
tonic::Response<Self::Response>,
|
tonic::Response<Self::Response>,
|
||||||
tonic::Status,
|
tonic::Status,
|
||||||
>;
|
>;
|
||||||
fn call(
|
fn call(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
request: tonic::Request<super::UpdateFieldValidationRequest>,
|
||||||
) -> Self::Future {
|
) -> Self::Future {
|
||||||
let inner = Arc::clone(&self.0);
|
let inner = Arc::clone(&self.0);
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
<T as TableDefinition>::get_profile_tree(&inner, request)
|
<T as TableValidationService>::update_field_validation(
|
||||||
|
&inner,
|
||||||
|
request,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
};
|
};
|
||||||
Box::pin(fut)
|
Box::pin(fut)
|
||||||
@@ -441,52 +465,7 @@ pub mod table_definition_server {
|
|||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
let max_encoding_message_size = self.max_encoding_message_size;
|
||||||
let inner = self.inner.clone();
|
let inner = self.inner.clone();
|
||||||
let fut = async move {
|
let fut = async move {
|
||||||
let method = GetProfileTreeSvc(inner);
|
let method = UpdateFieldValidationSvc(inner);
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.table_definition.TableDefinition/DeleteTable" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct DeleteTableSvc<T: TableDefinition>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TableDefinition,
|
|
||||||
> tonic::server::UnaryService<super::DeleteTableRequest>
|
|
||||||
for DeleteTableSvc<T> {
|
|
||||||
type Response = super::DeleteTableResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::DeleteTableRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TableDefinition>::delete_table(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = DeleteTableSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
let codec = tonic::codec::ProstCodec::default();
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
let mut grpc = tonic::server::Grpc::new(codec)
|
||||||
.apply_compression_config(
|
.apply_compression_config(
|
||||||
@@ -524,7 +503,7 @@ pub mod table_definition_server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<T> Clone for TableDefinitionServer<T> {
|
impl<T> Clone for TableValidationServiceServer<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
let inner = self.inner.clone();
|
let inner = self.inner.clone();
|
||||||
Self {
|
Self {
|
||||||
@@ -537,8 +516,8 @@ pub mod table_definition_server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Generated gRPC service name
|
/// Generated gRPC service name
|
||||||
pub const SERVICE_NAME: &str = "komp_ac.table_definition.TableDefinition";
|
pub const SERVICE_NAME: &str = "komp_ac.table_validation.TableValidationService";
|
||||||
impl<T> tonic::server::NamedService for TableDefinitionServer<T> {
|
impl<T> tonic::server::NamedService for TableValidationServiceServer<T> {
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
const NAME: &'static str = SERVICE_NAME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,791 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetAdresarRequest {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct DeleteAdresarRequest {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostAdresarRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub firma: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub kz: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub drc: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub ulica: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub psc: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "6")]
|
|
||||||
pub mesto: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "7")]
|
|
||||||
pub stat: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "8")]
|
|
||||||
pub banka: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "9")]
|
|
||||||
pub ucet: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "10")]
|
|
||||||
pub skladm: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "11")]
|
|
||||||
pub ico: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "12")]
|
|
||||||
pub kontakt: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "13")]
|
|
||||||
pub telefon: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "14")]
|
|
||||||
pub skladu: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "15")]
|
|
||||||
pub fax: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct AdresarResponse {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub firma: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub kz: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub drc: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub ulica: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "6")]
|
|
||||||
pub psc: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "7")]
|
|
||||||
pub mesto: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "8")]
|
|
||||||
pub stat: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "9")]
|
|
||||||
pub banka: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "10")]
|
|
||||||
pub ucet: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "11")]
|
|
||||||
pub skladm: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "12")]
|
|
||||||
pub ico: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "13")]
|
|
||||||
pub kontakt: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "14")]
|
|
||||||
pub telefon: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "15")]
|
|
||||||
pub skladu: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "16")]
|
|
||||||
pub fax: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PutAdresarRequest {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub firma: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub kz: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub drc: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub ulica: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "6")]
|
|
||||||
pub psc: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "7")]
|
|
||||||
pub mesto: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "8")]
|
|
||||||
pub stat: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "9")]
|
|
||||||
pub banka: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "10")]
|
|
||||||
pub ucet: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "11")]
|
|
||||||
pub skladm: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "12")]
|
|
||||||
pub ico: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "13")]
|
|
||||||
pub kontakt: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "14")]
|
|
||||||
pub telefon: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "15")]
|
|
||||||
pub skladu: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "16")]
|
|
||||||
pub fax: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct DeleteAdresarResponse {
|
|
||||||
#[prost(bool, tag = "1")]
|
|
||||||
pub success: bool,
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod adresar_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AdresarClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl AdresarClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> AdresarClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> AdresarClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
AdresarClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub async fn post_adresar(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PostAdresarRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::AdresarResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.adresar.Adresar/PostAdresar",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PostAdresar"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_adresar(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::GetAdresarRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::AdresarResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.adresar.Adresar/GetAdresar",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresar"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn put_adresar(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PutAdresarRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::AdresarResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.adresar.Adresar/PutAdresar",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PutAdresar"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn delete_adresar(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::DeleteAdresarRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::DeleteAdresarResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.adresar.Adresar/DeleteAdresar",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "DeleteAdresar"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_adresar_count(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::super::common::Empty>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::super::common::CountResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.adresar.Adresar/GetAdresarCount",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarCount"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_adresar_by_position(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::super::common::PositionRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::AdresarResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.adresar.Adresar/GetAdresarByPosition",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarByPosition"),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated server implementations.
|
|
||||||
pub mod adresar_server {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with AdresarServer.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Adresar: std::marker::Send + std::marker::Sync + 'static {
|
|
||||||
async fn post_adresar(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PostAdresarRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::AdresarResponse>, tonic::Status>;
|
|
||||||
async fn get_adresar(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::GetAdresarRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::AdresarResponse>, tonic::Status>;
|
|
||||||
async fn put_adresar(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PutAdresarRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::AdresarResponse>, tonic::Status>;
|
|
||||||
async fn delete_adresar(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::DeleteAdresarRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::DeleteAdresarResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_adresar_count(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::super::common::CountResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_adresar_by_position(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::super::common::PositionRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::AdresarResponse>, tonic::Status>;
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AdresarServer<T> {
|
|
||||||
inner: Arc<T>,
|
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
max_decoding_message_size: Option<usize>,
|
|
||||||
max_encoding_message_size: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<T> AdresarServer<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self::from_arc(Arc::new(inner))
|
|
||||||
}
|
|
||||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: Default::default(),
|
|
||||||
send_compression_encodings: Default::default(),
|
|
||||||
max_decoding_message_size: None,
|
|
||||||
max_encoding_message_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> InterceptedService<Self, F>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
{
|
|
||||||
InterceptedService::new(Self::new(inner), interceptor)
|
|
||||||
}
|
|
||||||
/// Enable decompressing requests with the given encoding.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.accept_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Compress responses with the given encoding, if the client supports it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.send_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_decoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_encoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for AdresarServer<T>
|
|
||||||
where
|
|
||||||
T: Adresar,
|
|
||||||
B: Body + std::marker::Send + 'static,
|
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = http::Response<tonic::body::Body>;
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
|
||||||
fn poll_ready(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
|
||||||
match req.uri().path() {
|
|
||||||
"/komp_ac.adresar.Adresar/PostAdresar" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PostAdresarSvc<T: Adresar>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Adresar,
|
|
||||||
> tonic::server::UnaryService<super::PostAdresarRequest>
|
|
||||||
for PostAdresarSvc<T> {
|
|
||||||
type Response = super::AdresarResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PostAdresarRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Adresar>::post_adresar(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PostAdresarSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.adresar.Adresar/GetAdresar" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetAdresarSvc<T: Adresar>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Adresar,
|
|
||||||
> tonic::server::UnaryService<super::GetAdresarRequest>
|
|
||||||
for GetAdresarSvc<T> {
|
|
||||||
type Response = super::AdresarResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::GetAdresarRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Adresar>::get_adresar(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetAdresarSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.adresar.Adresar/PutAdresar" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PutAdresarSvc<T: Adresar>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Adresar,
|
|
||||||
> tonic::server::UnaryService<super::PutAdresarRequest>
|
|
||||||
for PutAdresarSvc<T> {
|
|
||||||
type Response = super::AdresarResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PutAdresarRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Adresar>::put_adresar(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PutAdresarSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.adresar.Adresar/DeleteAdresar" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct DeleteAdresarSvc<T: Adresar>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Adresar,
|
|
||||||
> tonic::server::UnaryService<super::DeleteAdresarRequest>
|
|
||||||
for DeleteAdresarSvc<T> {
|
|
||||||
type Response = super::DeleteAdresarResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::DeleteAdresarRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Adresar>::delete_adresar(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = DeleteAdresarSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.adresar.Adresar/GetAdresarCount" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetAdresarCountSvc<T: Adresar>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Adresar,
|
|
||||||
> tonic::server::UnaryService<super::super::common::Empty>
|
|
||||||
for GetAdresarCountSvc<T> {
|
|
||||||
type Response = super::super::common::CountResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Adresar>::get_adresar_count(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetAdresarCountSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.adresar.Adresar/GetAdresarByPosition" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetAdresarByPositionSvc<T: Adresar>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Adresar,
|
|
||||||
> tonic::server::UnaryService<super::super::common::PositionRequest>
|
|
||||||
for GetAdresarByPositionSvc<T> {
|
|
||||||
type Response = super::AdresarResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<
|
|
||||||
super::super::common::PositionRequest,
|
|
||||||
>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Adresar>::get_adresar_by_position(&inner, request)
|
|
||||||
.await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetAdresarByPositionSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut response = http::Response::new(
|
|
||||||
tonic::body::Body::default(),
|
|
||||||
);
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
tonic::Status::GRPC_STATUS,
|
|
||||||
(tonic::Code::Unimplemented as i32).into(),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Clone for AdresarServer<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: self.accept_compression_encodings,
|
|
||||||
send_compression_encodings: self.send_compression_encodings,
|
|
||||||
max_decoding_message_size: self.max_decoding_message_size,
|
|
||||||
max_encoding_message_size: self.max_encoding_message_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated gRPC service name
|
|
||||||
pub const SERVICE_NAME: &str = "komp_ac.adresar.Adresar";
|
|
||||||
impl<T> tonic::server::NamedService for AdresarServer<T> {
|
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,418 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct RegisterRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub username: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub email: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub password: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub password_confirmation: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub role: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct AuthResponse {
|
|
||||||
/// UUID in string format
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub id: ::prost::alloc::string::String,
|
|
||||||
/// Registered username
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub username: ::prost::alloc::string::String,
|
|
||||||
/// Registered email (if provided)
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub email: ::prost::alloc::string::String,
|
|
||||||
/// Default role: 'accountant'
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub role: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct LoginRequest {
|
|
||||||
/// Can be username or email
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub identifier: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub password: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct LoginResponse {
|
|
||||||
/// JWT token
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub access_token: ::prost::alloc::string::String,
|
|
||||||
/// Usually "Bearer"
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub token_type: ::prost::alloc::string::String,
|
|
||||||
/// Expiration in seconds (86400 for 24 hours)
|
|
||||||
#[prost(int32, tag = "3")]
|
|
||||||
pub expires_in: i32,
|
|
||||||
/// User's UUID in string format
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub user_id: ::prost::alloc::string::String,
|
|
||||||
/// User's role
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub role: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "6")]
|
|
||||||
pub username: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod auth_service_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AuthServiceClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl AuthServiceClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> AuthServiceClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> AuthServiceClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
AuthServiceClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub async fn register(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::RegisterRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.auth.AuthService/Register",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Register"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn login(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::LoginRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::LoginResponse>, tonic::Status> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.auth.AuthService/Login",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Login"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated server implementations.
|
|
||||||
pub mod auth_service_server {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with AuthServiceServer.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AuthService: std::marker::Send + std::marker::Sync + 'static {
|
|
||||||
async fn register(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::RegisterRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::AuthResponse>, tonic::Status>;
|
|
||||||
async fn login(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::LoginRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::LoginResponse>, tonic::Status>;
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AuthServiceServer<T> {
|
|
||||||
inner: Arc<T>,
|
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
max_decoding_message_size: Option<usize>,
|
|
||||||
max_encoding_message_size: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<T> AuthServiceServer<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self::from_arc(Arc::new(inner))
|
|
||||||
}
|
|
||||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: Default::default(),
|
|
||||||
send_compression_encodings: Default::default(),
|
|
||||||
max_decoding_message_size: None,
|
|
||||||
max_encoding_message_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> InterceptedService<Self, F>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
{
|
|
||||||
InterceptedService::new(Self::new(inner), interceptor)
|
|
||||||
}
|
|
||||||
/// Enable decompressing requests with the given encoding.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.accept_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Compress responses with the given encoding, if the client supports it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.send_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_decoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_encoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for AuthServiceServer<T>
|
|
||||||
where
|
|
||||||
T: AuthService,
|
|
||||||
B: Body + std::marker::Send + 'static,
|
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = http::Response<tonic::body::Body>;
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
|
||||||
fn poll_ready(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
|
||||||
match req.uri().path() {
|
|
||||||
"/komp_ac.auth.AuthService/Register" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct RegisterSvc<T: AuthService>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: AuthService,
|
|
||||||
> tonic::server::UnaryService<super::RegisterRequest>
|
|
||||||
for RegisterSvc<T> {
|
|
||||||
type Response = super::AuthResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::RegisterRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as AuthService>::register(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = RegisterSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.auth.AuthService/Login" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct LoginSvc<T: AuthService>(pub Arc<T>);
|
|
||||||
impl<T: AuthService> tonic::server::UnaryService<super::LoginRequest>
|
|
||||||
for LoginSvc<T> {
|
|
||||||
type Response = super::LoginResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::LoginRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as AuthService>::login(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = LoginSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut response = http::Response::new(
|
|
||||||
tonic::body::Body::default(),
|
|
||||||
);
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
tonic::Status::GRPC_STATUS,
|
|
||||||
(tonic::Code::Unimplemented as i32).into(),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Clone for AuthServiceServer<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: self.accept_compression_encodings,
|
|
||||||
send_compression_encodings: self.send_compression_encodings,
|
|
||||||
max_decoding_message_size: self.max_decoding_message_size,
|
|
||||||
max_encoding_message_size: self.max_encoding_message_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated gRPC service name
|
|
||||||
pub const SERVICE_NAME: &str = "komp_ac.auth.AuthService";
|
|
||||||
impl<T> tonic::server::NamedService for AuthServiceServer<T> {
|
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Empty {}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct CountResponse {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub count: i64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PositionRequest {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub position: i64,
|
|
||||||
}
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct SearchRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub table_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub query: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct SearchResponse {
|
|
||||||
#[prost(message, repeated, tag = "1")]
|
|
||||||
pub hits: ::prost::alloc::vec::Vec<search_response::Hit>,
|
|
||||||
}
|
|
||||||
/// Nested message and enum types in `SearchResponse`.
|
|
||||||
pub mod search_response {
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct Hit {
|
|
||||||
/// PostgreSQL row ID
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
#[prost(float, tag = "2")]
|
|
||||||
pub score: f32,
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub content_json: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod searcher_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SearcherClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl SearcherClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> SearcherClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> SearcherClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
SearcherClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub async fn search_table(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::SearchRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.search.Searcher/SearchTable",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(GrpcMethod::new("komp_ac.search.Searcher", "SearchTable"));
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated server implementations.
|
|
||||||
pub mod searcher_server {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with SearcherServer.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Searcher: std::marker::Send + std::marker::Sync + 'static {
|
|
||||||
async fn search_table(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::SearchRequest>,
|
|
||||||
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status>;
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SearcherServer<T> {
|
|
||||||
inner: Arc<T>,
|
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
max_decoding_message_size: Option<usize>,
|
|
||||||
max_encoding_message_size: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<T> SearcherServer<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self::from_arc(Arc::new(inner))
|
|
||||||
}
|
|
||||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: Default::default(),
|
|
||||||
send_compression_encodings: Default::default(),
|
|
||||||
max_decoding_message_size: None,
|
|
||||||
max_encoding_message_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> InterceptedService<Self, F>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
{
|
|
||||||
InterceptedService::new(Self::new(inner), interceptor)
|
|
||||||
}
|
|
||||||
/// Enable decompressing requests with the given encoding.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.accept_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Compress responses with the given encoding, if the client supports it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.send_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_decoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_encoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for SearcherServer<T>
|
|
||||||
where
|
|
||||||
T: Searcher,
|
|
||||||
B: Body + std::marker::Send + 'static,
|
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = http::Response<tonic::body::Body>;
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
|
||||||
fn poll_ready(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
|
||||||
match req.uri().path() {
|
|
||||||
"/komp_ac.search.Searcher/SearchTable" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct SearchTableSvc<T: Searcher>(pub Arc<T>);
|
|
||||||
impl<T: Searcher> tonic::server::UnaryService<super::SearchRequest>
|
|
||||||
for SearchTableSvc<T> {
|
|
||||||
type Response = super::SearchResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::SearchRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Searcher>::search_table(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = SearchTableSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut response = http::Response::new(
|
|
||||||
tonic::body::Body::default(),
|
|
||||||
);
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
tonic::Status::GRPC_STATUS,
|
|
||||||
(tonic::Code::Unimplemented as i32).into(),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Clone for SearcherServer<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: self.accept_compression_encodings,
|
|
||||||
send_compression_encodings: self.send_compression_encodings,
|
|
||||||
max_decoding_message_size: self.max_decoding_message_size,
|
|
||||||
max_encoding_message_size: self.max_encoding_message_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated gRPC service name
|
|
||||||
pub const SERVICE_NAME: &str = "komp_ac.search.Searcher";
|
|
||||||
impl<T> tonic::server::NamedService for SearcherServer<T> {
|
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostTableScriptRequest {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub table_definition_id: i64,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub target_column: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub script: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub description: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct TableScriptResponse {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub warnings: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod table_script_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TableScriptClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl TableScriptClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> TableScriptClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> TableScriptClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
TableScriptClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub async fn post_table_script(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PostTableScriptRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::TableScriptResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.table_script.TableScript/PostTableScript",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.table_script.TableScript",
|
|
||||||
"PostTableScript",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated server implementations.
|
|
||||||
pub mod table_script_server {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with TableScriptServer.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait TableScript: std::marker::Send + std::marker::Sync + 'static {
|
|
||||||
async fn post_table_script(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PostTableScriptRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::TableScriptResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TableScriptServer<T> {
|
|
||||||
inner: Arc<T>,
|
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
max_decoding_message_size: Option<usize>,
|
|
||||||
max_encoding_message_size: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<T> TableScriptServer<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self::from_arc(Arc::new(inner))
|
|
||||||
}
|
|
||||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: Default::default(),
|
|
||||||
send_compression_encodings: Default::default(),
|
|
||||||
max_decoding_message_size: None,
|
|
||||||
max_encoding_message_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> InterceptedService<Self, F>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
{
|
|
||||||
InterceptedService::new(Self::new(inner), interceptor)
|
|
||||||
}
|
|
||||||
/// Enable decompressing requests with the given encoding.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.accept_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Compress responses with the given encoding, if the client supports it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.send_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_decoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_encoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for TableScriptServer<T>
|
|
||||||
where
|
|
||||||
T: TableScript,
|
|
||||||
B: Body + std::marker::Send + 'static,
|
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = http::Response<tonic::body::Body>;
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
|
||||||
fn poll_ready(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
|
||||||
match req.uri().path() {
|
|
||||||
"/komp_ac.table_script.TableScript/PostTableScript" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PostTableScriptSvc<T: TableScript>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TableScript,
|
|
||||||
> tonic::server::UnaryService<super::PostTableScriptRequest>
|
|
||||||
for PostTableScriptSvc<T> {
|
|
||||||
type Response = super::TableScriptResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PostTableScriptRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TableScript>::post_table_script(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PostTableScriptSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut response = http::Response::new(
|
|
||||||
tonic::body::Body::default(),
|
|
||||||
);
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
tonic::Status::GRPC_STATUS,
|
|
||||||
(tonic::Code::Unimplemented as i32).into(),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Clone for TableScriptServer<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: self.accept_compression_encodings,
|
|
||||||
send_compression_encodings: self.send_compression_encodings,
|
|
||||||
max_decoding_message_size: self.max_decoding_message_size,
|
|
||||||
max_encoding_message_size: self.max_encoding_message_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated gRPC service name
|
|
||||||
pub const SERVICE_NAME: &str = "komp_ac.table_script.TableScript";
|
|
||||||
impl<T> tonic::server::NamedService for TableScriptServer<T> {
|
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetTableStructureRequest {
|
|
||||||
/// e.g., "default"
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
|
||||||
/// e.g., "2025_adresar6"
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub table_name: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct TableStructureResponse {
|
|
||||||
#[prost(message, repeated, tag = "1")]
|
|
||||||
pub columns: ::prost::alloc::vec::Vec<TableColumn>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct TableColumn {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub name: ::prost::alloc::string::String,
|
|
||||||
/// e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ"
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub data_type: ::prost::alloc::string::String,
|
|
||||||
#[prost(bool, tag = "3")]
|
|
||||||
pub is_nullable: bool,
|
|
||||||
#[prost(bool, tag = "4")]
|
|
||||||
pub is_primary_key: bool,
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod table_structure_service_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TableStructureServiceClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl TableStructureServiceClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> TableStructureServiceClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> TableStructureServiceClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
TableStructureServiceClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub async fn get_table_structure(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::GetTableStructureRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::TableStructureResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.table_structure.TableStructureService/GetTableStructure",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.table_structure.TableStructureService",
|
|
||||||
"GetTableStructure",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated server implementations.
|
|
||||||
pub mod table_structure_service_server {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with TableStructureServiceServer.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static {
|
|
||||||
async fn get_table_structure(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::GetTableStructureRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::TableStructureResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TableStructureServiceServer<T> {
|
|
||||||
inner: Arc<T>,
|
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
max_decoding_message_size: Option<usize>,
|
|
||||||
max_encoding_message_size: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<T> TableStructureServiceServer<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self::from_arc(Arc::new(inner))
|
|
||||||
}
|
|
||||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: Default::default(),
|
|
||||||
send_compression_encodings: Default::default(),
|
|
||||||
max_decoding_message_size: None,
|
|
||||||
max_encoding_message_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> InterceptedService<Self, F>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
{
|
|
||||||
InterceptedService::new(Self::new(inner), interceptor)
|
|
||||||
}
|
|
||||||
/// Enable decompressing requests with the given encoding.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.accept_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Compress responses with the given encoding, if the client supports it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.send_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_decoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_encoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>>
|
|
||||||
for TableStructureServiceServer<T>
|
|
||||||
where
|
|
||||||
T: TableStructureService,
|
|
||||||
B: Body + std::marker::Send + 'static,
|
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = http::Response<tonic::body::Body>;
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
|
||||||
fn poll_ready(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
|
||||||
match req.uri().path() {
|
|
||||||
"/komp_ac.table_structure.TableStructureService/GetTableStructure" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetTableStructureSvc<T: TableStructureService>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TableStructureService,
|
|
||||||
> tonic::server::UnaryService<super::GetTableStructureRequest>
|
|
||||||
for GetTableStructureSvc<T> {
|
|
||||||
type Response = super::TableStructureResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::GetTableStructureRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TableStructureService>::get_table_structure(
|
|
||||||
&inner,
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetTableStructureSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut response = http::Response::new(
|
|
||||||
tonic::body::Body::default(),
|
|
||||||
);
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
tonic::Status::GRPC_STATUS,
|
|
||||||
(tonic::Code::Unimplemented as i32).into(),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Clone for TableStructureServiceServer<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: self.accept_compression_encodings,
|
|
||||||
send_compression_encodings: self.send_compression_encodings,
|
|
||||||
max_decoding_message_size: self.max_decoding_message_size,
|
|
||||||
max_encoding_message_size: self.max_encoding_message_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated gRPC service name
|
|
||||||
pub const SERVICE_NAME: &str = "komp_ac.table_structure.TableStructureService";
|
|
||||||
impl<T> tonic::server::NamedService for TableStructureServiceServer<T> {
|
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,797 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostTableDataRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub table_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(map = "string, message", tag = "3")]
|
|
||||||
pub data: ::std::collections::HashMap<
|
|
||||||
::prost::alloc::string::String,
|
|
||||||
::prost_types::Value,
|
|
||||||
>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostTableDataResponse {
|
|
||||||
#[prost(bool, tag = "1")]
|
|
||||||
pub success: bool,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub message: ::prost::alloc::string::String,
|
|
||||||
#[prost(int64, tag = "3")]
|
|
||||||
pub inserted_id: i64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PutTableDataRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub table_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(int64, tag = "3")]
|
|
||||||
pub id: i64,
|
|
||||||
#[prost(map = "string, message", tag = "4")]
|
|
||||||
pub data: ::std::collections::HashMap<
|
|
||||||
::prost::alloc::string::String,
|
|
||||||
::prost_types::Value,
|
|
||||||
>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PutTableDataResponse {
|
|
||||||
#[prost(bool, tag = "1")]
|
|
||||||
pub success: bool,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub message: ::prost::alloc::string::String,
|
|
||||||
#[prost(int64, tag = "3")]
|
|
||||||
pub updated_id: i64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct DeleteTableDataRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub table_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(int64, tag = "3")]
|
|
||||||
pub record_id: i64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct DeleteTableDataResponse {
|
|
||||||
#[prost(bool, tag = "1")]
|
|
||||||
pub success: bool,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetTableDataRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub table_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(int64, tag = "3")]
|
|
||||||
pub id: i64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetTableDataResponse {
|
|
||||||
#[prost(map = "string, string", tag = "1")]
|
|
||||||
pub data: ::std::collections::HashMap<
|
|
||||||
::prost::alloc::string::String,
|
|
||||||
::prost::alloc::string::String,
|
|
||||||
>,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetTableDataCountRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub table_name: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetTableDataByPositionRequest {
|
|
||||||
#[prost(string, tag = "1")]
|
|
||||||
pub profile_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub table_name: ::prost::alloc::string::String,
|
|
||||||
#[prost(int32, tag = "3")]
|
|
||||||
pub position: i32,
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod tables_data_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct TablesDataClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl TablesDataClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> TablesDataClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> TablesDataClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
TablesDataClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub async fn post_table_data(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PostTableDataRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::PostTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.tables_data.TablesData/PostTableData",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new("komp_ac.tables_data.TablesData", "PostTableData"),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn put_table_data(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PutTableDataRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::PutTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.tables_data.TablesData/PutTableData",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new("komp_ac.tables_data.TablesData", "PutTableData"),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn delete_table_data(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::DeleteTableDataRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::DeleteTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.tables_data.TablesData/DeleteTableData",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.tables_data.TablesData",
|
|
||||||
"DeleteTableData",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_table_data(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::GetTableDataRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::GetTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.tables_data.TablesData/GetTableData",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new("komp_ac.tables_data.TablesData", "GetTableData"),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_table_data_count(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::GetTableDataCountRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::super::common::CountResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.tables_data.TablesData/GetTableDataCount",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.tables_data.TablesData",
|
|
||||||
"GetTableDataCount",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_table_data_by_position(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::GetTableDataByPositionRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::GetTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.tables_data.TablesData/GetTableDataByPosition",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.tables_data.TablesData",
|
|
||||||
"GetTableDataByPosition",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated server implementations.
|
|
||||||
pub mod tables_data_server {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with TablesDataServer.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait TablesData: std::marker::Send + std::marker::Sync + 'static {
|
|
||||||
async fn post_table_data(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PostTableDataRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::PostTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn put_table_data(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PutTableDataRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::PutTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn delete_table_data(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::DeleteTableDataRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::DeleteTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_table_data(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::GetTableDataRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::GetTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_table_data_count(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::GetTableDataCountRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::super::common::CountResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_table_data_by_position(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::GetTableDataByPositionRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::GetTableDataResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TablesDataServer<T> {
|
|
||||||
inner: Arc<T>,
|
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
max_decoding_message_size: Option<usize>,
|
|
||||||
max_encoding_message_size: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<T> TablesDataServer<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self::from_arc(Arc::new(inner))
|
|
||||||
}
|
|
||||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: Default::default(),
|
|
||||||
send_compression_encodings: Default::default(),
|
|
||||||
max_decoding_message_size: None,
|
|
||||||
max_encoding_message_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> InterceptedService<Self, F>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
{
|
|
||||||
InterceptedService::new(Self::new(inner), interceptor)
|
|
||||||
}
|
|
||||||
/// Enable decompressing requests with the given encoding.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.accept_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Compress responses with the given encoding, if the client supports it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.send_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_decoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_encoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for TablesDataServer<T>
|
|
||||||
where
|
|
||||||
T: TablesData,
|
|
||||||
B: Body + std::marker::Send + 'static,
|
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = http::Response<tonic::body::Body>;
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
|
||||||
fn poll_ready(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
|
||||||
match req.uri().path() {
|
|
||||||
"/komp_ac.tables_data.TablesData/PostTableData" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PostTableDataSvc<T: TablesData>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TablesData,
|
|
||||||
> tonic::server::UnaryService<super::PostTableDataRequest>
|
|
||||||
for PostTableDataSvc<T> {
|
|
||||||
type Response = super::PostTableDataResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PostTableDataRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TablesData>::post_table_data(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PostTableDataSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.tables_data.TablesData/PutTableData" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PutTableDataSvc<T: TablesData>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TablesData,
|
|
||||||
> tonic::server::UnaryService<super::PutTableDataRequest>
|
|
||||||
for PutTableDataSvc<T> {
|
|
||||||
type Response = super::PutTableDataResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PutTableDataRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TablesData>::put_table_data(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PutTableDataSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.tables_data.TablesData/DeleteTableData" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct DeleteTableDataSvc<T: TablesData>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TablesData,
|
|
||||||
> tonic::server::UnaryService<super::DeleteTableDataRequest>
|
|
||||||
for DeleteTableDataSvc<T> {
|
|
||||||
type Response = super::DeleteTableDataResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::DeleteTableDataRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TablesData>::delete_table_data(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = DeleteTableDataSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.tables_data.TablesData/GetTableData" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetTableDataSvc<T: TablesData>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TablesData,
|
|
||||||
> tonic::server::UnaryService<super::GetTableDataRequest>
|
|
||||||
for GetTableDataSvc<T> {
|
|
||||||
type Response = super::GetTableDataResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::GetTableDataRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TablesData>::get_table_data(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetTableDataSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.tables_data.TablesData/GetTableDataCount" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetTableDataCountSvc<T: TablesData>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TablesData,
|
|
||||||
> tonic::server::UnaryService<super::GetTableDataCountRequest>
|
|
||||||
for GetTableDataCountSvc<T> {
|
|
||||||
type Response = super::super::common::CountResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::GetTableDataCountRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TablesData>::get_table_data_count(&inner, request)
|
|
||||||
.await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetTableDataCountSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.tables_data.TablesData/GetTableDataByPosition" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetTableDataByPositionSvc<T: TablesData>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: TablesData,
|
|
||||||
> tonic::server::UnaryService<super::GetTableDataByPositionRequest>
|
|
||||||
for GetTableDataByPositionSvc<T> {
|
|
||||||
type Response = super::GetTableDataResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::GetTableDataByPositionRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as TablesData>::get_table_data_by_position(
|
|
||||||
&inner,
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetTableDataByPositionSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut response = http::Response::new(
|
|
||||||
tonic::body::Body::default(),
|
|
||||||
);
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
tonic::Status::GRPC_STATUS,
|
|
||||||
(tonic::Code::Unimplemented as i32).into(),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Clone for TablesDataServer<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: self.accept_compression_encodings,
|
|
||||||
send_compression_encodings: self.send_compression_encodings,
|
|
||||||
max_decoding_message_size: self.max_decoding_message_size,
|
|
||||||
max_encoding_message_size: self.max_encoding_message_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated gRPC service name
|
|
||||||
pub const SERVICE_NAME: &str = "komp_ac.tables_data.TablesData";
|
|
||||||
impl<T> tonic::server::NamedService for TablesDataServer<T> {
|
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,721 +0,0 @@
|
|||||||
// This file is @generated by prost-build.
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PostUctovnictvoRequest {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub adresar_id: i64,
|
|
||||||
#[prost(string, tag = "2")]
|
|
||||||
pub c_dokladu: ::prost::alloc::string::String,
|
|
||||||
/// Use string for simplicity, or use google.protobuf.Timestamp for better date handling
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub datum: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub c_faktury: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub obsah: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "6")]
|
|
||||||
pub stredisko: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "7")]
|
|
||||||
pub c_uctu: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "8")]
|
|
||||||
pub md: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "9")]
|
|
||||||
pub identif: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "10")]
|
|
||||||
pub poznanka: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "11")]
|
|
||||||
pub firma: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct UctovnictvoResponse {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
#[prost(int64, tag = "2")]
|
|
||||||
pub adresar_id: i64,
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub c_dokladu: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub datum: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub c_faktury: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "6")]
|
|
||||||
pub obsah: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "7")]
|
|
||||||
pub stredisko: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "8")]
|
|
||||||
pub c_uctu: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "9")]
|
|
||||||
pub md: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "10")]
|
|
||||||
pub identif: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "11")]
|
|
||||||
pub poznanka: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "12")]
|
|
||||||
pub firma: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
|
||||||
pub struct PutUctovnictvoRequest {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
#[prost(int64, tag = "2")]
|
|
||||||
pub adresar_id: i64,
|
|
||||||
#[prost(string, tag = "3")]
|
|
||||||
pub c_dokladu: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "4")]
|
|
||||||
pub datum: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "5")]
|
|
||||||
pub c_faktury: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "6")]
|
|
||||||
pub obsah: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "7")]
|
|
||||||
pub stredisko: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "8")]
|
|
||||||
pub c_uctu: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "9")]
|
|
||||||
pub md: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "10")]
|
|
||||||
pub identif: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "11")]
|
|
||||||
pub poznanka: ::prost::alloc::string::String,
|
|
||||||
#[prost(string, tag = "12")]
|
|
||||||
pub firma: ::prost::alloc::string::String,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
|
|
||||||
pub struct GetUctovnictvoRequest {
|
|
||||||
#[prost(int64, tag = "1")]
|
|
||||||
pub id: i64,
|
|
||||||
}
|
|
||||||
/// Generated client implementations.
|
|
||||||
pub mod uctovnictvo_client {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
use tonic::codegen::http::Uri;
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UctovnictvoClient<T> {
|
|
||||||
inner: tonic::client::Grpc<T>,
|
|
||||||
}
|
|
||||||
impl UctovnictvoClient<tonic::transport::Channel> {
|
|
||||||
/// Attempt to create a new client by connecting to a given endpoint.
|
|
||||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
|
||||||
where
|
|
||||||
D: TryInto<tonic::transport::Endpoint>,
|
|
||||||
D::Error: Into<StdError>,
|
|
||||||
{
|
|
||||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
|
||||||
Ok(Self::new(conn))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> UctovnictvoClient<T>
|
|
||||||
where
|
|
||||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
|
||||||
T::Error: Into<StdError>,
|
|
||||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
|
||||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
|
||||||
{
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::new(inner);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
|
||||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
|
||||||
Self { inner }
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> UctovnictvoClient<InterceptedService<T, F>>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
T::ResponseBody: Default,
|
|
||||||
T: tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
Response = http::Response<
|
|
||||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
|
||||||
>,
|
|
||||||
>,
|
|
||||||
<T as tonic::codegen::Service<
|
|
||||||
http::Request<tonic::body::Body>,
|
|
||||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
|
||||||
{
|
|
||||||
UctovnictvoClient::new(InterceptedService::new(inner, interceptor))
|
|
||||||
}
|
|
||||||
/// Compress requests with the given encoding.
|
|
||||||
///
|
|
||||||
/// This requires the server to support it otherwise it might respond with an
|
|
||||||
/// error.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.send_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Enable decompressing responses.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.inner = self.inner.accept_compressed(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_decoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.inner = self.inner.max_encoding_message_size(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
pub async fn post_uctovnictvo(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PostUctovnictvoRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::UctovnictvoResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
|
||||||
"PostUctovnictvo",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_uctovnictvo(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::GetUctovnictvoRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::UctovnictvoResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
|
||||||
"GetUctovnictvo",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_uctovnictvo_count(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::super::common::Empty>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::super::common::CountResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
|
||||||
"GetUctovnictvoCount",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn get_uctovnictvo_by_position(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::super::common::PositionRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::UctovnictvoResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
|
||||||
"GetUctovnictvoByPosition",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
pub async fn put_uctovnictvo(
|
|
||||||
&mut self,
|
|
||||||
request: impl tonic::IntoRequest<super::PutUctovnictvoRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::UctovnictvoResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
> {
|
|
||||||
self.inner
|
|
||||||
.ready()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
tonic::Status::unknown(
|
|
||||||
format!("Service was not ready: {}", e.into()),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let path = http::uri::PathAndQuery::from_static(
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo",
|
|
||||||
);
|
|
||||||
let mut req = request.into_request();
|
|
||||||
req.extensions_mut()
|
|
||||||
.insert(
|
|
||||||
GrpcMethod::new(
|
|
||||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
|
||||||
"PutUctovnictvo",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.inner.unary(req, path, codec).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated server implementations.
|
|
||||||
pub mod uctovnictvo_server {
|
|
||||||
#![allow(
|
|
||||||
unused_variables,
|
|
||||||
dead_code,
|
|
||||||
missing_docs,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::let_unit_value,
|
|
||||||
)]
|
|
||||||
use tonic::codegen::*;
|
|
||||||
/// Generated trait containing gRPC methods that should be implemented for use with UctovnictvoServer.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Uctovnictvo: std::marker::Send + std::marker::Sync + 'static {
|
|
||||||
async fn post_uctovnictvo(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PostUctovnictvoRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::UctovnictvoResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_uctovnictvo(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::GetUctovnictvoRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::UctovnictvoResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_uctovnictvo_count(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::super::common::CountResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn get_uctovnictvo_by_position(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::super::common::PositionRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::UctovnictvoResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
async fn put_uctovnictvo(
|
|
||||||
&self,
|
|
||||||
request: tonic::Request<super::PutUctovnictvoRequest>,
|
|
||||||
) -> std::result::Result<
|
|
||||||
tonic::Response<super::UctovnictvoResponse>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct UctovnictvoServer<T> {
|
|
||||||
inner: Arc<T>,
|
|
||||||
accept_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
send_compression_encodings: EnabledCompressionEncodings,
|
|
||||||
max_decoding_message_size: Option<usize>,
|
|
||||||
max_encoding_message_size: Option<usize>,
|
|
||||||
}
|
|
||||||
impl<T> UctovnictvoServer<T> {
|
|
||||||
pub fn new(inner: T) -> Self {
|
|
||||||
Self::from_arc(Arc::new(inner))
|
|
||||||
}
|
|
||||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: Default::default(),
|
|
||||||
send_compression_encodings: Default::default(),
|
|
||||||
max_decoding_message_size: None,
|
|
||||||
max_encoding_message_size: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn with_interceptor<F>(
|
|
||||||
inner: T,
|
|
||||||
interceptor: F,
|
|
||||||
) -> InterceptedService<Self, F>
|
|
||||||
where
|
|
||||||
F: tonic::service::Interceptor,
|
|
||||||
{
|
|
||||||
InterceptedService::new(Self::new(inner), interceptor)
|
|
||||||
}
|
|
||||||
/// Enable decompressing requests with the given encoding.
|
|
||||||
#[must_use]
|
|
||||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.accept_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Compress responses with the given encoding, if the client supports it.
|
|
||||||
#[must_use]
|
|
||||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
|
||||||
self.send_compression_encodings.enable(encoding);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of a decoded message.
|
|
||||||
///
|
|
||||||
/// Default: `4MB`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_decoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Limits the maximum size of an encoded message.
|
|
||||||
///
|
|
||||||
/// Default: `usize::MAX`
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
|
||||||
self.max_encoding_message_size = Some(limit);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for UctovnictvoServer<T>
|
|
||||||
where
|
|
||||||
T: Uctovnictvo,
|
|
||||||
B: Body + std::marker::Send + 'static,
|
|
||||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
|
||||||
{
|
|
||||||
type Response = http::Response<tonic::body::Body>;
|
|
||||||
type Error = std::convert::Infallible;
|
|
||||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
|
||||||
fn poll_ready(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
|
||||||
Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
|
||||||
match req.uri().path() {
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PostUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Uctovnictvo,
|
|
||||||
> tonic::server::UnaryService<super::PostUctovnictvoRequest>
|
|
||||||
for PostUctovnictvoSvc<T> {
|
|
||||||
type Response = super::UctovnictvoResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PostUctovnictvoRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Uctovnictvo>::post_uctovnictvo(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PostUctovnictvoSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Uctovnictvo,
|
|
||||||
> tonic::server::UnaryService<super::GetUctovnictvoRequest>
|
|
||||||
for GetUctovnictvoSvc<T> {
|
|
||||||
type Response = super::UctovnictvoResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::GetUctovnictvoRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Uctovnictvo>::get_uctovnictvo(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetUctovnictvoSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetUctovnictvoCountSvc<T: Uctovnictvo>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Uctovnictvo,
|
|
||||||
> tonic::server::UnaryService<super::super::common::Empty>
|
|
||||||
for GetUctovnictvoCountSvc<T> {
|
|
||||||
type Response = super::super::common::CountResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::super::common::Empty>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Uctovnictvo>::get_uctovnictvo_count(&inner, request)
|
|
||||||
.await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetUctovnictvoCountSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct GetUctovnictvoByPositionSvc<T: Uctovnictvo>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Uctovnictvo,
|
|
||||||
> tonic::server::UnaryService<super::super::common::PositionRequest>
|
|
||||||
for GetUctovnictvoByPositionSvc<T> {
|
|
||||||
type Response = super::UctovnictvoResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<
|
|
||||||
super::super::common::PositionRequest,
|
|
||||||
>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Uctovnictvo>::get_uctovnictvo_by_position(
|
|
||||||
&inner,
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = GetUctovnictvoByPositionSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo" => {
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct PutUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
|
|
||||||
impl<
|
|
||||||
T: Uctovnictvo,
|
|
||||||
> tonic::server::UnaryService<super::PutUctovnictvoRequest>
|
|
||||||
for PutUctovnictvoSvc<T> {
|
|
||||||
type Response = super::UctovnictvoResponse;
|
|
||||||
type Future = BoxFuture<
|
|
||||||
tonic::Response<Self::Response>,
|
|
||||||
tonic::Status,
|
|
||||||
>;
|
|
||||||
fn call(
|
|
||||||
&mut self,
|
|
||||||
request: tonic::Request<super::PutUctovnictvoRequest>,
|
|
||||||
) -> Self::Future {
|
|
||||||
let inner = Arc::clone(&self.0);
|
|
||||||
let fut = async move {
|
|
||||||
<T as Uctovnictvo>::put_uctovnictvo(&inner, request).await
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let accept_compression_encodings = self.accept_compression_encodings;
|
|
||||||
let send_compression_encodings = self.send_compression_encodings;
|
|
||||||
let max_decoding_message_size = self.max_decoding_message_size;
|
|
||||||
let max_encoding_message_size = self.max_encoding_message_size;
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
let fut = async move {
|
|
||||||
let method = PutUctovnictvoSvc(inner);
|
|
||||||
let codec = tonic::codec::ProstCodec::default();
|
|
||||||
let mut grpc = tonic::server::Grpc::new(codec)
|
|
||||||
.apply_compression_config(
|
|
||||||
accept_compression_encodings,
|
|
||||||
send_compression_encodings,
|
|
||||||
)
|
|
||||||
.apply_max_message_size_config(
|
|
||||||
max_decoding_message_size,
|
|
||||||
max_encoding_message_size,
|
|
||||||
);
|
|
||||||
let res = grpc.unary(method, req).await;
|
|
||||||
Ok(res)
|
|
||||||
};
|
|
||||||
Box::pin(fut)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
Box::pin(async move {
|
|
||||||
let mut response = http::Response::new(
|
|
||||||
tonic::body::Body::default(),
|
|
||||||
);
|
|
||||||
let headers = response.headers_mut();
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
tonic::Status::GRPC_STATUS,
|
|
||||||
(tonic::Code::Unimplemented as i32).into(),
|
|
||||||
);
|
|
||||||
headers
|
|
||||||
.insert(
|
|
||||||
http::header::CONTENT_TYPE,
|
|
||||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
|
||||||
);
|
|
||||||
Ok(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T> Clone for UctovnictvoServer<T> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
let inner = self.inner.clone();
|
|
||||||
Self {
|
|
||||||
inner,
|
|
||||||
accept_compression_encodings: self.accept_compression_encodings,
|
|
||||||
send_compression_encodings: self.send_compression_encodings,
|
|
||||||
max_decoding_message_size: self.max_decoding_message_size,
|
|
||||||
max_encoding_message_size: self.max_encoding_message_size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Generated gRPC service name
|
|
||||||
pub const SERVICE_NAME: &str = "komp_ac.uctovnictvo.Uctovnictvo";
|
|
||||||
impl<T> tonic::server::NamedService for UctovnictvoServer<T> {
|
|
||||||
const NAME: &'static str = SERVICE_NAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,10 +10,10 @@ search = { path = "../search" }
|
|||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tantivy = { workspace = true }
|
tantivy = { workspace = true }
|
||||||
|
prost = "0.13.5"
|
||||||
prost-types = { workspace = true }
|
prost-types = { workspace = true }
|
||||||
chrono = { version = "0.4.40", features = ["serde"] }
|
chrono = { version = "0.4.40", features = ["serde"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
prost = "0.13.5"
|
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
sqlx = { version = "0.8.5", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "rust_decimal", "time", "uuid"] }
|
sqlx = { version = "0.8.5", features = ["chrono", "postgres", "runtime-tokio", "runtime-tokio-native-tls", "rust_decimal", "time", "uuid"] }
|
||||||
@@ -40,6 +40,9 @@ regex = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
steel-decimal = "1.0.0"
|
steel-decimal = "1.0.0"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
prost-build = "0.14.1"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "server"
|
name = "server"
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE TABLE table_validation_rules (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
table_def_id BIGINT NOT NULL
|
||||||
|
REFERENCES table_definitions(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
data_key TEXT NOT NULL,
|
||||||
|
config JSONB NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (table_def_id, data_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_table_validation_rules_table
|
||||||
|
ON table_validation_rules (table_def_id);
|
||||||
@@ -1,9 +1,39 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# scripts/reset_test_db.sh
|
# scripts/reset_test_db.sh
|
||||||
|
|
||||||
DATABASE_URL=${TEST_DATABASE_URL:-"postgres://multi_psql_dev:3@localhost:5432/multi_rust_test"}
|
# Load environment variables from .env_test if it exists
|
||||||
|
if [ -f .env_test ]; then
|
||||||
|
export $(grep -v '^#' .env_test | xargs)
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Reset db script"
|
DATABASE_URL=${TEST_DATABASE_URL:-"postgres://multi_psql_dev:3@localhost:5432/multi_rust_test"}
|
||||||
yes | sqlx database drop --database-url "$DATABASE_URL"
|
echo "Resetting test DB at $DATABASE_URL"
|
||||||
|
|
||||||
|
# Check if database exists and who owns it
|
||||||
|
DB_NAME=$(echo "$DATABASE_URL" | sed 's|.*/||')
|
||||||
|
DB_HOST=$(echo "$DATABASE_URL" | sed 's|.*@||' | sed 's|:.*||')
|
||||||
|
DB_PORT=$(echo "$DATABASE_URL" | sed 's|.*:||' | sed 's|/.*||')
|
||||||
|
DB_USER=$(echo "$DATABASE_URL" | sed 's|.*://||' | sed 's|:.*||')
|
||||||
|
|
||||||
|
# Check database owner (optional info)
|
||||||
|
DB_OWNER=$(psql "$DATABASE_URL" -t -c "SELECT pg_catalog.pg_get_userbyid(d.datdba) FROM pg_catalog.pg_database d WHERE d.datname = '$DB_NAME';" 2>/dev/null | xargs)
|
||||||
|
if [ ! -z "$DB_OWNER" ]; then
|
||||||
|
echo "Database owner: $DB_OWNER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Force drop database without confirmation
|
||||||
|
echo "Dropping database..."
|
||||||
|
sqlx database drop --database-url "$DATABASE_URL" --yes 2>/dev/null || {
|
||||||
|
# Fallback: use psql to force drop
|
||||||
|
psql "postgres://$DB_USER@$DB_HOST:$DB_PORT/postgres" -c "DROP DATABASE IF EXISTS \"$DB_NAME\";" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create database
|
||||||
|
echo "Creating database..."
|
||||||
sqlx database create --database-url "$DATABASE_URL"
|
sqlx database create --database-url "$DATABASE_URL"
|
||||||
echo "Test database reset complete."
|
|
||||||
|
# Apply migrations
|
||||||
|
echo "Applying migrations..."
|
||||||
|
sqlx migrate run --database-url "$DATABASE_URL"
|
||||||
|
|
||||||
|
echo "✅ Test database reset and migrated."
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub mod table_definition;
|
|||||||
pub mod tables_data;
|
pub mod tables_data;
|
||||||
pub mod table_script;
|
pub mod table_script;
|
||||||
pub mod steel;
|
pub mod steel;
|
||||||
|
pub mod table_validation;
|
||||||
|
|
||||||
// Re-export run_server from the inner server module:
|
// Re-export run_server from the inner server module:
|
||||||
pub use server::run_server;
|
pub use server::run_server;
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ use crate::server::services::{
|
|||||||
use common::proto::komp_ac::{
|
use common::proto::komp_ac::{
|
||||||
table_structure::table_structure_service_server::TableStructureServiceServer,
|
table_structure::table_structure_service_server::TableStructureServiceServer,
|
||||||
table_definition::table_definition_server::TableDefinitionServer,
|
table_definition::table_definition_server::TableDefinitionServer,
|
||||||
|
table_validation::table_validation_service_server::TableValidationServiceServer,
|
||||||
tables_data::tables_data_server::TablesDataServer,
|
tables_data::tables_data_server::TablesDataServer,
|
||||||
table_script::table_script_server::TableScriptServer,
|
table_script::table_script_server::TableScriptServer,
|
||||||
auth::auth_service_server::AuthServiceServer,
|
auth::auth_service_server::AuthServiceServer,
|
||||||
search2::search2_server::Search2Server,
|
search2::search2_server::Search2Server,
|
||||||
};
|
};
|
||||||
use search::{SearcherService, SearcherServer};
|
use search::{SearcherService, SearcherServer};
|
||||||
|
use crate::table_validation::get::service::TableValidationSvc;
|
||||||
|
|
||||||
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Initialize JWT for authentication
|
// Initialize JWT for authentication
|
||||||
@@ -43,6 +45,7 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
|
|||||||
|
|
||||||
// Initialize services, passing the indexer sender to the relevant ones
|
// Initialize services, passing the indexer sender to the relevant ones
|
||||||
let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() };
|
let table_definition_service = TableDefinitionService { db_pool: db_pool.clone() };
|
||||||
|
let table_validation_service = TableValidationSvc { db: db_pool.clone() };
|
||||||
let tables_data_service = TablesDataService {
|
let tables_data_service = TablesDataService {
|
||||||
db_pool: db_pool.clone(),
|
db_pool: db_pool.clone(),
|
||||||
indexer_tx: indexer_tx.clone(),
|
indexer_tx: indexer_tx.clone(),
|
||||||
@@ -55,6 +58,7 @@ pub async fn run_server(db_pool: sqlx::PgPool) -> Result<(), Box<dyn std::error:
|
|||||||
Server::builder()
|
Server::builder()
|
||||||
.add_service(TableStructureServiceServer::new(TableStructureHandler { db_pool: db_pool.clone() }))
|
.add_service(TableStructureServiceServer::new(TableStructureHandler { db_pool: db_pool.clone() }))
|
||||||
.add_service(TableDefinitionServer::new(table_definition_service))
|
.add_service(TableDefinitionServer::new(table_definition_service))
|
||||||
|
.add_service(TableValidationServiceServer::new(table_validation_service))
|
||||||
.add_service(TablesDataServer::new(tables_data_service))
|
.add_service(TablesDataServer::new(tables_data_service))
|
||||||
.add_service(TableScriptServer::new(table_script_service))
|
.add_service(TableScriptServer::new(table_script_service))
|
||||||
.add_service(AuthServiceServer::new(auth_service))
|
.add_service(AuthServiceServer::new(auth_service))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
pub mod table_structure_service;
|
pub mod table_structure_service;
|
||||||
pub mod table_definition_service;
|
pub mod table_definition_service;
|
||||||
|
pub mod table_validation_service;
|
||||||
pub mod tables_data_service;
|
pub mod tables_data_service;
|
||||||
pub mod table_script_service;
|
pub mod table_script_service;
|
||||||
pub mod auth_service;
|
pub mod auth_service;
|
||||||
@@ -9,6 +10,7 @@ pub mod search2_service;
|
|||||||
|
|
||||||
pub use table_structure_service::TableStructureHandler;
|
pub use table_structure_service::TableStructureHandler;
|
||||||
pub use table_definition_service::TableDefinitionService;
|
pub use table_definition_service::TableDefinitionService;
|
||||||
|
pub use table_validation_service::*;
|
||||||
pub use tables_data_service::TablesDataService;
|
pub use tables_data_service::TablesDataService;
|
||||||
pub use table_script_service::TableScriptService;
|
pub use table_script_service::TableScriptService;
|
||||||
pub use auth_service::AuthServiceImpl;
|
pub use auth_service::AuthServiceImpl;
|
||||||
|
|||||||
10
server/src/server/services/table_validation_service.rs
Normal file
10
server/src/server/services/table_validation_service.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// src/server/services/table_validation_service.rs
|
||||||
|
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use common::proto::komp_ac::table_validation::table_validation_service_server::TableValidationServiceServer;
|
||||||
|
use crate::table_validation::get::service::TableValidationSvc;
|
||||||
|
|
||||||
|
pub fn svc(db: PgPool) -> TableValidationServiceServer<TableValidationSvc> {
|
||||||
|
TableValidationServiceServer::new(TableValidationSvc { db })
|
||||||
|
}
|
||||||
@@ -3,21 +3,20 @@
|
|||||||
use steel::steel_vm::engine::Engine;
|
use steel::steel_vm::engine::Engine;
|
||||||
use steel::steel_vm::register_fn::RegisterFn;
|
use steel::steel_vm::register_fn::RegisterFn;
|
||||||
use steel::rvals::SteelVal;
|
use steel::rvals::SteelVal;
|
||||||
use super::functions::{SteelContext, convert_row_data_for_steel};
|
use super::functions::SteelContext;
|
||||||
use steel_decimal::registry::FunctionRegistry;
|
use steel_decimal::registry::FunctionRegistry;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
use regex::Regex; // NEW
|
||||||
|
|
||||||
/// Represents different types of values that can be returned from Steel script execution.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Value {
|
pub enum Value {
|
||||||
Strings(Vec<String>),
|
Strings(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can occur during Steel script execution.
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ExecutionError {
|
pub enum ExecutionError {
|
||||||
#[error("Script execution failed: {0}")]
|
#[error("Script execution failed: {0}")]
|
||||||
@@ -28,7 +27,83 @@ pub enum ExecutionError {
|
|||||||
UnsupportedType(String),
|
UnsupportedType(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a Steel execution context with proper boolean value conversion.
|
// NEW: upgrade steel_get_column -> steel_get_column_with_index using FK present in row_data
|
||||||
|
fn auto_promote_with_index(
|
||||||
|
script: &str,
|
||||||
|
current_table: &str,
|
||||||
|
row_data: &HashMap<String, String>,
|
||||||
|
) -> String {
|
||||||
|
// Matches: (steel_get_column "table" "column")
|
||||||
|
let re = Regex::new(
|
||||||
|
r#"\(\s*steel_get_column\s+"([^"]+)"\s+"([^"]+)"\s*\)"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
re.replace_all(script, |caps: ®ex::Captures| {
|
||||||
|
let table = caps.get(1).unwrap().as_str();
|
||||||
|
let column = caps.get(2).unwrap().as_str();
|
||||||
|
|
||||||
|
// Only upgrade cross-table calls, if FK is present in the request data
|
||||||
|
if table != current_table {
|
||||||
|
let fk_key = format!("{}_id", table);
|
||||||
|
if let Some(id_str) = row_data.get(&fk_key) {
|
||||||
|
if let Ok(_) = id_str.parse::<i64>() {
|
||||||
|
return format!(
|
||||||
|
r#"(steel_get_column_with_index "{}" {} "{}")"#,
|
||||||
|
table, id_str, column
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: keep original call
|
||||||
|
caps.get(0).unwrap().as_str().to_string()
|
||||||
|
})
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
|
|
||||||
|
// Converts row data boolean values to Steel script format during context initialization.
|
||||||
|
pub async fn convert_row_data_for_steel(
|
||||||
|
db_pool: &PgPool,
|
||||||
|
schema_id: i64,
|
||||||
|
table_name: &str,
|
||||||
|
row_data: &mut HashMap<String, String>,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
let table_def = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT columns FROM table_definitions
|
||||||
|
WHERE schema_id = $1 AND table_name = $2
|
||||||
|
"#,
|
||||||
|
schema_id,
|
||||||
|
table_name
|
||||||
|
)
|
||||||
|
.fetch_optional(db_pool)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| sqlx::Error::RowNotFound)?;
|
||||||
|
|
||||||
|
// Parse column definitions to identify boolean columns
|
||||||
|
if let Ok(columns) = serde_json::from_value::<Vec<ColumnDefinition>>(table_def.columns) {
|
||||||
|
for col_def in columns {
|
||||||
|
let normalized_type =
|
||||||
|
col_def.field_type.to_uppercase().split('(').next().unwrap().to_string();
|
||||||
|
|
||||||
|
if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {
|
||||||
|
if let Some(value) = row_data.get_mut(&col_def.name) {
|
||||||
|
*value = match value.to_lowercase().as_str() {
|
||||||
|
"true" | "t" | "1" | "yes" | "on" => "#true".to_string(),
|
||||||
|
"false" | "f" | "0" | "no" | "off" => "#false".to_string(),
|
||||||
|
_ => value.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_steel_context_with_boolean_conversion(
|
pub async fn create_steel_context_with_boolean_conversion(
|
||||||
current_table: String,
|
current_table: String,
|
||||||
schema_id: i64,
|
schema_id: i64,
|
||||||
@@ -36,7 +111,6 @@ pub async fn create_steel_context_with_boolean_conversion(
|
|||||||
mut row_data: HashMap<String, String>,
|
mut row_data: HashMap<String, String>,
|
||||||
db_pool: Arc<PgPool>,
|
db_pool: Arc<PgPool>,
|
||||||
) -> Result<SteelContext, ExecutionError> {
|
) -> Result<SteelContext, ExecutionError> {
|
||||||
// Convert boolean values in row_data to Steel format
|
|
||||||
convert_row_data_for_steel(&db_pool, schema_id, ¤t_table, &mut row_data)
|
convert_row_data_for_steel(&db_pool, schema_id, ¤t_table, &mut row_data)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -53,7 +127,6 @@ pub async fn create_steel_context_with_boolean_conversion(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes a Steel script with database context and type-safe result processing.
|
|
||||||
pub async fn execute_script(
|
pub async fn execute_script(
|
||||||
script: String,
|
script: String,
|
||||||
target_type: &str,
|
target_type: &str,
|
||||||
@@ -65,42 +138,40 @@ pub async fn execute_script(
|
|||||||
) -> Result<Value, ExecutionError> {
|
) -> Result<Value, ExecutionError> {
|
||||||
let mut vm = Engine::new();
|
let mut vm = Engine::new();
|
||||||
|
|
||||||
// Create execution context with proper boolean value conversion
|
// Upgrade to with_index based on FK presence in the posted data
|
||||||
|
let script = auto_promote_with_index(&script, ¤t_table, &row_data);
|
||||||
|
|
||||||
let context = create_steel_context_with_boolean_conversion(
|
let context = create_steel_context_with_boolean_conversion(
|
||||||
current_table,
|
current_table.clone(),
|
||||||
schema_id,
|
schema_id,
|
||||||
schema_name,
|
schema_name,
|
||||||
row_data,
|
row_data.clone(),
|
||||||
db_pool.clone(),
|
db_pool.clone(),
|
||||||
).await?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let context = Arc::new(context);
|
let context = Arc::new(context);
|
||||||
|
|
||||||
// Register database access functions
|
|
||||||
register_steel_functions(&mut vm, context.clone());
|
register_steel_functions(&mut vm, context.clone());
|
||||||
|
|
||||||
// Register decimal math operations
|
|
||||||
register_decimal_math_functions(&mut vm);
|
register_decimal_math_functions(&mut vm);
|
||||||
|
|
||||||
// Register row data as variables in the Steel VM for get-var access
|
|
||||||
let mut define_script = String::new();
|
let mut define_script = String::new();
|
||||||
|
|
||||||
for (key, value) in &context.row_data {
|
for (key, value) in &context.row_data {
|
||||||
// Register only bare variable names for get-var access
|
|
||||||
define_script.push_str(&format!("(define {} \"{}\")\n", key, value));
|
define_script.push_str(&format!("(define {} \"{}\")\n", key, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute variable definitions if any exist
|
|
||||||
if !define_script.is_empty() {
|
if !define_script.is_empty() {
|
||||||
vm.compile_and_run_raw_program(define_script)
|
vm.compile_and_run_raw_program(define_script)
|
||||||
.map_err(|e| ExecutionError::RuntimeError(format!("Failed to register variables: {}", e)))?;
|
.map_err(|e| ExecutionError::RuntimeError(format!(
|
||||||
|
"Failed to register variables: {}",
|
||||||
|
e
|
||||||
|
)))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also register variables using the decimal registry as backup method
|
|
||||||
FunctionRegistry::register_variables(&mut vm, context.row_data.clone());
|
FunctionRegistry::register_variables(&mut vm, context.row_data.clone());
|
||||||
|
|
||||||
// Execute the main script
|
let results = vm
|
||||||
let results = vm.compile_and_run_raw_program(script.clone())
|
.compile_and_run_raw_program(script.clone())
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("Steel script execution failed: {}", e);
|
error!("Steel script execution failed: {}", e);
|
||||||
error!("Script was: {}", script);
|
error!("Script was: {}", script);
|
||||||
@@ -108,22 +179,22 @@ pub async fn execute_script(
|
|||||||
ExecutionError::RuntimeError(e.to_string())
|
ExecutionError::RuntimeError(e.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Convert results to the requested target type
|
|
||||||
match target_type {
|
match target_type {
|
||||||
"STRINGS" => process_string_results(results),
|
"STRINGS" => process_string_results(results),
|
||||||
_ => Err(ExecutionError::UnsupportedType(target_type.into()))
|
_ => Err(ExecutionError::UnsupportedType(target_type.into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers Steel functions for database access within the VM context.
|
|
||||||
fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
|
fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
|
||||||
debug!("Registering Steel functions with context");
|
debug!("Registering Steel functions with context");
|
||||||
|
|
||||||
// Register column access function for current and related tables
|
|
||||||
vm.register_fn("steel_get_column", {
|
vm.register_fn("steel_get_column", {
|
||||||
let ctx = context.clone();
|
let ctx = context.clone();
|
||||||
move |table: String, column: String| {
|
move |table: String, column: String| {
|
||||||
debug!("steel_get_column called with table: '{}', column: '{}'", table, column);
|
debug!(
|
||||||
|
"steel_get_column called with table: '{}', column: '{}'",
|
||||||
|
table, column
|
||||||
|
);
|
||||||
ctx.steel_get_column(&table, &column)
|
ctx.steel_get_column(&table, &column)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("steel_get_column failed: {:?}", e);
|
error!("steel_get_column failed: {:?}", e);
|
||||||
@@ -132,11 +203,13 @@ fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register indexed column access for comma-separated values
|
|
||||||
vm.register_fn("steel_get_column_with_index", {
|
vm.register_fn("steel_get_column_with_index", {
|
||||||
let ctx = context.clone();
|
let ctx = context.clone();
|
||||||
move |table: String, index: i64, column: String| {
|
move |table: String, index: i64, column: String| {
|
||||||
debug!("steel_get_column_with_index called with table: '{}', index: {}, column: '{}'", table, index, column);
|
debug!(
|
||||||
|
"steel_get_column_with_index called with table: '{}', index: {}, column: '{}'",
|
||||||
|
table, index, column
|
||||||
|
);
|
||||||
ctx.steel_get_column_with_index(&table, index, &column)
|
ctx.steel_get_column_with_index(&table, index, &column)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("steel_get_column_with_index failed: {:?}", e);
|
error!("steel_get_column_with_index failed: {:?}", e);
|
||||||
@@ -145,13 +218,11 @@ fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register safe SQL query execution
|
|
||||||
vm.register_fn("steel_query_sql", {
|
vm.register_fn("steel_query_sql", {
|
||||||
let ctx = context.clone();
|
let ctx = context.clone();
|
||||||
move |query: String| {
|
move |query: String| {
|
||||||
debug!("steel_query_sql called with query: '{}'", query);
|
debug!("steel_query_sql called with query: '{}'", query);
|
||||||
ctx.steel_query_sql(&query)
|
ctx.steel_query_sql(&query).map_err(|e| {
|
||||||
.map_err(|e| {
|
|
||||||
error!("steel_query_sql failed: {:?}", e);
|
error!("steel_query_sql failed: {:?}", e);
|
||||||
e.to_string()
|
e.to_string()
|
||||||
})
|
})
|
||||||
@@ -159,13 +230,11 @@ fn register_steel_functions(vm: &mut Engine, context: Arc<SteelContext>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers decimal mathematics functions in the Steel VM.
|
|
||||||
fn register_decimal_math_functions(vm: &mut Engine) {
|
fn register_decimal_math_functions(vm: &mut Engine) {
|
||||||
debug!("Registering decimal math functions");
|
debug!("Registering decimal math functions");
|
||||||
FunctionRegistry::register_all(vm);
|
FunctionRegistry::register_all(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes Steel script results into string format for consistent output.
|
|
||||||
fn process_string_results(results: Vec<SteelVal>) -> Result<Value, ExecutionError> {
|
fn process_string_results(results: Vec<SteelVal>) -> Result<Value, ExecutionError> {
|
||||||
let mut strings = Vec::new();
|
let mut strings = Vec::new();
|
||||||
|
|
||||||
@@ -178,7 +247,7 @@ fn process_string_results(results: Vec<SteelVal>) -> Result<Value, ExecutionErro
|
|||||||
_ => {
|
_ => {
|
||||||
error!("Unexpected result type: {:?}", result);
|
error!("Unexpected result type: {:?}", result);
|
||||||
return Err(ExecutionError::TypeConversionError(
|
return Err(ExecutionError::TypeConversionError(
|
||||||
format!("Expected string-convertible type, got {:?}", result)
|
format!("Expected string-convertible type, got {:?}", result),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/steel/server/functions.rs
|
// src/steel/server/functions.rs
|
||||||
|
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
use steel::rvals::SteelVal;
|
use steel::rvals::SteelVal;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -21,10 +22,8 @@ pub enum FunctionError {
|
|||||||
ProhibitedTypeAccess(String),
|
ProhibitedTypeAccess(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data types that Steel scripts are prohibited from accessing for security reasons
|
|
||||||
const PROHIBITED_TYPES: &[&str] = &["BIGINT", "DATE", "TIMESTAMPTZ"];
|
const PROHIBITED_TYPES: &[&str] = &["BIGINT", "DATE", "TIMESTAMPTZ"];
|
||||||
|
|
||||||
/// Execution context for Steel scripts with database access capabilities.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SteelContext {
|
pub struct SteelContext {
|
||||||
pub current_table: String,
|
pub current_table: String,
|
||||||
@@ -35,26 +34,11 @@ pub struct SteelContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SteelContext {
|
impl SteelContext {
|
||||||
/// Resolves a base table name to its full qualified name in the current schema.
|
async fn get_column_type(
|
||||||
/// Used for foreign key relationship traversal in Steel scripts.
|
&self,
|
||||||
pub async fn get_related_table_name(&self, base_name: &str) -> Result<String, FunctionError> {
|
table_name: &str,
|
||||||
let table_def = sqlx::query!(
|
column_name: &str,
|
||||||
r#"SELECT table_name FROM table_definitions
|
) -> Result<String, FunctionError> {
|
||||||
WHERE schema_id = $1 AND table_name LIKE $2"#,
|
|
||||||
self.schema_id,
|
|
||||||
format!("%_{}", base_name)
|
|
||||||
)
|
|
||||||
.fetch_optional(&*self.db_pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| FunctionError::DatabaseError(e.to_string()))?
|
|
||||||
.ok_or_else(|| FunctionError::TableNotFound(base_name.to_string()))?;
|
|
||||||
|
|
||||||
Ok(table_def.table_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieves the SQL data type for a specific column in a table.
|
|
||||||
/// Parses the JSON column definitions to find type information.
|
|
||||||
async fn get_column_type(&self, table_name: &str, column_name: &str) -> Result<String, FunctionError> {
|
|
||||||
let table_def = sqlx::query!(
|
let table_def = sqlx::query!(
|
||||||
r#"SELECT columns FROM table_definitions
|
r#"SELECT columns FROM table_definitions
|
||||||
WHERE schema_id = $1 AND table_name = $2"#,
|
WHERE schema_id = $1 AND table_name = $2"#,
|
||||||
@@ -66,49 +50,43 @@ impl SteelContext {
|
|||||||
.map_err(|e| FunctionError::DatabaseError(e.to_string()))?
|
.map_err(|e| FunctionError::DatabaseError(e.to_string()))?
|
||||||
.ok_or_else(|| FunctionError::TableNotFound(table_name.to_string()))?;
|
.ok_or_else(|| FunctionError::TableNotFound(table_name.to_string()))?;
|
||||||
|
|
||||||
let columns: Vec<String> = serde_json::from_value(table_def.columns)
|
let columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns)
|
||||||
.map_err(|e| FunctionError::DatabaseError(format!("Invalid column data: {}", e)))?;
|
.map_err(|e| FunctionError::DatabaseError(format!(
|
||||||
|
"Invalid column data: {}",
|
||||||
|
e
|
||||||
|
)))?;
|
||||||
|
|
||||||
// Parse column definitions to find the requested column type
|
for col_def in columns {
|
||||||
for column_def in columns {
|
if col_def.name == column_name {
|
||||||
let mut parts = column_def.split_whitespace();
|
return Ok(col_def.field_type.to_uppercase());
|
||||||
if let (Some(name), Some(data_type)) = (parts.next(), parts.next()) {
|
|
||||||
let column_name_clean = name.trim_matches('"');
|
|
||||||
if column_name_clean == column_name {
|
|
||||||
return Ok(data_type.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(FunctionError::ColumnNotFound(format!(
|
Err(FunctionError::ColumnNotFound(format!(
|
||||||
"Column '{}' not found in table '{}'",
|
"Column '{}' not found in table '{}'",
|
||||||
column_name,
|
column_name, table_name
|
||||||
table_name
|
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts database values to Steel script format based on column type.
|
|
||||||
/// Currently handles boolean conversion to Steel's #true/#false syntax.
|
|
||||||
fn convert_value_to_steel_format(&self, value: &str, column_type: &str) -> String {
|
fn convert_value_to_steel_format(&self, value: &str, column_type: &str) -> String {
|
||||||
let normalized_type = normalize_data_type(column_type);
|
let normalized_type = normalize_data_type(column_type);
|
||||||
|
|
||||||
match normalized_type.as_str() {
|
match normalized_type.as_str() {
|
||||||
"BOOLEAN" | "BOOL" => {
|
"BOOLEAN" | "BOOL" => match value.to_lowercase().as_str() {
|
||||||
// Convert database boolean representations to Steel boolean syntax
|
|
||||||
match value.to_lowercase().as_str() {
|
|
||||||
"true" | "t" | "1" | "yes" | "on" => "#true".to_string(),
|
"true" | "t" | "1" | "yes" | "on" => "#true".to_string(),
|
||||||
"false" | "f" | "0" | "no" | "off" => "#false".to_string(),
|
"false" | "f" | "0" | "no" | "off" => "#false".to_string(),
|
||||||
_ => value.to_string(), // Return as-is if not a recognized boolean
|
_ => value.to_string(),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
"INTEGER" => value.to_string(),
|
"INTEGER" => value.to_string(),
|
||||||
_ => value.to_string(), // Return as-is for other types
|
_ => value.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates that a column type is allowed for Steel script access.
|
async fn validate_column_type_and_get_type(
|
||||||
/// Returns the column type if validation passes, error if prohibited.
|
&self,
|
||||||
async fn validate_column_type_and_get_type(&self, table_name: &str, column_name: &str) -> Result<String, FunctionError> {
|
table_name: &str,
|
||||||
|
column_name: &str,
|
||||||
|
) -> Result<String, FunctionError> {
|
||||||
let column_type = self.get_column_type(table_name, column_name).await?;
|
let column_type = self.get_column_type(table_name, column_name).await?;
|
||||||
|
|
||||||
if is_prohibited_type(&column_type) {
|
if is_prohibited_type(&column_type) {
|
||||||
@@ -124,15 +102,13 @@ impl SteelContext {
|
|||||||
Ok(column_type)
|
Ok(column_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves column value from current table or related tables via foreign keys.
|
pub fn steel_get_column(
|
||||||
///
|
&self,
|
||||||
/// # Behavior
|
table: &str,
|
||||||
/// - Current table: Returns value directly from row_data with type conversion
|
column: &str,
|
||||||
/// - Related table: Follows foreign key relationship and queries database
|
) -> Result<SteelVal, SteelVal> {
|
||||||
/// - All accesses are subject to prohibited type validation
|
|
||||||
pub fn steel_get_column(&self, table: &str, column: &str) -> Result<SteelVal, SteelVal> {
|
|
||||||
if table == self.current_table {
|
if table == self.current_table {
|
||||||
// Access current table data with type validation
|
// current table
|
||||||
let column_type = tokio::task::block_in_place(|| {
|
let column_type = tokio::task::block_in_place(|| {
|
||||||
let handle = tokio::runtime::Handle::current();
|
let handle = tokio::runtime::Handle::current();
|
||||||
handle.block_on(async {
|
handle.block_on(async {
|
||||||
@@ -145,70 +121,112 @@ impl SteelContext {
|
|||||||
Err(e) => return Err(SteelVal::StringV(e.to_string().into())),
|
Err(e) => return Err(SteelVal::StringV(e.to_string().into())),
|
||||||
};
|
};
|
||||||
|
|
||||||
return self.row_data.get(column)
|
return self
|
||||||
|
.row_data
|
||||||
|
.get(column)
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
let converted_value = self.convert_value_to_steel_format(v, &column_type);
|
let converted =
|
||||||
SteelVal::StringV(converted_value.into())
|
self.convert_value_to_steel_format(v, &column_type);
|
||||||
|
SteelVal::StringV(converted.into())
|
||||||
})
|
})
|
||||||
.ok_or_else(|| SteelVal::StringV(format!("Column {} not found", column).into()));
|
.ok_or_else(|| {
|
||||||
|
SteelVal::StringV(
|
||||||
|
format!("Column {} not found", column).into(),
|
||||||
|
)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access related table via foreign key relationship
|
// Cross-table via FK: use exact table name FK convention: "<table>_id"
|
||||||
let base_name = table.split_once('_')
|
|
||||||
.map(|(_, rest)| rest)
|
|
||||||
.unwrap_or(table);
|
|
||||||
|
|
||||||
let fk_column = format!("{}_id", base_name);
|
|
||||||
let fk_value = self.row_data.get(&fk_column)
|
|
||||||
.ok_or_else(|| SteelVal::StringV(format!("Foreign key {} not found", fk_column).into()))?;
|
|
||||||
|
|
||||||
let result = tokio::task::block_in_place(|| {
|
let result = tokio::task::block_in_place(|| {
|
||||||
let handle = tokio::runtime::Handle::current();
|
let handle = tokio::runtime::Handle::current();
|
||||||
handle.block_on(async {
|
handle.block_on(async {
|
||||||
let actual_table = self.get_related_table_name(base_name).await
|
let fk_column = format!("{}_id", table);
|
||||||
.map_err(|e| SteelVal::StringV(e.to_string().into()))?;
|
let fk_value = self
|
||||||
|
.row_data
|
||||||
|
.get(&fk_column)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
FunctionError::ForeignKeyNotFound(format!(
|
||||||
|
"Foreign key column '{}' not found on '{}'",
|
||||||
|
fk_column, self.current_table
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
// Validate column type and get type information
|
let column_type =
|
||||||
let column_type = self.validate_column_type_and_get_type(&actual_table, column).await
|
self.validate_column_type_and_get_type(table, column)
|
||||||
.map_err(|e| SteelVal::StringV(e.to_string().into()))?;
|
.await?;
|
||||||
|
|
||||||
// Query the related table for the column value
|
let raw_value = sqlx::query_scalar::<_, String>(&format!(
|
||||||
let raw_value = sqlx::query_scalar::<_, String>(
|
"SELECT \"{}\" FROM \"{}\".\"{}\" WHERE id = $1",
|
||||||
&format!("SELECT {} FROM \"{}\".\"{}\" WHERE id = $1", column, self.schema_name, actual_table)
|
column, self.schema_name, table
|
||||||
|
))
|
||||||
|
.bind(
|
||||||
|
fk_value
|
||||||
|
.parse::<i64>()
|
||||||
|
.map_err(|_| {
|
||||||
|
FunctionError::DatabaseError(
|
||||||
|
"Invalid foreign key format".into(),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
)
|
)
|
||||||
.bind(fk_value.parse::<i64>().map_err(|_|
|
|
||||||
SteelVal::StringV("Invalid foreign key format".into()))?)
|
|
||||||
.fetch_one(&*self.db_pool)
|
.fetch_one(&*self.db_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| SteelVal::StringV(e.to_string().into()))?;
|
.map_err(|e| FunctionError::DatabaseError(e.to_string()))?;
|
||||||
|
|
||||||
// Convert to appropriate Steel format
|
let converted =
|
||||||
let converted_value = self.convert_value_to_steel_format(&raw_value, &column_type);
|
self.convert_value_to_steel_format(&raw_value, &column_type);
|
||||||
Ok(converted_value)
|
Ok::<String, FunctionError>(converted)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
result.map(|v| SteelVal::StringV(v.into()))
|
match result {
|
||||||
|
Ok(v) => Ok(SteelVal::StringV(v.into())),
|
||||||
|
Err(e) => Err(SteelVal::StringV(e.to_string().into())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves a specific indexed element from a comma-separated column value.
|
|
||||||
/// Useful for accessing elements from array-like string representations.
|
|
||||||
pub fn steel_get_column_with_index(
|
pub fn steel_get_column_with_index(
|
||||||
&self,
|
&self,
|
||||||
table: &str,
|
table: &str,
|
||||||
index: i64,
|
index: i64,
|
||||||
column: &str
|
column: &str,
|
||||||
) -> Result<SteelVal, SteelVal> {
|
) -> Result<SteelVal, SteelVal> {
|
||||||
// Get the full value with proper type conversion
|
// Cross-table: interpret 'index' as the row id to fetch directly
|
||||||
let value = self.steel_get_column(table, column)?;
|
if table != self.current_table {
|
||||||
|
let result = tokio::task::block_in_place(|| {
|
||||||
|
let handle = tokio::runtime::Handle::current();
|
||||||
|
handle.block_on(async {
|
||||||
|
let column_type =
|
||||||
|
self.validate_column_type_and_get_type(table, column)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let raw_value = sqlx::query_scalar::<_, String>(&format!(
|
||||||
|
"SELECT \"{}\" FROM \"{}\".\"{}\" WHERE id = $1",
|
||||||
|
column, self.schema_name, table
|
||||||
|
))
|
||||||
|
.bind(index)
|
||||||
|
.fetch_one(&*self.db_pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| FunctionError::DatabaseError(e.to_string()))?;
|
||||||
|
|
||||||
|
let converted = self
|
||||||
|
.convert_value_to_steel_format(&raw_value, &column_type);
|
||||||
|
Ok::<String, FunctionError>(converted)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return match result {
|
||||||
|
Ok(v) => Ok(SteelVal::StringV(v.into())),
|
||||||
|
Err(e) => Err(SteelVal::StringV(e.to_string().into())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current table: existing behavior (index in comma-separated string)
|
||||||
|
let value = self.steel_get_column(table, column)?;
|
||||||
if let SteelVal::StringV(s) = value {
|
if let SteelVal::StringV(s) = value {
|
||||||
let parts: Vec<_> = s.split(',').collect();
|
let parts: Vec<_> = s.split(',').collect();
|
||||||
|
|
||||||
if let Some(part) = parts.get(index as usize) {
|
if let Some(part) = parts.get(index as usize) {
|
||||||
let trimmed_part = part.trim();
|
let trimmed = part.trim();
|
||||||
|
|
||||||
// Apply type conversion to the indexed part based on original column type
|
|
||||||
let column_type = tokio::task::block_in_place(|| {
|
let column_type = tokio::task::block_in_place(|| {
|
||||||
let handle = tokio::runtime::Handle::current();
|
let handle = tokio::runtime::Handle::current();
|
||||||
handle.block_on(async {
|
handle.block_on(async {
|
||||||
@@ -218,40 +236,35 @@ impl SteelContext {
|
|||||||
|
|
||||||
match column_type {
|
match column_type {
|
||||||
Ok(ct) => {
|
Ok(ct) => {
|
||||||
let converted_part = self.convert_value_to_steel_format(trimmed_part, &ct);
|
let converted =
|
||||||
Ok(SteelVal::StringV(converted_part.into()))
|
self.convert_value_to_steel_format(trimmed, &ct);
|
||||||
}
|
Ok(SteelVal::StringV(converted.into()))
|
||||||
Err(_) => {
|
|
||||||
// If type cannot be determined, return value as-is
|
|
||||||
Ok(SteelVal::StringV(trimmed_part.into()))
|
|
||||||
}
|
}
|
||||||
|
Err(_) => Ok(SteelVal::StringV(trimmed.into())),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(SteelVal::StringV("Index out of bounds".into()))
|
Err(SteelVal::StringV("Index out of bounds".into()))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(SteelVal::StringV("Expected comma-separated string".into()))
|
Err(SteelVal::StringV(
|
||||||
|
"Expected comma-separated string".into(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes read-only SQL queries from Steel scripts with safety restrictions.
|
|
||||||
///
|
|
||||||
/// # Security Features
|
|
||||||
/// - Only SELECT, SHOW, and EXPLAIN queries allowed
|
|
||||||
/// - Prohibited column type access validation
|
|
||||||
/// - Returns first column of all rows as comma-separated string
|
|
||||||
pub fn steel_query_sql(&self, query: &str) -> Result<SteelVal, SteelVal> {
|
pub fn steel_query_sql(&self, query: &str) -> Result<SteelVal, SteelVal> {
|
||||||
if !is_read_only_query(query) {
|
if !is_read_only_query(query) {
|
||||||
return Err(SteelVal::StringV(
|
return Err(SteelVal::StringV("Only SELECT queries are allowed".into()));
|
||||||
"Only SELECT queries are allowed".into()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if contains_prohibited_column_access(query) {
|
if contains_prohibited_column_access(query) {
|
||||||
return Err(SteelVal::StringV(format!(
|
return Err(SteelVal::StringV(
|
||||||
|
format!(
|
||||||
"SQL query may access prohibited column types. Steel scripts cannot access columns of type: {}",
|
"SQL query may access prohibited column types. Steel scripts cannot access columns of type: {}",
|
||||||
PROHIBITED_TYPES.join(", ")
|
PROHIBITED_TYPES.join(", ")
|
||||||
).into()));
|
)
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let pool = self.db_pool.clone();
|
let pool = self.db_pool.clone();
|
||||||
@@ -266,7 +279,8 @@ impl SteelContext {
|
|||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let val: String = row.try_get(0)
|
let val: String = row
|
||||||
|
.try_get(0)
|
||||||
.map_err(|e| SteelVal::StringV(e.to_string().into()))?;
|
.map_err(|e| SteelVal::StringV(e.to_string().into()))?;
|
||||||
results.push(val);
|
results.push(val);
|
||||||
}
|
}
|
||||||
@@ -279,85 +293,30 @@ impl SteelContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a data type is prohibited for Steel script access.
|
|
||||||
fn is_prohibited_type(data_type: &str) -> bool {
|
fn is_prohibited_type(data_type: &str) -> bool {
|
||||||
let normalized_type = normalize_data_type(data_type);
|
let normalized_type = normalize_data_type(data_type);
|
||||||
PROHIBITED_TYPES.iter().any(|&prohibited| normalized_type.starts_with(prohibited))
|
PROHIBITED_TYPES
|
||||||
|
.iter()
|
||||||
|
.any(|&prohibited| normalized_type.starts_with(prohibited))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Normalizes data type strings for consistent comparison.
|
|
||||||
/// Handles variations like NUMERIC(10,2) by extracting base type.
|
|
||||||
fn normalize_data_type(data_type: &str) -> String {
|
fn normalize_data_type(data_type: &str) -> String {
|
||||||
data_type.to_uppercase()
|
data_type
|
||||||
.split('(') // Remove precision/scale from NUMERIC(x,y)
|
.to_uppercase()
|
||||||
|
.split('(')
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or(data_type)
|
.unwrap_or(data_type)
|
||||||
.trim()
|
.trim()
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs basic heuristic check for prohibited column type access in SQL queries.
|
|
||||||
/// Looks for common patterns that might indicate access to restricted types.
|
|
||||||
fn contains_prohibited_column_access(query: &str) -> bool {
|
fn contains_prohibited_column_access(query: &str) -> bool {
|
||||||
let query_upper = query.to_uppercase();
|
let query_upper = query.to_uppercase();
|
||||||
|
let patterns = ["EXTRACT(", "DATE_PART(", "::DATE", "::TIMESTAMPTZ", "::BIGINT"];
|
||||||
let patterns = [
|
patterns.iter().any(|p| query_upper.contains(p))
|
||||||
"EXTRACT(", // Common with DATE/TIMESTAMPTZ
|
|
||||||
"DATE_PART(", // Common with DATE/TIMESTAMPTZ
|
|
||||||
"::DATE",
|
|
||||||
"::TIMESTAMPTZ",
|
|
||||||
"::BIGINT",
|
|
||||||
];
|
|
||||||
|
|
||||||
patterns.iter().any(|pattern| query_upper.contains(pattern))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates that a query is read-only and safe for Steel script execution.
|
|
||||||
fn is_read_only_query(query: &str) -> bool {
|
fn is_read_only_query(query: &str) -> bool {
|
||||||
let query = query.trim_start().to_uppercase();
|
let query = query.trim_start().to_uppercase();
|
||||||
query.starts_with("SELECT") ||
|
query.starts_with("SELECT") || query.starts_with("SHOW") || query.starts_with("EXPLAIN")
|
||||||
query.starts_with("SHOW") ||
|
|
||||||
query.starts_with("EXPLAIN")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts row data boolean values to Steel script format during context initialization.
|
|
||||||
pub async fn convert_row_data_for_steel(
|
|
||||||
db_pool: &PgPool,
|
|
||||||
schema_id: i64,
|
|
||||||
table_name: &str,
|
|
||||||
row_data: &mut HashMap<String, String>,
|
|
||||||
) -> Result<(), sqlx::Error> {
|
|
||||||
let table_def = sqlx::query!(
|
|
||||||
r#"SELECT columns FROM table_definitions
|
|
||||||
WHERE schema_id = $1 AND table_name = $2"#,
|
|
||||||
schema_id,
|
|
||||||
table_name
|
|
||||||
)
|
|
||||||
.fetch_optional(db_pool)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| sqlx::Error::RowNotFound)?;
|
|
||||||
|
|
||||||
// Parse column definitions to identify boolean columns for conversion
|
|
||||||
if let Ok(columns) = serde_json::from_value::<Vec<String>>(table_def.columns) {
|
|
||||||
for column_def in columns {
|
|
||||||
let mut parts = column_def.split_whitespace();
|
|
||||||
if let (Some(name), Some(data_type)) = (parts.next(), parts.next()) {
|
|
||||||
let column_name = name.trim_matches('"');
|
|
||||||
let normalized_type = normalize_data_type(data_type);
|
|
||||||
|
|
||||||
if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {
|
|
||||||
if let Some(value) = row_data.get_mut(column_name) {
|
|
||||||
// Convert boolean value to Steel format
|
|
||||||
*value = match value.to_lowercase().as_str() {
|
|
||||||
"true" | "t" | "1" | "yes" | "on" => "#true".to_string(),
|
|
||||||
"false" | "f" | "0" | "no" | "off" => "#false".to_string(),
|
|
||||||
_ => value.clone(), // Keep original if not recognized
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,9 @@
|
|||||||
|
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use sqlx::{PgPool, Transaction, Postgres};
|
use sqlx::{PgPool, Transaction, Postgres};
|
||||||
use serde_json::json;
|
|
||||||
use common::proto::komp_ac::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
|
use common::proto::komp_ac::table_definition::{PostTableDefinitionRequest, TableDefinitionResponse};
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
// TODO CRITICAL add decimal with optional precision"
|
use crate::table_definition::models::map_field_type;
|
||||||
const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
|
|
||||||
("text", "TEXT"),
|
|
||||||
("string", "TEXT"),
|
|
||||||
("boolean", "BOOLEAN"),
|
|
||||||
("timestamp", "TIMESTAMPTZ"),
|
|
||||||
("timestamptz", "TIMESTAMPTZ"),
|
|
||||||
("time", "TIMESTAMPTZ"),
|
|
||||||
("money", "NUMERIC(14, 4)"),
|
|
||||||
("integer", "INTEGER"),
|
|
||||||
("int", "INTEGER"),
|
|
||||||
("biginteger", "BIGINT"),
|
|
||||||
("bigint", "BIGINT"),
|
|
||||||
("date", "DATE"),
|
|
||||||
];
|
|
||||||
|
|
||||||
// NEW: Helper function to provide detailed error messages
|
// NEW: Helper function to provide detailed error messages
|
||||||
fn validate_identifier_format(s: &str, identifier_type: &str) -> Result<(), Status> {
|
fn validate_identifier_format(s: &str, identifier_type: &str) -> Result<(), Status> {
|
||||||
@@ -58,116 +43,6 @@ fn validate_identifier_format(s: &str, identifier_type: &str) -> Result<(), Stat
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_decimal_number_format(num_str: &str, param_name: &str) -> Result<(), Status> {
|
|
||||||
if num_str.is_empty() {
|
|
||||||
return Err(Status::invalid_argument(format!(
|
|
||||||
"{} cannot be empty",
|
|
||||||
param_name
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for explicit signs
|
|
||||||
if num_str.starts_with('+') || num_str.starts_with('-') {
|
|
||||||
return Err(Status::invalid_argument(format!(
|
|
||||||
"{} cannot have explicit positive or negative signs",
|
|
||||||
param_name
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for decimal points
|
|
||||||
if num_str.contains('.') {
|
|
||||||
return Err(Status::invalid_argument(format!(
|
|
||||||
"{} must be a whole number (no decimal points)",
|
|
||||||
param_name
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for leading zeros (but allow "0" itself)
|
|
||||||
if num_str.len() > 1 && num_str.starts_with('0') {
|
|
||||||
let trimmed = num_str.trim_start_matches('0');
|
|
||||||
let suggestion = if trimmed.is_empty() { "0" } else { trimmed };
|
|
||||||
return Err(Status::invalid_argument(format!(
|
|
||||||
"{} cannot have leading zeros (use '{}' instead of '{}')",
|
|
||||||
param_name,
|
|
||||||
suggestion,
|
|
||||||
num_str
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that all characters are digits
|
|
||||||
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
|
||||||
return Err(Status::invalid_argument(format!(
|
|
||||||
"{} contains invalid characters. Only digits 0-9 are allowed",
|
|
||||||
param_name
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_field_type(field_type: &str) -> Result<String, Status> {
|
|
||||||
let lower_field_type = field_type.to_lowercase();
|
|
||||||
|
|
||||||
// Special handling for "decimal(precision, scale)"
|
|
||||||
if lower_field_type.starts_with("decimal(") && lower_field_type.ends_with(')') {
|
|
||||||
// Extract the part inside the parentheses, e.g., "10, 2"
|
|
||||||
let args = lower_field_type
|
|
||||||
.strip_prefix("decimal(")
|
|
||||||
.and_then(|s| s.strip_suffix(')'))
|
|
||||||
.unwrap_or(""); // Should always succeed due to the checks above
|
|
||||||
|
|
||||||
// Split into precision and scale parts
|
|
||||||
if let Some((p_str, s_str)) = args.split_once(',') {
|
|
||||||
let precision_str = p_str.trim();
|
|
||||||
let scale_str = s_str.trim();
|
|
||||||
|
|
||||||
// NEW: Validate format BEFORE parsing
|
|
||||||
validate_decimal_number_format(precision_str, "precision")?;
|
|
||||||
validate_decimal_number_format(scale_str, "scale")?;
|
|
||||||
|
|
||||||
// Parse precision, returning an error if it's not a valid number
|
|
||||||
let precision = precision_str.parse::<u32>().map_err(|_| {
|
|
||||||
Status::invalid_argument("Invalid precision in decimal type")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Parse scale, returning an error if it's not a valid number
|
|
||||||
let scale = scale_str.parse::<u32>().map_err(|_| {
|
|
||||||
Status::invalid_argument("Invalid scale in decimal type")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Add validation based on PostgreSQL rules
|
|
||||||
if precision < 1 {
|
|
||||||
return Err(Status::invalid_argument("Precision must be at least 1"));
|
|
||||||
}
|
|
||||||
if scale > precision {
|
|
||||||
return Err(Status::invalid_argument(
|
|
||||||
"Scale cannot be greater than precision",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If everything is valid, build and return the NUMERIC type string
|
|
||||||
return Ok(format!("NUMERIC({}, {})", precision, scale));
|
|
||||||
} else {
|
|
||||||
// The format was wrong, e.g., "decimal(10)" or "decimal()"
|
|
||||||
return Err(Status::invalid_argument(
|
|
||||||
"Invalid decimal format. Expected: decimal(precision, scale)",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not a decimal, fall back to the predefined list
|
|
||||||
PREDEFINED_FIELD_TYPES
|
|
||||||
.iter()
|
|
||||||
.find(|(key, _)| *key == lower_field_type.as_str())
|
|
||||||
.map(|(_, sql_type)| sql_type.to_string()) // Convert to an owned String
|
|
||||||
.ok_or_else(|| {
|
|
||||||
Status::invalid_argument(format!(
|
|
||||||
"Invalid field type: {}",
|
|
||||||
field_type
|
|
||||||
))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_invalid_table_name(table_name: &str) -> bool {
|
fn is_invalid_table_name(table_name: &str) -> bool {
|
||||||
table_name.ends_with("_id") ||
|
table_name.ends_with("_id") ||
|
||||||
table_name == "id" ||
|
table_name == "id" ||
|
||||||
@@ -299,7 +174,9 @@ async fn execute_table_definition(
|
|||||||
links.push((linked_id, link.required));
|
links.push((linked_id, link.required));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut columns = Vec::new();
|
let mut stored_columns = Vec::new();
|
||||||
|
let mut sql_columns = Vec::new();
|
||||||
|
|
||||||
for col_def in request.columns.drain(..) {
|
for col_def in request.columns.drain(..) {
|
||||||
let col_name = col_def.name.trim().to_string();
|
let col_name = col_def.name.trim().to_string();
|
||||||
validate_identifier_format(&col_name, "Column name")?;
|
validate_identifier_format(&col_name, "Column name")?;
|
||||||
@@ -312,21 +189,33 @@ async fn execute_table_definition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sql_type = map_field_type(&col_def.field_type)?;
|
let sql_type = map_field_type(&col_def.field_type)?;
|
||||||
columns.push(format!("\"{}\" {}", col_name, sql_type));
|
sql_columns.push(format!("\"{}\" {}", col_name, sql_type));
|
||||||
|
|
||||||
|
// push the proto type (serde serializable)
|
||||||
|
stored_columns.push(ColumnDefinition {
|
||||||
|
name: col_name,
|
||||||
|
field_type: col_def.field_type,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Indexes
|
||||||
|
let mut stored_indexes = Vec::new();
|
||||||
let mut indexes = Vec::new();
|
let mut indexes = Vec::new();
|
||||||
for idx in request.indexes.drain(..) {
|
for idx in request.indexes.drain(..) {
|
||||||
let idx_name = idx.trim().to_string();
|
let idx_name = idx.trim().to_string();
|
||||||
validate_identifier_format(&idx_name, "Index name")?;
|
validate_identifier_format(&idx_name, "Index name")?;
|
||||||
|
|
||||||
if !columns.iter().any(|c| c.starts_with(&format!("\"{}\"", idx_name))) {
|
if !sql_columns.iter().any(|c| c.starts_with(&format!("\"{}\"", idx_name))) {
|
||||||
return Err(Status::invalid_argument(format!("Index column '{}' not found", idx_name)));
|
return Err(Status::invalid_argument(format!(
|
||||||
|
"Index column '{}' not found", idx_name
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stored_indexes.push(idx_name.clone());
|
||||||
indexes.push(idx_name);
|
indexes.push(idx_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (create_sql, index_sql) = generate_table_sql(tx, &profile_name, &table_name, &columns, &indexes, &links).await?;
|
let (create_sql, index_sql) = generate_table_sql(tx, &profile_name, &table_name, &sql_columns, &indexes, &links).await?;
|
||||||
|
|
||||||
// Use schema_id instead of profile_id
|
// Use schema_id instead of profile_id
|
||||||
let table_def = sqlx::query!(
|
let table_def = sqlx::query!(
|
||||||
@@ -336,8 +225,8 @@ async fn execute_table_definition(
|
|||||||
RETURNING id"#,
|
RETURNING id"#,
|
||||||
schema.id,
|
schema.id,
|
||||||
&table_name,
|
&table_name,
|
||||||
json!(columns),
|
serde_json::to_value(&stored_columns).unwrap(),
|
||||||
json!(indexes)
|
serde_json::to_value(&stored_indexes).unwrap()
|
||||||
)
|
)
|
||||||
.fetch_one(&mut **tx)
|
.fetch_one(&mut **tx)
|
||||||
.await
|
.await
|
||||||
@@ -351,6 +240,29 @@ async fn execute_table_definition(
|
|||||||
Status::internal(format!("Database error: {}", e))
|
Status::internal(format!("Database error: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
for col_def in &sql_columns {
|
||||||
|
// Column string looks like "\"name\" TYPE", split out identifier
|
||||||
|
let col_name = col_def.split_whitespace().next().unwrap_or("");
|
||||||
|
let clean_col = col_name.trim_matches('"');
|
||||||
|
|
||||||
|
// Default empty config — currently only character_limits block, none set.
|
||||||
|
let default_cfg = serde_json::json!({
|
||||||
|
"character_limits": { "min": 0, "max": 0, "warn_at": null, "count_mode": "CHARS" }
|
||||||
|
});
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"INSERT INTO table_validation_rules (table_def_id, data_key, config)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (table_def_id, data_key) DO NOTHING"#,
|
||||||
|
table_def.id,
|
||||||
|
clean_col,
|
||||||
|
default_cfg
|
||||||
|
)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("Failed to insert default validation rule for column {}: {}", clean_col, e)))?;
|
||||||
|
}
|
||||||
|
|
||||||
for (linked_id, is_required) in links {
|
for (linked_id, is_required) in links {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"INSERT INTO table_definition_links
|
"INSERT INTO table_definition_links
|
||||||
|
|||||||
@@ -2,3 +2,6 @@
|
|||||||
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
pub mod repo;
|
||||||
|
|
||||||
|
pub use repo::*;
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// src/table_definition/models.rs
|
||||||
|
use tonic::Status;
|
||||||
|
|
||||||
|
/// Predefined static field mappings
|
||||||
|
// TODO CRITICAL add decimal with optional precision"
|
||||||
|
pub const PREDEFINED_FIELD_TYPES: &[(&str, &str)] = &[
|
||||||
|
("text", "TEXT"),
|
||||||
|
("string", "TEXT"),
|
||||||
|
("boolean", "BOOLEAN"),
|
||||||
|
("timestamp", "TIMESTAMPTZ"),
|
||||||
|
("timestamptz", "TIMESTAMPTZ"),
|
||||||
|
("time", "TIMESTAMPTZ"),
|
||||||
|
("money", "NUMERIC(14, 4)"),
|
||||||
|
("integer", "INTEGER"),
|
||||||
|
("int", "INTEGER"),
|
||||||
|
("biginteger", "BIGINT"),
|
||||||
|
("bigint", "BIGINT"),
|
||||||
|
("date", "DATE"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// reusable decimal number validation
|
||||||
|
pub fn validate_decimal_number_format(num_str: &str, param_name: &str) -> Result<(), Status> {
|
||||||
|
if num_str.is_empty() {
|
||||||
|
return Err(Status::invalid_argument(format!("{} cannot be empty", param_name)));
|
||||||
|
}
|
||||||
|
if num_str.starts_with('+') || num_str.starts_with('-') {
|
||||||
|
return Err(Status::invalid_argument(format!(
|
||||||
|
"{} cannot have explicit positive/negative signs", param_name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if num_str.contains('.') {
|
||||||
|
return Err(Status::invalid_argument(format!(
|
||||||
|
"{} must be a whole number (no decimal point)", param_name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if num_str.len() > 1 && num_str.starts_with('0') {
|
||||||
|
let trimmed = num_str.trim_start_matches('0');
|
||||||
|
let suggestion = if trimmed.is_empty() { "0" } else { trimmed };
|
||||||
|
return Err(Status::invalid_argument(format!(
|
||||||
|
"{} cannot have leading zeros (use '{}' instead of '{}')",
|
||||||
|
param_name, suggestion, num_str
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if !num_str.chars().all(|c| c.is_ascii_digit()) {
|
||||||
|
return Err(Status::invalid_argument(format!(
|
||||||
|
"{} contains invalid characters. Only digits allowed", param_name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// reusable field type mapper
|
||||||
|
pub fn map_field_type(field_type: &str) -> Result<String, Status> {
|
||||||
|
let lower_field_type = field_type.to_lowercase();
|
||||||
|
|
||||||
|
if lower_field_type.starts_with("decimal(") && lower_field_type.ends_with(')') {
|
||||||
|
let args = lower_field_type.strip_prefix("decimal(").unwrap()
|
||||||
|
.strip_suffix(')').unwrap();
|
||||||
|
|
||||||
|
if let Some((p_str, s_str)) = args.split_once(',') {
|
||||||
|
let precision_str = p_str.trim();
|
||||||
|
let scale_str = s_str.trim();
|
||||||
|
|
||||||
|
validate_decimal_number_format(precision_str, "precision")?;
|
||||||
|
validate_decimal_number_format(scale_str, "scale")?;
|
||||||
|
|
||||||
|
let precision = precision_str.parse::<u32>()
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid precision"))?;
|
||||||
|
let scale = scale_str.parse::<u32>()
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid scale"))?;
|
||||||
|
|
||||||
|
if precision < 1 {
|
||||||
|
return Err(Status::invalid_argument("Precision must be >= 1"));
|
||||||
|
}
|
||||||
|
if scale > precision {
|
||||||
|
return Err(Status::invalid_argument("Scale cannot be > precision"));
|
||||||
|
}
|
||||||
|
return Ok(format!("NUMERIC({}, {})", precision, scale));
|
||||||
|
} else {
|
||||||
|
return Err(Status::invalid_argument(
|
||||||
|
"Invalid decimal format. Expected decimal(precision, scale)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PREDEFINED_FIELD_TYPES
|
||||||
|
.iter()
|
||||||
|
.find(|(key, _)| *key == lower_field_type.as_str())
|
||||||
|
.map(|(_, sql_type)| sql_type.to_string())
|
||||||
|
.ok_or_else(|| Status::invalid_argument(format!("Invalid field type: {}", field_type)))
|
||||||
|
}
|
||||||
|
|||||||
33
server/src/table_definition/repo.rs
Normal file
33
server/src/table_definition/repo.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// src/table_definition/repo.rs
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
pub struct TableDefRow {
|
||||||
|
pub id: i64,
|
||||||
|
pub table_name: String,
|
||||||
|
pub columns: Vec<ColumnDefinition>,
|
||||||
|
pub indexes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_table_definition(
|
||||||
|
db: &PgPool,
|
||||||
|
id: i64,
|
||||||
|
) -> Result<TableDefRow, anyhow::Error> {
|
||||||
|
let rec = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT id, table_name, columns, indexes
|
||||||
|
FROM table_definitions
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(TableDefRow {
|
||||||
|
id: rec.id,
|
||||||
|
table_name: rec.table_name,
|
||||||
|
columns: serde_json::from_value(rec.columns)?, // 🔑
|
||||||
|
indexes: serde_json::from_value(rec.indexes)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use serde_json::{json, Value};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
/// Represents the state of a node during dependency graph traversal.
|
/// Represents the state of a node during dependency graph traversal.
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
@@ -40,18 +41,38 @@ impl DependencyType {
|
|||||||
DependencyType::SqlQuery { .. } => "sql_query",
|
DependencyType::SqlQuery { .. } => "sql_query",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates context JSON for database storage.
|
/// Strongly-typed JSON for script_dependencies.context_info
|
||||||
pub fn context_json(&self) -> Value {
|
/// Using untagged so JSON stays minimal (no "type" field), and we can still
|
||||||
|
/// deserialize it into a proper enum.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ScriptDependencyContext {
|
||||||
|
ColumnAccess { column: String },
|
||||||
|
IndexedAccess { column: String, index: i64 },
|
||||||
|
SqlQuery { query_fragment: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DependencyType {
|
||||||
|
/// Convert this dependency into its JSON context struct.
|
||||||
|
pub fn to_context(&self) -> ScriptDependencyContext {
|
||||||
match self {
|
match self {
|
||||||
DependencyType::ColumnAccess { column } => {
|
DependencyType::ColumnAccess { column } => {
|
||||||
json!({ "column": column })
|
ScriptDependencyContext::ColumnAccess {
|
||||||
|
column: column.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DependencyType::IndexedAccess { column, index } => {
|
DependencyType::IndexedAccess { column, index } => {
|
||||||
json!({ "column": column, "index": index })
|
ScriptDependencyContext::IndexedAccess {
|
||||||
|
column: column.clone(),
|
||||||
|
index: *index,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DependencyType::SqlQuery { query_fragment } => {
|
DependencyType::SqlQuery { query_fragment } => {
|
||||||
json!({ "query_fragment": query_fragment })
|
ScriptDependencyContext::SqlQuery {
|
||||||
|
query_fragment: query_fragment.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,7 +575,7 @@ impl DependencyAnalyzer {
|
|||||||
table_id,
|
table_id,
|
||||||
target_id,
|
target_id,
|
||||||
dep.dependency_type.as_str(),
|
dep.dependency_type.as_str(),
|
||||||
dep.dependency_type.context_json()
|
serde_json::to_value(dep.dependency_type.to_context()).unwrap()
|
||||||
)
|
)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use sqlx::{PgPool, Error as SqlxError};
|
use sqlx::{PgPool, Error as SqlxError};
|
||||||
use common::proto::komp_ac::table_script::{PostTableScriptRequest, TableScriptResponse};
|
use common::proto::komp_ac::table_script::{PostTableScriptRequest, TableScriptResponse};
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use steel_decimal::SteelDecimal;
|
use steel_decimal::SteelDecimal;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -303,16 +304,12 @@ async fn validate_math_operations_column_types(
|
|||||||
let mut table_column_types: HashMap<String, HashMap<String, String>> = HashMap::new();
|
let mut table_column_types: HashMap<String, HashMap<String, String>> = HashMap::new();
|
||||||
|
|
||||||
for table_def in table_definitions {
|
for table_def in table_definitions {
|
||||||
let columns: Vec<String> = serde_json::from_value(table_def.columns)
|
let columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns)
|
||||||
.map_err(|e| Status::internal(format!("Invalid column data for table '{}': {}", table_def.table_name, e)))?;
|
.map_err(|e| Status::internal(format!("Invalid column data for table '{}': {}", table_def.table_name, e)))?;
|
||||||
|
|
||||||
let mut column_types = HashMap::new();
|
let mut column_types = HashMap::new();
|
||||||
for column_def in columns {
|
for col_def in columns {
|
||||||
let mut parts = column_def.split_whitespace();
|
column_types.insert(col_def.name.clone(), col_def.field_type.clone());
|
||||||
if let (Some(name), Some(data_type)) = (parts.next(), parts.next()) {
|
|
||||||
let column_name = name.trim_matches('"');
|
|
||||||
column_types.insert(column_name.to_string(), data_type.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
table_column_types.insert(table_def.table_name, column_types);
|
table_column_types.insert(table_def.table_name, column_types);
|
||||||
}
|
}
|
||||||
@@ -363,25 +360,13 @@ fn validate_target_column(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse the columns JSON into a vector of strings
|
// Parse the columns JSON into a vector of strings
|
||||||
let columns: Vec<String> = serde_json::from_value(table_columns.clone())
|
let columns: Vec<ColumnDefinition> = serde_json::from_value(table_columns.clone())
|
||||||
.map_err(|e| format!("Invalid column data: {}", e))?;
|
.map_err(|e| format!("Invalid column data: {}", e))?;
|
||||||
|
|
||||||
// Extract column names and types
|
let column_type = columns
|
||||||
let column_info: Vec<(&str, &str)> = columns
|
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|c| {
|
.find(|c| c.name == target)
|
||||||
let mut parts = c.split_whitespace();
|
.map(|c| c.field_type.clone())
|
||||||
let name = parts.next()?.trim_matches('"');
|
|
||||||
let data_type = parts.next()?;
|
|
||||||
Some((name, data_type))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Find the target column and return its type
|
|
||||||
let column_type = column_info
|
|
||||||
.iter()
|
|
||||||
.find(|(name, _)| *name == target)
|
|
||||||
.map(|(_, dt)| dt.to_string())
|
|
||||||
.ok_or_else(|| format!("Target column '{}' not defined in table '{}'", target, table_name))?;
|
.ok_or_else(|| format!("Target column '{}' not defined in table '{}'", target, table_name))?;
|
||||||
|
|
||||||
// Check if the target column type is prohibited
|
// Check if the target column type is prohibited
|
||||||
@@ -509,34 +494,21 @@ async fn validate_script_column_references(
|
|||||||
/// Validate that a referenced column doesn't have a prohibited type
|
/// Validate that a referenced column doesn't have a prohibited type
|
||||||
fn validate_referenced_column_type(table_name: &str, column_name: &str, table_columns: &Value) -> Result<(), String> {
|
fn validate_referenced_column_type(table_name: &str, column_name: &str, table_columns: &Value) -> Result<(), String> {
|
||||||
// Parse the columns JSON into a vector of strings
|
// Parse the columns JSON into a vector of strings
|
||||||
let columns: Vec<String> = serde_json::from_value(table_columns.clone())
|
let columns: Vec<ColumnDefinition> = serde_json::from_value(table_columns.clone())
|
||||||
.map_err(|e| format!("Invalid column data for table '{}': {}", table_name, e))?;
|
.map_err(|e| format!("Invalid column data for table '{}': {}", table_name, e))?;
|
||||||
|
|
||||||
// Extract column names and types
|
if let Some(col_def) = columns.iter().find(|c| c.name == column_name) {
|
||||||
let column_info: Vec<(&str, &str)> = columns
|
if is_prohibited_type(&col_def.field_type) {
|
||||||
.iter()
|
|
||||||
.filter_map(|c| {
|
|
||||||
let mut parts = c.split_whitespace();
|
|
||||||
let name = parts.next()?.trim_matches('"');
|
|
||||||
let data_type = parts.next()?;
|
|
||||||
Some((name, data_type))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Find the referenced column and check its type
|
|
||||||
if let Some((_, column_type)) = column_info.iter().find(|(name, _)| *name == column_name) {
|
|
||||||
if is_prohibited_type(column_type) {
|
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Script references column '{}' in table '{}' which has prohibited type '{}'. Steel scripts cannot access columns of type: {}",
|
"Script references column '{}' in table '{}' which has prohibited type '{}'. Steel scripts cannot access columns of type: {}",
|
||||||
column_name,
|
column_name,
|
||||||
table_name,
|
table_name,
|
||||||
column_type,
|
col_def.field_type,
|
||||||
PROHIBITED_TYPES.join(", ")
|
PROHIBITED_TYPES.join(", ")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log info for boolean columns
|
let normalized_type = normalize_data_type(&col_def.field_type);
|
||||||
let normalized_type = normalize_data_type(column_type);
|
|
||||||
if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {
|
if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {
|
||||||
println!("Info: Script references boolean column '{}' in table '{}'. Values will be converted to Steel format (#true/#false)", column_name, table_name);
|
println!("Info: Script references boolean column '{}' in table '{}'. Values will be converted to Steel format (#true/#false)", column_name, table_name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
// src/table_script/mod.rs
|
// src/table_script/mod.rs
|
||||||
|
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
pub mod repo;
|
||||||
|
|
||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
|
pub use repo::*;
|
||||||
|
|||||||
49
server/src/table_script/repo.rs
Normal file
49
server/src/table_script/repo.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// src/table_script/repo.rs
|
||||||
|
use anyhow::Result;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::table_script::handlers::dependency_analyzer::ScriptDependencyContext;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScriptDependencyRecord {
|
||||||
|
pub script_id: i64,
|
||||||
|
pub source_table_id: i64,
|
||||||
|
pub target_table_id: i64,
|
||||||
|
pub dependency_type: String,
|
||||||
|
pub context: Option<ScriptDependencyContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_dependencies_for_script(
|
||||||
|
db: &PgPool,
|
||||||
|
script_id: i64,
|
||||||
|
) -> Result<Vec<ScriptDependencyRecord>> {
|
||||||
|
let rows = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT script_id, source_table_id, target_table_id, dependency_type, context_info
|
||||||
|
FROM script_dependencies
|
||||||
|
WHERE script_id = $1
|
||||||
|
ORDER BY source_table_id, target_table_id
|
||||||
|
"#,
|
||||||
|
script_id
|
||||||
|
)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for r in rows {
|
||||||
|
let context = match r.context_info {
|
||||||
|
Some(value) => Some(serde_json::from_value::<ScriptDependencyContext>(value)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
out.push(ScriptDependencyRecord {
|
||||||
|
script_id: r.script_id,
|
||||||
|
source_table_id: r.source_table_id,
|
||||||
|
target_table_id: r.target_table_id,
|
||||||
|
dependency_type: r.dependency_type,
|
||||||
|
context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
2
server/src/table_validation/get/mod.rs
Normal file
2
server/src/table_validation/get/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// src/table_validation/get/mod.rs
|
||||||
|
pub mod service;
|
||||||
113
server/src/table_validation/get/service.rs
Normal file
113
server/src/table_validation/get/service.rs
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// src/table_validation/get/service.rs
|
||||||
|
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use common::proto::komp_ac::table_validation::{
|
||||||
|
table_validation_service_server::TableValidationService,
|
||||||
|
GetTableValidationRequest, TableValidationResponse,
|
||||||
|
UpdateFieldValidationRequest, UpdateFieldValidationResponse,
|
||||||
|
FieldValidation,
|
||||||
|
};
|
||||||
|
use crate::table_validation::post::repo; // repo still lives in post
|
||||||
|
|
||||||
|
pub struct TableValidationSvc {
|
||||||
|
pub db: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tonic::async_trait]
|
||||||
|
impl TableValidationService for TableValidationSvc {
|
||||||
|
async fn get_table_validation(
|
||||||
|
&self,
|
||||||
|
req: Request<GetTableValidationRequest>,
|
||||||
|
) -> Result<Response<TableValidationResponse>, Status> {
|
||||||
|
let req = req.into_inner();
|
||||||
|
|
||||||
|
// 1. Get table_def_id
|
||||||
|
let table_def_id = repo::get_table_def_id(&self.db, &req.profile_name, &req.table_name)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Status::not_found("Table definition not found"))?;
|
||||||
|
|
||||||
|
// 2. Get validations
|
||||||
|
let rules = repo::get_validations(&self.db, table_def_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("Failed to fetch rules: {}", e)))?;
|
||||||
|
|
||||||
|
// 3. Parse JSON directly into proto types
|
||||||
|
let mut fields_out = Vec::new();
|
||||||
|
for r in rules {
|
||||||
|
match serde_json::from_value::<FieldValidation>(r.config) {
|
||||||
|
Ok(mut fv) => {
|
||||||
|
// Set the data_key from the database row
|
||||||
|
fv.data_key = r.data_key;
|
||||||
|
|
||||||
|
// Keep entries that have either meaningful limits or a mask
|
||||||
|
let has_meaningful_limits = fv
|
||||||
|
.limits
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |l| l.min > 0 || l.max > 0 || l.warn_at.is_some());
|
||||||
|
let has_mask = fv.mask.is_some();
|
||||||
|
if !has_meaningful_limits && !has_mask {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fields_out.push(fv);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Invalid JSON for {}: {}", r.data_key, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response::new(TableValidationResponse { fields: fields_out }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_field_validation(
|
||||||
|
&self,
|
||||||
|
req: Request<UpdateFieldValidationRequest>,
|
||||||
|
) -> Result<Response<UpdateFieldValidationResponse>, Status> {
|
||||||
|
let req = req.into_inner();
|
||||||
|
|
||||||
|
let table_def_id = repo::get_table_def_id(
|
||||||
|
&self.db, &req.profile_name, &req.table_name,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Status::not_found("Table definition not found"))?;
|
||||||
|
|
||||||
|
// Check if validation is provided
|
||||||
|
if let Some(validation) = req.validation {
|
||||||
|
// Convert proto FieldValidation directly to JSON
|
||||||
|
let json_value = serde_json::to_value(&validation)
|
||||||
|
.map_err(|e| Status::internal(format!("serialize error: {e}")))?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO table_validation_rules (
|
||||||
|
table_def_id, data_key, config, updated_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, now())
|
||||||
|
ON CONFLICT (table_def_id, data_key)
|
||||||
|
DO UPDATE SET
|
||||||
|
config = EXCLUDED.config,
|
||||||
|
updated_at = now()
|
||||||
|
"#,
|
||||||
|
table_def_id,
|
||||||
|
req.data_key,
|
||||||
|
json_value
|
||||||
|
)
|
||||||
|
.execute(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("DB error: {e}")))?;
|
||||||
|
|
||||||
|
Ok(Response::new(UpdateFieldValidationResponse {
|
||||||
|
success: true,
|
||||||
|
message: format!(
|
||||||
|
"Validation rules updated for {}.{} column {}",
|
||||||
|
req.profile_name, req.table_name, req.data_key
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Err(Status::invalid_argument("No validation provided"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
server/src/table_validation/mod.rs
Normal file
4
server/src/table_validation/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// src/table_validation/mod.rs
|
||||||
|
|
||||||
|
pub mod post;
|
||||||
|
pub mod get;
|
||||||
3
server/src/table_validation/post/mod.rs
Normal file
3
server/src/table_validation/post/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// src/table_validation/post/mod.rs
|
||||||
|
|
||||||
|
pub mod repo;
|
||||||
52
server/src/table_validation/post/repo.rs
Normal file
52
server/src/table_validation/post/repo.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// src/table_validation/repo.rs
|
||||||
|
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub struct ValidationRuleRow {
|
||||||
|
pub data_key: String,
|
||||||
|
pub config: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_table_def_id(
|
||||||
|
db: &PgPool,
|
||||||
|
profile_name: &str,
|
||||||
|
table_name: &str,
|
||||||
|
) -> Result<i64> {
|
||||||
|
let rec = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT td.id
|
||||||
|
FROM table_definitions td
|
||||||
|
JOIN schemas s ON td.schema_id = s.id
|
||||||
|
WHERE s.name = $1 AND td.table_name = $2
|
||||||
|
"#,
|
||||||
|
profile_name,
|
||||||
|
table_name
|
||||||
|
)
|
||||||
|
.fetch_one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(rec.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_validations(
|
||||||
|
db: &PgPool,
|
||||||
|
table_def_id: i64,
|
||||||
|
) -> Result<Vec<ValidationRuleRow>> {
|
||||||
|
let rows = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT data_key, config
|
||||||
|
FROM table_validation_rules
|
||||||
|
WHERE table_def_id = $1
|
||||||
|
"#,
|
||||||
|
table_def_id
|
||||||
|
)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(rows.into_iter().map(|r| ValidationRuleRow {
|
||||||
|
data_key: r.data_key,
|
||||||
|
config: r.config,
|
||||||
|
}).collect())
|
||||||
|
}
|
||||||
18
server/src/table_validation/post/service.rs
Normal file
18
server/src/table_validation/post/service.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// src/table_validation/post/service.rs
|
||||||
|
|
||||||
|
use tonic::{Request, Response, Status};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use common::proto::komp_ac::table_validation::{
|
||||||
|
UpdateFieldValidationRequest, UpdateFieldValidationResponse,
|
||||||
|
};
|
||||||
|
use crate::table_validation::post::repo;
|
||||||
|
|
||||||
|
pub struct TableValidationUpdateSvc {
|
||||||
|
pub db: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableValidationUpdateSvc {
|
||||||
|
pub fn new(db: PgPool) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
// src/tables_data/handlers/get_table_data.rs
|
// src/tables_data/handlers/get_table_data.rs
|
||||||
|
|
||||||
|
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Row};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use common::proto::komp_ac::tables_data::{GetTableDataRequest, GetTableDataResponse};
|
use common::proto::komp_ac::tables_data::{GetTableDataRequest, GetTableDataResponse};
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
use crate::shared::schema_qualifier::qualify_table_name_for_data;
|
use crate::shared::schema_qualifier::qualify_table_name_for_data;
|
||||||
|
|
||||||
pub async fn get_table_data(
|
pub async fn get_table_data(
|
||||||
@@ -39,17 +41,13 @@ pub async fn get_table_data(
|
|||||||
let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?;
|
let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?;
|
||||||
|
|
||||||
// Parse user-defined columns from JSON
|
// Parse user-defined columns from JSON
|
||||||
let columns_json: Vec<String> = serde_json::from_value(table_def.columns.clone())
|
let stored_columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns.clone())
|
||||||
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
|
||||||
|
|
||||||
|
// Directly extract names, no split(" ") parsing anymore
|
||||||
let mut user_columns = Vec::new();
|
let mut user_columns = Vec::new();
|
||||||
for col_def in columns_json {
|
for col_def in stored_columns {
|
||||||
let parts: Vec<&str> = col_def.splitn(2, ' ').collect();
|
user_columns.push(col_def.name.trim().to_string());
|
||||||
if parts.len() != 2 {
|
|
||||||
return Err(Status::internal("Invalid column format"));
|
|
||||||
}
|
|
||||||
let name = parts[0].trim_matches('"').to_string();
|
|
||||||
user_columns.push(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- START OF FIX ---
|
// --- START OF FIX ---
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use sqlx::{PgPool, Arguments};
|
|||||||
use sqlx::postgres::PgArguments;
|
use sqlx::postgres::PgArguments;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use common::proto::komp_ac::tables_data::{PostTableDataRequest, PostTableDataResponse};
|
use common::proto::komp_ac::tables_data::{PostTableDataRequest, PostTableDataResponse};
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
|
use crate::table_definition::models::map_field_type;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use prost_types::value::Kind;
|
use prost_types::value::Kind;
|
||||||
@@ -56,18 +58,16 @@ pub async fn post_table_data(
|
|||||||
let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?;
|
let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?;
|
||||||
|
|
||||||
// Parse column definitions from JSON format
|
// Parse column definitions from JSON format
|
||||||
let columns_json: Vec<String> = serde_json::from_value(table_def.columns.clone())
|
let stored_columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns.clone())
|
||||||
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
|
||||||
|
|
||||||
|
// convert ColumnDefinition -> (name, sql_type) using the same map_field_type logic
|
||||||
let mut columns = Vec::new();
|
let mut columns = Vec::new();
|
||||||
for col_def in columns_json {
|
for col_def in stored_columns {
|
||||||
let parts: Vec<&str> = col_def.splitn(2, ' ').collect();
|
let col_name = col_def.name.trim().to_string();
|
||||||
if parts.len() != 2 {
|
let sql_type = map_field_type(&col_def.field_type)
|
||||||
return Err(Status::internal("Invalid column format"));
|
.map_err(|e| Status::invalid_argument(format!("Invalid type for column '{}': {}", col_name, e)))?;
|
||||||
}
|
columns.push((col_name, sql_type));
|
||||||
let name = parts[0].trim_matches('"').to_string();
|
|
||||||
let sql_type = parts[1].to_string();
|
|
||||||
columns.push((name, sql_type));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build list of valid system columns (foreign keys and special columns)
|
// Build list of valid system columns (foreign keys and special columns)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use sqlx::{PgPool, Arguments, Row};
|
|||||||
use sqlx::postgres::PgArguments;
|
use sqlx::postgres::PgArguments;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use common::proto::komp_ac::tables_data::{PutTableDataRequest, PutTableDataResponse};
|
use common::proto::komp_ac::tables_data::{PutTableDataRequest, PutTableDataResponse};
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use prost_types::value::Kind;
|
use prost_types::value::Kind;
|
||||||
@@ -14,6 +15,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use crate::steel::server::execution::{self, Value};
|
use crate::steel::server::execution::{self, Value};
|
||||||
use crate::indexer::{IndexCommand, IndexCommandData};
|
use crate::indexer::{IndexCommand, IndexCommandData};
|
||||||
|
use crate::table_definition::models::map_field_type;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
@@ -56,19 +58,20 @@ pub async fn put_table_data(
|
|||||||
.map_err(|e| Status::internal(format!("Table lookup error: {}", e)))?
|
.map_err(|e| Status::internal(format!("Table lookup error: {}", e)))?
|
||||||
.ok_or_else(|| Status::not_found("Table not found"))?;
|
.ok_or_else(|| Status::not_found("Table not found"))?;
|
||||||
|
|
||||||
// Parse column definitions from JSON format
|
// Parse column definitions from JSON format (now ColumnDefinition objects)
|
||||||
let columns_json: Vec<String> = serde_json::from_value(table_def.columns.clone())
|
let stored_columns: Vec<ColumnDefinition> = serde_json::from_value(table_def.columns.clone())
|
||||||
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
|
||||||
|
|
||||||
|
// Convert ColumnDefinition → (name, sql_type)
|
||||||
let mut columns = Vec::new();
|
let mut columns = Vec::new();
|
||||||
for col_def in columns_json {
|
for col_def in stored_columns {
|
||||||
let parts: Vec<&str> = col_def.splitn(2, ' ').collect();
|
let col_name = col_def.name.trim().to_string();
|
||||||
if parts.len() != 2 {
|
let sql_type = map_field_type(&col_def.field_type)
|
||||||
return Err(Status::internal("Invalid column format"));
|
.map_err(|e| Status::invalid_argument(format!(
|
||||||
}
|
"Invalid type for column '{}': {}",
|
||||||
let name = parts[0].trim_matches('"').to_string();
|
col_name, e
|
||||||
let sql_type = parts[1].to_string();
|
)))?;
|
||||||
columns.push((name, sql_type));
|
columns.push((col_name, sql_type));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build list of valid system columns (foreign keys and special columns)
|
// Build list of valid system columns (foreign keys and special columns)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
use crate::common::setup_isolated_db;
|
use crate::common::setup_isolated_db;
|
||||||
use server::table_script::handlers::post_table_script::post_table_script; // Fixed import
|
use server::table_script::handlers::post_table_script::post_table_script; // Fixed import
|
||||||
use common::proto::komp_ac::table_script::PostTableScriptRequest;
|
use common::proto::komp_ac::table_script::PostTableScriptRequest;
|
||||||
|
use common::proto::komp_ac::table_definition::ColumnDefinition;
|
||||||
use rstest::*;
|
use rstest::*;
|
||||||
use serde_json::json;
|
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
/// Helper function to create a test table with specified columns
|
/// Helper function to create a test table with specified columns
|
||||||
@@ -12,15 +12,10 @@ async fn create_test_table(
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
schema_id: i64,
|
schema_id: i64,
|
||||||
table_name: &str,
|
table_name: &str,
|
||||||
columns: Vec<(&str, &str)>,
|
columns: Vec<ColumnDefinition>,
|
||||||
) -> i64 {
|
) -> i64 {
|
||||||
let column_definitions: Vec<String> = columns
|
let columns_json = serde_json::to_value(columns).unwrap();
|
||||||
.iter()
|
let indexes_json = serde_json::json!([]);
|
||||||
.map(|(name, type_def)| format!("\"{}\" {}", name, type_def))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let columns_json = json!(column_definitions);
|
|
||||||
let indexes_json = json!([]);
|
|
||||||
|
|
||||||
sqlx::query_scalar!(
|
sqlx::query_scalar!(
|
||||||
r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
|
r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
|
||||||
@@ -115,22 +110,17 @@ async fn test_comprehensive_error_scenarios(
|
|||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
// Create comprehensive error test table
|
// Create comprehensive error test table
|
||||||
let columns = vec![
|
let columns: Vec<ColumnDefinition> = vec![
|
||||||
// Valid types
|
ColumnDefinition { name: "valid_numeric".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
("valid_numeric", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "valid_integer".into(), field_type: "INTEGER".into() },
|
||||||
("valid_integer", "INTEGER"),
|
ColumnDefinition { name: "text_col".into(), field_type: "TEXT".into() },
|
||||||
|
ColumnDefinition { name: "boolean_col".into(), field_type: "BOOLEAN".into() },
|
||||||
// Invalid for math operations
|
ColumnDefinition { name: "bigint_col".into(), field_type: "BIGINT".into() },
|
||||||
("text_col", "TEXT"),
|
ColumnDefinition { name: "date_col".into(), field_type: "DATE".into() },
|
||||||
("boolean_col", "BOOLEAN"),
|
ColumnDefinition { name: "timestamp_col".into(), field_type: "TIMESTAMPTZ".into() },
|
||||||
("bigint_col", "BIGINT"),
|
ColumnDefinition { name: "bigint_target".into(), field_type: "BIGINT".into() },
|
||||||
("date_col", "DATE"),
|
ColumnDefinition { name: "date_target".into(), field_type: "DATE".into() },
|
||||||
("timestamp_col", "TIMESTAMPTZ"),
|
ColumnDefinition { name: "timestamp_target".into(), field_type: "TIMESTAMPTZ".into() },
|
||||||
|
|
||||||
// Invalid target types
|
|
||||||
("bigint_target", "BIGINT"),
|
|
||||||
("date_target", "DATE"),
|
|
||||||
("timestamp_target", "TIMESTAMPTZ"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let table_id = create_test_table(&pool, schema_id, "error_table", columns).await;
|
let table_id = create_test_table(&pool, schema_id, "error_table", columns).await;
|
||||||
@@ -169,7 +159,9 @@ async fn test_malformed_script_scenarios(
|
|||||||
let pool = setup_isolated_db().await;
|
let pool = setup_isolated_db().await;
|
||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
let columns = vec![("result", "NUMERIC(10, 2)")];
|
let columns: Vec<ColumnDefinition> = vec![
|
||||||
|
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() }
|
||||||
|
];
|
||||||
let table_id = create_test_table(&pool, schema_id, "malformed_test", columns).await;
|
let table_id = create_test_table(&pool, schema_id, "malformed_test", columns).await;
|
||||||
|
|
||||||
let request = PostTableScriptRequest {
|
let request = PostTableScriptRequest {
|
||||||
@@ -194,7 +186,9 @@ async fn test_advanced_validation_scenarios(
|
|||||||
let pool = setup_isolated_db().await;
|
let pool = setup_isolated_db().await;
|
||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
let columns = vec![("result", "NUMERIC(10, 2)")];
|
let columns: Vec<ColumnDefinition> = vec![
|
||||||
|
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() }
|
||||||
|
];
|
||||||
let table_id = create_test_table(&pool, schema_id, "advanced_test", columns).await;
|
let table_id = create_test_table(&pool, schema_id, "advanced_test", columns).await;
|
||||||
|
|
||||||
let request = PostTableScriptRequest {
|
let request = PostTableScriptRequest {
|
||||||
@@ -236,16 +230,16 @@ async fn test_dependency_cycle_detection() {
|
|||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
// Create table_b first
|
// Create table_b first
|
||||||
let table_b_columns = vec![
|
let table_b_columns: Vec<ColumnDefinition> = vec![
|
||||||
("value_b", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "value_b".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
("result_b", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "result_b".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
];
|
];
|
||||||
let table_b_id = create_test_table(&pool, schema_id, "table_b", table_b_columns).await;
|
let table_b_id = create_test_table(&pool, schema_id, "table_b", table_b_columns).await;
|
||||||
|
|
||||||
// Create table_a
|
// Create table_a
|
||||||
let table_a_columns = vec![
|
let table_a_columns: Vec<ColumnDefinition> = vec![
|
||||||
("value_a", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "value_a".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
("result_a", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "result_a".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
];
|
];
|
||||||
let table_a_id = create_test_table(&pool, schema_id, "table_a", table_a_columns).await;
|
let table_a_id = create_test_table(&pool, schema_id, "table_a", table_a_columns).await;
|
||||||
|
|
||||||
@@ -305,7 +299,9 @@ async fn test_edge_case_identifiers(
|
|||||||
let pool = setup_isolated_db().await;
|
let pool = setup_isolated_db().await;
|
||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
let columns = vec![("result", "NUMERIC(10, 2)")];
|
let columns: Vec<ColumnDefinition> = vec![
|
||||||
|
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() }
|
||||||
|
];
|
||||||
let table_id = create_test_table(&pool, schema_id, "identifier_test", columns).await;
|
let table_id = create_test_table(&pool, schema_id, "identifier_test", columns).await;
|
||||||
|
|
||||||
// Test with edge case identifier in script
|
// Test with edge case identifier in script
|
||||||
@@ -342,7 +338,9 @@ async fn test_sql_injection_prevention() {
|
|||||||
let pool = setup_isolated_db().await;
|
let pool = setup_isolated_db().await;
|
||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
let columns = vec![("result", "NUMERIC(10, 2)")];
|
let columns: Vec<ColumnDefinition> = vec![
|
||||||
|
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() }
|
||||||
|
];
|
||||||
let table_id = create_test_table(&pool, schema_id, "injection_test", columns).await;
|
let table_id = create_test_table(&pool, schema_id, "injection_test", columns).await;
|
||||||
|
|
||||||
// Attempt SQL injection through script content
|
// Attempt SQL injection through script content
|
||||||
@@ -388,9 +386,9 @@ async fn test_performance_with_deeply_nested_expressions() {
|
|||||||
let pool = setup_isolated_db().await;
|
let pool = setup_isolated_db().await;
|
||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
let columns = vec![
|
let columns: Vec<ColumnDefinition> = vec![
|
||||||
("x", "NUMERIC(15, 8)"),
|
ColumnDefinition { name: "x".into(), field_type: "NUMERIC(15, 8)".into() },
|
||||||
("performance_result", "NUMERIC(25, 12)"),
|
ColumnDefinition { name: "performance_result".into(), field_type: "NUMERIC(25, 12)".into() },
|
||||||
];
|
];
|
||||||
let table_id = create_test_table(&pool, schema_id, "performance_test", columns).await;
|
let table_id = create_test_table(&pool, schema_id, "performance_test", columns).await;
|
||||||
|
|
||||||
@@ -437,11 +435,11 @@ async fn test_concurrent_script_creation() {
|
|||||||
let pool = setup_isolated_db().await;
|
let pool = setup_isolated_db().await;
|
||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
let columns = vec![
|
let columns: Vec<ColumnDefinition> = vec![
|
||||||
("value", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "value".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
("result1", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "result1".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
("result2", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "result2".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
("result3", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "result3".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
];
|
];
|
||||||
let table_id = create_test_table(&pool, schema_id, "concurrent_test", columns).await;
|
let table_id = create_test_table(&pool, schema_id, "concurrent_test", columns).await;
|
||||||
|
|
||||||
@@ -500,9 +498,10 @@ async fn test_error_message_localization_and_clarity() {
|
|||||||
let pool = setup_isolated_db().await;
|
let pool = setup_isolated_db().await;
|
||||||
let schema_id = get_default_schema_id(&pool).await;
|
let schema_id = get_default_schema_id(&pool).await;
|
||||||
|
|
||||||
let columns = vec![
|
|
||||||
("text_col", "TEXT"),
|
let columns: Vec<ColumnDefinition> = vec![
|
||||||
("result", "NUMERIC(10, 2)"),
|
ColumnDefinition { name: "text_col".into(), field_type: "TEXT".into() },
|
||||||
|
ColumnDefinition { name: "result".into(), field_type: "NUMERIC(10, 2)".into() },
|
||||||
];
|
];
|
||||||
let table_id = create_test_table(&pool, schema_id, "error_clarity_test", columns).await;
|
let table_id = create_test_table(&pool, schema_id, "error_clarity_test", columns).await;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user