Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1fa42e204 | ||
|
|
306cb956a0 | ||
|
|
d837acde63 | ||
|
|
db938a2c8d | ||
|
|
f24156775a | ||
|
|
2a7f94cf17 | ||
|
|
15922ed953 | ||
|
|
7129ec97fd | ||
|
|
a921806e62 | ||
|
|
d1b28b4fdd | ||
|
|
64fd7e4af2 | ||
|
|
7b52a739c2 | ||
|
|
8127c7bb1b | ||
|
|
7437908baf | ||
|
|
9eb46cb5d3 | ||
|
|
38a70128b0 | ||
|
|
c58ce52b33 | ||
|
|
c82813185f | ||
|
|
a96681e9d6 | ||
|
|
4df6c40034 | ||
|
|
089d728cc7 | ||
|
|
aca3d718b5 | ||
|
|
8a6a584cf3 | ||
|
|
00ed0cf796 | ||
|
|
7e54b2fe43 | ||
|
|
84871faad4 | ||
|
|
bcb433d7b2 | ||
|
|
7d1b130b68 | ||
|
|
24c2376ea1 | ||
|
|
810ef5fc10 | ||
|
|
fe246b1fe6 | ||
|
|
de42bb48aa | ||
|
|
17495c49ac | ||
|
|
0e3a7a06a3 | ||
|
|
e0ee48eb9c | ||
|
|
d2053b1d5a | ||
|
|
fbe8e53858 | ||
|
|
8fe2581b3f | ||
|
|
60cc0e562e | ||
|
|
26898d474f | ||
|
|
2311fbaa3b | ||
|
|
be99cd9423 | ||
|
|
a3dd6fa95b | ||
|
|
433d87c96d | ||
|
|
aff4383671 | ||
|
|
b7c8f6b1a2 | ||
|
|
3443839ba4 | ||
|
|
6c31d48f3b | ||
|
|
1770292fd8 | ||
|
|
afdd5c5740 | ||
|
|
11487f0833 | ||
|
|
4d5d22d0c2 | ||
|
|
314a957922 | ||
|
|
4c57b562e6 | ||
|
|
a757acf51c | ||
|
|
f4a23be1a2 | ||
|
|
93c67ffa14 | ||
|
|
d1ebe4732f | ||
|
|
7b7f3ca05a | ||
|
|
234613f831 | ||
|
|
f6d84e70cc | ||
|
|
5cd324b6ae | ||
|
|
a7457f5749 | ||
|
|
a5afc75099 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@
|
||||
.env
|
||||
/tantivy_indexes
|
||||
server/tantivy_indexes
|
||||
steel_decimal/tests/property_tests.proptest-regressions
|
||||
.direnv/
|
||||
|
||||
956
Cargo.lock
generated
956
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
@@ -1,17 +1,17 @@
|
||||
[workspace]
|
||||
members = ["client", "server", "common", "search"]
|
||||
members = ["client", "server", "common", "search", "canvas"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
# TODO: idk how to do the name, fix later
|
||||
# name = "Multieko2"
|
||||
version = "0.3.13"
|
||||
# name = "komp_ac"
|
||||
version = "0.4.1"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Filip Priečinský <filippriec@gmail.com>"]
|
||||
description = "Poriadny uctovnicky software."
|
||||
readme = "README.md"
|
||||
repository = "https://gitlab.com/filipriec/multieko2"
|
||||
repository = "https://gitlab.com/filipriec/komp_ac"
|
||||
categories = ["command-line-interface"]
|
||||
|
||||
# [workspace.metadata]
|
||||
@@ -40,4 +40,15 @@ tracing = "0.1.41"
|
||||
# Search crate
|
||||
tantivy = "0.24.1"
|
||||
|
||||
# Steel_decimal crate
|
||||
rust_decimal = { version = "1.37.2", features = ["maths", "serde"] }
|
||||
rust_decimal_macros = "1.37.1"
|
||||
thiserror = "2.0.12"
|
||||
regex = "1.11.1"
|
||||
|
||||
# Canvas crate
|
||||
ratatui = { version = "0.29.0", features = ["crossterm"] }
|
||||
crossterm = "0.28.1"
|
||||
toml = "0.8.20"
|
||||
|
||||
common = { path = "./common" }
|
||||
|
||||
160
canvas/CANVAS_MIGRATION.md
Normal file
160
canvas/CANVAS_MIGRATION.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Canvas Crate Migration Documentation
|
||||
|
||||
## Files Moved from Client to Canvas
|
||||
|
||||
### Core Canvas Files
|
||||
|
||||
| **Canvas Location** | **Original Client Location** | **Purpose** |
|
||||
|-------------------|---------------------------|-----------|
|
||||
| `canvas/src/state.rs` | `client/src/state/pages/canvas_state.rs` | Core CanvasState trait |
|
||||
| `canvas/src/actions/edit.rs` | `client/src/functions/modes/edit/form_e.rs` | Generic edit actions |
|
||||
| `canvas/src/renderer.rs` | `client/src/components/handlers/canvas.rs` | Canvas rendering logic |
|
||||
| `canvas/src/modes/highlight.rs` | `client/src/state/app/highlight.rs` | Highlight state types |
|
||||
| `canvas/src/modes/manager.rs` | `client/src/modes/handlers/mode_manager.rs` | Mode management |
|
||||
|
||||
## Import Replacements Needed in Client
|
||||
|
||||
### 1. CanvasState Trait Usage
|
||||
**Replace these imports:**
|
||||
```rust
|
||||
// OLD
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
|
||||
// NEW
|
||||
use canvas::CanvasState;
|
||||
```
|
||||
|
||||
**Files that need updating:**
|
||||
- `src/modes/canvas/edit.rs` - Line 9
|
||||
- `src/modes/canvas/read_only.rs` - Line 5
|
||||
- `src/ui/handlers/render.rs` - Line 17
|
||||
- `src/state/pages/auth.rs` - All CanvasState impls
|
||||
- `src/state/pages/form.rs` - CanvasState impl
|
||||
- `src/state/pages/add_table.rs` - CanvasState impl
|
||||
- `src/state/pages/add_logic.rs` - CanvasState impl
|
||||
|
||||
### 2. Edit Actions Usage
|
||||
**Replace these imports:**
|
||||
```rust
|
||||
// OLD
|
||||
use crate::functions::modes::edit::form_e::{execute_edit_action, execute_common_action};
|
||||
|
||||
// NEW
|
||||
use canvas::{execute_edit_action, execute_common_action};
|
||||
```
|
||||
|
||||
**Files that need updating:**
|
||||
- `src/modes/canvas/edit.rs` - Lines 3-5
|
||||
- `src/functions/modes/edit/auth_e.rs`
|
||||
- `src/functions/modes/edit/add_table_e.rs`
|
||||
- `src/functions/modes/edit/add_logic_e.rs`
|
||||
|
||||
### 3. Canvas Rendering Usage
|
||||
**Replace these imports:**
|
||||
```rust
|
||||
// OLD
|
||||
use crate::components::handlers::canvas::render_canvas;
|
||||
|
||||
// NEW
|
||||
use canvas::render_canvas;
|
||||
```
|
||||
|
||||
**Files that need updating:**
|
||||
- Any component that renders forms (login, register, add_table, add_logic, forms)
|
||||
|
||||
### 4. Mode System Usage
|
||||
**Replace these imports:**
|
||||
```rust
|
||||
// OLD
|
||||
use crate::modes::handlers::mode_manager::{AppMode, ModeManager};
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
|
||||
// NEW
|
||||
use canvas::{AppMode, ModeManager, HighlightState};
|
||||
```
|
||||
|
||||
**Files that need updating:**
|
||||
- `src/modes/handlers/event.rs` - Line 14
|
||||
- `src/ui/handlers/ui.rs` - Mode derivation calls
|
||||
- All mode handling files
|
||||
|
||||
## Theme Integration Required
|
||||
|
||||
The canvas crate expects a `CanvasTheme` trait. You need to implement this for your existing theme:
|
||||
|
||||
```rust
|
||||
// In client/src/config/colors/themes.rs
|
||||
use canvas::CanvasTheme;
|
||||
use ratatui::style::Color;
|
||||
|
||||
impl CanvasTheme for Theme {
|
||||
fn primary_fg(&self) -> Color { self.fg }
|
||||
fn primary_bg(&self) -> Color { self.bg }
|
||||
fn accent(&self) -> Color { self.accent }
|
||||
fn warning(&self) -> Color { self.warning }
|
||||
fn secondary(&self) -> Color { self.secondary }
|
||||
fn highlight(&self) -> Color { self.highlight }
|
||||
fn highlight_bg(&self) -> Color { self.highlight_bg }
|
||||
}
|
||||
```
|
||||
|
||||
## Systematic Replacement Strategy
|
||||
|
||||
### Phase 1: Fix Compilation (Do This First)
|
||||
1. Update `client/Cargo.toml` to depend on canvas
|
||||
2. Add theme implementation
|
||||
3. Replace imports in core files
|
||||
|
||||
### Phase 2: Replace Feature-Specific Usage
|
||||
1. Update auth components
|
||||
2. Update form components
|
||||
3. Update admin components
|
||||
4. Update mode handlers
|
||||
|
||||
### Phase 3: Remove Old Files (After Everything Works)
|
||||
1. Delete `src/state/pages/canvas_state.rs`
|
||||
2. Delete `src/functions/modes/edit/form_e.rs`
|
||||
3. Delete `src/components/handlers/canvas.rs`
|
||||
4. Delete `src/state/app/highlight.rs`
|
||||
5. Delete `src/modes/handlers/mode_manager.rs`
|
||||
|
||||
## Files Safe to Delete After Migration
|
||||
|
||||
**These can be removed once imports are updated:**
|
||||
- `client/src/state/pages/canvas_state.rs`
|
||||
- `client/src/functions/modes/edit/form_e.rs`
|
||||
- `client/src/components/handlers/canvas.rs`
|
||||
- `client/src/state/app/highlight.rs`
|
||||
- `client/src/modes/handlers/mode_manager.rs`
|
||||
|
||||
## Quick Start Commands
|
||||
|
||||
```bash
|
||||
# 1. Add canvas dependency to client
|
||||
cd client
|
||||
echo 'canvas = { path = "../canvas" }' >> Cargo.toml
|
||||
|
||||
# 2. Test compilation
|
||||
cargo check
|
||||
|
||||
# 3. Fix imports one file at a time
|
||||
# Start with: src/config/colors/themes.rs (add CanvasTheme impl)
|
||||
# Then: src/modes/canvas/edit.rs (replace form_e imports)
|
||||
# Then: src/modes/canvas/read_only.rs (replace canvas_state import)
|
||||
|
||||
# 4. After all imports fixed, delete old files
|
||||
rm src/state/pages/canvas_state.rs
|
||||
rm src/functions/modes/edit/form_e.rs
|
||||
rm src/components/handlers/canvas.rs
|
||||
rm src/state/app/highlight.rs
|
||||
rm src/modes/handlers/mode_manager.rs
|
||||
```
|
||||
|
||||
## Expected Compilation Errors
|
||||
|
||||
You'll get errors like:
|
||||
- `cannot find type 'CanvasState' in this scope`
|
||||
- `cannot find function 'execute_edit_action' in this scope`
|
||||
- `cannot find type 'AppMode' in this scope`
|
||||
|
||||
Fix these by replacing the imports as documented above.
|
||||
38
canvas/Cargo.toml
Normal file
38
canvas/Cargo.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "canvas"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
description.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
ratatui = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
[[example]]
|
||||
name = "simple_login"
|
||||
path = "examples/simple_login.rs"
|
||||
|
||||
[[example]]
|
||||
name = "config_screen"
|
||||
path = "examples/config_screen.rs"
|
||||
|
||||
[[example]]
|
||||
name = "basic_usage"
|
||||
path = "examples/basic_usage.rs"
|
||||
|
||||
[[example]]
|
||||
name = "integration_patterns"
|
||||
path = "examples/integration_patterns.rs"
|
||||
366
canvas/README.md
Normal file
366
canvas/README.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# Canvas 🎨
|
||||
|
||||
A reusable, type-safe canvas system for building form-based TUI applications with vim-like modal editing.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Type-Safe Actions**: No more string-based action names - everything is compile-time checked
|
||||
- **Generic Design**: Implement `CanvasState` once, get navigation, editing, and suggestions for free
|
||||
- **Vim-Like Experience**: Modal editing with familiar keybindings
|
||||
- **Suggestion System**: Built-in autocomplete and suggestions support
|
||||
- **Framework Agnostic**: Works with any TUI framework or raw terminal handling
|
||||
- **Async Ready**: Full async/await support for modern Rust applications
|
||||
- **Batch Operations**: Execute multiple actions atomically
|
||||
- **Extensible**: Custom actions and feature-specific handling
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Add to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
cargo add canvas
|
||||
```
|
||||
|
||||
Implement the `CanvasState` trait:
|
||||
|
||||
```rust
|
||||
use canvas::prelude::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LoginForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
username: String,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl CanvasState for LoginForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.username,
|
||||
1 => &self.password,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
1 => &mut self.password,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.username, &self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Username", "Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
```
|
||||
|
||||
Use the type-safe action dispatcher:
|
||||
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut form = LoginForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Type a character - compile-time safe!
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar('h'),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await?;
|
||||
|
||||
// Move to next field
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::NextField,
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await?;
|
||||
|
||||
// Batch operations
|
||||
let actions = vec![
|
||||
CanvasAction::InsertChar('p'),
|
||||
CanvasAction::InsertChar('a'),
|
||||
CanvasAction::InsertChar('s'),
|
||||
CanvasAction::InsertChar('s'),
|
||||
];
|
||||
|
||||
ActionDispatcher::dispatch_batch(actions, &mut form, &mut ideal_cursor).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 Examples
|
||||
|
||||
The `examples/` directory contains comprehensive examples showing different usage patterns:
|
||||
|
||||
### Run the Examples
|
||||
|
||||
```bash
|
||||
# Basic login form with TUI
|
||||
cargo run --example simple_login
|
||||
|
||||
# Advanced configuration screen with suggestions and validation
|
||||
cargo run --example config_screen
|
||||
|
||||
# API usage patterns and quick start guide
|
||||
cargo run --example basic_usage
|
||||
|
||||
# Advanced integration patterns (state machines, events, validation)
|
||||
cargo run --example integration_patterns
|
||||
```
|
||||
|
||||
### Example Overview
|
||||
|
||||
| Example | Description | Key Features |
|
||||
|---------|-------------|--------------|
|
||||
| `simple_login` | Interactive login form TUI | Basic form, custom actions, password masking |
|
||||
| `config_screen` | Configuration editor | Auto-suggestions, field validation, complex UI |
|
||||
| `basic_usage` | API demonstration | All core patterns, non-interactive |
|
||||
| `integration_patterns` | Architecture patterns | State machines, events, validation pipelines |
|
||||
|
||||
## 🎯 Type-Safe Actions
|
||||
|
||||
The Canvas system uses strongly-typed actions instead of error-prone strings:
|
||||
|
||||
```rust
|
||||
// ✅ Type-safe - impossible to make typos
|
||||
ActionDispatcher::dispatch(CanvasAction::MoveLeft, &mut form, &mut cursor).await?;
|
||||
|
||||
// ❌ Old way - runtime errors waiting to happen
|
||||
execute_edit_action("move_left", key, &mut form, &mut cursor).await?;
|
||||
execute_edit_action("move_leftt", key, &mut form, &mut cursor).await?; // Oops!
|
||||
```
|
||||
|
||||
### Available Actions
|
||||
|
||||
```rust
|
||||
pub enum CanvasAction {
|
||||
// Character input
|
||||
InsertChar(char),
|
||||
|
||||
// Deletion
|
||||
DeleteBackward,
|
||||
DeleteForward,
|
||||
|
||||
// Movement
|
||||
MoveLeft, MoveRight, MoveUp, MoveDown,
|
||||
MoveLineStart, MoveLineEnd,
|
||||
MoveWordNext, MoveWordPrev,
|
||||
|
||||
// Navigation
|
||||
NextField, PrevField,
|
||||
MoveFirstLine, MoveLastLine,
|
||||
|
||||
// Suggestions
|
||||
SuggestionUp, SuggestionDown,
|
||||
SelectSuggestion, ExitSuggestions,
|
||||
|
||||
// Extensibility
|
||||
Custom(String),
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Suggestions and Autocomplete
|
||||
|
||||
```rust
|
||||
impl CanvasState for MyForm {
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
if self.suggestions.is_active {
|
||||
Some(&self.suggestions.suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::InsertChar('@') => {
|
||||
// Trigger email suggestions
|
||||
let suggestions = vec![
|
||||
format!("{}@gmail.com", self.username),
|
||||
format!("{}@company.com", self.username),
|
||||
];
|
||||
self.activate_suggestions(suggestions);
|
||||
None // Let generic handler insert the '@'
|
||||
}
|
||||
CanvasAction::SelectSuggestion => {
|
||||
if let Some(suggestion) = self.suggestions.get_selected() {
|
||||
*self.get_current_input_mut() = suggestion.clone();
|
||||
self.deactivate_suggestions();
|
||||
Some("Applied suggestion".to_string())
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Actions
|
||||
|
||||
```rust
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"uppercase" => {
|
||||
*self.get_current_input_mut() = self.get_current_input().to_uppercase();
|
||||
Some("Converted to uppercase".to_string())
|
||||
}
|
||||
"validate_email" => {
|
||||
if self.get_current_input().contains('@') {
|
||||
Some("Email is valid".to_string())
|
||||
} else {
|
||||
Some("Invalid email format".to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with TUI Frameworks
|
||||
|
||||
Canvas is framework-agnostic and works with any TUI library:
|
||||
|
||||
```rust
|
||||
// Works with crossterm (see examples)
|
||||
// Works with termion
|
||||
// Works with ratatui/tui-rs
|
||||
// Works with cursive
|
||||
// Works with raw terminal I/O
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
Canvas follows a clean, layered architecture:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Your Application │
|
||||
├─────────────────────────────────────┤
|
||||
│ ActionDispatcher │ ← High-level API
|
||||
├─────────────────────────────────────┤
|
||||
│ CanvasAction (Type-Safe) │ ← Type safety layer
|
||||
├─────────────────────────────────────┤
|
||||
│ Action Handlers │ ← Core logic
|
||||
├─────────────────────────────────────┤
|
||||
│ CanvasState Trait │ ← Your implementation
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🤝 Why Canvas?
|
||||
|
||||
### Before Canvas
|
||||
```rust
|
||||
// ❌ Error-prone string actions
|
||||
execute_action("move_left", key, state)?;
|
||||
execute_action("move_leftt", key, state)?; // Runtime error!
|
||||
|
||||
// ❌ Duplicate navigation logic everywhere
|
||||
impl MyLoginForm { /* navigation code */ }
|
||||
impl MyConfigForm { /* same navigation code */ }
|
||||
impl MyDataForm { /* same navigation code again */ }
|
||||
|
||||
// ❌ Manual cursor and field management
|
||||
if key == Key::Tab {
|
||||
current_field = (current_field + 1) % fields.len();
|
||||
cursor_pos = cursor_pos.min(current_input.len());
|
||||
}
|
||||
```
|
||||
|
||||
### With Canvas
|
||||
```rust
|
||||
// ✅ Type-safe actions
|
||||
ActionDispatcher::dispatch(CanvasAction::MoveLeft, state, cursor)?;
|
||||
// Typos are impossible - won't compile!
|
||||
|
||||
// ✅ Implement once, use everywhere
|
||||
impl CanvasState for MyForm { /* minimal implementation */ }
|
||||
// All navigation, editing, suggestions work automatically!
|
||||
|
||||
// ✅ High-level operations
|
||||
ActionDispatcher::dispatch_batch(actions, state, cursor)?;
|
||||
```
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- **API Docs**: `cargo doc --open`
|
||||
- **Examples**: See `examples/` directory
|
||||
- **Migration Guide**: See `CANVAS_MIGRATION.md`
|
||||
|
||||
## 🔄 Migration from String-Based Actions
|
||||
|
||||
Canvas provides backwards compatibility during migration:
|
||||
|
||||
```rust
|
||||
// Legacy support (deprecated)
|
||||
execute_edit_action("move_left", key, state, cursor).await?;
|
||||
|
||||
// New type-safe way
|
||||
ActionDispatcher::dispatch(CanvasAction::MoveLeft, state, cursor).await?;
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run specific example
|
||||
cargo run --example simple_login
|
||||
|
||||
# Check type safety
|
||||
cargo check
|
||||
```
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
- Rust 1.70+
|
||||
- Terminal with cursor support
|
||||
- Optional: async runtime (tokio) for examples
|
||||
|
||||
## 🤔 FAQ
|
||||
|
||||
**Q: Does Canvas work with [my TUI framework]?**
|
||||
A: Yes! Canvas is framework-agnostic. Just implement `CanvasState` and handle the key events.
|
||||
|
||||
**Q: Can I extend Canvas with custom actions?**
|
||||
A: Absolutely! Use `CanvasAction::Custom("my_action")` or implement `handle_feature_action`.
|
||||
|
||||
**Q: Is Canvas suitable for complex forms?**
|
||||
A: Yes! See the `config_screen` example for validation, suggestions, and multi-field forms.
|
||||
|
||||
**Q: How do I migrate from string-based actions?**
|
||||
A: Canvas provides backwards compatibility. Migrate incrementally using the type-safe APIs.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under either of:
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
- MIT License ([LICENSE-MIT](LICENSE-MIT))
|
||||
|
||||
at your option.
|
||||
|
||||
## 🙏 Contributing
|
||||
|
||||
Will write here something later on, too busy rn
|
||||
|
||||
---
|
||||
|
||||
Built with ❤️ for the Rust TUI community
|
||||
56
canvas/canvas_config.toml
Normal file
56
canvas/canvas_config.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
# canvas_config.toml - Complete Canvas Configuration
|
||||
|
||||
[behavior]
|
||||
wrap_around_fields = true
|
||||
auto_save_on_field_change = false
|
||||
word_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
|
||||
max_suggestions = 6
|
||||
|
||||
[appearance]
|
||||
cursor_style = "block" # "block", "bar", "underline"
|
||||
show_field_numbers = false
|
||||
highlight_current_field = true
|
||||
|
||||
# Read-only mode keybindings (vim-style)
|
||||
[keybindings.read_only]
|
||||
move_left = ["h"]
|
||||
move_right = ["l"]
|
||||
move_up = ["k"]
|
||||
move_down = ["p"]
|
||||
move_word_next = ["w"]
|
||||
move_word_end = ["e"]
|
||||
move_word_prev = ["b"]
|
||||
move_word_end_prev = ["ge"]
|
||||
move_line_start = ["0"]
|
||||
move_line_end = ["$"]
|
||||
move_first_line = ["gg"]
|
||||
move_last_line = ["G"]
|
||||
next_field = ["Tab"]
|
||||
prev_field = ["Shift+Tab"]
|
||||
|
||||
# Edit mode keybindings
|
||||
[keybindings.edit]
|
||||
delete_char_backward = ["Backspace"]
|
||||
delete_char_forward = ["Delete"]
|
||||
move_left = ["Left"]
|
||||
move_right = ["Right"]
|
||||
move_up = ["Up"]
|
||||
move_down = ["Down"]
|
||||
move_line_start = ["Home"]
|
||||
move_line_end = ["End"]
|
||||
move_word_next = ["Ctrl+Right"]
|
||||
move_word_prev = ["Ctrl+Left"]
|
||||
next_field = ["Tab"]
|
||||
prev_field = ["Shift+Tab"]
|
||||
|
||||
# Suggestion/autocomplete keybindings
|
||||
[keybindings.suggestions]
|
||||
suggestion_up = ["Up", "Ctrl+p"]
|
||||
suggestion_down = ["Down", "Ctrl+n"]
|
||||
select_suggestion = ["Enter", "Tab"]
|
||||
exit_suggestions = ["Esc"]
|
||||
|
||||
# Global keybindings (work in both modes)
|
||||
[keybindings.global]
|
||||
move_up = ["Up"]
|
||||
move_down = ["Down"]
|
||||
378
canvas/examples/basic_usage.rs
Normal file
378
canvas/examples/basic_usage.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
// examples/basic_usage.rs
|
||||
//! Basic usage patterns and quick start guide
|
||||
//!
|
||||
//! This example demonstrates the core patterns for using the canvas crate:
|
||||
//! 1. Implementing CanvasState
|
||||
//! 2. Using the ActionDispatcher
|
||||
//! 3. Handling different types of actions
|
||||
//! 4. Working with suggestions
|
||||
//!
|
||||
//! Run with: cargo run --example basic_usage
|
||||
|
||||
use canvas::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("🎨 Canvas Crate - Basic Usage Patterns");
|
||||
println!("=====================================\n");
|
||||
|
||||
// Example 1: Minimal form implementation
|
||||
example_1_minimal_form();
|
||||
|
||||
// Example 2: Form with suggestions
|
||||
example_2_with_suggestions();
|
||||
|
||||
// Example 3: Custom actions
|
||||
example_3_custom_actions().await;
|
||||
|
||||
// Example 4: Batch operations
|
||||
example_4_batch_operations().await;
|
||||
}
|
||||
|
||||
// Example 1: Minimal form - just the required methods
|
||||
fn example_1_minimal_form() {
|
||||
println!("📝 Example 1: Minimal Form Implementation");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SimpleForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
name: String,
|
||||
email: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl SimpleForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
name: String::new(),
|
||||
email: String::new(),
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for SimpleForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.name,
|
||||
1 => &self.email,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.name,
|
||||
1 => &mut self.email,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.email] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Name", "Email"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
let form = SimpleForm::new();
|
||||
println!(" Created form with {} fields", form.fields().len());
|
||||
println!(" Current field: {}", form.fields()[form.current_field()]);
|
||||
println!(" ✅ Minimal implementation works!\n");
|
||||
}
|
||||
|
||||
// Example 2: Form with suggestion support
|
||||
fn example_2_with_suggestions() {
|
||||
println!("💡 Example 2: Form with Suggestions");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FormWithSuggestions {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
country: String,
|
||||
has_changes: bool,
|
||||
suggestions: SuggestionState,
|
||||
}
|
||||
|
||||
impl FormWithSuggestions {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
country: String::new(),
|
||||
has_changes: false,
|
||||
suggestions: SuggestionState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for FormWithSuggestions {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.country }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.country }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.country] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Country"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
// Suggestion support
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
if self.suggestions.is_active {
|
||||
Some(&self.suggestions.suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
self.suggestions.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_suggestion_index(&mut self, index: Option<usize>) {
|
||||
self.suggestions.selected_index = index;
|
||||
}
|
||||
|
||||
fn activate_suggestions(&mut self, suggestions: Vec<String>) {
|
||||
self.suggestions.activate_with_suggestions(suggestions);
|
||||
}
|
||||
|
||||
fn deactivate_suggestions(&mut self) {
|
||||
self.suggestions.deactivate();
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::SelectSuggestion => {
|
||||
// Fix: Clone the suggestion first to avoid borrow checker issues
|
||||
if let Some(suggestion) = self.suggestions.get_selected().cloned() {
|
||||
self.country = suggestion.clone();
|
||||
self.cursor_pos = suggestion.len();
|
||||
self.deactivate_suggestions();
|
||||
self.has_changes = true;
|
||||
return Some(format!("Selected: {}", suggestion));
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = FormWithSuggestions::new();
|
||||
|
||||
// Simulate user typing and triggering suggestions
|
||||
form.activate_suggestions(vec![
|
||||
"United States".to_string(),
|
||||
"United Kingdom".to_string(),
|
||||
"Ukraine".to_string(),
|
||||
]);
|
||||
|
||||
println!(" Activated suggestions: {:?}", form.get_suggestions().unwrap());
|
||||
println!(" Current selection: {:?}", form.get_selected_suggestion_index());
|
||||
|
||||
// Navigate suggestions
|
||||
form.set_selected_suggestion_index(Some(1));
|
||||
println!(" Navigated to: {}", form.suggestions.get_selected().unwrap());
|
||||
println!(" ✅ Suggestions work!\n");
|
||||
}
|
||||
|
||||
// Example 3: Custom actions
|
||||
async fn example_3_custom_actions() {
|
||||
println!("⚡ Example 3: Custom Actions");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FormWithCustomActions {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
text: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl FormWithCustomActions {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
text: "hello world".to_string(),
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for FormWithCustomActions {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.text }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.text }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.text] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Text"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"uppercase" => {
|
||||
self.text = self.text.to_uppercase();
|
||||
self.has_changes = true;
|
||||
Some("Converted to uppercase".to_string())
|
||||
}
|
||||
"reverse" => {
|
||||
self.text = self.text.chars().rev().collect();
|
||||
self.has_changes = true;
|
||||
Some("Reversed text".to_string())
|
||||
}
|
||||
"word_count" => {
|
||||
let count = self.text.split_whitespace().count();
|
||||
Some(format!("Word count: {}", count))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = FormWithCustomActions::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
println!(" Initial text: '{}'", form.text);
|
||||
|
||||
// Execute custom actions
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("uppercase".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" After uppercase: '{}' - {}", form.text, result.message().unwrap());
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("reverse".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" After reverse: '{}' - {}", form.text, result.message().unwrap());
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("word_count".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" {}", result.message().unwrap());
|
||||
println!(" ✅ Custom actions work!\n");
|
||||
}
|
||||
|
||||
// Example 4: Batch operations
|
||||
async fn example_4_batch_operations() {
|
||||
println!("📦 Example 4: Batch Operations");
|
||||
|
||||
// Reuse the simple form from example 1
|
||||
#[derive(Debug)]
|
||||
struct SimpleForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
name: String,
|
||||
email: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl SimpleForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
name: String::new(),
|
||||
email: String::new(),
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for SimpleForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.name,
|
||||
1 => &self.email,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.name,
|
||||
1 => &mut self.email,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.email] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Name", "Email"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
let mut form = SimpleForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Execute a sequence of actions to type "John" in the name field
|
||||
let actions = vec![
|
||||
CanvasAction::InsertChar('J'),
|
||||
CanvasAction::InsertChar('o'),
|
||||
CanvasAction::InsertChar('h'),
|
||||
CanvasAction::InsertChar('n'),
|
||||
CanvasAction::NextField, // Move to email field
|
||||
CanvasAction::InsertChar('j'),
|
||||
CanvasAction::InsertChar('o'),
|
||||
CanvasAction::InsertChar('h'),
|
||||
CanvasAction::InsertChar('n'),
|
||||
CanvasAction::InsertChar('@'),
|
||||
CanvasAction::InsertChar('e'),
|
||||
CanvasAction::InsertChar('x'),
|
||||
CanvasAction::InsertChar('a'),
|
||||
CanvasAction::InsertChar('m'),
|
||||
CanvasAction::InsertChar('p'),
|
||||
CanvasAction::InsertChar('l'),
|
||||
CanvasAction::InsertChar('e'),
|
||||
CanvasAction::InsertChar('.'),
|
||||
CanvasAction::InsertChar('c'),
|
||||
CanvasAction::InsertChar('o'),
|
||||
CanvasAction::InsertChar('m'),
|
||||
];
|
||||
|
||||
println!(" Executing {} actions in batch...", actions.len());
|
||||
|
||||
let results = ActionDispatcher::dispatch_batch(
|
||||
actions,
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" Completed {} actions", results.len());
|
||||
println!(" Final state:");
|
||||
println!(" Name: '{}'", form.name);
|
||||
println!(" Email: '{}'", form.email);
|
||||
println!(" Current field: {}", form.fields()[form.current_field()]);
|
||||
println!(" Has changes: {}", form.has_changes);
|
||||
}
|
||||
590
canvas/examples/config_screen.rs
Normal file
590
canvas/examples/config_screen.rs
Normal file
@@ -0,0 +1,590 @@
|
||||
// examples/config_screen.rs
|
||||
//! Advanced configuration screen with suggestions and validation
|
||||
//!
|
||||
//! This example demonstrates:
|
||||
//! - Multiple field types
|
||||
//! - Auto-suggestions
|
||||
//! - Field validation
|
||||
//! - Custom actions
|
||||
//!
|
||||
//! Run with: cargo run --example config_screen
|
||||
|
||||
use canvas::prelude::*;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ConfigForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
|
||||
// Configuration fields
|
||||
server_host: String,
|
||||
server_port: String,
|
||||
database_url: String,
|
||||
log_level: String,
|
||||
max_connections: String,
|
||||
|
||||
has_changes: bool,
|
||||
suggestions: SuggestionState,
|
||||
}
|
||||
|
||||
impl ConfigForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
server_host: "localhost".to_string(),
|
||||
server_port: "8080".to_string(),
|
||||
database_url: String::new(),
|
||||
log_level: "info".to_string(),
|
||||
max_connections: "100".to_string(),
|
||||
has_changes: false,
|
||||
suggestions: SuggestionState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn field_names() -> Vec<&'static str> {
|
||||
vec![
|
||||
"Server Host",
|
||||
"Server Port",
|
||||
"Database URL",
|
||||
"Log Level",
|
||||
"Max Connections"
|
||||
]
|
||||
}
|
||||
|
||||
fn get_field_value(&self, index: usize) -> &String {
|
||||
match index {
|
||||
0 => &self.server_host,
|
||||
1 => &self.server_port,
|
||||
2 => &self.database_url,
|
||||
3 => &self.log_level,
|
||||
4 => &self.max_connections,
|
||||
_ => panic!("Invalid field index: {}", index),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_field_value_mut(&mut self, index: usize) -> &mut String {
|
||||
match index {
|
||||
0 => &mut self.server_host,
|
||||
1 => &mut self.server_port,
|
||||
2 => &mut self.database_url,
|
||||
3 => &mut self.log_level,
|
||||
4 => &mut self.max_connections,
|
||||
_ => panic!("Invalid field index: {}", index),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_field(&self, index: usize) -> Option<String> {
|
||||
let value = self.get_field_value(index);
|
||||
match index {
|
||||
0 => { // Server Host
|
||||
if value.trim().is_empty() {
|
||||
Some("Server host cannot be empty".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
1 => { // Server Port
|
||||
if let Ok(port) = value.parse::<u16>() {
|
||||
if port == 0 {
|
||||
Some("Port must be greater than 0".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some("Port must be a valid number (1-65535)".to_string())
|
||||
}
|
||||
}
|
||||
2 => { // Database URL
|
||||
if !value.is_empty() && !value.starts_with("postgresql://") && !value.starts_with("mysql://") && !value.starts_with("sqlite://") {
|
||||
Some("Database URL should start with postgresql://, mysql://, or sqlite://".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
3 => { // Log Level
|
||||
let valid_levels = ["trace", "debug", "info", "warn", "error"];
|
||||
if !valid_levels.contains(&value.to_lowercase().as_str()) {
|
||||
Some("Log level must be one of: trace, debug, info, warn, error".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
4 => { // Max Connections
|
||||
if let Ok(connections) = value.parse::<u32>() {
|
||||
if connections == 0 {
|
||||
Some("Max connections must be greater than 0".to_string())
|
||||
} else if connections > 10000 {
|
||||
Some("Max connections seems too high (>10000)".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some("Max connections must be a valid number".to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_suggestions_for_field(&self, index: usize, current_value: &str) -> Vec<String> {
|
||||
match index {
|
||||
0 => { // Server Host
|
||||
vec![
|
||||
"localhost".to_string(),
|
||||
"127.0.0.1".to_string(),
|
||||
"0.0.0.0".to_string(),
|
||||
format!("{}.local", current_value),
|
||||
]
|
||||
}
|
||||
1 => { // Server Port
|
||||
vec![
|
||||
"8080".to_string(),
|
||||
"3000".to_string(),
|
||||
"8000".to_string(),
|
||||
"80".to_string(),
|
||||
"443".to_string(),
|
||||
]
|
||||
}
|
||||
2 => { // Database URL
|
||||
if current_value.is_empty() {
|
||||
vec![
|
||||
"postgresql://localhost:5432/mydb".to_string(),
|
||||
"mysql://localhost:3306/mydb".to_string(),
|
||||
"sqlite://./database.db".to_string(),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
3 => { // Log Level
|
||||
vec![
|
||||
"trace".to_string(),
|
||||
"debug".to_string(),
|
||||
"info".to_string(),
|
||||
"warn".to_string(),
|
||||
"error".to_string(),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|level| level.starts_with(¤t_value.to_lowercase()))
|
||||
.collect()
|
||||
}
|
||||
4 => { // Max Connections
|
||||
vec![
|
||||
"10".to_string(),
|
||||
"50".to_string(),
|
||||
"100".to_string(),
|
||||
"200".to_string(),
|
||||
"500".to_string(),
|
||||
]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for ConfigForm {
|
||||
fn current_field(&self) -> usize {
|
||||
self.current_field
|
||||
}
|
||||
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
self.cursor_pos
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
self.current_field = index.min(4); // 5 fields total (0-4)
|
||||
// Deactivate suggestions when changing fields
|
||||
self.deactivate_suggestions();
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
self.cursor_pos = pos;
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
self.get_field_value(self.current_field)
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
self.get_field_value_mut(self.current_field)
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
vec![
|
||||
&self.server_host,
|
||||
&self.server_port,
|
||||
&self.database_url,
|
||||
&self.log_level,
|
||||
&self.max_connections,
|
||||
]
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
Self::field_names()
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_changes
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
// Suggestion support
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
if self.suggestions.is_active {
|
||||
Some(&self.suggestions.suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
self.suggestions.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_suggestion_index(&mut self, index: Option<usize>) {
|
||||
self.suggestions.selected_index = index;
|
||||
}
|
||||
|
||||
fn activate_suggestions(&mut self, suggestions: Vec<String>) {
|
||||
self.suggestions.activate_with_suggestions(suggestions);
|
||||
}
|
||||
|
||||
fn deactivate_suggestions(&mut self) {
|
||||
self.suggestions.deactivate();
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::SelectSuggestion => {
|
||||
// Fix: Clone the suggestion first to avoid borrow checker issues
|
||||
if let Some(suggestion) = self.suggestions.get_selected().cloned() {
|
||||
*self.get_current_input_mut() = suggestion.clone();
|
||||
self.set_current_cursor_pos(suggestion.len());
|
||||
self.deactivate_suggestions();
|
||||
self.set_has_unsaved_changes(true);
|
||||
return Some("Applied suggestion".to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"trigger_suggestions" => {
|
||||
let current_value = self.get_current_input();
|
||||
let suggestions = self.get_suggestions_for_field(self.current_field, current_value);
|
||||
if !suggestions.is_empty() {
|
||||
self.activate_suggestions(suggestions);
|
||||
Some("Showing suggestions".to_string())
|
||||
} else {
|
||||
Some("No suggestions available".to_string())
|
||||
}
|
||||
}
|
||||
"validate_current" => {
|
||||
if let Some(error) = self.validate_field(self.current_field) {
|
||||
Some(format!("Validation Error: {}", error))
|
||||
} else {
|
||||
Some("Field is valid".to_string())
|
||||
}
|
||||
}
|
||||
"validate_all" => {
|
||||
let mut errors = Vec::new();
|
||||
for i in 0..5 {
|
||||
if let Some(error) = self.validate_field(i) {
|
||||
errors.push(format!("{}: {}", Self::field_names()[i], error));
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
Some("All fields are valid!".to_string())
|
||||
} else {
|
||||
Some(format!("Errors found: {}", errors.join("; ")))
|
||||
}
|
||||
}
|
||||
"save_config" => {
|
||||
// Validate all fields first
|
||||
for i in 0..5 {
|
||||
if self.validate_field(i).is_some() {
|
||||
return Some("Cannot save: Please fix validation errors first".to_string());
|
||||
}
|
||||
}
|
||||
self.set_has_unsaved_changes(false);
|
||||
Some("Configuration saved successfully!".to_string())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
|
||||
// Auto-trigger suggestions for certain fields
|
||||
CanvasAction::InsertChar(_) => {
|
||||
// After character insertion, check if we should show suggestions
|
||||
match self.current_field {
|
||||
3 => { // Log level - always show suggestions for autocomplete
|
||||
let current_value = self.get_current_input();
|
||||
let suggestions = self.get_suggestions_for_field(self.current_field, current_value);
|
||||
if !suggestions.is_empty() {
|
||||
self.activate_suggestions(suggestions);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None // Let the generic handler insert the character
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_ui(form: &ConfigForm, message: &str) -> io::Result<()> {
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
|
||||
println!("╔════════════════════════════════════════════════════════════════╗");
|
||||
println!("║ CONFIGURATION EDITOR ║");
|
||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
||||
|
||||
let field_names = ConfigForm::field_names();
|
||||
|
||||
for (i, field_name) in field_names.iter().enumerate() {
|
||||
let is_current = i == form.current_field;
|
||||
let indicator = if is_current { ">" } else { " " };
|
||||
let value = form.get_field_value(i);
|
||||
let display_value = if value.is_empty() {
|
||||
format!("<enter {}>", field_name.to_lowercase())
|
||||
} else {
|
||||
value.clone()
|
||||
};
|
||||
|
||||
// Truncate long values for display
|
||||
let display_value = if display_value.len() > 35 {
|
||||
format!("{}...", &display_value[..32])
|
||||
} else {
|
||||
display_value
|
||||
};
|
||||
|
||||
println!("║ {} {:15}: {:35} ║", indicator, field_name, display_value);
|
||||
|
||||
// Show cursor for current field
|
||||
if is_current {
|
||||
let cursor_pos = form.cursor_pos.min(value.len());
|
||||
let cursor_line = format!("║ {}{}║",
|
||||
" ".repeat(18 + cursor_pos),
|
||||
"▊"
|
||||
);
|
||||
println!("{:66}", cursor_line);
|
||||
}
|
||||
|
||||
// Show validation error if any
|
||||
if let Some(error) = form.validate_field(i) {
|
||||
let error_display = if error.len() > 58 {
|
||||
format!("{}...", &error[..55])
|
||||
} else {
|
||||
error
|
||||
};
|
||||
println!("║ ⚠️ {:58} ║", error_display);
|
||||
} else if is_current {
|
||||
println!("║{:64}║", "");
|
||||
}
|
||||
}
|
||||
|
||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
||||
|
||||
// Show suggestions if active
|
||||
if let Some(suggestions) = form.get_suggestions() {
|
||||
println!("║ SUGGESTIONS: ║");
|
||||
for (i, suggestion) in suggestions.iter().enumerate() {
|
||||
let selected = form.get_selected_suggestion_index() == Some(i);
|
||||
let marker = if selected { "→" } else { " " };
|
||||
let display_suggestion = if suggestion.len() > 55 {
|
||||
format!("{}...", &suggestion[..52])
|
||||
} else {
|
||||
suggestion.clone()
|
||||
};
|
||||
println!("║ {} {:58} ║", marker, display_suggestion);
|
||||
}
|
||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
||||
}
|
||||
|
||||
println!("║ CONTROLS: ║");
|
||||
println!("║ Tab/↑↓ - Navigate fields ║");
|
||||
println!("║ Ctrl+Space - Show suggestions ║");
|
||||
println!("║ ↑↓ - Navigate suggestions (when shown) ║");
|
||||
println!("║ Enter - Select suggestion / Validate field ║");
|
||||
println!("║ Ctrl+S - Save configuration ║");
|
||||
println!("║ Ctrl+V - Validate all fields ║");
|
||||
println!("║ Ctrl+C - Exit ║");
|
||||
println!("╠════════════════════════════════════════════════════════════════╣");
|
||||
|
||||
// Status
|
||||
let status = if !message.is_empty() {
|
||||
message.to_string()
|
||||
} else if form.has_changes {
|
||||
"Configuration modified - press Ctrl+S to save".to_string()
|
||||
} else {
|
||||
"Ready".to_string()
|
||||
};
|
||||
|
||||
let status_display = if status.len() > 58 {
|
||||
format!("{}...", &status[..55])
|
||||
} else {
|
||||
status
|
||||
};
|
||||
|
||||
println!("║ Status: {:55} ║", status_display);
|
||||
println!("╚════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
io::stdout().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
io::stdout().execute(EnterAlternateScreen)?;
|
||||
|
||||
let mut form = ConfigForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
let mut message = String::new();
|
||||
|
||||
draw_ui(&form, &message)?;
|
||||
|
||||
loop {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if !message.is_empty() {
|
||||
message.clear();
|
||||
}
|
||||
|
||||
match key {
|
||||
// Exit
|
||||
KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
break;
|
||||
}
|
||||
|
||||
// Show suggestions
|
||||
KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("trigger_suggestions".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate current field or select suggestion
|
||||
KeyEvent { code: KeyCode::Enter, .. } => {
|
||||
if form.get_suggestions().is_some() {
|
||||
// Select suggestion
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::SelectSuggestion,
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
} else {
|
||||
// Validate current field
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate_current".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save configuration
|
||||
KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("save_config".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Validate all fields
|
||||
KeyEvent { code: KeyCode::Char('v'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate_all".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle up/down for suggestions
|
||||
KeyEvent { code: KeyCode::Up, .. } => {
|
||||
let action = if form.get_suggestions().is_some() {
|
||||
CanvasAction::SuggestionUp
|
||||
} else {
|
||||
CanvasAction::MoveUp
|
||||
};
|
||||
|
||||
let _ = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await;
|
||||
}
|
||||
|
||||
KeyEvent { code: KeyCode::Down, .. } => {
|
||||
let action = if form.get_suggestions().is_some() {
|
||||
CanvasAction::SuggestionDown
|
||||
} else {
|
||||
CanvasAction::MoveDown
|
||||
};
|
||||
|
||||
let _ = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await;
|
||||
}
|
||||
|
||||
// Handle escape to close suggestions
|
||||
KeyEvent { code: KeyCode::Esc, .. } => {
|
||||
if form.get_suggestions().is_some() {
|
||||
let _ = ActionDispatcher::dispatch(
|
||||
CanvasAction::ExitSuggestions,
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular key handling
|
||||
_ => {
|
||||
if let Some(action) = CanvasAction::from_key(key.code) {
|
||||
let result = ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await.unwrap();
|
||||
|
||||
if !result.is_success() {
|
||||
if let Some(msg) = result.message() {
|
||||
message = format!("Error: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_ui(&form, &message)?;
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
io::stdout().execute(LeaveAlternateScreen)?;
|
||||
println!("Configuration editor closed!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
617
canvas/examples/integration_patterns.rs
Normal file
617
canvas/examples/integration_patterns.rs
Normal file
@@ -0,0 +1,617 @@
|
||||
// examples/integration_patterns.rs
|
||||
//! Advanced integration patterns showing how Canvas works with:
|
||||
//! - State management patterns
|
||||
//! - Event-driven architectures
|
||||
//! - Validation systems
|
||||
//! - Custom rendering
|
||||
//!
|
||||
//! Run with: cargo run --example integration_patterns
|
||||
|
||||
use canvas::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("🔧 Canvas Integration Patterns");
|
||||
println!("==============================\n");
|
||||
|
||||
// Pattern 1: State machine integration
|
||||
state_machine_example().await;
|
||||
|
||||
// Pattern 2: Event-driven architecture
|
||||
event_driven_example().await;
|
||||
|
||||
// Pattern 3: Validation pipeline
|
||||
validation_pipeline_example().await;
|
||||
|
||||
// Pattern 4: Multi-form orchestration
|
||||
multi_form_example().await;
|
||||
}
|
||||
|
||||
// Pattern 1: Canvas with state machine
|
||||
async fn state_machine_example() {
|
||||
println!("🔄 Pattern 1: State Machine Integration");
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum FormState {
|
||||
Initial,
|
||||
Editing,
|
||||
Validating,
|
||||
Submitting,
|
||||
Success,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StateMachineForm {
|
||||
// Canvas state
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
username: String,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
|
||||
// State machine
|
||||
state: FormState,
|
||||
}
|
||||
|
||||
impl StateMachineForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
state: FormState::Initial,
|
||||
}
|
||||
}
|
||||
|
||||
fn transition_to(&mut self, new_state: FormState) -> String {
|
||||
let old_state = self.state.clone();
|
||||
self.state = new_state;
|
||||
format!("State transition: {:?} -> {:?}", old_state, self.state)
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
matches!(self.state, FormState::Editing) &&
|
||||
!self.username.trim().is_empty() &&
|
||||
!self.password.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for StateMachineForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.username,
|
||||
1 => &self.password,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
1 => &mut self.password,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.username, &self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Username", "Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
// Transition to editing state when user starts typing
|
||||
if changed && self.state == FormState::Initial {
|
||||
self.state = FormState::Editing;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"submit" => {
|
||||
if self.can_submit() {
|
||||
let msg = self.transition_to(FormState::Submitting);
|
||||
// Simulate submission
|
||||
self.state = FormState::Success;
|
||||
Some(format!("{} -> Form submitted successfully", msg))
|
||||
} else {
|
||||
let msg = self.transition_to(FormState::Error("Invalid form data".to_string()));
|
||||
Some(msg)
|
||||
}
|
||||
}
|
||||
"reset" => {
|
||||
self.username.clear();
|
||||
self.password.clear();
|
||||
self.has_changes = false;
|
||||
Some(self.transition_to(FormState::Initial))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = StateMachineForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
println!(" Initial state: {:?}", form.state);
|
||||
|
||||
// Type some text to trigger state change
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar('u'),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" After typing: {:?}", form.state);
|
||||
|
||||
// Try to submit (should fail)
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("submit".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Submit result: {}", result.message().unwrap_or(""));
|
||||
println!(" ✅ State machine integration works!\n");
|
||||
}
|
||||
|
||||
// Pattern 2: Event-driven architecture
|
||||
async fn event_driven_example() {
|
||||
println!("📡 Pattern 2: Event-Driven Architecture");
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum FormEvent {
|
||||
FieldChanged { field: usize, old_value: String, new_value: String },
|
||||
ValidationTriggered { field: usize, is_valid: bool },
|
||||
ActionExecuted { action: String, success: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EventDrivenForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
email: String,
|
||||
has_changes: bool,
|
||||
events: Vec<FormEvent>,
|
||||
}
|
||||
|
||||
impl EventDrivenForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
email: String::new(),
|
||||
has_changes: false,
|
||||
events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_event(&mut self, event: FormEvent) {
|
||||
println!(" 📡 Event: {:?}", event);
|
||||
self.events.push(event);
|
||||
}
|
||||
|
||||
fn validate_email(&self) -> bool {
|
||||
self.email.contains('@') && self.email.contains('.')
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for EventDrivenForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.email }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.email }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.email] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Email"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
if changed != self.has_changes {
|
||||
let old_value = if self.has_changes { "modified" } else { "unmodified" };
|
||||
let new_value = if changed { "modified" } else { "unmodified" };
|
||||
|
||||
self.emit_event(FormEvent::FieldChanged {
|
||||
field: self.current_field,
|
||||
old_value: old_value.to_string(),
|
||||
new_value: new_value.to_string(),
|
||||
});
|
||||
}
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"validate" => {
|
||||
let is_valid = self.validate_email();
|
||||
self.emit_event(FormEvent::ValidationTriggered {
|
||||
field: self.current_field,
|
||||
is_valid,
|
||||
});
|
||||
|
||||
self.emit_event(FormEvent::ActionExecuted {
|
||||
action: "validate".to_string(),
|
||||
success: true,
|
||||
});
|
||||
|
||||
if is_valid {
|
||||
Some("Email is valid!".to_string())
|
||||
} else {
|
||||
Some("Email is invalid".to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = EventDrivenForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Type an email address
|
||||
let email = "user@example.com";
|
||||
for c in email.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
// Validate the email
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" Final email: {}", form.email);
|
||||
println!(" Validation result: {}", result.message().unwrap_or(""));
|
||||
println!(" Total events captured: {}", form.events.len());
|
||||
println!(" ✅ Event-driven architecture works!\n");
|
||||
}
|
||||
|
||||
// Pattern 3: Validation pipeline
|
||||
async fn validation_pipeline_example() {
|
||||
println!("✅ Pattern 3: Validation Pipeline");
|
||||
|
||||
type ValidationRule = Box<dyn Fn(&str) -> Result<(), String>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ValidatedForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
validators: HashMap<usize, Vec<ValidationRule>>,
|
||||
}
|
||||
|
||||
impl ValidatedForm {
|
||||
fn new() -> Self {
|
||||
let mut validators: HashMap<usize, Vec<ValidationRule>> = HashMap::new();
|
||||
|
||||
// Password validators
|
||||
let mut password_validators: Vec<ValidationRule> = Vec::new();
|
||||
password_validators.push(Box::new(|value| {
|
||||
if value.len() < 8 {
|
||||
Err("Password must be at least 8 characters".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
password_validators.push(Box::new(|value| {
|
||||
if !value.chars().any(|c| c.is_uppercase()) {
|
||||
Err("Password must contain at least one uppercase letter".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
password_validators.push(Box::new(|value| {
|
||||
if !value.chars().any(|c| c.is_numeric()) {
|
||||
Err("Password must contain at least one number".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
validators.insert(0, password_validators);
|
||||
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
validators,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_field(&self, field_index: usize) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if let Some(validators) = self.validators.get(&field_index) {
|
||||
let value = match field_index {
|
||||
0 => &self.password,
|
||||
_ => return errors,
|
||||
};
|
||||
|
||||
for validator in validators {
|
||||
if let Err(error) = validator(value) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for ValidatedForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.password }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.password }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"validate" => {
|
||||
let errors = self.validate_field(self.current_field);
|
||||
if errors.is_empty() {
|
||||
Some("Password meets all requirements!".to_string())
|
||||
} else {
|
||||
Some(format!("Validation errors: {}", errors.join(", ")))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = ValidatedForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Test with weak password
|
||||
let weak_password = "abc";
|
||||
for c in weak_password.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Weak password '{}': {}", form.password, result.message().unwrap_or(""));
|
||||
|
||||
// Clear and test with strong password
|
||||
form.password.clear();
|
||||
form.cursor_pos = 0;
|
||||
|
||||
let strong_password = "StrongPass123";
|
||||
for c in strong_password.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Strong password '{}': {}", form.password, result.message().unwrap_or(""));
|
||||
println!(" ✅ Validation pipeline works!\n");
|
||||
}
|
||||
|
||||
// Pattern 4: Multi-form orchestration
|
||||
async fn multi_form_example() {
|
||||
println!("🎭 Pattern 4: Multi-Form Orchestration");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PersonalInfoForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
name: String,
|
||||
age: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ContactInfoForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
email: String,
|
||||
phone: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
// Implement CanvasState for both forms
|
||||
impl CanvasState for PersonalInfoForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.name,
|
||||
1 => &self.age,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.name,
|
||||
1 => &mut self.age,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.age] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Name", "Age"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
impl CanvasState for ContactInfoForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.email,
|
||||
1 => &self.phone,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.email,
|
||||
1 => &mut self.phone,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.email, &self.phone] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Email", "Phone"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
// Form orchestrator
|
||||
#[derive(Debug)]
|
||||
struct FormOrchestrator {
|
||||
personal_form: PersonalInfoForm,
|
||||
contact_form: ContactInfoForm,
|
||||
current_form: usize, // 0 = personal, 1 = contact
|
||||
}
|
||||
|
||||
impl FormOrchestrator {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
personal_form: PersonalInfoForm {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
name: String::new(),
|
||||
age: String::new(),
|
||||
has_changes: false,
|
||||
},
|
||||
contact_form: ContactInfoForm {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
email: String::new(),
|
||||
phone: String::new(),
|
||||
has_changes: false,
|
||||
},
|
||||
current_form: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_action(&mut self, action: CanvasAction) -> ActionResult {
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
match self.current_form {
|
||||
0 => ActionDispatcher::dispatch(action, &mut self.personal_form, &mut ideal_cursor).await.unwrap(),
|
||||
1 => ActionDispatcher::dispatch(action, &mut self.contact_form, &mut ideal_cursor).await.unwrap(),
|
||||
_ => ActionResult::error("Invalid form index"),
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_form(&mut self) -> String {
|
||||
self.current_form = (self.current_form + 1) % 2;
|
||||
match self.current_form {
|
||||
0 => "Switched to Personal Info form".to_string(),
|
||||
1 => "Switched to Contact Info form".to_string(),
|
||||
_ => "Unknown form".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_form_name(&self) -> &str {
|
||||
match self.current_form {
|
||||
0 => "Personal Info",
|
||||
1 => "Contact Info",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut orchestrator = FormOrchestrator::new();
|
||||
|
||||
println!(" Current form: {}", orchestrator.current_form_name());
|
||||
|
||||
// Fill personal info
|
||||
let personal_data = vec![
|
||||
('J', 'o', 'h', 'n'),
|
||||
];
|
||||
|
||||
for &c in &['J', 'o', 'h', 'n'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
orchestrator.execute_action(CanvasAction::NextField).await;
|
||||
|
||||
for &c in &['2', '5'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
println!(" Personal form - Name: '{}', Age: '{}'",
|
||||
orchestrator.personal_form.name,
|
||||
orchestrator.personal_form.age);
|
||||
|
||||
// Switch to contact form
|
||||
let switch_msg = orchestrator.switch_form();
|
||||
println!(" {}", switch_msg);
|
||||
|
||||
// Fill contact info
|
||||
for &c in &['j', 'o', 'h', 'n', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
orchestrator.execute_action(CanvasAction::NextField).await;
|
||||
|
||||
for &c in &['5', '5', '5', '-', '1', '2', '3', '4'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
println!(" Contact form - Email: '{}', Phone: '{}'",
|
||||
orchestrator.contact_form.email,
|
||||
orchestrator.contact_form.phone);
|
||||
|
||||
println!(" ✅ Multi-form orchestration works!\n");
|
||||
|
||||
println!("🎉 All integration patterns completed!");
|
||||
println!("The Canvas crate seamlessly integrates with various architectural patterns!");
|
||||
}
|
||||
354
canvas/examples/simple_login.rs
Normal file
354
canvas/examples/simple_login.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
// examples/simple_login.rs
|
||||
//! A simple login form demonstrating basic canvas usage
|
||||
//!
|
||||
//! Run with: cargo run --example simple_login
|
||||
|
||||
use canvas::prelude::*;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LoginForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
username: String,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl LoginForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.username.clear();
|
||||
self.password.clear();
|
||||
self.current_field = 0;
|
||||
self.cursor_pos = 0;
|
||||
self.has_changes = false;
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
!self.username.trim().is_empty() && !self.password.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for LoginForm {
|
||||
fn current_field(&self) -> usize {
|
||||
self.current_field
|
||||
}
|
||||
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
self.cursor_pos
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
self.current_field = index.min(1); // Only 2 fields: username(0), password(1)
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
self.cursor_pos = pos;
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.username,
|
||||
1 => &self.password,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
1 => &mut self.password,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
vec![&self.username, &self.password]
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
vec!["Username", "Password"]
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_changes
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
// Custom action handling
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"submit" => {
|
||||
if self.is_valid() {
|
||||
Some(format!("Login successful! Welcome, {}", self.username))
|
||||
} else {
|
||||
Some("Error: Username and password are required".to_string())
|
||||
}
|
||||
}
|
||||
"clear" => {
|
||||
self.reset();
|
||||
Some("Form cleared".to_string())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// Override display for password field
|
||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||
match index {
|
||||
0 => &self.username, // Username shows normally
|
||||
1 => &self.password, // We'll handle masking in the UI drawing
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn has_display_override(&self, index: usize) -> bool {
|
||||
index == 1 // Password field has display override
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_ui(form: &LoginForm, message: &str) -> io::Result<()> {
|
||||
// Clear screen and move cursor to top-left
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
|
||||
println!("╔═══════════════════════════════════════╗");
|
||||
println!("║ LOGIN FORM ║");
|
||||
println!("╠═══════════════════════════════════════╣");
|
||||
|
||||
// Username field
|
||||
let username_indicator = if form.current_field == 0 { "→" } else { " " };
|
||||
let username_display = if form.username.is_empty() {
|
||||
"<enter username>".to_string()
|
||||
} else {
|
||||
form.username.clone()
|
||||
};
|
||||
println!("║ {} Username: {:22} ║", username_indicator,
|
||||
if username_display.len() > 22 {
|
||||
format!("{}...", &username_display[..19])
|
||||
} else {
|
||||
format!("{:22}", username_display)
|
||||
});
|
||||
|
||||
// Show cursor for username field
|
||||
if form.current_field == 0 && !form.username.is_empty() {
|
||||
let cursor_pos = form.cursor_pos.min(form.username.len());
|
||||
let spaces_before = 11 + cursor_pos; // "Username: " = 10 chars + 1 space
|
||||
let cursor_line = format!("║ {}█{:width$}║",
|
||||
" ".repeat(spaces_before),
|
||||
"",
|
||||
width = 25_usize.saturating_sub(spaces_before)
|
||||
);
|
||||
println!("{}", cursor_line);
|
||||
} else {
|
||||
println!("║{:37}║", "");
|
||||
}
|
||||
|
||||
// Password field
|
||||
let password_indicator = if form.current_field == 1 { "→" } else { " " };
|
||||
let password_display = if form.password.is_empty() {
|
||||
"<enter password>".to_string()
|
||||
} else {
|
||||
"*".repeat(form.password.len())
|
||||
};
|
||||
println!("║ {} Password: {:22} ║", password_indicator,
|
||||
if password_display.len() > 22 {
|
||||
format!("{}...", &password_display[..19])
|
||||
} else {
|
||||
format!("{:22}", password_display)
|
||||
});
|
||||
|
||||
// Show cursor for password field
|
||||
if form.current_field == 1 && !form.password.is_empty() {
|
||||
let cursor_pos = form.cursor_pos.min(form.password.len());
|
||||
let spaces_before = 11 + cursor_pos; // "Password: " = 10 chars + 1 space
|
||||
let cursor_line = format!("║ {}█{:width$}║",
|
||||
" ".repeat(spaces_before),
|
||||
"",
|
||||
width = 25_usize.saturating_sub(spaces_before)
|
||||
);
|
||||
println!("{}", cursor_line);
|
||||
} else {
|
||||
println!("║{:37}║", "");
|
||||
}
|
||||
|
||||
println!("╠═══════════════════════════════════════╣");
|
||||
println!("║ CONTROLS: ║");
|
||||
println!("║ Tab/↑↓ - Navigate fields ║");
|
||||
println!("║ Enter - Submit form ║");
|
||||
println!("║ Ctrl+R - Clear form ║");
|
||||
println!("║ Ctrl+C - Exit ║");
|
||||
println!("╠═══════════════════════════════════════╣");
|
||||
|
||||
// Status message
|
||||
let status = if !message.is_empty() {
|
||||
message.to_string()
|
||||
} else if form.has_changes {
|
||||
"Form modified".to_string()
|
||||
} else {
|
||||
"Ready - enter your credentials".to_string()
|
||||
};
|
||||
|
||||
let status_display = if status.len() > 33 {
|
||||
format!("{}...", &status[..30])
|
||||
} else {
|
||||
format!("{:33}", status)
|
||||
};
|
||||
|
||||
println!("║ Status: {} ║", status_display);
|
||||
println!("╚═══════════════════════════════════════╝");
|
||||
|
||||
// Show current state info
|
||||
println!();
|
||||
println!("Current field: {} ({})",
|
||||
form.current_field,
|
||||
form.fields()[form.current_field]);
|
||||
println!("Cursor position: {}", form.cursor_pos);
|
||||
println!("Has changes: {}", form.has_changes);
|
||||
|
||||
io::stdout().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
println!("Starting Canvas Login Demo...");
|
||||
println!("Setting up terminal...");
|
||||
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
io::stdout().execute(EnterAlternateScreen)?;
|
||||
|
||||
let mut form = LoginForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
let mut message = String::new();
|
||||
|
||||
// Initial draw
|
||||
if let Err(e) = draw_ui(&form, &message) {
|
||||
// Cleanup on error
|
||||
let _ = disable_raw_mode();
|
||||
let _ = io::stdout().execute(LeaveAlternateScreen);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
println!("Canvas Login Demo started. Use Ctrl+C to exit.");
|
||||
|
||||
loop {
|
||||
match event::read() {
|
||||
Ok(Event::Key(key)) => {
|
||||
// Clear message after key press
|
||||
if !message.is_empty() {
|
||||
message.clear();
|
||||
}
|
||||
|
||||
match key {
|
||||
// Exit
|
||||
KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
break;
|
||||
}
|
||||
|
||||
// Clear form
|
||||
KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::CONTROL, .. } => {
|
||||
match ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("clear".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await {
|
||||
Ok(result) => {
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
message = format!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit form
|
||||
KeyEvent { code: KeyCode::Enter, .. } => {
|
||||
match ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("submit".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await {
|
||||
Ok(result) => {
|
||||
if let Some(msg) = result.message() {
|
||||
message = msg.to_string();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
message = format!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regular key handling - let canvas handle it!
|
||||
_ => {
|
||||
if let Some(action) = CanvasAction::from_key(key.code) {
|
||||
match ActionDispatcher::dispatch(action, &mut form, &mut ideal_cursor).await {
|
||||
Ok(result) => {
|
||||
if !result.is_success() {
|
||||
if let Some(msg) = result.message() {
|
||||
message = format!("Error: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
message = format!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw UI
|
||||
if let Err(e) = draw_ui(&form, &message) {
|
||||
eprintln!("Error drawing UI: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
// Ignore other events (mouse, resize, etc.)
|
||||
}
|
||||
Err(e) => {
|
||||
message = format!("Event error: {}", e);
|
||||
if let Err(_) = draw_ui(&form, &message) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
disable_raw_mode()?;
|
||||
io::stdout().execute(LeaveAlternateScreen)?;
|
||||
|
||||
println!("Thanks for using Canvas Login Demo!");
|
||||
println!("Final form state:");
|
||||
println!(" Username: '{}'", form.username);
|
||||
println!(" Password: '{}'", "*".repeat(form.password.len()));
|
||||
println!(" Valid: {}", form.is_valid());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
620
canvas/integration_patterns.rs
Normal file
620
canvas/integration_patterns.rs
Normal file
@@ -0,0 +1,620 @@
|
||||
// examples/integration_patterns.rs
|
||||
//! Advanced integration patterns showing how Canvas works with:
|
||||
//! - State management patterns
|
||||
//! - Event-driven architectures
|
||||
//! - Validation systems
|
||||
//! - Custom rendering
|
||||
//!
|
||||
//! Run with: cargo run --example integration_patterns
|
||||
|
||||
use canvas::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("🔧 Canvas Integration Patterns");
|
||||
println!("==============================\n");
|
||||
|
||||
// Pattern 1: State machine integration
|
||||
state_machine_example().await;
|
||||
|
||||
// Pattern 2: Event-driven architecture
|
||||
event_driven_example().await;
|
||||
|
||||
// Pattern 3: Validation pipeline
|
||||
validation_pipeline_example().await;
|
||||
|
||||
// Pattern 4: Multi-form orchestration
|
||||
multi_form_example().await;
|
||||
}
|
||||
|
||||
// Pattern 1: Canvas with state machine
|
||||
async fn state_machine_example() {
|
||||
println!("🔄 Pattern 1: State Machine Integration");
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum FormState {
|
||||
Initial,
|
||||
Editing,
|
||||
Validating,
|
||||
Submitting,
|
||||
Success,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StateMachineForm {
|
||||
// Canvas state
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
username: String,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
|
||||
// State machine
|
||||
state: FormState,
|
||||
}
|
||||
|
||||
impl StateMachineForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
state: FormState::Initial,
|
||||
}
|
||||
}
|
||||
|
||||
fn transition_to(&mut self, new_state: FormState) -> String {
|
||||
let old_state = self.state.clone();
|
||||
self.state = new_state;
|
||||
format!("State transition: {:?} -> {:?}", old_state, self.state)
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
matches!(self.state, FormState::Editing) &&
|
||||
!self.username.trim().is_empty() &&
|
||||
!self.password.trim().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for StateMachineForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.username,
|
||||
1 => &self.password,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.username,
|
||||
1 => &mut self.password,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.username, &self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Username", "Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_changes = changed;
|
||||
// Transition to editing state when user starts typing
|
||||
if changed && self.state == FormState::Initial {
|
||||
self.state = FormState::Editing;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"submit" => {
|
||||
if self.can_submit() {
|
||||
let msg = self.transition_to(FormState::Submitting);
|
||||
// Simulate submission
|
||||
self.state = FormState::Success;
|
||||
Some(format!("{} -> Form submitted successfully", msg))
|
||||
} else {
|
||||
let msg = self.transition_to(FormState::Error("Invalid form data".to_string()));
|
||||
Some(msg)
|
||||
}
|
||||
}
|
||||
"reset" => {
|
||||
self.username.clear();
|
||||
self.password.clear();
|
||||
self.has_changes = false;
|
||||
Some(self.transition_to(FormState::Initial))
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = StateMachineForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
println!(" Initial state: {:?}", form.state);
|
||||
|
||||
// Type some text to trigger state change
|
||||
let _result = ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar('u'),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" After typing: {:?}", form.state);
|
||||
|
||||
// Try to submit (should fail)
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("submit".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Submit result: {}", result.message().unwrap_or(""));
|
||||
println!(" ✅ State machine integration works!\n");
|
||||
}
|
||||
|
||||
// Pattern 2: Event-driven architecture
|
||||
async fn event_driven_example() {
|
||||
println!("📡 Pattern 2: Event-Driven Architecture");
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum FormEvent {
|
||||
FieldChanged { field: usize, old_value: String, new_value: String },
|
||||
ValidationTriggered { field: usize, is_valid: bool },
|
||||
ActionExecuted { action: String, success: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EventDrivenForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
email: String,
|
||||
has_changes: bool,
|
||||
events: Vec<FormEvent>,
|
||||
}
|
||||
|
||||
impl EventDrivenForm {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
email: String::new(),
|
||||
has_changes: false,
|
||||
events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_event(&mut self, event: FormEvent) {
|
||||
println!(" 📡 Event: {:?}", event);
|
||||
self.events.push(event);
|
||||
}
|
||||
|
||||
fn validate_email(&self) -> bool {
|
||||
self.email.contains('@') && self.email.contains('.')
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for EventDrivenForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.email }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.email }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.email] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Email"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
if changed != self.has_changes {
|
||||
let old_value = if self.has_changes { "modified" } else { "unmodified" };
|
||||
let new_value = if changed { "modified" } else { "unmodified" };
|
||||
|
||||
self.emit_event(FormEvent::FieldChanged {
|
||||
field: self.current_field,
|
||||
old_value: old_value.to_string(),
|
||||
new_value: new_value.to_string(),
|
||||
});
|
||||
}
|
||||
self.has_changes = changed;
|
||||
}
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"validate" => {
|
||||
let is_valid = self.validate_email();
|
||||
self.emit_event(FormEvent::ValidationTriggered {
|
||||
field: self.current_field,
|
||||
is_valid,
|
||||
});
|
||||
|
||||
self.emit_event(FormEvent::ActionExecuted {
|
||||
action: "validate".to_string(),
|
||||
success: true,
|
||||
});
|
||||
|
||||
if is_valid {
|
||||
Some("Email is valid!".to_string())
|
||||
} else {
|
||||
Some("Email is invalid".to_string())
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = EventDrivenForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Type an email address
|
||||
let email = "user@example.com";
|
||||
for c in email.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
// Validate the email
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
println!(" Final email: {}", form.email);
|
||||
println!(" Validation result: {}", result.message().unwrap_or(""));
|
||||
println!(" Total events captured: {}", form.events.len());
|
||||
println!(" ✅ Event-driven architecture works!\n");
|
||||
}
|
||||
|
||||
// Pattern 3: Validation pipeline
|
||||
async fn validation_pipeline_example() {
|
||||
println!("✅ Pattern 3: Validation Pipeline");
|
||||
|
||||
type ValidationRule = Box<dyn Fn(&str) -> Result<(), String>>;
|
||||
|
||||
// Custom Debug implementation since function pointers don't implement Debug
|
||||
struct ValidatedForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
password: String,
|
||||
has_changes: bool,
|
||||
validators: HashMap<usize, Vec<ValidationRule>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ValidatedForm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ValidatedForm")
|
||||
.field("current_field", &self.current_field)
|
||||
.field("cursor_pos", &self.cursor_pos)
|
||||
.field("password", &self.password)
|
||||
.field("has_changes", &self.has_changes)
|
||||
.field("validators", &format!("HashMap with {} entries", self.validators.len()))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidatedForm {
|
||||
fn new() -> Self {
|
||||
let mut validators: HashMap<usize, Vec<ValidationRule>> = HashMap::new();
|
||||
|
||||
// Password validators
|
||||
let mut password_validators: Vec<ValidationRule> = Vec::new();
|
||||
password_validators.push(Box::new(|value| {
|
||||
if value.len() < 8 {
|
||||
Err("Password must be at least 8 characters".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
password_validators.push(Box::new(|value| {
|
||||
if !value.chars().any(|c| c.is_uppercase()) {
|
||||
Err("Password must contain at least one uppercase letter".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
password_validators.push(Box::new(|value| {
|
||||
if !value.chars().any(|c| c.is_numeric()) {
|
||||
Err("Password must contain at least one number".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}));
|
||||
|
||||
validators.insert(0, password_validators);
|
||||
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
password: String::new(),
|
||||
has_changes: false,
|
||||
validators,
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_field(&self, field_index: usize) -> Vec<String> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if let Some(validators) = self.validators.get(&field_index) {
|
||||
let value = match field_index {
|
||||
0 => &self.password,
|
||||
_ => return errors,
|
||||
};
|
||||
|
||||
for validator in validators {
|
||||
if let Err(error) = validator(value) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for ValidatedForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.password }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.password }
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.password] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Password"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(cmd) => match cmd.as_str() {
|
||||
"validate" => {
|
||||
let errors = self.validate_field(self.current_field);
|
||||
if errors.is_empty() {
|
||||
Some("Password meets all requirements!".to_string())
|
||||
} else {
|
||||
Some(format!("Validation errors: {}", errors.join(", ")))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut form = ValidatedForm::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Test with weak password
|
||||
let weak_password = "abc";
|
||||
for c in weak_password.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Weak password '{}': {}", form.password, result.message().unwrap_or(""));
|
||||
|
||||
// Clear and test with strong password
|
||||
form.password.clear();
|
||||
form.cursor_pos = 0;
|
||||
|
||||
let strong_password = "StrongPass123";
|
||||
for c in strong_password.chars() {
|
||||
ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar(c),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
}
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("validate".to_string()),
|
||||
&mut form,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
println!(" Strong password '{}': {}", form.password, result.message().unwrap_or(""));
|
||||
println!(" ✅ Validation pipeline works!\n");
|
||||
}
|
||||
|
||||
// Pattern 4: Multi-form orchestration
|
||||
async fn multi_form_example() {
|
||||
println!("🎭 Pattern 4: Multi-Form Orchestration");
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PersonalInfoForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
name: String,
|
||||
age: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ContactInfoForm {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
email: String,
|
||||
phone: String,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
// Implement CanvasState for both forms
|
||||
impl CanvasState for PersonalInfoForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.name,
|
||||
1 => &self.age,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.name,
|
||||
1 => &mut self.age,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.name, &self.age] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Name", "Age"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
impl CanvasState for ContactInfoForm {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index.min(1); }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
match self.current_field {
|
||||
0 => &self.email,
|
||||
1 => &self.phone,
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
match self.current_field {
|
||||
0 => &mut self.email,
|
||||
1 => &mut self.phone,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> { vec![&self.email, &self.phone] }
|
||||
fn fields(&self) -> Vec<&str> { vec!["Email", "Phone"] }
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
}
|
||||
|
||||
// Form orchestrator
|
||||
#[derive(Debug)]
|
||||
struct FormOrchestrator {
|
||||
personal_form: PersonalInfoForm,
|
||||
contact_form: ContactInfoForm,
|
||||
current_form: usize, // 0 = personal, 1 = contact
|
||||
}
|
||||
|
||||
impl FormOrchestrator {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
personal_form: PersonalInfoForm {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
name: String::new(),
|
||||
age: String::new(),
|
||||
has_changes: false,
|
||||
},
|
||||
contact_form: ContactInfoForm {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
email: String::new(),
|
||||
phone: String::new(),
|
||||
has_changes: false,
|
||||
},
|
||||
current_form: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_action(&mut self, action: CanvasAction) -> ActionResult {
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
match self.current_form {
|
||||
0 => ActionDispatcher::dispatch(action, &mut self.personal_form, &mut ideal_cursor).await.unwrap(),
|
||||
1 => ActionDispatcher::dispatch(action, &mut self.contact_form, &mut ideal_cursor).await.unwrap(),
|
||||
_ => ActionResult::error("Invalid form index"),
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_form(&mut self) -> String {
|
||||
self.current_form = (self.current_form + 1) % 2;
|
||||
match self.current_form {
|
||||
0 => "Switched to Personal Info form".to_string(),
|
||||
1 => "Switched to Contact Info form".to_string(),
|
||||
_ => "Unknown form".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_form_name(&self) -> &str {
|
||||
match self.current_form {
|
||||
0 => "Personal Info",
|
||||
1 => "Contact Info",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut orchestrator = FormOrchestrator::new();
|
||||
|
||||
println!(" Current form: {}", orchestrator.current_form_name());
|
||||
|
||||
// Fill personal info
|
||||
for &c in &['J', 'o', 'h', 'n'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
orchestrator.execute_action(CanvasAction::NextField).await;
|
||||
|
||||
for &c in &['2', '5'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
println!(" Personal form - Name: '{}', Age: '{}'",
|
||||
orchestrator.personal_form.name,
|
||||
orchestrator.personal_form.age);
|
||||
|
||||
// Switch to contact form
|
||||
let switch_msg = orchestrator.switch_form();
|
||||
println!(" {}", switch_msg);
|
||||
|
||||
// Fill contact info
|
||||
for &c in &['j', 'o', 'h', 'n', '@', 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
orchestrator.execute_action(CanvasAction::NextField).await;
|
||||
|
||||
for &c in &['5', '5', '5', '-', '1', '2', '3', '4'] {
|
||||
orchestrator.execute_action(CanvasAction::InsertChar(c)).await;
|
||||
}
|
||||
|
||||
println!(" Contact form - Email: '{}', Phone: '{}'",
|
||||
orchestrator.contact_form.email,
|
||||
orchestrator.contact_form.phone);
|
||||
}
|
||||
455
canvas/src/actions/edit.rs
Normal file
455
canvas/src/actions/edit.rs
Normal file
@@ -0,0 +1,455 @@
|
||||
// canvas/src/actions/edit.rs
|
||||
|
||||
use crate::state::{CanvasState, ActionContext};
|
||||
use crate::actions::types::{CanvasAction, ActionResult};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use anyhow::Result;
|
||||
|
||||
/// Execute a typed canvas action on any CanvasState implementation
|
||||
pub async fn execute_canvas_action<S: CanvasState>(
|
||||
action: CanvasAction,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<ActionResult> {
|
||||
// 1. Try feature-specific handler first
|
||||
let context = ActionContext {
|
||||
key_code: None, // We don't need KeyCode anymore since action is typed
|
||||
ideal_cursor_column: *ideal_cursor_column,
|
||||
current_input: state.get_current_input().to_string(),
|
||||
current_field: state.current_field(),
|
||||
};
|
||||
|
||||
if let Some(result) = state.handle_feature_action(&action, &context) {
|
||||
return Ok(ActionResult::HandledByFeature(result));
|
||||
}
|
||||
|
||||
// 2. Handle suggestion actions
|
||||
if let Some(result) = handle_suggestion_action(&action, state)? {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// 3. Handle generic canvas actions
|
||||
handle_generic_canvas_action(action, state, ideal_cursor_column).await
|
||||
}
|
||||
|
||||
/// Legacy function for string-based actions (backwards compatibility)
|
||||
pub async fn execute_edit_action<S: CanvasState>(
|
||||
action: &str,
|
||||
key: KeyEvent,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
let typed_action = match action {
|
||||
"insert_char" => {
|
||||
if let KeyCode::Char(c) = key.code {
|
||||
CanvasAction::InsertChar(c)
|
||||
} else {
|
||||
return Ok("Error: insert_char called without a char key.".to_string());
|
||||
}
|
||||
}
|
||||
_ => CanvasAction::from_string(action),
|
||||
};
|
||||
|
||||
let result = execute_canvas_action(typed_action, state, ideal_cursor_column).await?;
|
||||
|
||||
// Convert ActionResult back to string for backwards compatibility
|
||||
Ok(result.message().unwrap_or("").to_string())
|
||||
}
|
||||
|
||||
/// Handle suggestion-related actions
|
||||
fn handle_suggestion_action<S: CanvasState>(
|
||||
action: &CanvasAction,
|
||||
state: &mut S,
|
||||
) -> Result<Option<ActionResult>> {
|
||||
match action {
|
||||
CanvasAction::SuggestionDown => {
|
||||
if let Some(suggestions) = state.get_suggestions() {
|
||||
if !suggestions.is_empty() {
|
||||
let current = state.get_selected_suggestion_index().unwrap_or(0);
|
||||
let next = (current + 1) % suggestions.len();
|
||||
state.set_selected_suggestion_index(Some(next));
|
||||
return Ok(Some(ActionResult::success()));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
CanvasAction::SuggestionUp => {
|
||||
if let Some(suggestions) = state.get_suggestions() {
|
||||
if !suggestions.is_empty() {
|
||||
let current = state.get_selected_suggestion_index().unwrap_or(0);
|
||||
let prev = if current == 0 { suggestions.len() - 1 } else { current - 1 };
|
||||
state.set_selected_suggestion_index(Some(prev));
|
||||
return Ok(Some(ActionResult::success()));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
CanvasAction::SelectSuggestion => {
|
||||
// Let feature handle this via handle_feature_action since it's feature-specific
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
CanvasAction::ExitSuggestions => {
|
||||
state.deactivate_suggestions();
|
||||
Ok(Some(ActionResult::success()))
|
||||
}
|
||||
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle core canvas actions with full type safety
|
||||
async fn handle_generic_canvas_action<S: CanvasState>(
|
||||
action: CanvasAction,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<ActionResult> {
|
||||
match action {
|
||||
CanvasAction::InsertChar(c) => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
|
||||
if cursor_pos <= chars.len() {
|
||||
chars.insert(cursor_pos, c);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_current_cursor_pos(cursor_pos + 1);
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = state.current_cursor_pos();
|
||||
Ok(ActionResult::success())
|
||||
} else {
|
||||
Ok(ActionResult::error("Invalid cursor position for character insertion"))
|
||||
}
|
||||
}
|
||||
|
||||
CanvasAction::DeleteBackward => {
|
||||
if state.current_cursor_pos() > 0 {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
|
||||
if cursor_pos <= chars.len() {
|
||||
chars.remove(cursor_pos - 1);
|
||||
*field_value = chars.into_iter().collect();
|
||||
let new_pos = cursor_pos - 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::DeleteForward => {
|
||||
let cursor_pos = state.current_cursor_pos();
|
||||
let field_value = state.get_current_input_mut();
|
||||
let mut chars: Vec<char> = field_value.chars().collect();
|
||||
|
||||
if cursor_pos < chars.len() {
|
||||
chars.remove(cursor_pos);
|
||||
*field_value = chars.into_iter().collect();
|
||||
state.set_has_unsaved_changes(true);
|
||||
*ideal_cursor_column = cursor_pos;
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::NextField => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = (current_field + 1) % num_fields;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::PrevField => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = if current_field == 0 {
|
||||
num_fields - 1
|
||||
} else {
|
||||
current_field - 1
|
||||
};
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveLeft => {
|
||||
let new_pos = state.current_cursor_pos().saturating_sub(1);
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveRight => {
|
||||
let current_input = state.get_current_input();
|
||||
let current_pos = state.current_cursor_pos();
|
||||
if current_pos < current_input.len() {
|
||||
let new_pos = current_pos + 1;
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveUp => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let current_field = state.current_field();
|
||||
let new_field = current_field.saturating_sub(1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveDown => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = (state.current_field() + 1).min(num_fields - 1);
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveLineStart => {
|
||||
state.set_current_cursor_pos(0);
|
||||
*ideal_cursor_column = 0;
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveLineEnd => {
|
||||
let current_input = state.get_current_input();
|
||||
let new_pos = current_input.len();
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveFirstLine => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
state.set_current_field(0);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok(ActionResult::success_with_message("Moved to first field"))
|
||||
}
|
||||
|
||||
CanvasAction::MoveLastLine => {
|
||||
let num_fields = state.fields().len();
|
||||
if num_fields > 0 {
|
||||
let new_field = num_fields - 1;
|
||||
state.set_current_field(new_field);
|
||||
let current_input = state.get_current_input();
|
||||
let max_pos = current_input.len();
|
||||
state.set_current_cursor_pos((*ideal_cursor_column).min(max_pos));
|
||||
}
|
||||
Ok(ActionResult::success_with_message("Moved to last field"))
|
||||
}
|
||||
|
||||
CanvasAction::MoveWordNext => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_next_word_start(current_input, state.current_cursor_pos());
|
||||
let final_pos = new_pos.min(current_input.len());
|
||||
state.set_current_cursor_pos(final_pos);
|
||||
*ideal_cursor_column = final_pos;
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveWordEnd => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let current_pos = state.current_cursor_pos();
|
||||
let new_pos = find_word_end(current_input, current_pos);
|
||||
|
||||
let final_pos = if new_pos == current_pos {
|
||||
find_word_end(current_input, new_pos + 1)
|
||||
} else {
|
||||
new_pos
|
||||
};
|
||||
|
||||
let max_valid_index = current_input.len().saturating_sub(1);
|
||||
let clamped_pos = final_pos.min(max_valid_index);
|
||||
state.set_current_cursor_pos(clamped_pos);
|
||||
*ideal_cursor_column = clamped_pos;
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveWordPrev => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_start(current_input, state.current_cursor_pos());
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok(ActionResult::success())
|
||||
}
|
||||
|
||||
CanvasAction::MoveWordEndPrev => {
|
||||
let current_input = state.get_current_input();
|
||||
if !current_input.is_empty() {
|
||||
let new_pos = find_prev_word_end(current_input, state.current_cursor_pos());
|
||||
state.set_current_cursor_pos(new_pos);
|
||||
*ideal_cursor_column = new_pos;
|
||||
}
|
||||
Ok(ActionResult::success_with_message("Moved to previous word end"))
|
||||
}
|
||||
|
||||
CanvasAction::Custom(action_str) => {
|
||||
Ok(ActionResult::error(format!("Unknown or unhandled custom action: {}", action_str)))
|
||||
}
|
||||
|
||||
// Suggestion actions should have been handled above
|
||||
CanvasAction::SuggestionUp | CanvasAction::SuggestionDown |
|
||||
CanvasAction::SelectSuggestion | CanvasAction::ExitSuggestions => {
|
||||
Ok(ActionResult::error("Suggestion action not handled properly"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Word movement helper functions
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CharType {
|
||||
Whitespace,
|
||||
Alphanumeric,
|
||||
Punctuation,
|
||||
}
|
||||
|
||||
fn get_char_type(c: char) -> CharType {
|
||||
if c.is_whitespace() {
|
||||
CharType::Whitespace
|
||||
} else if c.is_alphanumeric() {
|
||||
CharType::Alphanumeric
|
||||
} else {
|
||||
CharType::Punctuation
|
||||
}
|
||||
}
|
||||
|
||||
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos >= len {
|
||||
return len;
|
||||
}
|
||||
|
||||
let mut pos = current_pos;
|
||||
let initial_type = get_char_type(chars[pos]);
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == initial_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
while pos < len && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.min(len - 1);
|
||||
|
||||
if get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos = find_next_word_start(text, pos);
|
||||
}
|
||||
|
||||
if pos >= len {
|
||||
return len.saturating_sub(1);
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos < len && get_char_type(chars[pos]) == word_type {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
pos.saturating_sub(1).min(len.saturating_sub(1))
|
||||
}
|
||||
|
||||
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
if chars.is_empty() || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
pos
|
||||
}
|
||||
|
||||
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let len = chars.len();
|
||||
if len == 0 || current_pos == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut pos = current_pos.saturating_sub(1);
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos == 0 && get_char_type(chars[pos]) == CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
if pos == 0 && get_char_type(chars[pos]) != CharType::Whitespace {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let word_type = get_char_type(chars[pos]);
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
if pos > 0 {
|
||||
pos - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
8
canvas/src/actions/mod.rs
Normal file
8
canvas/src/actions/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
// canvas/src/actions/mod.rs
|
||||
|
||||
pub mod types;
|
||||
pub mod edit;
|
||||
|
||||
// Re-export the main types for convenience
|
||||
pub use types::{CanvasAction, ActionResult};
|
||||
pub use edit::{execute_canvas_action, execute_edit_action};
|
||||
225
canvas/src/actions/types.rs
Normal file
225
canvas/src/actions/types.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
// canvas/src/actions/types.rs
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
/// All possible canvas actions, type-safe and exhaustive
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CanvasAction {
|
||||
// Character input
|
||||
InsertChar(char),
|
||||
|
||||
// Deletion
|
||||
DeleteBackward,
|
||||
DeleteForward,
|
||||
|
||||
// Basic cursor movement
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
MoveUp,
|
||||
MoveDown,
|
||||
|
||||
// Line movement
|
||||
MoveLineStart,
|
||||
MoveLineEnd,
|
||||
MoveFirstLine,
|
||||
MoveLastLine,
|
||||
|
||||
// Word movement
|
||||
MoveWordNext,
|
||||
MoveWordEnd,
|
||||
MoveWordPrev,
|
||||
MoveWordEndPrev,
|
||||
|
||||
// Field navigation
|
||||
NextField,
|
||||
PrevField,
|
||||
|
||||
// Suggestions
|
||||
SuggestionUp,
|
||||
SuggestionDown,
|
||||
SelectSuggestion,
|
||||
ExitSuggestions,
|
||||
|
||||
// Custom actions (escape hatch for feature-specific behavior)
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl CanvasAction {
|
||||
/// Convert a string action to typed action (for backwards compatibility during migration)
|
||||
pub fn from_string(action: &str) -> Self {
|
||||
match action {
|
||||
"insert_char" => {
|
||||
// This is a bit tricky - we need the char from context
|
||||
// For now, we'll use Custom until we refactor the call sites
|
||||
Self::Custom(action.to_string())
|
||||
}
|
||||
"delete_char_backward" => Self::DeleteBackward,
|
||||
"delete_char_forward" => Self::DeleteForward,
|
||||
"move_left" => Self::MoveLeft,
|
||||
"move_right" => Self::MoveRight,
|
||||
"move_up" => Self::MoveUp,
|
||||
"move_down" => Self::MoveDown,
|
||||
"move_line_start" => Self::MoveLineStart,
|
||||
"move_line_end" => Self::MoveLineEnd,
|
||||
"move_first_line" => Self::MoveFirstLine,
|
||||
"move_last_line" => Self::MoveLastLine,
|
||||
"move_word_next" => Self::MoveWordNext,
|
||||
"move_word_end" => Self::MoveWordEnd,
|
||||
"move_word_prev" => Self::MoveWordPrev,
|
||||
"move_word_end_prev" => Self::MoveWordEndPrev,
|
||||
"next_field" => Self::NextField,
|
||||
"prev_field" => Self::PrevField,
|
||||
"suggestion_up" => Self::SuggestionUp,
|
||||
"suggestion_down" => Self::SuggestionDown,
|
||||
"select_suggestion" => Self::SelectSuggestion,
|
||||
"exit_suggestions" => Self::ExitSuggestions,
|
||||
_ => Self::Custom(action.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get string representation (for logging, debugging)
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::InsertChar(_) => "insert_char",
|
||||
Self::DeleteBackward => "delete_char_backward",
|
||||
Self::DeleteForward => "delete_char_forward",
|
||||
Self::MoveLeft => "move_left",
|
||||
Self::MoveRight => "move_right",
|
||||
Self::MoveUp => "move_up",
|
||||
Self::MoveDown => "move_down",
|
||||
Self::MoveLineStart => "move_line_start",
|
||||
Self::MoveLineEnd => "move_line_end",
|
||||
Self::MoveFirstLine => "move_first_line",
|
||||
Self::MoveLastLine => "move_last_line",
|
||||
Self::MoveWordNext => "move_word_next",
|
||||
Self::MoveWordEnd => "move_word_end",
|
||||
Self::MoveWordPrev => "move_word_prev",
|
||||
Self::MoveWordEndPrev => "move_word_end_prev",
|
||||
Self::NextField => "next_field",
|
||||
Self::PrevField => "prev_field",
|
||||
Self::SuggestionUp => "suggestion_up",
|
||||
Self::SuggestionDown => "suggestion_down",
|
||||
Self::SelectSuggestion => "select_suggestion",
|
||||
Self::ExitSuggestions => "exit_suggestions",
|
||||
Self::Custom(s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create action from KeyCode for common cases
|
||||
pub fn from_key(key: KeyCode) -> Option<Self> {
|
||||
match key {
|
||||
KeyCode::Char(c) => Some(Self::InsertChar(c)),
|
||||
KeyCode::Backspace => Some(Self::DeleteBackward),
|
||||
KeyCode::Delete => Some(Self::DeleteForward),
|
||||
KeyCode::Left => Some(Self::MoveLeft),
|
||||
KeyCode::Right => Some(Self::MoveRight),
|
||||
KeyCode::Up => Some(Self::MoveUp),
|
||||
KeyCode::Down => Some(Self::MoveDown),
|
||||
KeyCode::Home => Some(Self::MoveLineStart),
|
||||
KeyCode::End => Some(Self::MoveLineEnd),
|
||||
KeyCode::Tab => Some(Self::NextField),
|
||||
KeyCode::BackTab => Some(Self::PrevField),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this action modifies content
|
||||
pub fn is_modifying(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::InsertChar(_) |
|
||||
Self::DeleteBackward |
|
||||
Self::DeleteForward |
|
||||
Self::SelectSuggestion
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if this action moves the cursor
|
||||
pub fn is_movement(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::MoveLeft | Self::MoveRight | Self::MoveUp | Self::MoveDown |
|
||||
Self::MoveLineStart | Self::MoveLineEnd | Self::MoveFirstLine | Self::MoveLastLine |
|
||||
Self::MoveWordNext | Self::MoveWordEnd | Self::MoveWordPrev | Self::MoveWordEndPrev |
|
||||
Self::NextField | Self::PrevField
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if this is a suggestion-related action
|
||||
pub fn is_suggestion(&self) -> bool {
|
||||
matches!(self,
|
||||
Self::SuggestionUp | Self::SuggestionDown |
|
||||
Self::SelectSuggestion | Self::ExitSuggestions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of executing a canvas action
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ActionResult {
|
||||
/// Action completed successfully, optional message for user feedback
|
||||
Success(Option<String>),
|
||||
/// Action was handled by custom feature logic
|
||||
HandledByFeature(String),
|
||||
/// Action requires additional context or cannot be performed
|
||||
RequiresContext(String),
|
||||
/// Action failed with error message
|
||||
Error(String),
|
||||
}
|
||||
|
||||
impl ActionResult {
|
||||
pub fn success() -> Self {
|
||||
Self::Success(None)
|
||||
}
|
||||
|
||||
pub fn success_with_message(msg: impl Into<String>) -> Self {
|
||||
Self::Success(Some(msg.into()))
|
||||
}
|
||||
|
||||
pub fn error(msg: impl Into<String>) -> Self {
|
||||
Self::Error(msg.into())
|
||||
}
|
||||
|
||||
pub fn is_success(&self) -> bool {
|
||||
matches!(self, Self::Success(_) | Self::HandledByFeature(_))
|
||||
}
|
||||
|
||||
pub fn message(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Success(msg) => msg.as_deref(),
|
||||
Self::HandledByFeature(msg) => Some(msg),
|
||||
Self::RequiresContext(msg) => Some(msg),
|
||||
Self::Error(msg) => Some(msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_action_from_string() {
|
||||
assert_eq!(CanvasAction::from_string("move_left"), CanvasAction::MoveLeft);
|
||||
assert_eq!(CanvasAction::from_string("delete_char_backward"), CanvasAction::DeleteBackward);
|
||||
assert_eq!(CanvasAction::from_string("unknown"), CanvasAction::Custom("unknown".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_from_key() {
|
||||
assert_eq!(CanvasAction::from_key(KeyCode::Char('a')), Some(CanvasAction::InsertChar('a')));
|
||||
assert_eq!(CanvasAction::from_key(KeyCode::Left), Some(CanvasAction::MoveLeft));
|
||||
assert_eq!(CanvasAction::from_key(KeyCode::Backspace), Some(CanvasAction::DeleteBackward));
|
||||
assert_eq!(CanvasAction::from_key(KeyCode::F(1)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_properties() {
|
||||
assert!(CanvasAction::InsertChar('a').is_modifying());
|
||||
assert!(!CanvasAction::MoveLeft.is_modifying());
|
||||
|
||||
assert!(CanvasAction::MoveLeft.is_movement());
|
||||
assert!(!CanvasAction::InsertChar('a').is_movement());
|
||||
|
||||
assert!(CanvasAction::SuggestionUp.is_suggestion());
|
||||
assert!(!CanvasAction::MoveLeft.is_suggestion());
|
||||
}
|
||||
}
|
||||
480
canvas/src/config.rs
Normal file
480
canvas/src/config.rs
Normal file
@@ -0,0 +1,480 @@
|
||||
// canvas/src/config.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CanvasConfig {
|
||||
#[serde(default)]
|
||||
pub keybindings: CanvasKeybindings,
|
||||
#[serde(default)]
|
||||
pub behavior: CanvasBehavior,
|
||||
#[serde(default)]
|
||||
pub appearance: CanvasAppearance,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CanvasKeybindings {
|
||||
#[serde(default)]
|
||||
pub read_only: HashMap<String, Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub edit: HashMap<String, Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub suggestions: HashMap<String, Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub global: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CanvasBehavior {
|
||||
#[serde(default = "default_wrap_around")]
|
||||
pub wrap_around_fields: bool,
|
||||
#[serde(default = "default_auto_save")]
|
||||
pub auto_save_on_field_change: bool,
|
||||
#[serde(default = "default_word_chars")]
|
||||
pub word_chars: String,
|
||||
#[serde(default = "default_suggestion_limit")]
|
||||
pub max_suggestions: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CanvasAppearance {
|
||||
#[serde(default = "default_cursor_style")]
|
||||
pub cursor_style: String, // "block", "bar", "underline"
|
||||
#[serde(default = "default_show_field_numbers")]
|
||||
pub show_field_numbers: bool,
|
||||
#[serde(default = "default_highlight_current_field")]
|
||||
pub highlight_current_field: bool,
|
||||
}
|
||||
|
||||
// Default values
|
||||
fn default_wrap_around() -> bool { true }
|
||||
fn default_auto_save() -> bool { false }
|
||||
fn default_word_chars() -> String { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_".to_string() }
|
||||
fn default_suggestion_limit() -> usize { 10 }
|
||||
fn default_cursor_style() -> String { "block".to_string() }
|
||||
fn default_show_field_numbers() -> bool { false }
|
||||
fn default_highlight_current_field() -> bool { true }
|
||||
|
||||
impl Default for CanvasBehavior {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
wrap_around_fields: default_wrap_around(),
|
||||
auto_save_on_field_change: default_auto_save(),
|
||||
word_chars: default_word_chars(),
|
||||
max_suggestions: default_suggestion_limit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CanvasAppearance {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cursor_style: default_cursor_style(),
|
||||
show_field_numbers: default_show_field_numbers(),
|
||||
highlight_current_field: default_highlight_current_field(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CanvasConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keybindings: CanvasKeybindings::with_vim_defaults(),
|
||||
behavior: CanvasBehavior::default(),
|
||||
appearance: CanvasAppearance::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasKeybindings {
|
||||
pub fn with_vim_defaults() -> Self {
|
||||
let mut keybindings = Self::default();
|
||||
|
||||
// Read-only mode (vim-style navigation)
|
||||
keybindings.read_only.insert("move_left".to_string(), vec!["h".to_string()]);
|
||||
keybindings.read_only.insert("move_right".to_string(), vec!["l".to_string()]);
|
||||
keybindings.read_only.insert("move_up".to_string(), vec!["k".to_string()]);
|
||||
keybindings.read_only.insert("move_down".to_string(), vec!["j".to_string()]);
|
||||
keybindings.read_only.insert("move_word_next".to_string(), vec!["w".to_string()]);
|
||||
keybindings.read_only.insert("move_word_end".to_string(), vec!["e".to_string()]);
|
||||
keybindings.read_only.insert("move_word_prev".to_string(), vec!["b".to_string()]);
|
||||
keybindings.read_only.insert("move_word_end_prev".to_string(), vec!["ge".to_string()]);
|
||||
keybindings.read_only.insert("move_line_start".to_string(), vec!["0".to_string()]);
|
||||
keybindings.read_only.insert("move_line_end".to_string(), vec!["$".to_string()]);
|
||||
keybindings.read_only.insert("move_first_line".to_string(), vec!["gg".to_string()]);
|
||||
keybindings.read_only.insert("move_last_line".to_string(), vec!["G".to_string()]);
|
||||
keybindings.read_only.insert("next_field".to_string(), vec!["Tab".to_string()]);
|
||||
keybindings.read_only.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]);
|
||||
|
||||
// Edit mode
|
||||
keybindings.edit.insert("delete_char_backward".to_string(), vec!["Backspace".to_string()]);
|
||||
keybindings.edit.insert("delete_char_forward".to_string(), vec!["Delete".to_string()]);
|
||||
keybindings.edit.insert("move_left".to_string(), vec!["Left".to_string()]);
|
||||
keybindings.edit.insert("move_right".to_string(), vec!["Right".to_string()]);
|
||||
keybindings.edit.insert("move_up".to_string(), vec!["Up".to_string()]);
|
||||
keybindings.edit.insert("move_down".to_string(), vec!["Down".to_string()]);
|
||||
keybindings.edit.insert("move_line_start".to_string(), vec!["Home".to_string()]);
|
||||
keybindings.edit.insert("move_line_end".to_string(), vec!["End".to_string()]);
|
||||
keybindings.edit.insert("move_word_next".to_string(), vec!["Ctrl+Right".to_string()]);
|
||||
keybindings.edit.insert("move_word_prev".to_string(), vec!["Ctrl+Left".to_string()]);
|
||||
keybindings.edit.insert("next_field".to_string(), vec!["Tab".to_string()]);
|
||||
keybindings.edit.insert("prev_field".to_string(), vec!["Shift+Tab".to_string()]);
|
||||
|
||||
// Suggestions
|
||||
keybindings.suggestions.insert("suggestion_up".to_string(), vec!["Up".to_string(), "Ctrl+p".to_string()]);
|
||||
keybindings.suggestions.insert("suggestion_down".to_string(), vec!["Down".to_string(), "Ctrl+n".to_string()]);
|
||||
keybindings.suggestions.insert("select_suggestion".to_string(), vec!["Enter".to_string(), "Tab".to_string()]);
|
||||
keybindings.suggestions.insert("exit_suggestions".to_string(), vec!["Esc".to_string()]);
|
||||
|
||||
// Global (works in both modes)
|
||||
keybindings.global.insert("move_up".to_string(), vec!["Up".to_string()]);
|
||||
keybindings.global.insert("move_down".to_string(), vec!["Down".to_string()]);
|
||||
|
||||
keybindings
|
||||
}
|
||||
|
||||
pub fn with_emacs_defaults() -> Self {
|
||||
let mut keybindings = Self::default();
|
||||
|
||||
// Emacs-style bindings
|
||||
keybindings.read_only.insert("move_left".to_string(), vec!["Ctrl+b".to_string()]);
|
||||
keybindings.read_only.insert("move_right".to_string(), vec!["Ctrl+f".to_string()]);
|
||||
keybindings.read_only.insert("move_up".to_string(), vec!["Ctrl+p".to_string()]);
|
||||
keybindings.read_only.insert("move_down".to_string(), vec!["Ctrl+n".to_string()]);
|
||||
keybindings.read_only.insert("move_word_next".to_string(), vec!["Alt+f".to_string()]);
|
||||
keybindings.read_only.insert("move_word_prev".to_string(), vec!["Alt+b".to_string()]);
|
||||
keybindings.read_only.insert("move_line_start".to_string(), vec!["Ctrl+a".to_string()]);
|
||||
keybindings.read_only.insert("move_line_end".to_string(), vec!["Ctrl+e".to_string()]);
|
||||
|
||||
keybindings.edit.insert("delete_char_backward".to_string(), vec!["Ctrl+h".to_string(), "Backspace".to_string()]);
|
||||
keybindings.edit.insert("delete_char_forward".to_string(), vec!["Ctrl+d".to_string(), "Delete".to_string()]);
|
||||
|
||||
keybindings
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasConfig {
|
||||
/// Load from canvas_config.toml or fallback to vim defaults
|
||||
pub fn load() -> Self {
|
||||
// Try to load canvas_config.toml from current directory
|
||||
if let Ok(config) = Self::from_file(std::path::Path::new("canvas_config.toml")) {
|
||||
return config;
|
||||
}
|
||||
|
||||
// Fallback to vim defaults
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Load from TOML string
|
||||
pub fn from_toml(toml_str: &str) -> Result<Self> {
|
||||
toml::from_str(toml_str)
|
||||
.with_context(|| "Failed to parse canvas config TOML")
|
||||
}
|
||||
|
||||
/// Load from file
|
||||
pub fn from_file(path: &std::path::Path) -> Result<Self> {
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read config file: {:?}", path))?;
|
||||
Self::from_toml(&contents)
|
||||
}
|
||||
|
||||
/// Get action for key in read-only mode
|
||||
pub fn get_read_only_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||
self.get_action_in_mode(&self.keybindings.read_only, key, modifiers)
|
||||
.or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers))
|
||||
}
|
||||
|
||||
/// Get action for key in edit mode
|
||||
pub fn get_edit_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||
self.get_action_in_mode(&self.keybindings.edit, key, modifiers)
|
||||
.or_else(|| self.get_action_in_mode(&self.keybindings.global, key, modifiers))
|
||||
}
|
||||
|
||||
/// Get action for key in suggestions mode
|
||||
pub fn get_suggestion_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
|
||||
self.get_action_in_mode(&self.keybindings.suggestions, key, modifiers)
|
||||
}
|
||||
|
||||
/// Get action for key (mode-aware)
|
||||
pub fn get_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers, is_edit_mode: bool, has_suggestions: bool) -> Option<&str> {
|
||||
// Suggestions take priority when active
|
||||
if has_suggestions {
|
||||
if let Some(action) = self.get_suggestion_action(key, modifiers) {
|
||||
return Some(action);
|
||||
}
|
||||
}
|
||||
|
||||
// Then check mode-specific
|
||||
if is_edit_mode {
|
||||
self.get_edit_action(key, modifiers)
|
||||
} else {
|
||||
self.get_read_only_action(key, modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_action_in_mode<'a>(&self, mode_bindings: &'a HashMap<String, Vec<String>>, key: KeyCode, modifiers: KeyModifiers) -> Option<&'a str> {
|
||||
for (action, bindings) in mode_bindings {
|
||||
for binding in bindings {
|
||||
if self.matches_keybinding(binding, key, modifiers) {
|
||||
return Some(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn matches_keybinding(&self, binding: &str, key: KeyCode, modifiers: KeyModifiers) -> bool {
|
||||
// Special handling for shift+character combinations
|
||||
if binding.to_lowercase().starts_with("shift+") {
|
||||
let parts: Vec<&str> = binding.split('+').collect();
|
||||
if parts.len() == 2 && parts[1].len() == 1 {
|
||||
let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap();
|
||||
let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap();
|
||||
if let KeyCode::Char(actual_char) = key {
|
||||
if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Shift+Tab -> BackTab
|
||||
if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle multi-character bindings (all standard keys without modifiers)
|
||||
if binding.len() > 1 && !binding.contains('+') {
|
||||
return match binding.to_lowercase().as_str() {
|
||||
// Navigation keys
|
||||
"left" => key == KeyCode::Left,
|
||||
"right" => key == KeyCode::Right,
|
||||
"up" => key == KeyCode::Up,
|
||||
"down" => key == KeyCode::Down,
|
||||
"home" => key == KeyCode::Home,
|
||||
"end" => key == KeyCode::End,
|
||||
"pageup" | "pgup" => key == KeyCode::PageUp,
|
||||
"pagedown" | "pgdn" => key == KeyCode::PageDown,
|
||||
|
||||
// Editing keys
|
||||
"insert" | "ins" => key == KeyCode::Insert,
|
||||
"delete" | "del" => key == KeyCode::Delete,
|
||||
"backspace" => key == KeyCode::Backspace,
|
||||
|
||||
// Tab keys
|
||||
"tab" => key == KeyCode::Tab,
|
||||
"backtab" => key == KeyCode::BackTab,
|
||||
|
||||
// Special keys
|
||||
"enter" | "return" => key == KeyCode::Enter,
|
||||
"escape" | "esc" => key == KeyCode::Esc,
|
||||
"space" => key == KeyCode::Char(' '),
|
||||
|
||||
// Function keys F1-F24
|
||||
"f1" => key == KeyCode::F(1),
|
||||
"f2" => key == KeyCode::F(2),
|
||||
"f3" => key == KeyCode::F(3),
|
||||
"f4" => key == KeyCode::F(4),
|
||||
"f5" => key == KeyCode::F(5),
|
||||
"f6" => key == KeyCode::F(6),
|
||||
"f7" => key == KeyCode::F(7),
|
||||
"f8" => key == KeyCode::F(8),
|
||||
"f9" => key == KeyCode::F(9),
|
||||
"f10" => key == KeyCode::F(10),
|
||||
"f11" => key == KeyCode::F(11),
|
||||
"f12" => key == KeyCode::F(12),
|
||||
"f13" => key == KeyCode::F(13),
|
||||
"f14" => key == KeyCode::F(14),
|
||||
"f15" => key == KeyCode::F(15),
|
||||
"f16" => key == KeyCode::F(16),
|
||||
"f17" => key == KeyCode::F(17),
|
||||
"f18" => key == KeyCode::F(18),
|
||||
"f19" => key == KeyCode::F(19),
|
||||
"f20" => key == KeyCode::F(20),
|
||||
"f21" => key == KeyCode::F(21),
|
||||
"f22" => key == KeyCode::F(22),
|
||||
"f23" => key == KeyCode::F(23),
|
||||
"f24" => key == KeyCode::F(24),
|
||||
|
||||
// Lock keys (may not work reliably in all terminals)
|
||||
"capslock" => key == KeyCode::CapsLock,
|
||||
"scrolllock" => key == KeyCode::ScrollLock,
|
||||
"numlock" => key == KeyCode::NumLock,
|
||||
|
||||
// System keys
|
||||
"printscreen" => key == KeyCode::PrintScreen,
|
||||
"pause" => key == KeyCode::Pause,
|
||||
"menu" => key == KeyCode::Menu,
|
||||
"keypadbegin" => key == KeyCode::KeypadBegin,
|
||||
|
||||
// Media keys (rarely supported but included for completeness)
|
||||
"mediaplay" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Play),
|
||||
"mediapause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Pause),
|
||||
"mediaplaypause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::PlayPause),
|
||||
"mediareverse" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Reverse),
|
||||
"mediastop" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Stop),
|
||||
"mediafastforward" => key == KeyCode::Media(crossterm::event::MediaKeyCode::FastForward),
|
||||
"mediarewind" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Rewind),
|
||||
"mediatracknext" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackNext),
|
||||
"mediatrackprevious" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackPrevious),
|
||||
"mediarecord" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Record),
|
||||
"medialowervolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::LowerVolume),
|
||||
"mediaraisevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::RaiseVolume),
|
||||
"mediamutevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::MuteVolume),
|
||||
|
||||
// Modifier keys (these work better as part of combinations)
|
||||
"leftshift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftShift),
|
||||
"leftcontrol" | "leftctrl" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftControl),
|
||||
"leftalt" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftAlt),
|
||||
"leftsuper" | "leftwindows" | "leftcmd" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftSuper),
|
||||
"lefthyper" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftHyper),
|
||||
"leftmeta" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::LeftMeta),
|
||||
"rightshift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightShift),
|
||||
"rightcontrol" | "rightctrl" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightControl),
|
||||
"rightalt" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightAlt),
|
||||
"rightsuper" | "rightwindows" | "rightcmd" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightSuper),
|
||||
"righthyper" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightHyper),
|
||||
"rightmeta" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::RightMeta),
|
||||
"isolevel3shift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::IsoLevel3Shift),
|
||||
"isolevel5shift" => key == KeyCode::Modifier(crossterm::event::ModifierKeyCode::IsoLevel5Shift),
|
||||
|
||||
// Multi-key sequences need special handling
|
||||
"gg" => false, // This needs sequence handling
|
||||
_ => {
|
||||
// Handle single characters and punctuation
|
||||
if binding.len() == 1 {
|
||||
if let Some(c) = binding.chars().next() {
|
||||
key == KeyCode::Char(c)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle modifier combinations (like "Ctrl+F5", "Alt+Shift+A")
|
||||
let parts: Vec<&str> = binding.split('+').collect();
|
||||
let mut expected_modifiers = KeyModifiers::empty();
|
||||
let mut expected_key = None;
|
||||
|
||||
for part in parts {
|
||||
match part.to_lowercase().as_str() {
|
||||
// Modifiers
|
||||
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
|
||||
"shift" => expected_modifiers |= KeyModifiers::SHIFT,
|
||||
"alt" => expected_modifiers |= KeyModifiers::ALT,
|
||||
"super" | "windows" | "cmd" => expected_modifiers |= KeyModifiers::SUPER,
|
||||
"hyper" => expected_modifiers |= KeyModifiers::HYPER,
|
||||
"meta" => expected_modifiers |= KeyModifiers::META,
|
||||
|
||||
// Navigation keys
|
||||
"left" => expected_key = Some(KeyCode::Left),
|
||||
"right" => expected_key = Some(KeyCode::Right),
|
||||
"up" => expected_key = Some(KeyCode::Up),
|
||||
"down" => expected_key = Some(KeyCode::Down),
|
||||
"home" => expected_key = Some(KeyCode::Home),
|
||||
"end" => expected_key = Some(KeyCode::End),
|
||||
"pageup" | "pgup" => expected_key = Some(KeyCode::PageUp),
|
||||
"pagedown" | "pgdn" => expected_key = Some(KeyCode::PageDown),
|
||||
|
||||
// Editing keys
|
||||
"insert" | "ins" => expected_key = Some(KeyCode::Insert),
|
||||
"delete" | "del" => expected_key = Some(KeyCode::Delete),
|
||||
"backspace" => expected_key = Some(KeyCode::Backspace),
|
||||
|
||||
// Tab keys
|
||||
"tab" => expected_key = Some(KeyCode::Tab),
|
||||
"backtab" => expected_key = Some(KeyCode::BackTab),
|
||||
|
||||
// Special keys
|
||||
"enter" | "return" => expected_key = Some(KeyCode::Enter),
|
||||
"escape" | "esc" => expected_key = Some(KeyCode::Esc),
|
||||
"space" => expected_key = Some(KeyCode::Char(' ')),
|
||||
|
||||
// Function keys
|
||||
"f1" => expected_key = Some(KeyCode::F(1)),
|
||||
"f2" => expected_key = Some(KeyCode::F(2)),
|
||||
"f3" => expected_key = Some(KeyCode::F(3)),
|
||||
"f4" => expected_key = Some(KeyCode::F(4)),
|
||||
"f5" => expected_key = Some(KeyCode::F(5)),
|
||||
"f6" => expected_key = Some(KeyCode::F(6)),
|
||||
"f7" => expected_key = Some(KeyCode::F(7)),
|
||||
"f8" => expected_key = Some(KeyCode::F(8)),
|
||||
"f9" => expected_key = Some(KeyCode::F(9)),
|
||||
"f10" => expected_key = Some(KeyCode::F(10)),
|
||||
"f11" => expected_key = Some(KeyCode::F(11)),
|
||||
"f12" => expected_key = Some(KeyCode::F(12)),
|
||||
"f13" => expected_key = Some(KeyCode::F(13)),
|
||||
"f14" => expected_key = Some(KeyCode::F(14)),
|
||||
"f15" => expected_key = Some(KeyCode::F(15)),
|
||||
"f16" => expected_key = Some(KeyCode::F(16)),
|
||||
"f17" => expected_key = Some(KeyCode::F(17)),
|
||||
"f18" => expected_key = Some(KeyCode::F(18)),
|
||||
"f19" => expected_key = Some(KeyCode::F(19)),
|
||||
"f20" => expected_key = Some(KeyCode::F(20)),
|
||||
"f21" => expected_key = Some(KeyCode::F(21)),
|
||||
"f22" => expected_key = Some(KeyCode::F(22)),
|
||||
"f23" => expected_key = Some(KeyCode::F(23)),
|
||||
"f24" => expected_key = Some(KeyCode::F(24)),
|
||||
|
||||
// Lock keys
|
||||
"capslock" => expected_key = Some(KeyCode::CapsLock),
|
||||
"scrolllock" => expected_key = Some(KeyCode::ScrollLock),
|
||||
"numlock" => expected_key = Some(KeyCode::NumLock),
|
||||
|
||||
// System keys
|
||||
"printscreen" => expected_key = Some(KeyCode::PrintScreen),
|
||||
"pause" => expected_key = Some(KeyCode::Pause),
|
||||
"menu" => expected_key = Some(KeyCode::Menu),
|
||||
"keypadbegin" => expected_key = Some(KeyCode::KeypadBegin),
|
||||
|
||||
// Single character (letters, numbers, punctuation)
|
||||
part => {
|
||||
if part.len() == 1 {
|
||||
if let Some(c) = part.chars().next() {
|
||||
expected_key = Some(KeyCode::Char(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modifiers == expected_modifiers && Some(key) == expected_key
|
||||
}
|
||||
|
||||
/// Convenience method to create vim preset
|
||||
pub fn vim_preset() -> Self {
|
||||
Self {
|
||||
keybindings: CanvasKeybindings::with_vim_defaults(),
|
||||
behavior: CanvasBehavior::default(),
|
||||
appearance: CanvasAppearance::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method to create emacs preset
|
||||
pub fn emacs_preset() -> Self {
|
||||
Self {
|
||||
keybindings: CanvasKeybindings::with_emacs_defaults(),
|
||||
behavior: CanvasBehavior::default(),
|
||||
appearance: CanvasAppearance::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug method to print loaded keybindings
|
||||
pub fn debug_keybindings(&self) {
|
||||
println!("📋 Canvas keybindings loaded:");
|
||||
println!(" Read-only: {} actions", self.keybindings.read_only.len());
|
||||
println!(" Edit: {} actions", self.keybindings.edit.len());
|
||||
println!(" Suggestions: {} actions", self.keybindings.suggestions.len());
|
||||
println!(" Global: {} actions", self.keybindings.global.len());
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export for convenience
|
||||
pub use crate::actions::CanvasAction;
|
||||
pub use crate::dispatcher::ActionDispatcher;
|
||||
180
canvas/src/dispatcher.rs
Normal file
180
canvas/src/dispatcher.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
// canvas/src/dispatcher.rs
|
||||
|
||||
use crate::state::CanvasState;
|
||||
use crate::actions::{CanvasAction, ActionResult, execute_canvas_action};
|
||||
|
||||
/// High-level action dispatcher that coordinates between different action types
|
||||
pub struct ActionDispatcher;
|
||||
|
||||
impl ActionDispatcher {
|
||||
/// Dispatch any action to the appropriate handler
|
||||
pub async fn dispatch<S: CanvasState>(
|
||||
action: CanvasAction,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> anyhow::Result<ActionResult> {
|
||||
execute_canvas_action(action, state, ideal_cursor_column).await
|
||||
}
|
||||
|
||||
/// Quick action dispatch from KeyCode
|
||||
pub async fn dispatch_key<S: CanvasState>(
|
||||
key: crossterm::event::KeyCode,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> anyhow::Result<Option<ActionResult>> {
|
||||
if let Some(action) = CanvasAction::from_key(key) {
|
||||
let result = Self::dispatch(action, state, ideal_cursor_column).await?;
|
||||
Ok(Some(result))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch dispatch multiple actions
|
||||
pub async fn dispatch_batch<S: CanvasState>(
|
||||
actions: Vec<CanvasAction>,
|
||||
state: &mut S,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> anyhow::Result<Vec<ActionResult>> {
|
||||
let mut results = Vec::new();
|
||||
for action in actions {
|
||||
let result = Self::dispatch(action, state, ideal_cursor_column).await?;
|
||||
let is_success = result.is_success(); // Check success before moving
|
||||
results.push(result);
|
||||
|
||||
// Stop on first error
|
||||
if !is_success {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::actions::CanvasAction;
|
||||
|
||||
// Simple test implementation
|
||||
struct TestFormState {
|
||||
current_field: usize,
|
||||
cursor_pos: usize,
|
||||
inputs: Vec<String>,
|
||||
field_names: Vec<String>,
|
||||
has_changes: bool,
|
||||
}
|
||||
|
||||
impl TestFormState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
current_field: 0,
|
||||
cursor_pos: 0,
|
||||
inputs: vec!["".to_string(), "".to_string()],
|
||||
field_names: vec!["username".to_string(), "password".to_string()],
|
||||
has_changes: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CanvasState for TestFormState {
|
||||
fn current_field(&self) -> usize { self.current_field }
|
||||
fn current_cursor_pos(&self) -> usize { self.cursor_pos }
|
||||
fn set_current_field(&mut self, index: usize) { self.current_field = index; }
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) { self.cursor_pos = pos; }
|
||||
|
||||
fn get_current_input(&self) -> &str { &self.inputs[self.current_field] }
|
||||
fn get_current_input_mut(&mut self) -> &mut String { &mut self.inputs[self.current_field] }
|
||||
fn inputs(&self) -> Vec<&String> { self.inputs.iter().collect() }
|
||||
fn fields(&self) -> Vec<&str> { self.field_names.iter().map(|s| s.as_str()).collect() }
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool { self.has_changes }
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) { self.has_changes = changed; }
|
||||
|
||||
// Custom action handling for testing
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &crate::state::ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::Custom(s) if s == "test_custom" => {
|
||||
Some("Custom action handled".to_string())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_typed_action_dispatch() {
|
||||
let mut state = TestFormState::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
// Test character insertion
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::InsertChar('a'),
|
||||
&mut state,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
assert!(result.is_success());
|
||||
assert_eq!(state.get_current_input(), "a");
|
||||
assert_eq!(state.cursor_pos, 1);
|
||||
assert!(state.has_changes);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_key_dispatch() {
|
||||
let mut state = TestFormState::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
let result = ActionDispatcher::dispatch_key(
|
||||
crossterm::event::KeyCode::Char('b'),
|
||||
&mut state,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
assert!(result.is_some());
|
||||
assert!(result.unwrap().is_success());
|
||||
assert_eq!(state.get_current_input(), "b");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_custom_action() {
|
||||
let mut state = TestFormState::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
let result = ActionDispatcher::dispatch(
|
||||
CanvasAction::Custom("test_custom".to_string()),
|
||||
&mut state,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
match result {
|
||||
ActionResult::HandledByFeature(msg) => {
|
||||
assert_eq!(msg, "Custom action handled");
|
||||
}
|
||||
_ => panic!("Expected HandledByFeature result"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_batch_dispatch() {
|
||||
let mut state = TestFormState::new();
|
||||
let mut ideal_cursor = 0;
|
||||
|
||||
let actions = vec![
|
||||
CanvasAction::InsertChar('h'),
|
||||
CanvasAction::InsertChar('i'),
|
||||
CanvasAction::MoveLeft,
|
||||
CanvasAction::InsertChar('e'),
|
||||
];
|
||||
|
||||
let results = ActionDispatcher::dispatch_batch(
|
||||
actions,
|
||||
&mut state,
|
||||
&mut ideal_cursor,
|
||||
).await.unwrap();
|
||||
|
||||
assert_eq!(results.len(), 4);
|
||||
assert!(results.iter().all(|r| r.is_success()));
|
||||
assert_eq!(state.get_current_input(), "hei");
|
||||
}
|
||||
}
|
||||
36
canvas/src/lib.rs
Normal file
36
canvas/src/lib.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// canvas/src/lib.rs
|
||||
//! Canvas - A reusable text editing and form canvas system
|
||||
//!
|
||||
//! This crate provides a generic canvas abstraction for building text-based interfaces
|
||||
//! with multiple input fields, cursor management, and mode-based editing.
|
||||
|
||||
pub mod state;
|
||||
pub mod actions;
|
||||
pub mod modes;
|
||||
pub mod config;
|
||||
pub mod suggestions;
|
||||
pub mod dispatcher;
|
||||
|
||||
// Re-export the main types for easy use
|
||||
pub use state::{CanvasState, ActionContext};
|
||||
pub use actions::{CanvasAction, ActionResult, execute_edit_action, execute_canvas_action};
|
||||
pub use modes::{AppMode, ModeManager, HighlightState};
|
||||
pub use suggestions::SuggestionState;
|
||||
pub use dispatcher::ActionDispatcher;
|
||||
|
||||
// High-level convenience API
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
CanvasState,
|
||||
ActionContext,
|
||||
CanvasAction,
|
||||
ActionResult,
|
||||
execute_edit_action,
|
||||
execute_canvas_action,
|
||||
ActionDispatcher,
|
||||
AppMode,
|
||||
ModeManager,
|
||||
HighlightState,
|
||||
SuggestionState,
|
||||
};
|
||||
}
|
||||
15
canvas/src/modes/highlight.rs
Normal file
15
canvas/src/modes/highlight.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
// src/state/app/highlight.rs
|
||||
// canvas/src/modes/highlight.rs
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum HighlightState {
|
||||
Off,
|
||||
Characterwise { anchor: (usize, usize) }, // (field_index, char_position)
|
||||
Linewise { anchor_line: usize }, // field_index
|
||||
}
|
||||
|
||||
impl Default for HighlightState {
|
||||
fn default() -> Self {
|
||||
HighlightState::Off
|
||||
}
|
||||
}
|
||||
33
canvas/src/modes/manager.rs
Normal file
33
canvas/src/modes/manager.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
// src/modes/handlers/mode_manager.rs
|
||||
// canvas/src/modes/manager.rs
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AppMode {
|
||||
General, // For intro and admin screens
|
||||
ReadOnly, // Canvas read-only mode
|
||||
Edit, // Canvas edit mode
|
||||
Highlight, // Canvas highlight/visual mode
|
||||
Command, // Command mode overlay
|
||||
}
|
||||
|
||||
pub struct ModeManager;
|
||||
|
||||
impl ModeManager {
|
||||
// Mode transition rules
|
||||
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
|
||||
!matches!(current_mode, AppMode::Edit)
|
||||
}
|
||||
|
||||
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
|
||||
matches!(current_mode, AppMode::ReadOnly)
|
||||
}
|
||||
|
||||
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
|
||||
matches!(current_mode, AppMode::Edit | AppMode::Command | AppMode::Highlight)
|
||||
}
|
||||
|
||||
pub fn can_enter_highlight_mode(current_mode: AppMode) -> bool {
|
||||
matches!(current_mode, AppMode::ReadOnly)
|
||||
}
|
||||
}
|
||||
7
canvas/src/modes/mod.rs
Normal file
7
canvas/src/modes/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// canvas/src/modes/mod.rs
|
||||
|
||||
pub mod highlight;
|
||||
pub mod manager;
|
||||
|
||||
pub use highlight::HighlightState;
|
||||
pub use manager::{AppMode, ModeManager};
|
||||
83
canvas/src/state.rs
Normal file
83
canvas/src/state.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
// canvas/src/state.rs
|
||||
|
||||
use crate::actions::CanvasAction;
|
||||
|
||||
/// Context passed to feature-specific action handlers
|
||||
#[derive(Debug)]
|
||||
pub struct ActionContext {
|
||||
pub key_code: Option<crossterm::event::KeyCode>, // Kept for backwards compatibility
|
||||
pub ideal_cursor_column: usize,
|
||||
pub current_input: String,
|
||||
pub current_field: usize,
|
||||
}
|
||||
|
||||
/// Core trait that any form-like state must implement to work with the canvas system.
|
||||
/// This enables the same mode behaviors (edit, read-only, highlight) to work across
|
||||
/// any implementation - login forms, data entry forms, configuration screens, etc.
|
||||
pub trait CanvasState {
|
||||
// --- Core Navigation ---
|
||||
fn current_field(&self) -> usize;
|
||||
fn current_cursor_pos(&self) -> usize;
|
||||
fn set_current_field(&mut self, index: usize);
|
||||
fn set_current_cursor_pos(&mut self, pos: usize);
|
||||
|
||||
// --- Data Access ---
|
||||
fn get_current_input(&self) -> &str;
|
||||
fn get_current_input_mut(&mut self) -> &mut String;
|
||||
fn inputs(&self) -> Vec<&String>;
|
||||
fn fields(&self) -> Vec<&str>;
|
||||
|
||||
// --- State Management ---
|
||||
fn has_unsaved_changes(&self) -> bool;
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool);
|
||||
|
||||
// --- Autocomplete/Suggestions (Optional) ---
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
None
|
||||
}
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
fn set_selected_suggestion_index(&mut self, _index: Option<usize>) {
|
||||
// Default: no-op (override if you support suggestions)
|
||||
}
|
||||
fn activate_suggestions(&mut self, _suggestions: Vec<String>) {
|
||||
// Default: no-op (override if you support suggestions)
|
||||
}
|
||||
fn deactivate_suggestions(&mut self) {
|
||||
// Default: no-op (override if you support suggestions)
|
||||
}
|
||||
|
||||
// --- Feature-specific action handling (NEW: Type-safe) ---
|
||||
fn handle_feature_action(&mut self, _action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
None // Default: no feature-specific handling
|
||||
}
|
||||
|
||||
// --- Legacy string-based action handling (for backwards compatibility) ---
|
||||
fn handle_feature_action_legacy(&mut self, action: &str, context: &ActionContext) -> Option<String> {
|
||||
// Convert string to typed action and delegate
|
||||
let typed_action = match action {
|
||||
"insert_char" => {
|
||||
// This is tricky - we need the char from the KeyCode in context
|
||||
if let Some(crossterm::event::KeyCode::Char(c)) = context.key_code {
|
||||
CanvasAction::InsertChar(c)
|
||||
} else {
|
||||
CanvasAction::Custom(action.to_string())
|
||||
}
|
||||
}
|
||||
_ => CanvasAction::from_string(action),
|
||||
};
|
||||
self.handle_feature_action(&typed_action, context)
|
||||
}
|
||||
|
||||
// --- Display Overrides (for links, computed values, etc.) ---
|
||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||
self.inputs()
|
||||
.get(index)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
fn has_display_override(&self, _index: usize) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
67
canvas/src/suggestions.rs
Normal file
67
canvas/src/suggestions.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// canvas/src/suggestions.rs
|
||||
|
||||
/// Generic suggestion system that can be implemented by any CanvasState
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SuggestionState {
|
||||
pub suggestions: Vec<String>,
|
||||
pub selected_index: Option<usize>,
|
||||
pub is_active: bool,
|
||||
pub trigger_chars: Vec<char>, // Characters that trigger suggestions
|
||||
}
|
||||
|
||||
impl Default for SuggestionState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
suggestions: Vec::new(),
|
||||
selected_index: None,
|
||||
is_active: false,
|
||||
trigger_chars: vec![], // No auto-trigger by default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SuggestionState {
|
||||
pub fn new(trigger_chars: Vec<char>) -> Self {
|
||||
Self {
|
||||
trigger_chars,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_with_suggestions(&mut self, suggestions: Vec<String>) {
|
||||
self.suggestions = suggestions;
|
||||
self.is_active = !self.suggestions.is_empty();
|
||||
self.selected_index = if self.is_active { Some(0) } else { None };
|
||||
}
|
||||
|
||||
pub fn deactivate(&mut self) {
|
||||
self.suggestions.clear();
|
||||
self.selected_index = None;
|
||||
self.is_active = false;
|
||||
}
|
||||
|
||||
pub fn select_next(&mut self) {
|
||||
if !self.suggestions.is_empty() {
|
||||
let current = self.selected_index.unwrap_or(0);
|
||||
self.selected_index = Some((current + 1) % self.suggestions.len());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(&mut self) {
|
||||
if !self.suggestions.is_empty() {
|
||||
let current = self.selected_index.unwrap_or(0);
|
||||
self.selected_index = Some(
|
||||
if current == 0 { self.suggestions.len() - 1 } else { current - 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected(&self) -> Option<&String> {
|
||||
self.selected_index
|
||||
.and_then(|idx| self.suggestions.get(idx))
|
||||
}
|
||||
|
||||
pub fn should_trigger(&self, c: char) -> bool {
|
||||
self.trigger_chars.contains(&c)
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,23 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.98"
|
||||
anyhow = { workspace = true }
|
||||
async-trait = "0.1.88"
|
||||
common = { path = "../common" }
|
||||
canvas = { path = "../canvas" }
|
||||
|
||||
ratatui = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
crossterm = "0.28.1"
|
||||
dirs = "6.0.0"
|
||||
dotenvy = "0.15.7"
|
||||
lazy_static = "1.5.0"
|
||||
prost = "0.13.5"
|
||||
ratatui = { version = "0.29.0", features = ["crossterm"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
time = "0.3.41"
|
||||
tokio = { version = "1.44.2", features = ["full", "macros"] }
|
||||
toml = "0.8.20"
|
||||
toml = { workspace = true }
|
||||
tonic = "0.13.0"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3.19"
|
||||
@@ -31,3 +32,9 @@ unicode-width = "0.2.0"
|
||||
[features]
|
||||
default = []
|
||||
ui-debug = []
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.25.0"
|
||||
tokio-test = "0.4.4"
|
||||
uuid = { version = "1.17.0", features = ["v4"] }
|
||||
futures = "0.3.31"
|
||||
|
||||
56
client/canvas_config.toml
Normal file
56
client/canvas_config.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
# canvas_config.toml - Complete Canvas Configuration
|
||||
|
||||
[behavior]
|
||||
wrap_around_fields = true
|
||||
auto_save_on_field_change = false
|
||||
word_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
|
||||
max_suggestions = 6
|
||||
|
||||
[appearance]
|
||||
cursor_style = "block" # "block", "bar", "underline"
|
||||
show_field_numbers = false
|
||||
highlight_current_field = true
|
||||
|
||||
# Read-only mode keybindings (vim-style)
|
||||
[keybindings.read_only]
|
||||
move_left = ["h"]
|
||||
move_right = ["l"]
|
||||
move_up = ["k"]
|
||||
move_down = ["j"]
|
||||
move_word_next = ["w"]
|
||||
move_word_end = ["e"]
|
||||
move_word_prev = ["b"]
|
||||
move_word_end_prev = ["ge"]
|
||||
move_line_start = ["0"]
|
||||
move_line_end = ["$"]
|
||||
move_first_line = ["gg"]
|
||||
move_last_line = ["CapsLock"]
|
||||
next_field = ["Tab"]
|
||||
prev_field = ["Shift+Tab"]
|
||||
|
||||
# Edit mode keybindings
|
||||
[keybindings.edit]
|
||||
delete_char_backward = ["Backspace"]
|
||||
delete_char_forward = ["Delete"]
|
||||
move_left = ["Left"]
|
||||
move_right = ["Right"]
|
||||
move_up = ["Up"]
|
||||
move_down = ["Down"]
|
||||
move_line_start = ["Home"]
|
||||
move_line_end = ["End"]
|
||||
move_word_next = ["Ctrl+Right"]
|
||||
move_word_prev = ["Ctrl+Left"]
|
||||
next_field = ["Tab"]
|
||||
prev_field = ["Shift+Tab"]
|
||||
|
||||
# Suggestion/autocomplete keybindings
|
||||
[keybindings.suggestions]
|
||||
suggestion_up = ["Up", "Ctrl+p"]
|
||||
suggestion_down = ["Down", "Ctrl+n"]
|
||||
select_suggestion = ["Enter", "Tab"]
|
||||
exit_suggestions = ["Esc"]
|
||||
|
||||
# Global keybindings (work in both modes)
|
||||
[keybindings.global]
|
||||
move_up = ["Up"]
|
||||
move_down = ["Down"]
|
||||
@@ -2,9 +2,9 @@
|
||||
[keybindings]
|
||||
|
||||
enter_command_mode = [":", "ctrl+;"]
|
||||
next_buffer = ["ctrl+l"]
|
||||
previous_buffer = ["ctrl+h"]
|
||||
close_buffer = ["ctrl+k"]
|
||||
next_buffer = ["space+b+n"]
|
||||
previous_buffer = ["space+b+p"]
|
||||
close_buffer = ["space+b+d"]
|
||||
|
||||
[keybindings.general]
|
||||
move_up = ["k", "Up"]
|
||||
@@ -22,8 +22,6 @@ open_search = ["ctrl+f"]
|
||||
[keybindings.common]
|
||||
save = ["ctrl+s"]
|
||||
quit = ["ctrl+q"]
|
||||
# !!!change to space b r in the future and from edit mode
|
||||
revert = ["ctrl+r"]
|
||||
|
||||
force_quit = ["ctrl+shift+q"]
|
||||
save_and_quit = ["ctrl+shift+s"]
|
||||
@@ -39,6 +37,7 @@ enter_edit_mode_before = ["i"]
|
||||
enter_edit_mode_after = ["a"]
|
||||
previous_entry = ["left","q"]
|
||||
next_entry = ["right","1"]
|
||||
revert = ["space+b+r"]
|
||||
|
||||
move_left = ["h"]
|
||||
move_right = ["l"]
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::config::colors::themes::Theme;
|
||||
use crate::state::pages::auth::AuthState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::admin::AdminState;
|
||||
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
||||
use common::proto::komp_ac::table_definition::ProfileTreeResponse;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::Style,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::state::pages::form::FormState;
|
||||
use common::proto::multieko2::search::search_response::Hit;
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn render_status_line(
|
||||
}
|
||||
|
||||
// --- The normal status line rendering logic (unchanged) ---
|
||||
let program_info = format!("multieko2 v{}", env!("CARGO_PKG_VERSION"));
|
||||
let program_info = format!("komp_ac v{}", env!("CARGO_PKG_VERSION"));
|
||||
let mode_text = if is_edit_mode { "[EDIT]" } else { "[READ-ONLY]" };
|
||||
|
||||
let home_dir = dirs::home_dir()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// src/components/form/form.rs
|
||||
use crate::components::common::autocomplete; // <--- ADD THIS IMPORT
|
||||
use crate::components::common::autocomplete;
|
||||
use crate::components::handlers::canvas::render_canvas;
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use crate::state::pages::form::FormState; // <--- CHANGE THIS IMPORT
|
||||
use canvas::CanvasState;
|
||||
use crate::state::pages::form::FormState;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||
style::Style,
|
||||
@@ -64,7 +64,7 @@ pub fn render_form(
|
||||
f.render_widget(count_para, main_layout[0]);
|
||||
|
||||
// Get the active field's rect from render_canvas
|
||||
let active_field_rect = render_canvas(
|
||||
let active_field_rect = crate::components::handlers::canvas::render_canvas_library(
|
||||
f,
|
||||
main_layout[1],
|
||||
form_state,
|
||||
|
||||
@@ -9,13 +9,15 @@ use ratatui::{
|
||||
};
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use canvas::CanvasState as LibraryCanvasState;
|
||||
use std::cmp::{max, min};
|
||||
|
||||
/// Render canvas for legacy CanvasState (AddTableState, LoginState, RegisterState, AddLogicState)
|
||||
pub fn render_canvas(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
form_state: &impl CanvasState,
|
||||
form_state: &impl LegacyCanvasState,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
@@ -23,12 +25,75 @@ pub fn render_canvas(
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
) -> Option<Rect> {
|
||||
render_canvas_impl(
|
||||
f,
|
||||
area,
|
||||
fields,
|
||||
current_field_idx,
|
||||
inputs,
|
||||
theme,
|
||||
is_edit_mode,
|
||||
highlight_state,
|
||||
form_state.current_cursor_pos(),
|
||||
form_state.has_unsaved_changes(),
|
||||
|i| form_state.get_display_value_for_field(i).to_string(),
|
||||
|i| form_state.has_display_override(i),
|
||||
)
|
||||
}
|
||||
|
||||
/// Render canvas for library CanvasState (FormState)
|
||||
pub fn render_canvas_library(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
form_state: &impl LibraryCanvasState,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
) -> Option<Rect> {
|
||||
render_canvas_impl(
|
||||
f,
|
||||
area,
|
||||
fields,
|
||||
current_field_idx,
|
||||
inputs,
|
||||
theme,
|
||||
is_edit_mode,
|
||||
highlight_state,
|
||||
form_state.current_cursor_pos(),
|
||||
form_state.has_unsaved_changes(),
|
||||
|i| form_state.get_display_value_for_field(i).to_string(),
|
||||
|i| form_state.has_display_override(i),
|
||||
)
|
||||
}
|
||||
|
||||
/// Internal implementation shared by both render functions
|
||||
fn render_canvas_impl<F1, F2>(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
fields: &[&str],
|
||||
current_field_idx: &usize,
|
||||
inputs: &[&String],
|
||||
theme: &Theme,
|
||||
is_edit_mode: bool,
|
||||
highlight_state: &HighlightState,
|
||||
current_cursor_pos: usize,
|
||||
has_unsaved_changes: bool,
|
||||
get_display_value: F1,
|
||||
has_display_override: F2,
|
||||
) -> Option<Rect>
|
||||
where
|
||||
F1: Fn(usize) -> String,
|
||||
F2: Fn(usize) -> bool,
|
||||
{
|
||||
let columns = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
|
||||
.split(area);
|
||||
|
||||
let border_style = if form_state.has_unsaved_changes() {
|
||||
let border_style = if has_unsaved_changes {
|
||||
Style::default().fg(theme.warning)
|
||||
} else if is_edit_mode {
|
||||
Style::default().fg(theme.accent)
|
||||
@@ -75,17 +140,16 @@ pub fn render_canvas(
|
||||
|
||||
for (i, _input) in inputs.iter().enumerate() {
|
||||
let is_active = i == *current_field_idx;
|
||||
let current_cursor_pos = form_state.current_cursor_pos();
|
||||
|
||||
// Use the trait method to get display value
|
||||
let text = form_state.get_display_value_for_field(i);
|
||||
// Use the provided closure to get display value
|
||||
let text = get_display_value(i);
|
||||
let text_len = text.chars().count();
|
||||
let line: Line;
|
||||
|
||||
match highlight_state {
|
||||
HighlightState::Off => {
|
||||
line = Line::from(Span::styled(
|
||||
text,
|
||||
&text,
|
||||
if is_active {
|
||||
Style::default().fg(theme.highlight)
|
||||
} else {
|
||||
@@ -141,11 +205,11 @@ pub fn render_canvas(
|
||||
Span::styled(after, normal_style_in_highlight),
|
||||
]);
|
||||
} else {
|
||||
line = Line::from(Span::styled(text, highlight_style));
|
||||
line = Line::from(Span::styled(&text, highlight_style));
|
||||
}
|
||||
} else {
|
||||
line = Line::from(Span::styled(
|
||||
text,
|
||||
&text,
|
||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||
));
|
||||
}
|
||||
@@ -158,10 +222,10 @@ pub fn render_canvas(
|
||||
let normal_style_outside = Style::default().fg(theme.fg);
|
||||
|
||||
if i >= start_field && i <= end_field {
|
||||
line = Line::from(Span::styled(text, highlight_style));
|
||||
line = Line::from(Span::styled(&text, highlight_style));
|
||||
} else {
|
||||
line = Line::from(Span::styled(
|
||||
text,
|
||||
&text,
|
||||
if is_active { normal_style_in_highlight } else { normal_style_outside }
|
||||
));
|
||||
}
|
||||
@@ -174,14 +238,13 @@ pub fn render_canvas(
|
||||
if is_active {
|
||||
active_field_input_rect = Some(input_rows[i]);
|
||||
|
||||
// --- CORRECTED CURSOR POSITIONING LOGIC ---
|
||||
// Use the new generic trait method to check for an override.
|
||||
let cursor_x = if form_state.has_display_override(i) {
|
||||
// Use the provided closure to check for display override
|
||||
let cursor_x = if has_display_override(i) {
|
||||
// If an override exists, place the cursor at the end.
|
||||
input_rows[i].x + text.chars().count() as u16
|
||||
} else {
|
||||
// Otherwise, use the real cursor position.
|
||||
input_rows[i].x + form_state.current_cursor_pos() as u16
|
||||
input_rows[i].x + current_cursor_pos as u16
|
||||
};
|
||||
let cursor_y = input_rows[i].y;
|
||||
f.set_cursor_position((cursor_x, cursor_y));
|
||||
|
||||
@@ -6,7 +6,7 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
use crate::config::colors::themes::Theme;
|
||||
use common::proto::multieko2::table_definition::{ProfileTreeResponse};
|
||||
use common::proto::komp_ac::table_definition::{ProfileTreeResponse};
|
||||
use ratatui::text::{Span, Line};
|
||||
use crate::components::utils::text::truncate_string;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn render_intro(f: &mut Frame, intro_state: &IntroState, area: Rect, theme:
|
||||
|
||||
// Title
|
||||
let title = Line::from(vec![
|
||||
Span::styled("multieko2", Style::default().fg(theme.highlight)),
|
||||
Span::styled("komp_ac", Style::default().fg(theme.highlight)),
|
||||
Span::styled(" v", Style::default().fg(theme.fg)),
|
||||
Span::styled(env!("CARGO_PKG_VERSION"), Style::default().fg(theme.secondary)),
|
||||
]);
|
||||
|
||||
@@ -251,47 +251,206 @@ impl Config {
|
||||
key: KeyCode,
|
||||
modifiers: KeyModifiers,
|
||||
) -> bool {
|
||||
// For multi-character bindings without modifiers, handle them in matches_key_sequence.
|
||||
// Special handling for shift+character combinations
|
||||
if binding.to_lowercase().starts_with("shift+") {
|
||||
let parts: Vec<&str> = binding.split('+').collect();
|
||||
if parts.len() == 2 && parts[1].len() == 1 {
|
||||
let expected_lowercase = parts[1].chars().next().unwrap().to_lowercase().next().unwrap();
|
||||
let expected_uppercase = expected_lowercase.to_uppercase().next().unwrap();
|
||||
if let KeyCode::Char(actual_char) = key {
|
||||
if actual_char == expected_uppercase && modifiers.contains(KeyModifiers::SHIFT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Shift+Tab -> BackTab
|
||||
if binding.to_lowercase() == "shift+tab" && key == KeyCode::BackTab && modifiers.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle multi-character bindings (all standard keys without modifiers)
|
||||
if binding.len() > 1 && !binding.contains('+') {
|
||||
return match binding.to_lowercase().as_str() {
|
||||
// Navigation keys
|
||||
"left" => key == KeyCode::Left,
|
||||
"right" => key == KeyCode::Right,
|
||||
"up" => key == KeyCode::Up,
|
||||
"down" => key == KeyCode::Down,
|
||||
"esc" => key == KeyCode::Esc,
|
||||
"enter" => key == KeyCode::Enter,
|
||||
"delete" => key == KeyCode::Delete,
|
||||
"home" => key == KeyCode::Home,
|
||||
"end" => key == KeyCode::End,
|
||||
"pageup" | "pgup" => key == KeyCode::PageUp,
|
||||
"pagedown" | "pgdn" => key == KeyCode::PageDown,
|
||||
|
||||
// Editing keys
|
||||
"insert" | "ins" => key == KeyCode::Insert,
|
||||
"delete" | "del" => key == KeyCode::Delete,
|
||||
"backspace" => key == KeyCode::Backspace,
|
||||
|
||||
// Tab keys
|
||||
"tab" => key == KeyCode::Tab,
|
||||
"backtab" => key == KeyCode::BackTab,
|
||||
_ => false,
|
||||
|
||||
// Special keys
|
||||
"enter" | "return" => key == KeyCode::Enter,
|
||||
"escape" | "esc" => key == KeyCode::Esc,
|
||||
"space" => key == KeyCode::Char(' '),
|
||||
|
||||
// Function keys F1-F24
|
||||
"f1" => key == KeyCode::F(1),
|
||||
"f2" => key == KeyCode::F(2),
|
||||
"f3" => key == KeyCode::F(3),
|
||||
"f4" => key == KeyCode::F(4),
|
||||
"f5" => key == KeyCode::F(5),
|
||||
"f6" => key == KeyCode::F(6),
|
||||
"f7" => key == KeyCode::F(7),
|
||||
"f8" => key == KeyCode::F(8),
|
||||
"f9" => key == KeyCode::F(9),
|
||||
"f10" => key == KeyCode::F(10),
|
||||
"f11" => key == KeyCode::F(11),
|
||||
"f12" => key == KeyCode::F(12),
|
||||
"f13" => key == KeyCode::F(13),
|
||||
"f14" => key == KeyCode::F(14),
|
||||
"f15" => key == KeyCode::F(15),
|
||||
"f16" => key == KeyCode::F(16),
|
||||
"f17" => key == KeyCode::F(17),
|
||||
"f18" => key == KeyCode::F(18),
|
||||
"f19" => key == KeyCode::F(19),
|
||||
"f20" => key == KeyCode::F(20),
|
||||
"f21" => key == KeyCode::F(21),
|
||||
"f22" => key == KeyCode::F(22),
|
||||
"f23" => key == KeyCode::F(23),
|
||||
"f24" => key == KeyCode::F(24),
|
||||
|
||||
// Lock keys
|
||||
"capslock" => key == KeyCode::CapsLock,
|
||||
"scrolllock" => key == KeyCode::ScrollLock,
|
||||
"numlock" => key == KeyCode::NumLock,
|
||||
|
||||
// System keys
|
||||
"printscreen" => key == KeyCode::PrintScreen,
|
||||
"pause" => key == KeyCode::Pause,
|
||||
"menu" => key == KeyCode::Menu,
|
||||
"keypadbegin" => key == KeyCode::KeypadBegin,
|
||||
|
||||
// Media keys
|
||||
"mediaplay" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Play),
|
||||
"mediapause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Pause),
|
||||
"mediaplaypause" => key == KeyCode::Media(crossterm::event::MediaKeyCode::PlayPause),
|
||||
"mediareverse" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Reverse),
|
||||
"mediastop" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Stop),
|
||||
"mediafastforward" => key == KeyCode::Media(crossterm::event::MediaKeyCode::FastForward),
|
||||
"mediarewind" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Rewind),
|
||||
"mediatracknext" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackNext),
|
||||
"mediatrackprevious" => key == KeyCode::Media(crossterm::event::MediaKeyCode::TrackPrevious),
|
||||
"mediarecord" => key == KeyCode::Media(crossterm::event::MediaKeyCode::Record),
|
||||
"medialowervolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::LowerVolume),
|
||||
"mediaraisevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::RaiseVolume),
|
||||
"mediamutevolume" => key == KeyCode::Media(crossterm::event::MediaKeyCode::MuteVolume),
|
||||
|
||||
// Multi-key sequences need special handling
|
||||
"gg" => false, // This needs sequence handling
|
||||
_ => {
|
||||
// Handle single characters and punctuation
|
||||
if binding.len() == 1 {
|
||||
if let Some(c) = binding.chars().next() {
|
||||
key == KeyCode::Char(c)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle modifier combinations (like "Ctrl+F5", "Alt+Shift+A")
|
||||
let parts: Vec<&str> = binding.split('+').collect();
|
||||
let mut expected_modifiers = KeyModifiers::empty();
|
||||
let mut expected_key = None;
|
||||
|
||||
for part in parts {
|
||||
match part.to_lowercase().as_str() {
|
||||
"ctrl" => expected_modifiers |= KeyModifiers::CONTROL,
|
||||
// Modifiers
|
||||
"ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
|
||||
"shift" => expected_modifiers |= KeyModifiers::SHIFT,
|
||||
"alt" => expected_modifiers |= KeyModifiers::ALT,
|
||||
"super" | "windows" | "cmd" => expected_modifiers |= KeyModifiers::SUPER,
|
||||
"hyper" => expected_modifiers |= KeyModifiers::HYPER,
|
||||
"meta" => expected_modifiers |= KeyModifiers::META,
|
||||
|
||||
// Navigation keys
|
||||
"left" => expected_key = Some(KeyCode::Left),
|
||||
"right" => expected_key = Some(KeyCode::Right),
|
||||
"up" => expected_key = Some(KeyCode::Up),
|
||||
"down" => expected_key = Some(KeyCode::Down),
|
||||
"esc" => expected_key = Some(KeyCode::Esc),
|
||||
"enter" => expected_key = Some(KeyCode::Enter),
|
||||
"delete" => expected_key = Some(KeyCode::Delete),
|
||||
"home" => expected_key = Some(KeyCode::Home),
|
||||
"end" => expected_key = Some(KeyCode::End),
|
||||
"pageup" | "pgup" => expected_key = Some(KeyCode::PageUp),
|
||||
"pagedown" | "pgdn" => expected_key = Some(KeyCode::PageDown),
|
||||
|
||||
// Editing keys
|
||||
"insert" | "ins" => expected_key = Some(KeyCode::Insert),
|
||||
"delete" | "del" => expected_key = Some(KeyCode::Delete),
|
||||
"backspace" => expected_key = Some(KeyCode::Backspace),
|
||||
|
||||
// Tab keys
|
||||
"tab" => expected_key = Some(KeyCode::Tab),
|
||||
"backtab" => expected_key = Some(KeyCode::BackTab),
|
||||
|
||||
// Special keys
|
||||
"enter" | "return" => expected_key = Some(KeyCode::Enter),
|
||||
"escape" | "esc" => expected_key = Some(KeyCode::Esc),
|
||||
"space" => expected_key = Some(KeyCode::Char(' ')),
|
||||
|
||||
// Function keys
|
||||
"f1" => expected_key = Some(KeyCode::F(1)),
|
||||
"f2" => expected_key = Some(KeyCode::F(2)),
|
||||
"f3" => expected_key = Some(KeyCode::F(3)),
|
||||
"f4" => expected_key = Some(KeyCode::F(4)),
|
||||
"f5" => expected_key = Some(KeyCode::F(5)),
|
||||
"f6" => expected_key = Some(KeyCode::F(6)),
|
||||
"f7" => expected_key = Some(KeyCode::F(7)),
|
||||
"f8" => expected_key = Some(KeyCode::F(8)),
|
||||
"f9" => expected_key = Some(KeyCode::F(9)),
|
||||
"f10" => expected_key = Some(KeyCode::F(10)),
|
||||
"f11" => expected_key = Some(KeyCode::F(11)),
|
||||
"f12" => expected_key = Some(KeyCode::F(12)),
|
||||
"f13" => expected_key = Some(KeyCode::F(13)),
|
||||
"f14" => expected_key = Some(KeyCode::F(14)),
|
||||
"f15" => expected_key = Some(KeyCode::F(15)),
|
||||
"f16" => expected_key = Some(KeyCode::F(16)),
|
||||
"f17" => expected_key = Some(KeyCode::F(17)),
|
||||
"f18" => expected_key = Some(KeyCode::F(18)),
|
||||
"f19" => expected_key = Some(KeyCode::F(19)),
|
||||
"f20" => expected_key = Some(KeyCode::F(20)),
|
||||
"f21" => expected_key = Some(KeyCode::F(21)),
|
||||
"f22" => expected_key = Some(KeyCode::F(22)),
|
||||
"f23" => expected_key = Some(KeyCode::F(23)),
|
||||
"f24" => expected_key = Some(KeyCode::F(24)),
|
||||
|
||||
// Lock keys
|
||||
"capslock" => expected_key = Some(KeyCode::CapsLock),
|
||||
"scrolllock" => expected_key = Some(KeyCode::ScrollLock),
|
||||
"numlock" => expected_key = Some(KeyCode::NumLock),
|
||||
|
||||
// System keys
|
||||
"printscreen" => expected_key = Some(KeyCode::PrintScreen),
|
||||
"pause" => expected_key = Some(KeyCode::Pause),
|
||||
"menu" => expected_key = Some(KeyCode::Menu),
|
||||
"keypadbegin" => expected_key = Some(KeyCode::KeypadBegin),
|
||||
|
||||
// Special characters and colon (legacy support)
|
||||
":" => expected_key = Some(KeyCode::Char(':')),
|
||||
|
||||
// Single character (letters, numbers, punctuation)
|
||||
part => {
|
||||
if part.len() == 1 {
|
||||
let c = part.chars().next().unwrap();
|
||||
expected_key = Some(KeyCode::Char(c));
|
||||
if let Some(c) = part.chars().next() {
|
||||
expected_key = Some(KeyCode::Char(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,14 +149,17 @@ fn parse_key_part(part: &str) -> Option<ParsedKey> {
|
||||
let mut code = None;
|
||||
|
||||
if part.contains('+') {
|
||||
// This handles modifiers like "ctrl+s"
|
||||
// This handles modifiers like "ctrl+s", "super+shift+f5"
|
||||
let components: Vec<&str> = part.split('+').collect();
|
||||
|
||||
for component in components {
|
||||
match component.to_lowercase().as_str() {
|
||||
"ctrl" => modifiers |= KeyModifiers::CONTROL,
|
||||
"ctrl" | "control" => modifiers |= KeyModifiers::CONTROL,
|
||||
"shift" => modifiers |= KeyModifiers::SHIFT,
|
||||
"alt" => modifiers |= KeyModifiers::ALT,
|
||||
"super" | "windows" | "cmd" => modifiers |= KeyModifiers::SUPER,
|
||||
"hyper" => modifiers |= KeyModifiers::HYPER,
|
||||
"meta" => modifiers |= KeyModifiers::META,
|
||||
_ => {
|
||||
// Last component is the key
|
||||
code = string_to_keycode(component);
|
||||
|
||||
@@ -9,7 +9,7 @@ use tracing::{error, info};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
pub const APP_NAME: &str = "multieko2_client";
|
||||
pub const APP_NAME: &str = "komp_ac_client";
|
||||
pub const TOKEN_FILE_NAME: &str = "auth.token";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// src/functions/modes/edit/form_e.rs
|
||||
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use crate::state::pages::form::FormState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::tui::functions::common::form::{revert, save};
|
||||
use crate::tui::functions::common::form::SaveOutcome;
|
||||
use crate::modes::handlers::event::EventOutcome;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use canvas::CanvasState;
|
||||
use std::any::Any;
|
||||
use anyhow::Result;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/functions/modes/read_only/form_ro.rs
|
||||
|
||||
use crate::config::binds::key_sequences::KeySequenceTracker;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use canvas::CanvasState;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
||||
@@ -9,11 +9,12 @@ use crate::state::app::state::AppState;
|
||||
use crate::state::pages::admin::AdminState;
|
||||
use crate::state::pages::{
|
||||
auth::{LoginState, RegisterState},
|
||||
canvas_state::CanvasState,
|
||||
form::FormState,
|
||||
};
|
||||
use canvas::CanvasState;
|
||||
use canvas::{CanvasAction, ActionDispatcher, ActionResult};
|
||||
use anyhow::Result;
|
||||
use common::proto::multieko2::search::search_response::Hit;
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, info};
|
||||
@@ -74,6 +75,57 @@ async fn trigger_form_autocomplete_search(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_form_edit_with_canvas(
|
||||
key_event: KeyEvent,
|
||||
config: &Config,
|
||||
form_state: &mut FormState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
// Try canvas action from key first
|
||||
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
||||
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => {
|
||||
return Ok(msg.unwrap_or_default());
|
||||
}
|
||||
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||
return Ok(msg);
|
||||
}
|
||||
Ok(ActionResult::Error(msg)) => {
|
||||
return Ok(format!("Error: {}", msg));
|
||||
}
|
||||
Ok(ActionResult::RequiresContext(msg)) => {
|
||||
return Ok(format!("Context needed: {}", msg));
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall through to try config mapping
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try config-mapped action
|
||||
if let Some(action_str) = config.get_edit_action_for_key(key_event.code, key_event.modifiers) {
|
||||
let canvas_action = CanvasAction::from_string(&action_str);
|
||||
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => {
|
||||
return Ok(msg.unwrap_or_default());
|
||||
}
|
||||
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||
return Ok(msg);
|
||||
}
|
||||
Ok(ActionResult::Error(msg)) => {
|
||||
return Ok(format!("Error: {}", msg));
|
||||
}
|
||||
Ok(ActionResult::RequiresContext(msg)) => {
|
||||
return Ok(format!("Context needed: {}", msg));
|
||||
}
|
||||
Err(e) => {
|
||||
return Ok(format!("Action failed: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn handle_edit_event(
|
||||
@@ -118,7 +170,7 @@ pub async fn handle_edit_event(
|
||||
return Ok(EditEventOutcome::Message(String::new()));
|
||||
}
|
||||
"exit" => {
|
||||
form_state.deactivate_autocomplete();
|
||||
form_state.deactivate_suggestions();
|
||||
return Ok(EditEventOutcome::Message(
|
||||
"Autocomplete cancelled".to_string(),
|
||||
));
|
||||
@@ -150,14 +202,14 @@ pub async fn handle_edit_event(
|
||||
);
|
||||
|
||||
// 4. Finalize state
|
||||
form_state.deactivate_autocomplete();
|
||||
form_state.deactivate_suggestions();
|
||||
form_state.set_has_unsaved_changes(true);
|
||||
return Ok(EditEventOutcome::Message(
|
||||
"Selection made".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
form_state.deactivate_autocomplete();
|
||||
form_state.deactivate_suggestions();
|
||||
// Fall through to default 'enter' behavior
|
||||
}
|
||||
_ => {} // Let other keys fall through to the live search logic
|
||||
|
||||
@@ -10,9 +10,62 @@ use crate::state::pages::add_logic::AddLogicState;
|
||||
use crate::state::pages::add_table::AddTableState;
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::functions::modes::read_only::{add_logic_ro, auth_ro, form_ro, add_table_ro};
|
||||
use canvas::{CanvasAction, ActionDispatcher, ActionResult};
|
||||
use crossterm::event::KeyEvent;
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn handle_form_readonly_with_canvas(
|
||||
key_event: KeyEvent,
|
||||
config: &Config,
|
||||
form_state: &mut FormState,
|
||||
ideal_cursor_column: &mut usize,
|
||||
) -> Result<String> {
|
||||
// Try canvas action from key first
|
||||
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
||||
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => {
|
||||
return Ok(msg.unwrap_or_default());
|
||||
}
|
||||
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||
return Ok(msg);
|
||||
}
|
||||
Ok(ActionResult::Error(msg)) => {
|
||||
return Ok(format!("Error: {}", msg));
|
||||
}
|
||||
Ok(ActionResult::RequiresContext(msg)) => {
|
||||
return Ok(format!("Context needed: {}", msg));
|
||||
}
|
||||
Err(_) => {
|
||||
// Fall through to try config mapping
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try config-mapped action
|
||||
if let Some(action_str) = config.get_read_only_action_for_key(key_event.code, key_event.modifiers) {
|
||||
let canvas_action = CanvasAction::from_string(&action_str);
|
||||
match ActionDispatcher::dispatch(canvas_action, form_state, ideal_cursor_column).await {
|
||||
Ok(ActionResult::Success(msg)) => {
|
||||
return Ok(msg.unwrap_or_default());
|
||||
}
|
||||
Ok(ActionResult::HandledByFeature(msg)) => {
|
||||
return Ok(msg);
|
||||
}
|
||||
Ok(ActionResult::Error(msg)) => {
|
||||
return Ok(format!("Error: {}", msg));
|
||||
}
|
||||
Ok(ActionResult::RequiresContext(msg)) => {
|
||||
return Ok(format!("Context needed: {}", msg));
|
||||
}
|
||||
Err(e) => {
|
||||
return Ok(format!("Action failed: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
pub async fn handle_read_only_event(
|
||||
app_state: &mut AppState,
|
||||
key: KeyEvent,
|
||||
@@ -36,18 +89,46 @@ pub async fn handle_read_only_event(
|
||||
|
||||
if config.is_enter_edit_mode_after(key.code, key.modifiers) {
|
||||
// Determine target state to adjust cursor
|
||||
let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state }
|
||||
else if app_state.ui.show_add_logic { add_logic_state }
|
||||
else if app_state.ui.show_register { register_state }
|
||||
else if app_state.ui.show_add_table { add_table_state }
|
||||
else { form_state };
|
||||
let current_input = target_state.get_current_input();
|
||||
let current_pos = target_state.current_cursor_pos();
|
||||
|
||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||
target_state.set_current_cursor_pos(current_pos + 1);
|
||||
*ideal_cursor_column = target_state.current_cursor_pos();
|
||||
if app_state.ui.show_login {
|
||||
let current_input = login_state.get_current_input();
|
||||
let current_pos = login_state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||
login_state.set_current_cursor_pos(current_pos + 1);
|
||||
*ideal_cursor_column = login_state.current_cursor_pos();
|
||||
}
|
||||
} else if app_state.ui.show_add_logic {
|
||||
let current_input = add_logic_state.get_current_input();
|
||||
let current_pos = add_logic_state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||
add_logic_state.set_current_cursor_pos(current_pos + 1);
|
||||
*ideal_cursor_column = add_logic_state.current_cursor_pos();
|
||||
}
|
||||
} else if app_state.ui.show_register {
|
||||
let current_input = register_state.get_current_input();
|
||||
let current_pos = register_state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||
register_state.set_current_cursor_pos(current_pos + 1);
|
||||
*ideal_cursor_column = register_state.current_cursor_pos();
|
||||
}
|
||||
} else if app_state.ui.show_add_table {
|
||||
let current_input = add_table_state.get_current_input();
|
||||
let current_pos = add_table_state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||
add_table_state.set_current_cursor_pos(current_pos + 1);
|
||||
*ideal_cursor_column = add_table_state.current_cursor_pos();
|
||||
}
|
||||
} else {
|
||||
// Handle FormState (uses library CanvasState)
|
||||
use canvas::CanvasState as LibraryCanvasState; // Import at the top of the function
|
||||
let current_input = form_state.get_current_input();
|
||||
let current_pos = form_state.current_cursor_pos();
|
||||
if !current_input.is_empty() && current_pos < current_input.len() {
|
||||
form_state.set_current_cursor_pos(current_pos + 1);
|
||||
*ideal_cursor_column = form_state.current_cursor_pos();
|
||||
}
|
||||
}
|
||||
|
||||
*edit_mode_cooldown = true;
|
||||
*command_message = "Entering Edit mode (after cursor)".to_string();
|
||||
return Ok((false, command_message.clone()));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use crate::config::binds::config::Config;
|
||||
use crate::modes::handlers::event::EventOutcome;
|
||||
use anyhow::Result;
|
||||
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
||||
use common::proto::komp_ac::table_definition::ProfileTreeResponse;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// src/client/modes/handlers.rs
|
||||
// src/modes/handlers.rs
|
||||
pub mod event;
|
||||
pub mod event_helper;
|
||||
pub mod mode_manager;
|
||||
|
||||
@@ -15,8 +15,12 @@ use crate::modes::{
|
||||
general::{dialog, navigation},
|
||||
handlers::mode_manager::{AppMode, ModeManager},
|
||||
};
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use crate::services::auth::AuthClient;
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use canvas::{CanvasAction, ActionDispatcher, ActionResult};
|
||||
use canvas::CanvasState as LibraryCanvasState;
|
||||
use super::event_helper::*;
|
||||
use crate::state::{
|
||||
app::{
|
||||
buffer::{AppView, BufferState},
|
||||
@@ -42,7 +46,7 @@ use crate::tui::{
|
||||
use crate::ui::handlers::context::UiContext;
|
||||
use crate::ui::handlers::rat_state::UiStateHandler;
|
||||
use anyhow::Result;
|
||||
use common::proto::multieko2::search::search_response::Hit;
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
use crossterm::cursor::SetCursorStyle;
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||
use tokio::sync::mpsc;
|
||||
@@ -573,55 +577,106 @@ impl EventHandler {
|
||||
}
|
||||
|
||||
AppMode::ReadOnly => {
|
||||
if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise") && ModeManager::can_enter_highlight_mode(current_mode) {
|
||||
let current_field_index = if app_state.ui.show_login { login_state.current_field() } else if app_state.ui.show_register { register_state.current_field() } else { form_state.current_field() };
|
||||
self.highlight_state = HighlightState::Linewise { anchor_line: current_field_index };
|
||||
// Handle highlight mode transitions
|
||||
if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode_linewise")
|
||||
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||
{
|
||||
let current_field_index = get_current_field_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
self.highlight_state = HighlightState::Linewise {
|
||||
anchor_line: current_field_index
|
||||
};
|
||||
self.command_message = "-- LINE HIGHLIGHT --".to_string();
|
||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||
} else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode") && ModeManager::can_enter_highlight_mode(current_mode) {
|
||||
let current_field_index = if app_state.ui.show_login { login_state.current_field() } else if app_state.ui.show_register { register_state.current_field() } else { form_state.current_field() };
|
||||
let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() } else if app_state.ui.show_register { register_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
|
||||
}
|
||||
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_highlight_mode")
|
||||
&& ModeManager::can_enter_highlight_mode(current_mode)
|
||||
{
|
||||
let current_field_index = get_current_field_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let anchor = (current_field_index, current_cursor_pos);
|
||||
self.highlight_state = HighlightState::Characterwise { anchor };
|
||||
self.command_message = "-- HIGHLIGHT --".to_string();
|
||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||
} else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_before") && ModeManager::can_enter_edit_mode(current_mode) {
|
||||
}
|
||||
|
||||
// Handle edit mode transitions
|
||||
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_before")
|
||||
&& ModeManager::can_enter_edit_mode(current_mode)
|
||||
{
|
||||
self.is_edit_mode = true;
|
||||
self.edit_mode_cooldown = true;
|
||||
self.command_message = "Edit mode".to_string();
|
||||
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||
} else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after") && ModeManager::can_enter_edit_mode(current_mode) {
|
||||
let current_input = if app_state.ui.show_login || app_state.ui.show_register { login_state.get_current_input() } else { form_state.get_current_input() };
|
||||
let current_cursor_pos = if app_state.ui.show_login || app_state.ui.show_register { login_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
|
||||
}
|
||||
else if config.get_read_only_action_for_key(key_code, modifiers).as_deref() == Some("enter_edit_mode_after")
|
||||
&& ModeManager::can_enter_edit_mode(current_mode)
|
||||
{
|
||||
let current_input = get_current_input_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_cursor_pos_for_mixed_state(
|
||||
app_state,
|
||||
login_state,
|
||||
form_state
|
||||
);
|
||||
|
||||
// Move cursor forward if possible
|
||||
if !current_input.is_empty() && current_cursor_pos < current_input.len() {
|
||||
if app_state.ui.show_login || app_state.ui.show_register {
|
||||
login_state.set_current_cursor_pos(current_cursor_pos + 1);
|
||||
self.ideal_cursor_column = login_state.current_cursor_pos();
|
||||
} else {
|
||||
form_state.set_current_cursor_pos(current_cursor_pos + 1);
|
||||
self.ideal_cursor_column = form_state.current_cursor_pos();
|
||||
}
|
||||
let new_cursor_pos = current_cursor_pos + 1;
|
||||
set_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state,
|
||||
new_cursor_pos
|
||||
);
|
||||
self.ideal_cursor_column = get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
}
|
||||
|
||||
self.is_edit_mode = true;
|
||||
self.edit_mode_cooldown = true;
|
||||
app_state.ui.focus_outside_canvas = false;
|
||||
self.command_message = "Edit mode (after cursor)".to_string();
|
||||
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
|
||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||
} else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_command_mode") && ModeManager::can_enter_command_mode(current_mode) {
|
||||
}
|
||||
else if config.get_read_only_action_for_key(key_code, modifiers) == Some("enter_command_mode")
|
||||
&& ModeManager::can_enter_command_mode(current_mode)
|
||||
{
|
||||
self.command_mode = true;
|
||||
self.command_input.clear();
|
||||
self.command_message.clear();
|
||||
return Ok(EventOutcome::Ok(String::new()));
|
||||
}
|
||||
|
||||
if let Some(action) =
|
||||
config.get_common_action(key_code, modifiers)
|
||||
{
|
||||
// Handle common actions (save, quit, etc.)
|
||||
if let Some(action) = config.get_common_action(key_code, modifiers) {
|
||||
match action {
|
||||
"save" | "force_quit" | "save_and_quit"
|
||||
| "revert" => {
|
||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||
return common_mode::handle_core_action(
|
||||
action,
|
||||
form_state,
|
||||
@@ -639,23 +694,36 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
let (_should_exit, message) =
|
||||
read_only::handle_read_only_event(
|
||||
app_state,
|
||||
// Try canvas action for form first (NEW: Canvas library integration)
|
||||
if app_state.ui.show_form {
|
||||
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
|
||||
key_event,
|
||||
config,
|
||||
form_state,
|
||||
login_state,
|
||||
register_state,
|
||||
&mut admin_state.add_table_state,
|
||||
&mut admin_state.add_logic_state,
|
||||
&mut self.key_sequence_tracker,
|
||||
&mut self.grpc_client, // <-- FIX 1
|
||||
&mut self.command_message,
|
||||
&mut self.edit_mode_cooldown,
|
||||
&mut self.ideal_cursor_column,
|
||||
)
|
||||
.await?;
|
||||
false, // not edit mode
|
||||
).await {
|
||||
return Ok(EventOutcome::Ok(canvas_message));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to legacy read-only event handling
|
||||
let (_should_exit, message) = read_only::handle_read_only_event(
|
||||
app_state,
|
||||
key_event,
|
||||
config,
|
||||
form_state,
|
||||
login_state,
|
||||
register_state,
|
||||
&mut admin_state.add_table_state,
|
||||
&mut admin_state.add_logic_state,
|
||||
&mut self.key_sequence_tracker,
|
||||
&mut self.grpc_client,
|
||||
&mut self.command_message,
|
||||
&mut self.edit_mode_cooldown,
|
||||
&mut self.ideal_cursor_column,
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(EventOutcome::Ok(message));
|
||||
}
|
||||
|
||||
@@ -695,12 +763,10 @@ impl EventHandler {
|
||||
}
|
||||
|
||||
AppMode::Edit => {
|
||||
if let Some(action) =
|
||||
config.get_common_action(key_code, modifiers)
|
||||
{
|
||||
// Handle common actions (save, quit, etc.)
|
||||
if let Some(action) = config.get_common_action(key_code, modifiers) {
|
||||
match action {
|
||||
"save" | "force_quit" | "save_and_quit"
|
||||
| "revert" => {
|
||||
"save" | "force_quit" | "save_and_quit" | "revert" => {
|
||||
return common_mode::handle_core_action(
|
||||
action,
|
||||
form_state,
|
||||
@@ -718,9 +784,25 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Try canvas action for form first (NEW: Canvas library integration)
|
||||
if app_state.ui.show_form {
|
||||
if let Ok(Some(canvas_message)) = self.handle_form_canvas_action(
|
||||
key_event,
|
||||
config,
|
||||
form_state,
|
||||
true, // edit mode
|
||||
).await {
|
||||
if !canvas_message.is_empty() {
|
||||
self.command_message = canvas_message.clone();
|
||||
}
|
||||
return Ok(EventOutcome::Ok(canvas_message));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle legacy edit events
|
||||
let mut current_position = form_state.current_position;
|
||||
let total_count = form_state.total_count;
|
||||
// --- MODIFIED: Pass `self` instead of `grpc_client` ---
|
||||
|
||||
let edit_result = edit::handle_edit_event(
|
||||
key_event,
|
||||
config,
|
||||
@@ -739,30 +821,62 @@ impl EventHandler {
|
||||
Ok(edit::EditEventOutcome::ExitEditMode) => {
|
||||
self.is_edit_mode = false;
|
||||
self.edit_mode_cooldown = true;
|
||||
let has_changes = if app_state.ui.show_login { login_state.has_unsaved_changes() } else if app_state.ui.show_register { register_state.has_unsaved_changes() } else { form_state.has_unsaved_changes() };
|
||||
self.command_message = if has_changes { "Exited edit mode (unsaved changes remain)".to_string() } else { "Read-only mode".to_string() };
|
||||
|
||||
// Check for unsaved changes across all states
|
||||
let has_changes = get_has_unsaved_changes_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
|
||||
// Set appropriate message based on changes
|
||||
self.command_message = if has_changes {
|
||||
"Exited edit mode (unsaved changes remain)".to_string()
|
||||
} else {
|
||||
"Read-only mode".to_string()
|
||||
};
|
||||
|
||||
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
|
||||
let current_input = if app_state.ui.show_login { login_state.get_current_input() } else if app_state.ui.show_register { register_state.get_current_input() } else { form_state.get_current_input() };
|
||||
let current_cursor_pos = if app_state.ui.show_login { login_state.current_cursor_pos() } else if app_state.ui.show_register { register_state.current_cursor_pos() } else { form_state.current_cursor_pos() };
|
||||
|
||||
// Get current input and cursor position
|
||||
let current_input = get_current_input_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
let current_cursor_pos = get_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state
|
||||
);
|
||||
|
||||
// Adjust cursor if it's beyond the input length
|
||||
if !current_input.is_empty() && current_cursor_pos >= current_input.len() {
|
||||
let new_pos = current_input.len() - 1;
|
||||
let target_state: &mut dyn CanvasState = if app_state.ui.show_login { login_state } else if app_state.ui.show_register { register_state } else { form_state };
|
||||
target_state.set_current_cursor_pos(new_pos);
|
||||
set_current_cursor_pos_for_state(
|
||||
app_state,
|
||||
login_state,
|
||||
register_state,
|
||||
form_state,
|
||||
new_pos
|
||||
);
|
||||
self.ideal_cursor_column = new_pos;
|
||||
}
|
||||
return Ok(EventOutcome::Ok(
|
||||
self.command_message.clone(),
|
||||
));
|
||||
|
||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||
}
|
||||
|
||||
Ok(edit::EditEventOutcome::Message(msg)) => {
|
||||
if !msg.is_empty() {
|
||||
self.command_message = msg;
|
||||
}
|
||||
self.key_sequence_tracker.reset();
|
||||
return Ok(EventOutcome::Ok(
|
||||
self.command_message.clone(),
|
||||
));
|
||||
return Ok(EventOutcome::Ok(self.command_message.clone()));
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
return Err(e.into());
|
||||
}
|
||||
@@ -906,4 +1020,180 @@ impl EventHandler {
|
||||
fn is_processed_command(&self, command: &str) -> bool {
|
||||
matches!(command, "w" | "q" | "q!" | "wq" | "r")
|
||||
}
|
||||
|
||||
async fn handle_form_canvas_action(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
_config: &Config, // Not used anymore - canvas has its own config
|
||||
form_state: &mut FormState,
|
||||
is_edit_mode: bool,
|
||||
) -> Result<Option<String>> {
|
||||
// Load canvas config (canvas_config.toml or vim defaults)
|
||||
let canvas_config = canvas::config::CanvasConfig::load();
|
||||
|
||||
// Handle suggestion actions first if suggestions are active
|
||||
if form_state.autocomplete_active {
|
||||
if let Some(action_str) = canvas_config.get_suggestion_action(key_event.code, key_event.modifiers) {
|
||||
let canvas_action = CanvasAction::from_string(&action_str);
|
||||
match ActionDispatcher::dispatch(canvas_action, form_state, &mut self.ideal_cursor_column).await {
|
||||
Ok(result) => return Ok(Some(result.message().unwrap_or("").to_string())),
|
||||
Err(_) => return Ok(Some("Suggestion action failed".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback hardcoded suggestion handling
|
||||
match key_event.code {
|
||||
KeyCode::Up => {
|
||||
if let Ok(result) = ActionDispatcher::dispatch(
|
||||
CanvasAction::SuggestionUp,
|
||||
form_state,
|
||||
&mut self.ideal_cursor_column,
|
||||
).await {
|
||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Ok(result) = ActionDispatcher::dispatch(
|
||||
CanvasAction::SuggestionDown,
|
||||
form_state,
|
||||
&mut self.ideal_cursor_column,
|
||||
).await {
|
||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if let Ok(result) = ActionDispatcher::dispatch(
|
||||
CanvasAction::SelectSuggestion,
|
||||
form_state,
|
||||
&mut self.ideal_cursor_column,
|
||||
).await {
|
||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
||||
}
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
if let Ok(result) = ActionDispatcher::dispatch(
|
||||
CanvasAction::ExitSuggestions,
|
||||
form_state,
|
||||
&mut self.ideal_cursor_column,
|
||||
).await {
|
||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXED: Use canvas config instead of client config
|
||||
let action_str = canvas_config.get_action_for_key(
|
||||
key_event.code,
|
||||
key_event.modifiers,
|
||||
is_edit_mode,
|
||||
form_state.autocomplete_active
|
||||
);
|
||||
|
||||
if let Some(action_str) = action_str {
|
||||
// Filter out mode transition actions - let legacy handlers deal with these
|
||||
if Self::is_mode_transition_action(action_str) {
|
||||
return Ok(None); // Let legacy handler handle mode transitions
|
||||
}
|
||||
|
||||
let canvas_action = CanvasAction::from_string(&action_str);
|
||||
match ActionDispatcher::dispatch(
|
||||
canvas_action,
|
||||
form_state,
|
||||
&mut self.ideal_cursor_column,
|
||||
).await {
|
||||
Ok(result) => {
|
||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(Some("Canvas action failed".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to automatic key handling for edit mode
|
||||
if is_edit_mode {
|
||||
if let Some(canvas_action) = CanvasAction::from_key(key_event.code) {
|
||||
match ActionDispatcher::dispatch(
|
||||
canvas_action,
|
||||
form_state,
|
||||
&mut self.ideal_cursor_column,
|
||||
).await {
|
||||
Ok(result) => {
|
||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(Some("Auto action failed".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// In read-only mode, only handle non-character keys
|
||||
let canvas_action = match key_event.code {
|
||||
// Only handle special keys that don't conflict with vim bindings
|
||||
KeyCode::Left => Some(CanvasAction::MoveLeft),
|
||||
KeyCode::Right => Some(CanvasAction::MoveRight),
|
||||
KeyCode::Up => Some(CanvasAction::MoveUp),
|
||||
KeyCode::Down => Some(CanvasAction::MoveDown),
|
||||
KeyCode::Home => Some(CanvasAction::MoveLineStart),
|
||||
KeyCode::End => Some(CanvasAction::MoveLineEnd),
|
||||
KeyCode::Tab => Some(CanvasAction::NextField),
|
||||
KeyCode::BackTab => Some(CanvasAction::PrevField),
|
||||
KeyCode::Delete => Some(CanvasAction::DeleteForward),
|
||||
KeyCode::Backspace => Some(CanvasAction::DeleteBackward),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(canvas_action) = canvas_action {
|
||||
match ActionDispatcher::dispatch(
|
||||
canvas_action,
|
||||
form_state,
|
||||
&mut self.ideal_cursor_column,
|
||||
).await {
|
||||
Ok(result) => {
|
||||
return Ok(Some(result.message().unwrap_or("").to_string()));
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(Some("Action failed".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// ADDED: Helper function to identify mode transition actions
|
||||
fn is_mode_transition_action(action: &str) -> bool {
|
||||
matches!(action,
|
||||
"exit" |
|
||||
"exit_edit_mode" |
|
||||
"enter_edit_mode_before" |
|
||||
"enter_edit_mode_after" |
|
||||
"enter_command_mode" |
|
||||
"exit_command_mode" |
|
||||
"enter_highlight_mode" |
|
||||
"enter_highlight_mode_linewise" |
|
||||
"exit_highlight_mode" |
|
||||
"save" |
|
||||
"quit" |
|
||||
"force_quit" |
|
||||
"save_and_quit" |
|
||||
"revert" |
|
||||
"enter_decider" | // This is also handled specially by legacy system
|
||||
"trigger_autocomplete" | // This is handled specially by legacy system
|
||||
"suggestion_up" | // These are handled above in suggestion logic
|
||||
"suggestion_down" |
|
||||
"previous_entry" | // Navigation between records
|
||||
"next_entry" |
|
||||
"toggle_sidebar" |
|
||||
"toggle_buffer_list" |
|
||||
"next_buffer" |
|
||||
"previous_buffer" |
|
||||
"close_buffer" |
|
||||
"open_search" |
|
||||
"find_file_palette_toggle"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
105
client/src/modes/handlers/event_helper.rs
Normal file
105
client/src/modes/handlers/event_helper.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
|
||||
// src/modes/handlers/event_helper.rs
|
||||
//! Helper functions to handle the differences between legacy and library CanvasState traits
|
||||
|
||||
use crate::state::app::state::AppState;
|
||||
use crate::state::pages::{
|
||||
form::FormState,
|
||||
auth::{LoginState, RegisterState},
|
||||
};
|
||||
use crate::state::pages::canvas_state::CanvasState as LegacyCanvasState;
|
||||
use canvas::CanvasState as LibraryCanvasState;
|
||||
|
||||
/// Get the current field index from the appropriate state based on which UI is active
|
||||
pub fn get_current_field_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_field() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_field() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_field() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current cursor position from the appropriate state based on which UI is active
|
||||
pub fn get_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login {
|
||||
login_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_cursor_pos() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the appropriate state has unsaved changes based on which UI is active
|
||||
pub fn get_has_unsaved_changes_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
register_state: &RegisterState,
|
||||
form_state: &FormState,
|
||||
) -> bool {
|
||||
if app_state.ui.show_login {
|
||||
login_state.has_unsaved_changes() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.has_unsaved_changes() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.has_unsaved_changes() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current input from the appropriate state based on which UI is active
|
||||
pub fn get_current_input_for_state<'a>(
|
||||
app_state: &AppState,
|
||||
login_state: &'a LoginState,
|
||||
register_state: &'a RegisterState,
|
||||
form_state: &'a FormState,
|
||||
) -> &'a str {
|
||||
if app_state.ui.show_login {
|
||||
login_state.get_current_input() // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.get_current_input() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.get_current_input() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the cursor position for the appropriate state based on which UI is active
|
||||
pub fn set_current_cursor_pos_for_state(
|
||||
app_state: &AppState,
|
||||
login_state: &mut LoginState,
|
||||
register_state: &mut RegisterState,
|
||||
form_state: &mut FormState,
|
||||
pos: usize,
|
||||
) {
|
||||
if app_state.ui.show_login {
|
||||
login_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState
|
||||
} else if app_state.ui.show_register {
|
||||
register_state.set_current_cursor_pos(pos); // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.set_current_cursor_pos(pos); // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cursor position for mixed login/register vs form logic
|
||||
pub fn get_cursor_pos_for_mixed_state(
|
||||
app_state: &AppState,
|
||||
login_state: &LoginState,
|
||||
form_state: &FormState,
|
||||
) -> usize {
|
||||
if app_state.ui.show_login || app_state.ui.show_register {
|
||||
login_state.current_cursor_pos() // Uses LegacyCanvasState
|
||||
} else {
|
||||
form_state.current_cursor_pos() // Uses LibraryCanvasState
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/services/auth.rs
|
||||
use tonic::transport::Channel;
|
||||
use common::proto::multieko2::auth::{
|
||||
use common::proto::komp_ac::auth::{
|
||||
auth_service_client::AuthServiceClient,
|
||||
LoginRequest, LoginResponse,
|
||||
RegisterRequest, AuthResponse,
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
// src/services/grpc_client.rs
|
||||
|
||||
use common::proto::multieko2::common::Empty;
|
||||
use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient;
|
||||
use common::proto::multieko2::table_structure::{GetTableStructureRequest, TableStructureResponse};
|
||||
use common::proto::multieko2::table_definition::{
|
||||
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::{GetTableStructureRequest, TableStructureResponse};
|
||||
use common::proto::komp_ac::table_definition::{
|
||||
table_definition_client::TableDefinitionClient,
|
||||
PostTableDefinitionRequest, ProfileTreeResponse, TableDefinitionResponse,
|
||||
};
|
||||
use common::proto::multieko2::table_script::{
|
||||
use common::proto::komp_ac::table_script::{
|
||||
table_script_client::TableScriptClient,
|
||||
PostTableScriptRequest, TableScriptResponse,
|
||||
};
|
||||
use common::proto::multieko2::tables_data::{
|
||||
use common::proto::komp_ac::tables_data::{
|
||||
tables_data_client::TablesDataClient,
|
||||
GetTableDataByPositionRequest,
|
||||
GetTableDataRequest, // ADD THIS
|
||||
GetTableDataResponse,
|
||||
DeleteTableDataRequest, // ADD THIS
|
||||
DeleteTableDataResponse, // ADD THIS
|
||||
GetTableDataCountRequest,
|
||||
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
||||
PutTableDataResponse,
|
||||
};
|
||||
use common::proto::multieko2::search::{
|
||||
use common::proto::komp_ac::search::{
|
||||
searcher_client::SearcherClient, SearchRequest, SearchResponse,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
@@ -116,7 +119,7 @@ impl GrpcClient {
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
// NEW Methods for TablesData service
|
||||
// Existing TablesData methods
|
||||
pub async fn get_table_data_count(
|
||||
&mut self,
|
||||
profile_name: String,
|
||||
@@ -135,7 +138,7 @@ impl GrpcClient {
|
||||
Ok(response.into_inner().count as u64)
|
||||
}
|
||||
|
||||
pub async fn get_table_data_by_position(
|
||||
pub async fn get_table_data_by_position(
|
||||
&mut self,
|
||||
profile_name: String,
|
||||
table_name: String,
|
||||
@@ -155,18 +158,58 @@ pub async fn get_table_data_by_position(
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
// ADD THIS: Missing get_table_data method
|
||||
pub async fn get_table_data(
|
||||
&mut self,
|
||||
profile_name: String,
|
||||
table_name: String,
|
||||
id: i64,
|
||||
) -> Result<GetTableDataResponse> {
|
||||
let grpc_request = GetTableDataRequest {
|
||||
profile_name,
|
||||
table_name,
|
||||
id,
|
||||
};
|
||||
let request = tonic::Request::new(grpc_request);
|
||||
let response = self
|
||||
.tables_data_client
|
||||
.get_table_data(request)
|
||||
.await
|
||||
.context("gRPC GetTableData call failed")?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
// ADD THIS: Missing delete_table_data method
|
||||
pub async fn delete_table_data(
|
||||
&mut self,
|
||||
profile_name: String,
|
||||
table_name: String,
|
||||
record_id: i64,
|
||||
) -> Result<DeleteTableDataResponse> {
|
||||
let grpc_request = DeleteTableDataRequest {
|
||||
profile_name,
|
||||
table_name,
|
||||
record_id,
|
||||
};
|
||||
let request = tonic::Request::new(grpc_request);
|
||||
let response = self
|
||||
.tables_data_client
|
||||
.delete_table_data(request)
|
||||
.await
|
||||
.context("gRPC DeleteTableData call failed")?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn post_table_data(
|
||||
&mut self,
|
||||
profile_name: String,
|
||||
table_name: String,
|
||||
// CHANGE THIS: Accept the pre-converted data
|
||||
data: HashMap<String, Value>,
|
||||
) -> Result<PostTableDataResponse> {
|
||||
// The conversion logic is now gone from here.
|
||||
let grpc_request = PostTableDataRequest {
|
||||
profile_name,
|
||||
table_name,
|
||||
data, // This is now the correct type
|
||||
data,
|
||||
};
|
||||
let request = tonic::Request::new(grpc_request);
|
||||
let response = self
|
||||
@@ -182,15 +225,13 @@ pub async fn get_table_data_by_position(
|
||||
profile_name: String,
|
||||
table_name: String,
|
||||
id: i64,
|
||||
// CHANGE THIS: Accept the pre-converted data
|
||||
data: HashMap<String, Value>,
|
||||
) -> Result<PutTableDataResponse> {
|
||||
// The conversion logic is now gone from here.
|
||||
let grpc_request = PutTableDataRequest {
|
||||
profile_name,
|
||||
table_name,
|
||||
id,
|
||||
data, // This is now the correct type
|
||||
data,
|
||||
};
|
||||
let request = tonic::Request::new(grpc_request);
|
||||
let response = self
|
||||
|
||||
@@ -98,7 +98,7 @@ impl UiService {
|
||||
pub async fn initialize_add_logic_table_data(
|
||||
grpc_client: &mut GrpcClient,
|
||||
add_logic_state: &mut AddLogicState,
|
||||
profile_tree: &common::proto::multieko2::table_definition::ProfileTreeResponse,
|
||||
profile_tree: &common::proto::komp_ac::table_definition::ProfileTreeResponse,
|
||||
) -> Result<String> {
|
||||
let profile_name_clone_opt = Some(add_logic_state.profile_name.clone());
|
||||
let table_name_opt_clone = add_logic_state.selected_table_name.clone();
|
||||
@@ -176,7 +176,7 @@ impl UiService {
|
||||
}
|
||||
}
|
||||
|
||||
// REFACTOR THIS FUNCTION
|
||||
// TODO REFACTOR (maybe)
|
||||
pub async fn initialize_app_state_and_form(
|
||||
grpc_client: &mut GrpcClient,
|
||||
app_state: &mut AppState,
|
||||
@@ -185,28 +185,27 @@ impl UiService {
|
||||
.get_profile_tree()
|
||||
.await
|
||||
.context("Failed to get profile tree")?;
|
||||
|
||||
app_state.profile_tree = profile_tree;
|
||||
|
||||
let initial_profile_name = app_state
|
||||
// Find first profile that contains tables
|
||||
let (initial_profile_name, initial_table_name) = app_state
|
||||
.profile_tree
|
||||
.profiles
|
||||
.first()
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
|
||||
let initial_table_name = app_state
|
||||
.profile_tree
|
||||
.profiles
|
||||
.first()
|
||||
.and_then(|p| p.tables.first().map(|t| t.name.clone()))
|
||||
.unwrap_or_else(|| "2025_company_data1".to_string());
|
||||
.iter()
|
||||
.find(|profile| !profile.tables.is_empty())
|
||||
.and_then(|profile| {
|
||||
profile.tables.first().map(|table| {
|
||||
(profile.name.clone(), table.name.clone())
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| anyhow!("No profiles with tables found. Create a table first."))?;
|
||||
|
||||
app_state.set_current_view_table(
|
||||
initial_profile_name.clone(),
|
||||
initial_table_name.clone(),
|
||||
);
|
||||
|
||||
// NOW, just call our new central function. This avoids code duplication.
|
||||
let form_state = Self::load_table_view(
|
||||
grpc_client,
|
||||
app_state,
|
||||
@@ -215,7 +214,6 @@ impl UiService {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// The field names for the UI are derived from the new form_state
|
||||
let field_names = form_state.fields.iter().map(|f| f.display_name.clone()).collect();
|
||||
|
||||
Ok((initial_profile_name, initial_table_name, field_names))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/state/app/search.rs
|
||||
|
||||
use common::proto::multieko2::search::search_response::Hit;
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
|
||||
/// Holds the complete state for the search palette.
|
||||
pub struct SearchState {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// src/state/app/state.rs
|
||||
|
||||
use anyhow::Result;
|
||||
use common::proto::multieko2::table_definition::ProfileTreeResponse;
|
||||
use common::proto::komp_ac::table_definition::ProfileTreeResponse;
|
||||
// NEW: Import the types we need for the cache
|
||||
use common::proto::multieko2::table_structure::TableStructureResponse;
|
||||
use common::proto::komp_ac::table_structure::TableStructureResponse;
|
||||
use crate::modes::handlers::mode_manager::AppMode;
|
||||
use crate::state::app::search::SearchState;
|
||||
use crate::ui::handlers::context::DialogPurpose;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/state/pages/canvas_state.rs
|
||||
|
||||
use common::proto::multieko2::search::search_response::Hit;
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
|
||||
pub trait CanvasState {
|
||||
// --- Existing methods (unchanged) ---
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use crate::config::colors::themes::Theme;
|
||||
use crate::state::app::highlight::HighlightState;
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use common::proto::multieko2::search::search_response::Hit;
|
||||
use canvas::{CanvasState, CanvasAction, ActionContext}; // CHANGED: Use canvas crate
|
||||
use common::proto::komp_ac::search::search_response::Hit;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::Frame;
|
||||
use std::collections::HashMap;
|
||||
@@ -146,7 +146,7 @@ impl FormState {
|
||||
} else {
|
||||
self.current_position = 1;
|
||||
}
|
||||
self.deactivate_autocomplete();
|
||||
self.deactivate_suggestions(); // CHANGED: Use canvas trait method
|
||||
self.link_display_map.clear();
|
||||
}
|
||||
|
||||
@@ -205,14 +205,25 @@ impl FormState {
|
||||
self.has_unsaved_changes = false;
|
||||
self.current_field = 0;
|
||||
self.current_cursor_pos = 0;
|
||||
self.deactivate_autocomplete();
|
||||
self.deactivate_suggestions(); // CHANGED: Use canvas trait method
|
||||
self.link_display_map.clear();
|
||||
}
|
||||
|
||||
pub fn deactivate_autocomplete(&mut self) {
|
||||
self.autocomplete_active = false;
|
||||
self.autocomplete_suggestions.clear();
|
||||
self.selected_suggestion_index = None;
|
||||
// REMOVED: deactivate_autocomplete() - now using trait method
|
||||
|
||||
// NEW: Keep the rich suggestions methods for compatibility
|
||||
pub fn get_rich_suggestions(&self) -> Option<&[Hit]> {
|
||||
if self.autocomplete_active {
|
||||
Some(&self.autocomplete_suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate_rich_suggestions(&mut self, suggestions: Vec<Hit>) {
|
||||
self.autocomplete_suggestions = suggestions;
|
||||
self.autocomplete_active = !self.autocomplete_suggestions.is_empty();
|
||||
self.selected_suggestion_index = if self.autocomplete_active { Some(0) } else { None };
|
||||
self.autocomplete_loading = false;
|
||||
}
|
||||
}
|
||||
@@ -221,49 +232,54 @@ impl CanvasState for FormState {
|
||||
fn current_field(&self) -> usize {
|
||||
self.current_field
|
||||
}
|
||||
|
||||
fn current_cursor_pos(&self) -> usize {
|
||||
self.current_cursor_pos
|
||||
}
|
||||
|
||||
fn has_unsaved_changes(&self) -> bool {
|
||||
self.has_unsaved_changes
|
||||
}
|
||||
|
||||
fn inputs(&self) -> Vec<&String> {
|
||||
self.values.iter().collect()
|
||||
}
|
||||
|
||||
fn get_current_input(&self) -> &str {
|
||||
FormState::get_current_input(self)
|
||||
}
|
||||
|
||||
fn get_current_input_mut(&mut self) -> &mut String {
|
||||
FormState::get_current_input_mut(self)
|
||||
}
|
||||
|
||||
fn fields(&self) -> Vec<&str> {
|
||||
self.fields
|
||||
.iter()
|
||||
.map(|f| f.display_name.as_str())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn set_current_field(&mut self, index: usize) {
|
||||
if index < self.fields.len() {
|
||||
self.current_field = index;
|
||||
}
|
||||
self.deactivate_autocomplete();
|
||||
self.deactivate_suggestions(); // CHANGED: Use canvas trait method
|
||||
}
|
||||
|
||||
fn set_current_cursor_pos(&mut self, pos: usize) {
|
||||
self.current_cursor_pos = pos;
|
||||
}
|
||||
|
||||
fn set_has_unsaved_changes(&mut self, changed: bool) {
|
||||
self.has_unsaved_changes = changed;
|
||||
}
|
||||
|
||||
// --- CANVAS CRATE SUGGESTIONS SUPPORT ---
|
||||
fn get_suggestions(&self) -> Option<&[String]> {
|
||||
None
|
||||
}
|
||||
fn get_rich_suggestions(&self) -> Option<&[Hit]> {
|
||||
if self.autocomplete_active {
|
||||
Some(&self.autocomplete_suggestions)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
None // We use rich suggestions instead
|
||||
}
|
||||
|
||||
fn get_selected_suggestion_index(&self) -> Option<usize> {
|
||||
if self.autocomplete_active {
|
||||
self.selected_suggestion_index
|
||||
@@ -272,6 +288,52 @@ impl CanvasState for FormState {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selected_suggestion_index(&mut self, index: Option<usize>) {
|
||||
if self.autocomplete_active {
|
||||
self.selected_suggestion_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
fn activate_suggestions(&mut self, suggestions: Vec<String>) {
|
||||
// For compatibility - convert to rich format if needed
|
||||
self.autocomplete_active = true;
|
||||
self.selected_suggestion_index = if suggestions.is_empty() { None } else { Some(0) };
|
||||
}
|
||||
|
||||
fn deactivate_suggestions(&mut self) {
|
||||
self.autocomplete_active = false;
|
||||
self.autocomplete_suggestions.clear();
|
||||
self.selected_suggestion_index = None;
|
||||
self.autocomplete_loading = false;
|
||||
}
|
||||
|
||||
// --- FEATURE-SPECIFIC ACTION HANDLING ---
|
||||
fn handle_feature_action(&mut self, action: &CanvasAction, _context: &ActionContext) -> Option<String> {
|
||||
match action {
|
||||
CanvasAction::SelectSuggestion => {
|
||||
if let Some(selected_idx) = self.selected_suggestion_index {
|
||||
if let Some(hit) = self.autocomplete_suggestions.get(selected_idx).cloned() { // ADD .cloned()
|
||||
// Extract the value from the selected suggestion
|
||||
if let Ok(content_map) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&hit.content_json) {
|
||||
let current_field_def = &self.fields[self.current_field];
|
||||
if let Some(value) = content_map.get(¤t_field_def.data_key) {
|
||||
let new_value = json_value_to_string(value);
|
||||
let display_name = self.get_display_name_for_hit(&hit); // Calculate first
|
||||
*self.get_current_input_mut() = new_value.clone();
|
||||
self.set_current_cursor_pos(new_value.len());
|
||||
self.set_has_unsaved_changes(true);
|
||||
self.deactivate_suggestions();
|
||||
return Some(format!("Selected: {}", display_name)); // Use calculated value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None, // Let canvas handle other actions
|
||||
}
|
||||
}
|
||||
|
||||
fn get_display_value_for_field(&self, index: usize) -> &str {
|
||||
if let Some(display_text) = self.link_display_map.get(&index) {
|
||||
return display_text.as_str();
|
||||
@@ -282,7 +344,6 @@ impl CanvasState for FormState {
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
// --- IMPLEMENT THE NEW TRAIT METHOD ---
|
||||
fn has_display_override(&self, index: usize) -> bool {
|
||||
self.link_display_map.contains_key(&index)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::state::pages::add_table::{
|
||||
};
|
||||
use crate::services::GrpcClient;
|
||||
use anyhow::{anyhow, Result};
|
||||
use common::proto::multieko2::table_definition::{
|
||||
use common::proto::komp_ac::table_definition::{
|
||||
PostTableDefinitionRequest,
|
||||
ColumnDefinition as ProtoColumnDefinition,
|
||||
TableLink as ProtoTableLink,
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::state::app::buffer::{AppView, BufferState};
|
||||
use crate::config::storage::storage::{StoredAuthData, save_auth_data};
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use crate::ui::handlers::context::DialogPurpose;
|
||||
use common::proto::multieko2::auth::LoginResponse;
|
||||
use common::proto::komp_ac::auth::LoginResponse;
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::state::{
|
||||
};
|
||||
use crate::ui::handlers::context::DialogPurpose;
|
||||
use crate::state::app::buffer::{AppView, BufferState};
|
||||
use common::proto::multieko2::auth::AuthResponse;
|
||||
use common::proto::komp_ac::auth::AuthResponse;
|
||||
use anyhow::Context;
|
||||
use tokio::spawn;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/tui/functions/form.rs
|
||||
use crate::state::pages::canvas_state::CanvasState;
|
||||
use crate::state::pages::form::FormState;
|
||||
use crate::services::grpc_client::GrpcClient;
|
||||
use canvas::CanvasState;
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
pub async fn handle_action(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/utils/data_converter.rs
|
||||
|
||||
use common::proto::multieko2::table_structure::TableStructureResponse;
|
||||
use common::proto::komp_ac::table_structure::TableStructureResponse;
|
||||
use prost_types::{value::Kind, NullValue, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
262
client/tests/form/gui/form_tests.rs
Normal file
262
client/tests/form/gui/form_tests.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
// client/tests/form_tests.rs
|
||||
use rstest::{fixture, rstest};
|
||||
use std::collections::HashMap;
|
||||
use client::state::pages::form::{FormState, FieldDefinition};
|
||||
use client::state::pages::canvas_state::CanvasState;
|
||||
|
||||
#[fixture]
|
||||
fn test_form_state() -> FormState {
|
||||
let fields = vec![
|
||||
FieldDefinition {
|
||||
display_name: "Company".to_string(),
|
||||
data_key: "firma".to_string(),
|
||||
is_link: false,
|
||||
link_target_table: None,
|
||||
},
|
||||
FieldDefinition {
|
||||
display_name: "Phone".to_string(),
|
||||
data_key: "telefon".to_string(),
|
||||
is_link: false,
|
||||
link_target_table: None,
|
||||
},
|
||||
FieldDefinition {
|
||||
display_name: "Email".to_string(),
|
||||
data_key: "email".to_string(),
|
||||
is_link: false,
|
||||
link_target_table: None,
|
||||
},
|
||||
];
|
||||
|
||||
FormState::new("test_profile".to_string(), "test_table".to_string(), fields)
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn test_form_data() -> HashMap<String, String> {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), "Test Company".to_string());
|
||||
data.insert("telefon".to_string(), "+421123456789".to_string());
|
||||
data.insert("email".to_string(), "test@example.com".to_string());
|
||||
data
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_state_creation(test_form_state: FormState) {
|
||||
assert_eq!(test_form_state.profile_name, "test_profile");
|
||||
assert_eq!(test_form_state.table_name, "test_table");
|
||||
assert_eq!(test_form_state.fields.len(), 3);
|
||||
assert_eq!(test_form_state.current_field(), 0);
|
||||
assert!(!test_form_state.has_unsaved_changes());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_field_navigation(mut test_form_state: FormState) {
|
||||
// Test initial field
|
||||
assert_eq!(test_form_state.current_field(), 0);
|
||||
|
||||
// Test navigation to next field
|
||||
test_form_state.set_current_field(1);
|
||||
assert_eq!(test_form_state.current_field(), 1);
|
||||
|
||||
// Test navigation to last field
|
||||
test_form_state.set_current_field(2);
|
||||
assert_eq!(test_form_state.current_field(), 2);
|
||||
|
||||
// Test invalid field (should not crash)
|
||||
test_form_state.set_current_field(999);
|
||||
assert_eq!(test_form_state.current_field(), 2); // Should stay at valid field
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_data_entry(mut test_form_state: FormState) {
|
||||
// Test entering data in first field
|
||||
*test_form_state.get_current_input_mut() = "Test Company".to_string();
|
||||
test_form_state.set_has_unsaved_changes(true);
|
||||
|
||||
assert_eq!(test_form_state.get_current_input(), "Test Company");
|
||||
assert!(test_form_state.has_unsaved_changes());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_field_switching_with_data(mut test_form_state: FormState) {
|
||||
// Enter data in first field
|
||||
*test_form_state.get_current_input_mut() = "Company Name".to_string();
|
||||
|
||||
// Switch to second field
|
||||
test_form_state.set_current_field(1);
|
||||
*test_form_state.get_current_input_mut() = "+421123456789".to_string();
|
||||
|
||||
// Switch back to first field
|
||||
test_form_state.set_current_field(0);
|
||||
assert_eq!(test_form_state.get_current_input(), "Company Name");
|
||||
|
||||
// Switch to second field again
|
||||
test_form_state.set_current_field(1);
|
||||
assert_eq!(test_form_state.get_current_input(), "+421123456789");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_reset_functionality(mut test_form_state: FormState) {
|
||||
// Add some data
|
||||
test_form_state.set_current_field(0);
|
||||
*test_form_state.get_current_input_mut() = "Test Company".to_string();
|
||||
test_form_state.set_current_field(1);
|
||||
*test_form_state.get_current_input_mut() = "+421123456789".to_string();
|
||||
test_form_state.set_has_unsaved_changes(true);
|
||||
test_form_state.id = 123;
|
||||
test_form_state.current_position = 5;
|
||||
|
||||
// Reset the form
|
||||
test_form_state.reset_to_empty();
|
||||
|
||||
// Verify reset
|
||||
assert_eq!(test_form_state.id, 0);
|
||||
assert!(!test_form_state.has_unsaved_changes());
|
||||
assert_eq!(test_form_state.current_field(), 0);
|
||||
|
||||
// Check all fields are empty
|
||||
for i in 0..test_form_state.fields.len() {
|
||||
test_form_state.set_current_field(i);
|
||||
assert!(test_form_state.get_current_input().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_update_from_response(mut test_form_state: FormState, test_form_data: HashMap<String, String>) {
|
||||
let position = 3;
|
||||
|
||||
// Update form with response data
|
||||
test_form_state.update_from_response(&test_form_data, position);
|
||||
|
||||
// Verify data was loaded
|
||||
assert_eq!(test_form_state.current_position, position);
|
||||
assert!(!test_form_state.has_unsaved_changes());
|
||||
assert_eq!(test_form_state.current_field(), 0);
|
||||
|
||||
// Check field values
|
||||
test_form_state.set_current_field(0);
|
||||
assert_eq!(test_form_state.get_current_input(), "Test Company");
|
||||
|
||||
test_form_state.set_current_field(1);
|
||||
assert_eq!(test_form_state.get_current_input(), "+421123456789");
|
||||
|
||||
test_form_state.set_current_field(2);
|
||||
assert_eq!(test_form_state.get_current_input(), "test@example.com");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_cursor_position(mut test_form_state: FormState) {
|
||||
// Test initial cursor position
|
||||
assert_eq!(test_form_state.current_cursor_pos(), 0);
|
||||
|
||||
// Add some text
|
||||
*test_form_state.get_current_input_mut() = "Test Company".to_string();
|
||||
|
||||
// Test cursor positioning
|
||||
test_form_state.set_current_cursor_pos(5);
|
||||
assert_eq!(test_form_state.current_cursor_pos(), 5);
|
||||
|
||||
// Test cursor bounds
|
||||
test_form_state.set_current_cursor_pos(999);
|
||||
// Should be clamped to text length
|
||||
assert!(test_form_state.current_cursor_pos() <= "Test Company".len());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_field_display_names(test_form_state: FormState) {
|
||||
let field_names = test_form_state.fields();
|
||||
|
||||
assert_eq!(field_names.len(), 3);
|
||||
assert_eq!(field_names[0], "Company");
|
||||
assert_eq!(field_names[1], "Phone");
|
||||
assert_eq!(field_names[2], "Email");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_inputs_vector(mut test_form_state: FormState) {
|
||||
// Add data to fields
|
||||
test_form_state.set_current_field(0);
|
||||
*test_form_state.get_current_input_mut() = "Company A".to_string();
|
||||
|
||||
test_form_state.set_current_field(1);
|
||||
*test_form_state.get_current_input_mut() = "123456789".to_string();
|
||||
|
||||
test_form_state.set_current_field(2);
|
||||
*test_form_state.get_current_input_mut() = "test@test.com".to_string();
|
||||
|
||||
// Get inputs vector
|
||||
let inputs = test_form_state.inputs();
|
||||
|
||||
assert_eq!(inputs.len(), 3);
|
||||
assert_eq!(inputs[0], "Company A");
|
||||
assert_eq!(inputs[1], "123456789");
|
||||
assert_eq!(inputs[2], "test@test.com");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_position_management(mut test_form_state: FormState) {
|
||||
// Test initial position
|
||||
assert_eq!(test_form_state.current_position, 1);
|
||||
assert_eq!(test_form_state.total_count, 0);
|
||||
|
||||
// Set some values
|
||||
test_form_state.total_count = 10;
|
||||
test_form_state.current_position = 5;
|
||||
|
||||
assert_eq!(test_form_state.current_position, 5);
|
||||
assert_eq!(test_form_state.total_count, 10);
|
||||
|
||||
// Test reset affects position
|
||||
test_form_state.reset_to_empty();
|
||||
assert_eq!(test_form_state.current_position, 11); // total_count + 1
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_autocomplete_state(mut test_form_state: FormState) {
|
||||
// Test initial autocomplete state
|
||||
assert!(!test_form_state.autocomplete_active);
|
||||
assert!(test_form_state.autocomplete_suggestions.is_empty());
|
||||
assert!(test_form_state.selected_suggestion_index.is_none());
|
||||
|
||||
// Test deactivating autocomplete
|
||||
test_form_state.autocomplete_active = true;
|
||||
test_form_state.deactivate_autocomplete();
|
||||
|
||||
assert!(!test_form_state.autocomplete_active);
|
||||
assert!(test_form_state.autocomplete_suggestions.is_empty());
|
||||
assert!(test_form_state.selected_suggestion_index.is_none());
|
||||
assert!(!test_form_state.autocomplete_loading);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_empty_data_handling(mut test_form_state: FormState) {
|
||||
let empty_data = HashMap::new();
|
||||
|
||||
// Update with empty data
|
||||
test_form_state.update_from_response(&empty_data, 1);
|
||||
|
||||
// All fields should be empty
|
||||
for i in 0..test_form_state.fields.len() {
|
||||
test_form_state.set_current_field(i);
|
||||
assert!(test_form_state.get_current_input().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_form_partial_data_handling(mut test_form_state: FormState) {
|
||||
let mut partial_data = HashMap::new();
|
||||
partial_data.insert("firma".to_string(), "Partial Company".to_string());
|
||||
// Intentionally missing telefon and email
|
||||
|
||||
test_form_state.update_from_response(&partial_data, 1);
|
||||
|
||||
// First field should have data
|
||||
test_form_state.set_current_field(0);
|
||||
assert_eq!(test_form_state.get_current_input(), "Partial Company");
|
||||
|
||||
// Other fields should be empty
|
||||
test_form_state.set_current_field(1);
|
||||
assert!(test_form_state.get_current_input().is_empty());
|
||||
|
||||
test_form_state.set_current_field(2);
|
||||
assert!(test_form_state.get_current_input().is_empty());
|
||||
}
|
||||
1
client/tests/form/gui/mod.rs
Normal file
1
client/tests/form/gui/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod form_tests;
|
||||
2
client/tests/form/mod.rs
Normal file
2
client/tests/form/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod gui;
|
||||
pub mod requests;
|
||||
1019
client/tests/form/requests/form_request_tests.rs
Normal file
1019
client/tests/form/requests/form_request_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
267
client/tests/form/requests/form_request_tests2.rs
Normal file
267
client/tests/form/requests/form_request_tests2.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
// ========================================================================
|
||||
// ROBUST WORKFLOW AND INTEGRATION TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_partial_update_preserves_other_fields(
|
||||
#[future] populated_test_context: FormTestContext,
|
||||
) {
|
||||
let mut context = populated_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Create a record with multiple fields
|
||||
let mut initial_data = context.create_test_form_data();
|
||||
let original_email = "preserve.this@email.com";
|
||||
initial_data.insert(
|
||||
"email".to_string(),
|
||||
create_string_value(original_email),
|
||||
);
|
||||
|
||||
let post_res = context
|
||||
.client
|
||||
.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
initial_data,
|
||||
)
|
||||
.await
|
||||
.expect("Setup: Failed to create record for partial update test");
|
||||
let created_id = post_res.inserted_id;
|
||||
println!("Partial Update Test: Created record ID {}", created_id);
|
||||
|
||||
// 2. Update only ONE field
|
||||
let mut partial_update = HashMap::new();
|
||||
let updated_firma = "Partially Updated Inc.";
|
||||
partial_update.insert(
|
||||
"firma".to_string(),
|
||||
create_string_value(updated_firma),
|
||||
);
|
||||
|
||||
context
|
||||
.client
|
||||
.put_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
partial_update,
|
||||
)
|
||||
.await
|
||||
.expect("Partial update failed");
|
||||
println!("Partial Update Test: Updated only 'firma' field");
|
||||
|
||||
// 3. Get the record back and verify ALL fields
|
||||
let get_res = context
|
||||
.client
|
||||
.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to get record after partial update");
|
||||
|
||||
let final_data = get_res.data;
|
||||
assert_eq!(
|
||||
final_data.get("firma").unwrap(),
|
||||
updated_firma,
|
||||
"The 'firma' field should be updated"
|
||||
);
|
||||
assert_eq!(
|
||||
final_data.get("email").unwrap(),
|
||||
original_email,
|
||||
"The 'email' field should have been preserved"
|
||||
);
|
||||
println!("Partial Update Test: Verified other fields were preserved. OK.");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_data_edge_cases_and_unicode(
|
||||
#[future] form_test_context: FormTestContext,
|
||||
) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let edge_case_strings = vec![
|
||||
("Unicode", "José María González, Москва, 北京市"),
|
||||
("Emoji", "🚀 Tech Company 🌟"),
|
||||
("Quotes", "Quote\"Test'Apostrophe"),
|
||||
("Symbols", "Price: $1,000.50 (50% off!)"),
|
||||
("Empty", ""),
|
||||
("Whitespace", " "),
|
||||
];
|
||||
|
||||
for (case_name, test_string) in edge_case_strings {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value(test_string));
|
||||
data.insert(
|
||||
"kz".to_string(),
|
||||
create_string_value(&format!("EDGE-{}", case_name)),
|
||||
);
|
||||
|
||||
let post_res = context
|
||||
.client
|
||||
.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
)
|
||||
.await
|
||||
.expect(&format!("POST should succeed for case: {}", case_name));
|
||||
let created_id = post_res.inserted_id;
|
||||
|
||||
let get_res = context
|
||||
.client
|
||||
.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
)
|
||||
.await
|
||||
.expect(&format!(
|
||||
"GET should succeed for case: {}",
|
||||
case_name
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
get_res.data.get("firma").unwrap(),
|
||||
test_string,
|
||||
"Data should be identical after round-trip for case: {}",
|
||||
case_name
|
||||
);
|
||||
println!("Edge Case Test: '{}' passed.", case_name);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_numeric_and_null_edge_cases(
|
||||
#[future] form_test_context: FormTestContext,
|
||||
) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Test NULL value
|
||||
let mut null_data = HashMap::new();
|
||||
null_data.insert(
|
||||
"firma".to_string(),
|
||||
create_string_value("Company With Null Phone"),
|
||||
);
|
||||
null_data.insert("telefon".to_string(), create_null_value());
|
||||
let post_res_null = context
|
||||
.client
|
||||
.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
null_data,
|
||||
)
|
||||
.await
|
||||
.expect("POST with NULL value should succeed");
|
||||
let get_res_null = context
|
||||
.client
|
||||
.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
post_res_null.inserted_id,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// Depending on DB, NULL may come back as empty string or be absent.
|
||||
// The important part is that the operation doesn't fail.
|
||||
assert!(
|
||||
get_res_null.data.get("telefon").unwrap_or(&"".to_string()).is_empty(),
|
||||
"NULL value should result in an empty or absent field"
|
||||
);
|
||||
println!("Edge Case Test: NULL value handled correctly. OK.");
|
||||
|
||||
// 2. Test Zero value for a numeric field (assuming 'age' is numeric)
|
||||
let mut zero_data = HashMap::new();
|
||||
zero_data.insert(
|
||||
"firma".to_string(),
|
||||
create_string_value("Newborn Company"),
|
||||
);
|
||||
// Assuming 'age' is a field in your actual table definition
|
||||
// zero_data.insert("age".to_string(), create_number_value(0.0));
|
||||
// let post_res_zero = context.client.post_table_data(...).await.expect("POST with zero should succeed");
|
||||
// ... then get and verify it's "0"
|
||||
println!("Edge Case Test: Zero value test skipped (uncomment if 'age' field exists).");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_updates_on_same_record(
|
||||
#[future] populated_test_context: FormTestContext,
|
||||
) {
|
||||
let mut context = populated_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Create a single record to be updated by all tasks
|
||||
let initial_data = context.create_minimal_form_data();
|
||||
let post_res = context
|
||||
.client
|
||||
.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
initial_data,
|
||||
)
|
||||
.await
|
||||
.expect("Setup: Failed to create record for concurrency test");
|
||||
let record_id = post_res.inserted_id;
|
||||
println!("Concurrency Test: Target record ID is {}", record_id);
|
||||
|
||||
// 2. Spawn multiple concurrent UPDATE operations
|
||||
let mut handles = Vec::new();
|
||||
let num_concurrent_tasks = 5;
|
||||
let mut final_values = Vec::new();
|
||||
|
||||
for i in 0..num_concurrent_tasks {
|
||||
let mut client_clone = context.client.clone();
|
||||
let profile_name = context.profile_name.clone();
|
||||
let table_name = context.table_name.clone();
|
||||
let final_value = format!("Concurrent Update {}", i);
|
||||
final_values.push(final_value.clone());
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut update_data = HashMap::new();
|
||||
update_data.insert(
|
||||
"firma".to_string(),
|
||||
create_string_value(&final_value),
|
||||
);
|
||||
client_clone
|
||||
.put_table_data(profile_name, table_name, record_id, update_data)
|
||||
.await
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// 3. Wait for all tasks to complete and check for panics
|
||||
let results = futures::future::join_all(handles).await;
|
||||
assert!(
|
||||
results.iter().all(|r| r.is_ok()),
|
||||
"No concurrent task should panic"
|
||||
);
|
||||
println!("Concurrency Test: All update tasks completed without panicking.");
|
||||
|
||||
// 4. Get the final state of the record
|
||||
let final_get_res = context
|
||||
.client
|
||||
.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
)
|
||||
.await
|
||||
.expect("Should be able to get the record after concurrent updates");
|
||||
|
||||
let final_firma = final_get_res.data.get("firma").unwrap();
|
||||
assert!(
|
||||
final_values.contains(final_firma),
|
||||
"The final state '{}' must be one of the states set by the tasks",
|
||||
final_firma
|
||||
);
|
||||
println!(
|
||||
"Concurrency Test: Final state is '{}', which is a valid outcome. OK.",
|
||||
final_firma
|
||||
);
|
||||
}
|
||||
727
client/tests/form/requests/form_request_tests3.rs
Normal file
727
client/tests/form/requests/form_request_tests3.rs
Normal file
@@ -0,0 +1,727 @@
|
||||
// form_request_tests3.rs - Comprehensive and Robust Testing
|
||||
|
||||
// ========================================================================
|
||||
// STEEL SCRIPT VALIDATION TESTS (HIGHEST PRIORITY)
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_steel_script_validation_success(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test with data that should pass script validation
|
||||
// Assuming there's a script that validates 'kz' field to start with "KZ" and be 5 chars
|
||||
let mut valid_data = HashMap::new();
|
||||
valid_data.insert("firma".to_string(), create_string_value("Script Test Company"));
|
||||
valid_data.insert("kz".to_string(), create_string_value("KZ123"));
|
||||
valid_data.insert("telefon".to_string(), create_string_value("+421123456789"));
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
valid_data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
assert!(response.success, "Valid data should pass script validation");
|
||||
println!("Script Validation Test: Valid data passed - ID {}", response.inserted_id);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
if status.code() == tonic::Code::Unavailable {
|
||||
println!("Script validation test skipped - backend not available");
|
||||
return;
|
||||
}
|
||||
// If there are no scripts configured, this might still work
|
||||
println!("Script validation test: {}", status.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_steel_script_validation_failure(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test with data that should fail script validation
|
||||
let invalid_script_data = vec![
|
||||
("TooShort", "KZ12"), // Too short
|
||||
("TooLong", "KZ12345"), // Too long
|
||||
("WrongPrefix", "AB123"), // Wrong prefix
|
||||
("NoPrefix", "12345"), // No prefix
|
||||
("Empty", ""), // Empty
|
||||
];
|
||||
|
||||
for (test_case, invalid_kz) in invalid_script_data {
|
||||
let mut invalid_data = HashMap::new();
|
||||
invalid_data.insert("firma".to_string(), create_string_value("Script Fail Company"));
|
||||
invalid_data.insert("kz".to_string(), create_string_value(invalid_kz));
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
invalid_data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
println!("Script Validation Test: {} passed (no validation script configured)", test_case);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::InvalidArgument,
|
||||
"Script validation failure should return InvalidArgument for case: {}", test_case);
|
||||
println!("Script Validation Test: {} correctly failed - {}", test_case, status.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_steel_script_validation_on_update(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Create a valid record first
|
||||
let mut initial_data = HashMap::new();
|
||||
initial_data.insert("firma".to_string(), create_string_value("Update Script Test"));
|
||||
initial_data.insert("kz".to_string(), create_string_value("KZ123"));
|
||||
|
||||
let post_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
initial_data,
|
||||
).await;
|
||||
|
||||
if let Ok(post_response) = post_result {
|
||||
let record_id = post_response.inserted_id;
|
||||
|
||||
// 2. Try to update with invalid data
|
||||
let mut invalid_update = HashMap::new();
|
||||
invalid_update.insert("kz".to_string(), create_string_value("INVALID"));
|
||||
|
||||
let update_result = context.client.put_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
invalid_update,
|
||||
).await;
|
||||
|
||||
match update_result {
|
||||
Ok(_) => {
|
||||
println!("Script Validation on Update: No validation script configured for updates");
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::InvalidArgument,
|
||||
"Update with invalid data should fail script validation");
|
||||
println!("Script Validation on Update: Correctly rejected invalid update");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// COMPREHENSIVE DATA TYPE TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_boolean_data_type(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test valid boolean values
|
||||
let boolean_test_cases = vec![
|
||||
("true", true),
|
||||
("false", false),
|
||||
];
|
||||
|
||||
for (case_name, bool_value) in boolean_test_cases {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Boolean Test Company"));
|
||||
// Assuming there's a boolean field called 'active'
|
||||
data.insert("active".to_string(), create_bool_value(bool_value));
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
println!("Boolean Test: {} value succeeded", case_name);
|
||||
|
||||
// Verify the value round-trip
|
||||
if let Ok(get_response) = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
response.inserted_id,
|
||||
).await {
|
||||
if let Some(retrieved_value) = get_response.data.get("active") {
|
||||
println!("Boolean Test: {} round-trip value: {}", case_name, retrieved_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Boolean Test: {} failed (field may not exist): {}", case_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_numeric_data_types(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test various numeric values
|
||||
let numeric_test_cases = vec![
|
||||
("Zero", 0.0),
|
||||
("Positive", 123.45),
|
||||
("Negative", -67.89),
|
||||
("Large", 999999.99),
|
||||
("SmallDecimal", 0.01),
|
||||
];
|
||||
|
||||
for (case_name, numeric_value) in numeric_test_cases {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Numeric Test Company"));
|
||||
// Assuming there's a numeric field called 'price' or 'amount'
|
||||
data.insert("amount".to_string(), create_number_value(numeric_value));
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
println!("Numeric Test: {} ({}) succeeded", case_name, numeric_value);
|
||||
|
||||
// Verify round-trip
|
||||
if let Ok(get_response) = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
response.inserted_id,
|
||||
).await {
|
||||
if let Some(retrieved_value) = get_response.data.get("amount") {
|
||||
println!("Numeric Test: {} round-trip value: {}", case_name, retrieved_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Numeric Test: {} failed (field may not exist): {}", case_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_timestamp_data_type(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test various timestamp formats
|
||||
let timestamp_test_cases = vec![
|
||||
("ISO8601", "2024-01-15T10:30:00Z"),
|
||||
("WithTimezone", "2024-01-15T10:30:00+01:00"),
|
||||
("WithMilliseconds", "2024-01-15T10:30:00.123Z"),
|
||||
];
|
||||
|
||||
for (case_name, timestamp_str) in timestamp_test_cases {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Timestamp Test Company"));
|
||||
// Assuming there's a timestamp field called 'created_at'
|
||||
data.insert("created_at".to_string(), create_string_value(timestamp_str));
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
println!("Timestamp Test: {} succeeded", case_name);
|
||||
|
||||
// Verify round-trip
|
||||
if let Ok(get_response) = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
response.inserted_id,
|
||||
).await {
|
||||
if let Some(retrieved_value) = get_response.data.get("created_at") {
|
||||
println!("Timestamp Test: {} round-trip value: {}", case_name, retrieved_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Timestamp Test: {} failed (field may not exist): {}", case_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_invalid_data_types(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test invalid data type combinations
|
||||
let invalid_type_cases = vec![
|
||||
("StringForNumber", "amount", create_string_value("not-a-number")),
|
||||
("NumberForBoolean", "active", create_number_value(123.0)),
|
||||
("StringForBoolean", "active", create_string_value("maybe")),
|
||||
("InvalidTimestamp", "created_at", create_string_value("not-a-date")),
|
||||
];
|
||||
|
||||
for (case_name, field_name, invalid_value) in invalid_type_cases {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Invalid Type Test"));
|
||||
data.insert(field_name.to_string(), invalid_value);
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
println!("Invalid Type Test: {} passed (no type validation or field doesn't exist)", case_name);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::InvalidArgument,
|
||||
"Invalid data type should return InvalidArgument for case: {}", case_name);
|
||||
println!("Invalid Type Test: {} correctly rejected - {}", case_name, status.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// FOREIGN KEY RELATIONSHIP TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_foreign_key_valid_relationship(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Create a parent record first (e.g., company)
|
||||
let mut parent_data = HashMap::new();
|
||||
parent_data.insert("firma".to_string(), create_string_value("Parent Company"));
|
||||
|
||||
let parent_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
"companies".to_string(), // Assuming companies table exists
|
||||
parent_data,
|
||||
).await;
|
||||
|
||||
if let Ok(parent_response) = parent_result {
|
||||
let parent_id = parent_response.inserted_id;
|
||||
|
||||
// 2. Create a child record that references the parent
|
||||
let mut child_data = HashMap::new();
|
||||
child_data.insert("name".to_string(), create_string_value("Child Record"));
|
||||
child_data.insert("company_id".to_string(), create_number_value(parent_id as f64));
|
||||
|
||||
let child_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
"contacts".to_string(), // Assuming contacts table exists
|
||||
child_data,
|
||||
).await;
|
||||
|
||||
match child_result {
|
||||
Ok(child_response) => {
|
||||
assert!(child_response.success, "Valid foreign key relationship should succeed");
|
||||
println!("Foreign Key Test: Valid relationship created - Parent ID: {}, Child ID: {}",
|
||||
parent_id, child_response.inserted_id);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Foreign Key Test: Failed (tables may not exist or no FK constraint): {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Foreign Key Test: Could not create parent record");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_foreign_key_invalid_relationship(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Try to create a child record with non-existent parent ID
|
||||
let mut invalid_child_data = HashMap::new();
|
||||
invalid_child_data.insert("name".to_string(), create_string_value("Orphan Record"));
|
||||
invalid_child_data.insert("company_id".to_string(), create_number_value(99999.0)); // Non-existent ID
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
"contacts".to_string(),
|
||||
invalid_child_data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
println!("Foreign Key Test: Invalid relationship passed (no FK constraint configured)");
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
// Could be InvalidArgument or NotFound depending on implementation
|
||||
assert!(matches!(status.code(), tonic::Code::InvalidArgument | tonic::Code::NotFound),
|
||||
"Invalid foreign key should return InvalidArgument or NotFound");
|
||||
println!("Foreign Key Test: Invalid relationship correctly rejected - {}", status.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DELETED RECORD INTERACTION TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_update_deleted_record_behavior(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Create a record
|
||||
let initial_data = context.create_test_form_data();
|
||||
let post_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
initial_data,
|
||||
).await;
|
||||
|
||||
if let Ok(post_response) = post_result {
|
||||
let record_id = post_response.inserted_id;
|
||||
println!("Deleted Record Test: Created record ID {}", record_id);
|
||||
|
||||
// 2. Delete the record (soft delete)
|
||||
let delete_result = context.client.delete_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
).await;
|
||||
|
||||
assert!(delete_result.is_ok(), "Delete should succeed");
|
||||
println!("Deleted Record Test: Soft-deleted record {}", record_id);
|
||||
|
||||
// 3. Try to UPDATE the deleted record
|
||||
let mut update_data = HashMap::new();
|
||||
update_data.insert("firma".to_string(), create_string_value("Updated Deleted Record"));
|
||||
|
||||
let update_result = context.client.put_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
update_data,
|
||||
).await;
|
||||
|
||||
match update_result {
|
||||
Ok(_) => {
|
||||
// This might be a bug - updating deleted records should probably fail
|
||||
println!("Deleted Record Test: UPDATE on deleted record succeeded (potential bug?)");
|
||||
|
||||
// Check if the record is still considered deleted
|
||||
let get_result = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
).await;
|
||||
|
||||
if get_result.is_err() {
|
||||
println!("Deleted Record Test: Record still appears deleted after update");
|
||||
} else {
|
||||
println!("Deleted Record Test: Record appears to be undeleted after update");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::NotFound,
|
||||
"UPDATE on deleted record should return NotFound");
|
||||
println!("Deleted Record Test: UPDATE correctly rejected on deleted record");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_delete_already_deleted_record(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Create and delete a record
|
||||
let initial_data = context.create_test_form_data();
|
||||
let post_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
initial_data,
|
||||
).await;
|
||||
|
||||
if let Ok(post_response) = post_result {
|
||||
let record_id = post_response.inserted_id;
|
||||
|
||||
// First deletion
|
||||
let delete_result1 = context.client.delete_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
).await;
|
||||
assert!(delete_result1.is_ok(), "First delete should succeed");
|
||||
|
||||
// Second deletion (idempotent)
|
||||
let delete_result2 = context.client.delete_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
).await;
|
||||
|
||||
assert!(delete_result2.is_ok(), "Second delete should succeed (idempotent)");
|
||||
if let Ok(response) = delete_result2 {
|
||||
assert!(response.success, "Delete should report success even for already-deleted record");
|
||||
}
|
||||
println!("Double Delete Test: Both deletions succeeded (idempotent behavior)");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// VALIDATION AND BOUNDARY TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_large_data_handling(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test with very large string values
|
||||
let large_string = "A".repeat(10000); // 10KB string
|
||||
let very_large_string = "B".repeat(100000); // 100KB string
|
||||
|
||||
let test_cases = vec![
|
||||
("Large", large_string),
|
||||
("VeryLarge", very_large_string),
|
||||
];
|
||||
|
||||
for (case_name, large_value) in test_cases {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value(&large_value));
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
println!("Large Data Test: {} string handled successfully", case_name);
|
||||
|
||||
// Verify round-trip
|
||||
if let Ok(get_response) = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
response.inserted_id,
|
||||
).await {
|
||||
if let Some(retrieved_value) = get_response.data.get("firma") {
|
||||
assert_eq!(retrieved_value.len(), large_value.len(),
|
||||
"Large string should survive round-trip for case: {}", case_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Large Data Test: {} failed (may hit size limits): {}", case_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_sql_injection_attempts(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test potential SQL injection strings
|
||||
let injection_attempts = vec![
|
||||
("SingleQuote", "'; DROP TABLE users; --"),
|
||||
("DoubleQuote", "\"; DROP TABLE users; --"),
|
||||
("Union", "' UNION SELECT * FROM users --"),
|
||||
("Comment", "/* malicious comment */"),
|
||||
("Semicolon", "; DELETE FROM users;"),
|
||||
];
|
||||
|
||||
for (case_name, injection_string) in injection_attempts {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value(injection_string));
|
||||
data.insert("kz".to_string(), create_string_value("KZ123"));
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
println!("SQL Injection Test: {} handled safely (parameterized queries)", case_name);
|
||||
|
||||
// Verify the malicious string was stored as-is (not executed)
|
||||
if let Ok(get_response) = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
response.inserted_id,
|
||||
).await {
|
||||
if let Some(retrieved_value) = get_response.data.get("firma") {
|
||||
assert_eq!(retrieved_value, injection_string,
|
||||
"Injection string should be stored literally for case: {}", case_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("SQL Injection Test: {} rejected: {}", case_name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_operations_with_same_data(#[future] form_test_context: FormTestContext) {
|
||||
let context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test multiple concurrent operations with identical data
|
||||
let mut handles = Vec::new();
|
||||
let num_tasks = 10;
|
||||
|
||||
for i in 0..num_tasks {
|
||||
let mut context_clone = context.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Concurrent Identical"));
|
||||
data.insert("kz".to_string(), create_string_value(&format!("SAME{:02}", i)));
|
||||
|
||||
context_clone.client.post_table_data(
|
||||
context_clone.profile_name,
|
||||
context_clone.table_name,
|
||||
data,
|
||||
).await
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all to complete
|
||||
let mut success_count = 0;
|
||||
let mut inserted_ids = Vec::new();
|
||||
|
||||
for (i, handle) in handles.into_iter().enumerate() {
|
||||
match handle.await {
|
||||
Ok(Ok(response)) => {
|
||||
success_count += 1;
|
||||
inserted_ids.push(response.inserted_id);
|
||||
println!("Concurrent Identical Data: Task {} succeeded with ID {}", i, response.inserted_id);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("Concurrent Identical Data: Task {} failed: {}", i, e);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Concurrent Identical Data: Task {} panicked: {}", i, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(success_count > 0, "At least some concurrent operations should succeed");
|
||||
|
||||
// Verify all IDs are unique
|
||||
let unique_ids: std::collections::HashSet<_> = inserted_ids.iter().collect();
|
||||
assert_eq!(unique_ids.len(), inserted_ids.len(), "All inserted IDs should be unique");
|
||||
|
||||
println!("Concurrent Identical Data: {}/{} operations succeeded with unique IDs",
|
||||
success_count, num_tasks);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PERFORMANCE AND STRESS TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_bulk_operations_performance(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let operation_count = 50;
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let mut successful_operations = 0;
|
||||
let mut created_ids = Vec::new();
|
||||
|
||||
// Bulk create
|
||||
for i in 0..operation_count {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value(&format!("Bulk Company {}", i)));
|
||||
data.insert("kz".to_string(), create_string_value(&format!("BLK{:02}", i)));
|
||||
|
||||
if let Ok(response) = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await {
|
||||
successful_operations += 1;
|
||||
created_ids.push(response.inserted_id);
|
||||
}
|
||||
}
|
||||
|
||||
let create_duration = start_time.elapsed();
|
||||
println!("Bulk Performance: Created {} records in {:?}", successful_operations, create_duration);
|
||||
|
||||
// Bulk read
|
||||
let read_start = std::time::Instant::now();
|
||||
let mut successful_reads = 0;
|
||||
|
||||
for &record_id in &created_ids {
|
||||
if context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
).await.is_ok() {
|
||||
successful_reads += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let read_duration = read_start.elapsed();
|
||||
println!("Bulk Performance: Read {} records in {:?}", successful_reads, read_duration);
|
||||
|
||||
// Performance assertions
|
||||
assert!(successful_operations > operation_count * 8 / 10,
|
||||
"At least 80% of operations should succeed");
|
||||
assert!(create_duration.as_secs() < 60,
|
||||
"Bulk operations should complete in reasonable time");
|
||||
|
||||
println!("Bulk Performance Test: {}/{} creates, {}/{} reads successful",
|
||||
successful_operations, operation_count, successful_reads, created_ids.len());
|
||||
}
|
||||
1
client/tests/form/requests/mod.rs
Normal file
1
client/tests/form/requests/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod form_request_tests;
|
||||
3
client/tests/mod.rs
Normal file
3
client/tests/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
// tests/mod.rs
|
||||
|
||||
pub mod form;
|
||||
@@ -1,6 +1,6 @@
|
||||
// proto/adresar.proto
|
||||
syntax = "proto3";
|
||||
package multieko2.adresar;
|
||||
package komp_ac.adresar;
|
||||
|
||||
import "common.proto";
|
||||
// import "table_structure.proto";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// proto/auth.proto
|
||||
syntax = "proto3";
|
||||
package multieko2.auth;
|
||||
package komp_ac.auth;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// proto/common.proto
|
||||
syntax = "proto3";
|
||||
package multieko2.common;
|
||||
package komp_ac.common;
|
||||
|
||||
message Empty {}
|
||||
message CountResponse { int64 count = 1; }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// In common/proto/search.proto
|
||||
syntax = "proto3";
|
||||
package multieko2.search;
|
||||
package komp_ac.search;
|
||||
|
||||
service Searcher {
|
||||
rpc SearchTable(SearchRequest) returns (SearchResponse);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// common/proto/table_definition.proto
|
||||
syntax = "proto3";
|
||||
package multieko2.table_definition;
|
||||
package komp_ac.table_definition;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
service TableDefinition {
|
||||
rpc PostTableDefinition (PostTableDefinitionRequest) returns (TableDefinitionResponse);
|
||||
rpc GetProfileTree (multieko2.common.Empty) returns (ProfileTreeResponse);
|
||||
rpc GetProfileTree (komp_ac.common.Empty) returns (ProfileTreeResponse);
|
||||
rpc DeleteTable (DeleteTableRequest) returns (DeleteTableResponse);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
syntax = "proto3";
|
||||
package multieko2.table_script;
|
||||
package komp_ac.table_script;
|
||||
|
||||
service TableScript {
|
||||
rpc PostTableScript(PostTableScriptRequest) returns (TableScriptResponse);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// proto/table_structure.proto
|
||||
syntax = "proto3";
|
||||
package multieko2.table_structure;
|
||||
package komp_ac.table_structure;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// common/proto/tables_data.proto
|
||||
syntax = "proto3";
|
||||
package multieko2.tables_data;
|
||||
package komp_ac.tables_data;
|
||||
|
||||
import "common.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
@@ -10,7 +10,7 @@ service TablesData {
|
||||
rpc PutTableData (PutTableDataRequest) returns (PutTableDataResponse);
|
||||
rpc DeleteTableData (DeleteTableDataRequest) returns (DeleteTableDataResponse);
|
||||
rpc GetTableData(GetTableDataRequest) returns (GetTableDataResponse);
|
||||
rpc GetTableDataCount(GetTableDataCountRequest) returns (multieko2.common.CountResponse);
|
||||
rpc GetTableDataCount(GetTableDataCountRequest) returns (komp_ac.common.CountResponse);
|
||||
rpc GetTableDataByPosition(GetTableDataByPositionRequest) returns (GetTableDataResponse);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// proto/uctovnictvo.proto
|
||||
syntax = "proto3";
|
||||
package multieko2.uctovnictvo;
|
||||
package komp_ac.uctovnictvo;
|
||||
|
||||
import "common.proto";
|
||||
|
||||
|
||||
@@ -3,33 +3,33 @@
|
||||
pub mod search;
|
||||
|
||||
pub mod proto {
|
||||
pub mod multieko2 {
|
||||
pub mod komp_ac {
|
||||
pub mod adresar {
|
||||
include!("proto/multieko2.adresar.rs");
|
||||
include!("proto/komp_ac.adresar.rs");
|
||||
}
|
||||
pub mod auth {
|
||||
include!("proto/multieko2.auth.rs");
|
||||
include!("proto/komp_ac.auth.rs");
|
||||
}
|
||||
pub mod common {
|
||||
include!("proto/multieko2.common.rs");
|
||||
include!("proto/komp_ac.common.rs");
|
||||
}
|
||||
pub mod table_structure {
|
||||
include!("proto/multieko2.table_structure.rs");
|
||||
include!("proto/komp_ac.table_structure.rs");
|
||||
}
|
||||
pub mod uctovnictvo {
|
||||
include!("proto/multieko2.uctovnictvo.rs");
|
||||
include!("proto/komp_ac.uctovnictvo.rs");
|
||||
}
|
||||
pub mod table_definition {
|
||||
include!("proto/multieko2.table_definition.rs");
|
||||
include!("proto/komp_ac.table_definition.rs");
|
||||
}
|
||||
pub mod tables_data {
|
||||
include!("proto/multieko2.tables_data.rs");
|
||||
include!("proto/komp_ac.tables_data.rs");
|
||||
}
|
||||
pub mod table_script {
|
||||
include!("proto/multieko2.table_script.rs");
|
||||
include!("proto/komp_ac.table_script.rs");
|
||||
}
|
||||
pub mod search {
|
||||
include!("proto/multieko2.search.rs");
|
||||
include!("proto/komp_ac.search.rs");
|
||||
}
|
||||
pub const FILE_DESCRIPTOR_SET: &[u8] =
|
||||
include_bytes!("proto/descriptor.bin");
|
||||
|
||||
Binary file not shown.
791
common/src/proto/komp_ac.adresar.rs
Normal file
791
common/src/proto/komp_ac.adresar.rs
Normal file
@@ -0,0 +1,791 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
418
common/src/proto/komp_ac.auth.rs
Normal file
418
common/src/proto/komp_ac.auth.rs
Normal file
@@ -0,0 +1,418 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
13
common/src/proto/komp_ac.common.rs
Normal file
13
common/src/proto/komp_ac.common.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
// 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,
|
||||
}
|
||||
317
common/src/proto/komp_ac.search.rs
Normal file
317
common/src/proto/komp_ac.search.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
544
common/src/proto/komp_ac.table_definition.rs
Normal file
544
common/src/proto/komp_ac.table_definition.rs
Normal file
@@ -0,0 +1,544 @@
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct TableLink {
|
||||
#[prost(string, tag = "1")]
|
||||
pub linked_table_name: ::prost::alloc::string::String,
|
||||
#[prost(bool, tag = "2")]
|
||||
pub required: bool,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PostTableDefinitionRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub table_name: ::prost::alloc::string::String,
|
||||
#[prost(message, repeated, tag = "2")]
|
||||
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")]
|
||||
pub field_type: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct TableDefinitionResponse {
|
||||
#[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")]
|
||||
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 DeleteTableResponse {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub success: bool,
|
||||
#[prost(string, tag = "2")]
|
||||
pub message: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod table_definition_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 TableDefinitionClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl TableDefinitionClient<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> TableDefinitionClient<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,
|
||||
) -> TableDefinitionClient<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,
|
||||
{
|
||||
TableDefinitionClient::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_definition(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::PostTableDefinitionRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::TableDefinitionResponse>,
|
||||
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/PostTableDefinition",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"komp_ac.table_definition.TableDefinition",
|
||||
"PostTableDefinition",
|
||||
),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_profile_tree(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::super::common::Empty>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::ProfileTreeResponse>,
|
||||
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/GetProfileTree",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"komp_ac.table_definition.TableDefinition",
|
||||
"GetProfileTree",
|
||||
),
|
||||
);
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated server implementations.
|
||||
pub mod table_definition_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 TableDefinitionServer.
|
||||
#[async_trait]
|
||||
pub trait TableDefinition: std::marker::Send + std::marker::Sync + 'static {
|
||||
async fn post_table_definition(
|
||||
&self,
|
||||
request: tonic::Request<super::PostTableDefinitionRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::TableDefinitionResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn get_profile_tree(
|
||||
&self,
|
||||
request: tonic::Request<super::super::common::Empty>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::ProfileTreeResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn delete_table(
|
||||
&self,
|
||||
request: tonic::Request<super::DeleteTableRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::DeleteTableResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct TableDefinitionServer<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> TableDefinitionServer<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 TableDefinitionServer<T>
|
||||
where
|
||||
T: TableDefinition,
|
||||
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_definition.TableDefinition/PostTableDefinition" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PostTableDefinitionSvc<T: TableDefinition>(pub Arc<T>);
|
||||
impl<
|
||||
T: TableDefinition,
|
||||
> tonic::server::UnaryService<super::PostTableDefinitionRequest>
|
||||
for PostTableDefinitionSvc<T> {
|
||||
type Response = super::TableDefinitionResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::PostTableDefinitionRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as TableDefinition>::post_table_definition(
|
||||
&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 = PostTableDefinitionSvc(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/GetProfileTree" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetProfileTreeSvc<T: TableDefinition>(pub Arc<T>);
|
||||
impl<
|
||||
T: TableDefinition,
|
||||
> tonic::server::UnaryService<super::super::common::Empty>
|
||||
for GetProfileTreeSvc<T> {
|
||||
type Response = super::ProfileTreeResponse;
|
||||
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 TableDefinition>::get_profile_tree(&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 = GetProfileTreeSvc(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 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 TableDefinitionServer<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_definition.TableDefinition";
|
||||
impl<T> tonic::server::NamedService for TableDefinitionServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
}
|
||||
323
common/src/proto/komp_ac.table_script.rs
Normal file
323
common/src/proto/komp_ac.table_script.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
336
common/src/proto/komp_ac.table_structure.rs
Normal file
336
common/src/proto/komp_ac.table_structure.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
794
common/src/proto/komp_ac.tables_data.rs
Normal file
794
common/src/proto/komp_ac.tables_data.rs
Normal file
@@ -0,0 +1,794 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
712
common/src/proto/komp_ac.uctovnictvo.rs
Normal file
712
common/src/proto/komp_ac.uctovnictvo.rs
Normal file
@@ -0,0 +1,712 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -225,11 +225,11 @@ pub mod adresar_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.adresar.Adresar/PostAdresar",
|
||||
"/komp_ac.adresar.Adresar/PostAdresar",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "PostAdresar"));
|
||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PostAdresar"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_adresar(
|
||||
@@ -249,11 +249,11 @@ pub mod adresar_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.adresar.Adresar/GetAdresar",
|
||||
"/komp_ac.adresar.Adresar/GetAdresar",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresar"));
|
||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresar"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn put_adresar(
|
||||
@@ -273,11 +273,11 @@ pub mod adresar_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.adresar.Adresar/PutAdresar",
|
||||
"/komp_ac.adresar.Adresar/PutAdresar",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "PutAdresar"));
|
||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PutAdresar"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn delete_adresar(
|
||||
@@ -297,11 +297,11 @@ pub mod adresar_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.adresar.Adresar/DeleteAdresar",
|
||||
"/komp_ac.adresar.Adresar/DeleteAdresar",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "DeleteAdresar"));
|
||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "DeleteAdresar"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_adresar_count(
|
||||
@@ -321,11 +321,11 @@ pub mod adresar_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.adresar.Adresar/GetAdresarCount",
|
||||
"/komp_ac.adresar.Adresar/GetAdresarCount",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresarCount"));
|
||||
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarCount"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_adresar_by_position(
|
||||
@@ -345,12 +345,12 @@ pub mod adresar_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.adresar.Adresar/GetAdresarByPosition",
|
||||
"/komp_ac.adresar.Adresar/GetAdresarByPosition",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresarByPosition"),
|
||||
GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarByPosition"),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
@@ -476,7 +476,7 @@ pub mod adresar_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/multieko2.adresar.Adresar/PostAdresar" => {
|
||||
"/komp_ac.adresar.Adresar/PostAdresar" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PostAdresarSvc<T: Adresar>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -521,7 +521,7 @@ pub mod adresar_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.adresar.Adresar/GetAdresar" => {
|
||||
"/komp_ac.adresar.Adresar/GetAdresar" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetAdresarSvc<T: Adresar>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -566,7 +566,7 @@ pub mod adresar_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.adresar.Adresar/PutAdresar" => {
|
||||
"/komp_ac.adresar.Adresar/PutAdresar" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PutAdresarSvc<T: Adresar>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -611,7 +611,7 @@ pub mod adresar_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.adresar.Adresar/DeleteAdresar" => {
|
||||
"/komp_ac.adresar.Adresar/DeleteAdresar" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct DeleteAdresarSvc<T: Adresar>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -656,7 +656,7 @@ pub mod adresar_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.adresar.Adresar/GetAdresarCount" => {
|
||||
"/komp_ac.adresar.Adresar/GetAdresarCount" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetAdresarCountSvc<T: Adresar>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -701,7 +701,7 @@ pub mod adresar_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.adresar.Adresar/GetAdresarByPosition" => {
|
||||
"/komp_ac.adresar.Adresar/GetAdresarByPosition" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetAdresarByPositionSvc<T: Adresar>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -784,7 +784,7 @@ pub mod adresar_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "multieko2.adresar.Adresar";
|
||||
pub const SERVICE_NAME: &str = "komp_ac.adresar.Adresar";
|
||||
impl<T> tonic::server::NamedService for AdresarServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -160,11 +160,11 @@ pub mod auth_service_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.auth.AuthService/Register",
|
||||
"/komp_ac.auth.AuthService/Register",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Register"));
|
||||
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Register"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn login(
|
||||
@@ -181,11 +181,11 @@ pub mod auth_service_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.auth.AuthService/Login",
|
||||
"/komp_ac.auth.AuthService/Login",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Login"));
|
||||
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Login"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
@@ -288,7 +288,7 @@ pub mod auth_service_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/multieko2.auth.AuthService/Register" => {
|
||||
"/komp_ac.auth.AuthService/Register" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct RegisterSvc<T: AuthService>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -333,7 +333,7 @@ pub mod auth_service_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.auth.AuthService/Login" => {
|
||||
"/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>
|
||||
@@ -411,7 +411,7 @@ pub mod auth_service_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "multieko2.auth.AuthService";
|
||||
pub const SERVICE_NAME: &str = "komp_ac.auth.AuthService";
|
||||
impl<T> tonic::server::NamedService for AuthServiceServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -129,11 +129,11 @@ pub mod searcher_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.search.Searcher/SearchTable",
|
||||
"/komp_ac.search.Searcher/SearchTable",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("multieko2.search.Searcher", "SearchTable"));
|
||||
.insert(GrpcMethod::new("komp_ac.search.Searcher", "SearchTable"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
@@ -232,7 +232,7 @@ pub mod searcher_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/multieko2.search.Searcher/SearchTable" => {
|
||||
"/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>
|
||||
@@ -310,7 +310,7 @@ pub mod searcher_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "multieko2.search.Searcher";
|
||||
pub const SERVICE_NAME: &str = "komp_ac.search.Searcher";
|
||||
impl<T> tonic::server::NamedService for SearcherServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -179,13 +179,13 @@ pub mod table_definition_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.table_definition.TableDefinition/PostTableDefinition",
|
||||
"/komp_ac.table_definition.TableDefinition/PostTableDefinition",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.table_definition.TableDefinition",
|
||||
"komp_ac.table_definition.TableDefinition",
|
||||
"PostTableDefinition",
|
||||
),
|
||||
);
|
||||
@@ -208,13 +208,13 @@ pub mod table_definition_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.table_definition.TableDefinition/GetProfileTree",
|
||||
"/komp_ac.table_definition.TableDefinition/GetProfileTree",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.table_definition.TableDefinition",
|
||||
"komp_ac.table_definition.TableDefinition",
|
||||
"GetProfileTree",
|
||||
),
|
||||
);
|
||||
@@ -237,13 +237,13 @@ pub mod table_definition_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.table_definition.TableDefinition/DeleteTable",
|
||||
"/komp_ac.table_definition.TableDefinition/DeleteTable",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.table_definition.TableDefinition",
|
||||
"komp_ac.table_definition.TableDefinition",
|
||||
"DeleteTable",
|
||||
),
|
||||
);
|
||||
@@ -362,7 +362,7 @@ pub mod table_definition_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/multieko2.table_definition.TableDefinition/PostTableDefinition" => {
|
||||
"/komp_ac.table_definition.TableDefinition/PostTableDefinition" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PostTableDefinitionSvc<T: TableDefinition>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -411,7 +411,7 @@ pub mod table_definition_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.table_definition.TableDefinition/GetProfileTree" => {
|
||||
"/komp_ac.table_definition.TableDefinition/GetProfileTree" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetProfileTreeSvc<T: TableDefinition>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -457,7 +457,7 @@ pub mod table_definition_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.table_definition.TableDefinition/DeleteTable" => {
|
||||
"/komp_ac.table_definition.TableDefinition/DeleteTable" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct DeleteTableSvc<T: TableDefinition>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -537,7 +537,7 @@ pub mod table_definition_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "multieko2.table_definition.TableDefinition";
|
||||
pub const SERVICE_NAME: &str = "komp_ac.table_definition.TableDefinition";
|
||||
impl<T> tonic::server::NamedService for TableDefinitionServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -125,13 +125,13 @@ pub mod table_script_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.table_script.TableScript/PostTableScript",
|
||||
"/komp_ac.table_script.TableScript/PostTableScript",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.table_script.TableScript",
|
||||
"komp_ac.table_script.TableScript",
|
||||
"PostTableScript",
|
||||
),
|
||||
);
|
||||
@@ -236,7 +236,7 @@ pub mod table_script_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/multieko2.table_script.TableScript/PostTableScript" => {
|
||||
"/komp_ac.table_script.TableScript/PostTableScript" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PostTableScriptSvc<T: TableScript>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -316,7 +316,7 @@ pub mod table_script_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "multieko2.table_script.TableScript";
|
||||
pub const SERVICE_NAME: &str = "komp_ac.table_script.TableScript";
|
||||
impl<T> tonic::server::NamedService for TableScriptServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -133,13 +133,13 @@ pub mod table_structure_service_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.table_structure.TableStructureService/GetTableStructure",
|
||||
"/komp_ac.table_structure.TableStructureService/GetTableStructure",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.table_structure.TableStructureService",
|
||||
"komp_ac.table_structure.TableStructureService",
|
||||
"GetTableStructure",
|
||||
),
|
||||
);
|
||||
@@ -245,7 +245,7 @@ pub mod table_structure_service_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/multieko2.table_structure.TableStructureService/GetTableStructure" => {
|
||||
"/komp_ac.table_structure.TableStructureService/GetTableStructure" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetTableStructureSvc<T: TableStructureService>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -329,7 +329,7 @@ pub mod table_structure_service_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "multieko2.table_structure.TableStructureService";
|
||||
pub const SERVICE_NAME: &str = "komp_ac.table_structure.TableStructureService";
|
||||
impl<T> tonic::server::NamedService for TableStructureServiceServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -198,12 +198,12 @@ pub mod tables_data_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.tables_data.TablesData/PostTableData",
|
||||
"/komp_ac.tables_data.TablesData/PostTableData",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("multieko2.tables_data.TablesData", "PostTableData"),
|
||||
GrpcMethod::new("komp_ac.tables_data.TablesData", "PostTableData"),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
@@ -224,12 +224,12 @@ pub mod tables_data_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.tables_data.TablesData/PutTableData",
|
||||
"/komp_ac.tables_data.TablesData/PutTableData",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("multieko2.tables_data.TablesData", "PutTableData"),
|
||||
GrpcMethod::new("komp_ac.tables_data.TablesData", "PutTableData"),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
@@ -250,13 +250,13 @@ pub mod tables_data_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.tables_data.TablesData/DeleteTableData",
|
||||
"/komp_ac.tables_data.TablesData/DeleteTableData",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.tables_data.TablesData",
|
||||
"komp_ac.tables_data.TablesData",
|
||||
"DeleteTableData",
|
||||
),
|
||||
);
|
||||
@@ -279,12 +279,12 @@ pub mod tables_data_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.tables_data.TablesData/GetTableData",
|
||||
"/komp_ac.tables_data.TablesData/GetTableData",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("multieko2.tables_data.TablesData", "GetTableData"),
|
||||
GrpcMethod::new("komp_ac.tables_data.TablesData", "GetTableData"),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
@@ -305,13 +305,13 @@ pub mod tables_data_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.tables_data.TablesData/GetTableDataCount",
|
||||
"/komp_ac.tables_data.TablesData/GetTableDataCount",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.tables_data.TablesData",
|
||||
"komp_ac.tables_data.TablesData",
|
||||
"GetTableDataCount",
|
||||
),
|
||||
);
|
||||
@@ -334,13 +334,13 @@ pub mod tables_data_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.tables_data.TablesData/GetTableDataByPosition",
|
||||
"/komp_ac.tables_data.TablesData/GetTableDataByPosition",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.tables_data.TablesData",
|
||||
"komp_ac.tables_data.TablesData",
|
||||
"GetTableDataByPosition",
|
||||
),
|
||||
);
|
||||
@@ -480,7 +480,7 @@ pub mod tables_data_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/multieko2.tables_data.TablesData/PostTableData" => {
|
||||
"/komp_ac.tables_data.TablesData/PostTableData" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PostTableDataSvc<T: TablesData>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -525,7 +525,7 @@ pub mod tables_data_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.tables_data.TablesData/PutTableData" => {
|
||||
"/komp_ac.tables_data.TablesData/PutTableData" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PutTableDataSvc<T: TablesData>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -570,7 +570,7 @@ pub mod tables_data_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.tables_data.TablesData/DeleteTableData" => {
|
||||
"/komp_ac.tables_data.TablesData/DeleteTableData" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct DeleteTableDataSvc<T: TablesData>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -615,7 +615,7 @@ pub mod tables_data_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.tables_data.TablesData/GetTableData" => {
|
||||
"/komp_ac.tables_data.TablesData/GetTableData" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetTableDataSvc<T: TablesData>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -660,7 +660,7 @@ pub mod tables_data_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.tables_data.TablesData/GetTableDataCount" => {
|
||||
"/komp_ac.tables_data.TablesData/GetTableDataCount" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetTableDataCountSvc<T: TablesData>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -706,7 +706,7 @@ pub mod tables_data_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.tables_data.TablesData/GetTableDataByPosition" => {
|
||||
"/komp_ac.tables_data.TablesData/GetTableDataByPosition" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetTableDataByPositionSvc<T: TablesData>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -790,7 +790,7 @@ pub mod tables_data_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "multieko2.tables_data.TablesData";
|
||||
pub const SERVICE_NAME: &str = "komp_ac.tables_data.TablesData";
|
||||
impl<T> tonic::server::NamedService for TablesDataServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -192,13 +192,13 @@ pub mod uctovnictvo_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/PostUctovnictvo",
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.uctovnictvo.Uctovnictvo",
|
||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
||||
"PostUctovnictvo",
|
||||
),
|
||||
);
|
||||
@@ -221,13 +221,13 @@ pub mod uctovnictvo_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvo",
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.uctovnictvo.Uctovnictvo",
|
||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
||||
"GetUctovnictvo",
|
||||
),
|
||||
);
|
||||
@@ -250,13 +250,13 @@ pub mod uctovnictvo_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.uctovnictvo.Uctovnictvo",
|
||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
||||
"GetUctovnictvoCount",
|
||||
),
|
||||
);
|
||||
@@ -279,13 +279,13 @@ pub mod uctovnictvo_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.uctovnictvo.Uctovnictvo",
|
||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
||||
"GetUctovnictvoByPosition",
|
||||
),
|
||||
);
|
||||
@@ -308,13 +308,13 @@ pub mod uctovnictvo_client {
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/PutUctovnictvo",
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new(
|
||||
"multieko2.uctovnictvo.Uctovnictvo",
|
||||
"komp_ac.uctovnictvo.Uctovnictvo",
|
||||
"PutUctovnictvo",
|
||||
),
|
||||
);
|
||||
@@ -447,7 +447,7 @@ pub mod uctovnictvo_server {
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/PostUctovnictvo" => {
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PostUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -492,7 +492,7 @@ pub mod uctovnictvo_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvo" => {
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -537,7 +537,7 @@ pub mod uctovnictvo_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoCount" => {
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetUctovnictvoCountSvc<T: Uctovnictvo>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -583,7 +583,7 @@ pub mod uctovnictvo_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition" => {
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetUctovnictvoByPositionSvc<T: Uctovnictvo>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -634,7 +634,7 @@ pub mod uctovnictvo_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/multieko2.uctovnictvo.Uctovnictvo/PutUctovnictvo" => {
|
||||
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct PutUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
|
||||
impl<
|
||||
@@ -714,7 +714,7 @@ pub mod uctovnictvo_server {
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "multieko2.uctovnictvo.Uctovnictvo";
|
||||
pub const SERVICE_NAME: &str = "komp_ac.uctovnictvo.Uctovnictvo";
|
||||
impl<T> tonic::server::NamedService for UctovnictvoServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ feature-depth = 1
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2024-0014", reason = "generational-arena is archived but no safe upgrade path exists; accepted for now" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste is archived; no immediate alternative in dependency tree; accepted for now" },
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
@@ -100,6 +102,7 @@ allow = [
|
||||
"HPND",
|
||||
"ISC",
|
||||
"LGPL-3.0",
|
||||
"AGPL-3.0",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"NCSA",
|
||||
|
||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1753549186,
|
||||
"narHash": "sha256-Znl7rzuxKg/Mdm6AhimcKynM7V3YeNDIcLjBuoBcmNs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "17f6bd177404d6d43017595c5264756764444ab8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
52
flake.nix
Normal file
52
flake.nix
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
description = "Komp AC - Kompress Accounting";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# Rust toolchain
|
||||
rustc
|
||||
cargo
|
||||
rustfmt
|
||||
clippy
|
||||
cargo-watch
|
||||
|
||||
# C build tools (for your linker issue)
|
||||
gcc
|
||||
binutils
|
||||
pkg-config
|
||||
|
||||
# OpenSSL for crypto dependencies
|
||||
openssl
|
||||
openssl.dev
|
||||
|
||||
# PostgreSQL for sqlx
|
||||
postgresql
|
||||
sqlx-cli
|
||||
|
||||
# Protocol Buffers compiler for gRPC
|
||||
protobuf
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export OPENSSL_DIR="${pkgs.openssl.dev}"
|
||||
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
|
||||
export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include"
|
||||
echo "🦀 Rust development environment loaded"
|
||||
echo "OpenSSL and PostgreSQL available"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -11,11 +11,11 @@ use tantivy::schema::{IndexRecordOption, Value};
|
||||
use tantivy::{Index, TantivyDocument, Term};
|
||||
use tonic::{Request, Response, Status};
|
||||
|
||||
use common::proto::multieko2::search::{
|
||||
use common::proto::komp_ac::search::{
|
||||
search_response::Hit, SearchRequest, SearchResponse,
|
||||
};
|
||||
pub use common::proto::multieko2::search::searcher_server::SearcherServer;
|
||||
use common::proto::multieko2::search::searcher_server::Searcher;
|
||||
pub use common::proto::komp_ac::search::searcher_server::SearcherServer;
|
||||
use common::proto::komp_ac::search::searcher_server::Searcher;
|
||||
use common::search::register_slovak_tokenizers;
|
||||
use sqlx::{PgPool, Row};
|
||||
use tracing::info;
|
||||
|
||||
@@ -22,19 +22,23 @@ tonic = "0.13.0"
|
||||
tonic-reflection = "0.13.0"
|
||||
tracing = "0.1.41"
|
||||
time = { version = "0.3.41", features = ["local-offset"] }
|
||||
steel-derive = { git = "https://github.com/mattwparas/steel.git", branch = "master", package = "steel-derive" }
|
||||
steel-core = { git = "https://github.com/mattwparas/steel.git", version = "0.6.0", features = ["anyhow", "dylibs", "sync"] }
|
||||
thiserror = "2.0.12"
|
||||
|
||||
steel-derive = "0.6.0"
|
||||
steel-core = { version = "0.7.0", features = ["anyhow", "dylibs", "sync"] }
|
||||
|
||||
dashmap = "6.1.0"
|
||||
lazy_static = "1.5.0"
|
||||
regex = "1.11.1"
|
||||
bcrypt = "0.17.0"
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
uuid = { version = "1.16.0", features = ["serde", "v4"] }
|
||||
jsonwebtoken = "9.3.1"
|
||||
rust-stemmers = "1.2.0"
|
||||
rust_decimal = "1.37.2"
|
||||
rust_decimal_macros = "1.37.1"
|
||||
|
||||
rust_decimal = { workspace = true }
|
||||
rust_decimal_macros = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
steel-decimal = "1.0.0"
|
||||
|
||||
[lib]
|
||||
name = "server"
|
||||
@@ -46,3 +50,4 @@ rstest = "0.25.0"
|
||||
lazy_static = "1.5.0"
|
||||
rand = "0.9.1"
|
||||
futures = "0.3.31"
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
14
server/docs-prod/book.toml
Normal file
14
server/docs-prod/book.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[book]
|
||||
authors = ["Priec"]
|
||||
language = "en"
|
||||
src = "src"
|
||||
title = "Server API Documentation"
|
||||
|
||||
[output.html.search]
|
||||
enable = true
|
||||
limit-results = 30
|
||||
teaser-word-count = 30
|
||||
use-boolean-and = true
|
||||
boost-title = 2
|
||||
boost-hierarchy = 1
|
||||
boost-paragraph = 1
|
||||
1
server/docs-prod/book/.nojekyll
Normal file
1
server/docs-prod/book/.nojekyll
Normal file
@@ -0,0 +1 @@
|
||||
This file makes sure that Github Pages doesn't process mdBook's output.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user