Compare commits
57 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 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@
|
||||
.env
|
||||
/tantivy_indexes
|
||||
server/tantivy_indexes
|
||||
steel_decimal/tests/property_tests.proptest-regressions
|
||||
.direnv/
|
||||
|
||||
911
Cargo.lock
generated
911
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"
|
||||
|
||||
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()
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 canvas::CanvasState;
|
||||
use crate::state::pages::form::FormState;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||
@@ -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,17 +1,17 @@
|
||||
// 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
|
||||
@@ -22,7 +22,7 @@ use common::proto::multieko2::tables_data::{
|
||||
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
||||
PutTableDataResponse,
|
||||
};
|
||||
use common::proto::multieko2::search::{
|
||||
use common::proto::komp_ac::search::{
|
||||
searcher_client::SearcherClient, SearchRequest, SearchResponse,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 = { version = "1.37.2", features = ["maths", "serde"] }
|
||||
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.
|
||||
216
server/docs-prod/book/404.html
Normal file
216
server/docs-prod/book/404.html
Normal file
@@ -0,0 +1,216 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en" class="light sidebar-visible" dir="ltr">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>Page not found - Server API Documentation</title>
|
||||
<base href="/">
|
||||
|
||||
|
||||
<!-- Custom HTML head -->
|
||||
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="icon" href="favicon.svg">
|
||||
<link rel="shortcut icon" href="favicon.png">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/general.css">
|
||||
<link rel="stylesheet" href="css/chrome.css">
|
||||
<link rel="stylesheet" href="css/print.css" media="print">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||
<link rel="stylesheet" href="fonts/fonts.css">
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" id="highlight-css" href="highlight.css">
|
||||
<link rel="stylesheet" id="tomorrow-night-css" href="tomorrow-night.css">
|
||||
<link rel="stylesheet" id="ayu-highlight-css" href="ayu-highlight.css">
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
|
||||
|
||||
<!-- Provide site root and default themes to javascript -->
|
||||
<script>
|
||||
const path_to_root = "";
|
||||
const default_light_theme = "light";
|
||||
const default_dark_theme = "navy";
|
||||
</script>
|
||||
<!-- Start loading toc.js asap -->
|
||||
<script src="toc.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="body-container">
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script>
|
||||
try {
|
||||
let theme = localStorage.getItem('mdbook-theme');
|
||||
let sidebar = localStorage.getItem('mdbook-sidebar');
|
||||
|
||||
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||
}
|
||||
|
||||
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||
}
|
||||
} catch (e) { }
|
||||
</script>
|
||||
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script>
|
||||
const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
|
||||
let theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
const html = document.documentElement;
|
||||
html.classList.remove('light')
|
||||
html.classList.add(theme);
|
||||
html.classList.add("js");
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
let sidebar = null;
|
||||
const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
} else {
|
||||
sidebar = 'hidden';
|
||||
}
|
||||
sidebar_toggle.checked = sidebar === 'visible';
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<!-- populated by js -->
|
||||
<mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
|
||||
<noscript>
|
||||
<iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
|
||||
</noscript>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
<div id="menu-bar-hover-placeholder"></div>
|
||||
<div id="menu-bar" class="menu-bar sticky">
|
||||
<div class="left-buttons">
|
||||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</label>
|
||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
</ul>
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">Server API Documentation</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="search-wrapper" class="hidden">
|
||||
<form id="searchbar-outer" class="searchbar-outer">
|
||||
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||
</form>
|
||||
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||
<div id="searchresults-header" class="searchresults-header"></div>
|
||||
<ul id="searchresults">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||
<script>
|
||||
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="content" class="content">
|
||||
<main>
|
||||
<h1 id="document-not-found-404"><a class="header" href="#document-not-found-404">Document not found (404)</a></h1>
|
||||
<p>This URL is invalid, sorry. Please use the navigation bar or search to continue.</p>
|
||||
|
||||
</main>
|
||||
|
||||
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||
<!-- Mobile navigation buttons -->
|
||||
|
||||
|
||||
<div style="clear: both"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
<script>
|
||||
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsAddress = wsProtocol + "//" + location.host + "/" + "__livereload";
|
||||
const socket = new WebSocket(wsAddress);
|
||||
socket.onmessage = function (event) {
|
||||
if (event.data === "reload") {
|
||||
socket.close();
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
window.onbeforeunload = function() {
|
||||
socket.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
window.playground_copyable = true;
|
||||
</script>
|
||||
|
||||
|
||||
<script src="elasticlunr.min.js"></script>
|
||||
<script src="mark.min.js"></script>
|
||||
<script src="searcher.js"></script>
|
||||
|
||||
<script src="clipboard.min.js"></script>
|
||||
<script src="highlight.js"></script>
|
||||
<script src="book.js"></script>
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
4
server/docs-prod/book/FontAwesome/css/font-awesome.css
vendored
Normal file
4
server/docs-prod/book/FontAwesome/css/font-awesome.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
server/docs-prod/book/FontAwesome/fonts/FontAwesome.ttf
Normal file
BIN
server/docs-prod/book/FontAwesome/fonts/FontAwesome.ttf
Normal file
Binary file not shown.
BIN
server/docs-prod/book/FontAwesome/fonts/fontawesome-webfont.eot
Normal file
BIN
server/docs-prod/book/FontAwesome/fonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
2671
server/docs-prod/book/FontAwesome/fonts/fontawesome-webfont.svg
Normal file
2671
server/docs-prod/book/FontAwesome/fonts/fontawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 434 KiB |
BIN
server/docs-prod/book/FontAwesome/fonts/fontawesome-webfont.ttf
Normal file
BIN
server/docs-prod/book/FontAwesome/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
server/docs-prod/book/FontAwesome/fonts/fontawesome-webfont.woff
Normal file
BIN
server/docs-prod/book/FontAwesome/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user