# plcbundle-rs Coding Rules
## Core Design Principles
1. **Single Entry Point**: All operations go through `BundleManager`
2. **Options Pattern**: Complex operations use dedicated option structs
3. **Result Types**: Operations return structured result types, not raw tuples
4. **Streaming by Default**: Use iterators for large datasets
5. **NO DIRECT FILE ACCESS**: CLI commands and server code NEVER open bundle files directly
## Critical Rules
### File Access
- ❌ **NEVER** use `std::fs::File::open` for bundle files in CLI commands or server code
- ❌ **NEVER** use `std::fs::read` for bundle data in CLI commands or server code
- ❌ **NEVER** use `std::fs::remove_file` for bundles in CLI commands or server code
- ❌ **NEVER** access core components (Index, bundle_format, etc.) directly from CLI or server
- ✅ **ALWAYS** use `BundleManager` methods for bundle operations
- ✅ **ALWAYS** add new operations to `BundleManager` API if needed
### API-First Development
- All CLI commands in `src/bin/commands/` must use **ONLY** public `BundleManager` APIs
- All server code must use **ONLY** public `BundleManager` APIs
- If CLI or server code needs file access, add the method to `BundleManager` first
- The `BundleManager` is the single source of truth for all bundle operations
### Examples
**❌ WRONG - Direct file access:**
```rust
let bundle_path = dir.join(format!("{:06}.jsonl.zst", bundle_num));
std::fs::remove_file(&bundle_path)?;
```
**✅ CORRECT - Via BundleManager API:**
```rust
manager.delete_bundle_files(&[bundle_num])?;
```
## Architecture
```
CLI Commands / Server → BundleManager API → Internal Implementation
↓ ↓ ↓
Uses Provides Opens files
Only Public API Directly
```
## When Adding New Features
1. Design the API method in `BundleManager` first
2. Document it in `docs/API.md`
3. Implement the method in `src/manager.rs`
4. Export types in `src/lib.rs` if needed
5. Use the API from CLI command or server
## Reference Documentation
- Full API design: `docs/API.md`
- Bundle format: `docs/BUNDLE_FORMAT.md`
- All public APIs are listed in `docs/API.md` Quick Reference
## Common Patterns
### Loading Bundles
```rust
// ❌ Don't open files directly
let file = File::open(bundle_path)?;
// ✅ Use the API
let result = manager.load_bundle(bundle_num, LoadOptions::default())?;
```
### Getting Operations
```rust
// ❌ Don't parse files directly
let json = std::fs::read_to_string(bundle_path)?;
// ✅ Use the API
let json = manager.get_operation_raw(bundle_num, position)?;
```
### Deleting Bundles
```rust
// ❌ Don't remove files directly
std::fs::remove_file(bundle_path)?;
// ✅ Use the API
manager.delete_bundle_files(&[bundle_num])?;
```
### JSON Parsing
- ✅ **ALWAYS** use `sonic_rs` for JSON parsing (performance-critical)
- ❌ **NEVER** use `serde_json` for parsing JSON from bundles or operations
- Use `sonic_rs::from_str` for parsing JSON strings
- Use `sonic_rs::Value` and `JsonValueTrait` for accessing JSON values
**Examples:**
```rust
// ✅ CORRECT - Use sonic_rs
use sonic_rs::JsonValueTrait;
if let Ok(value) = sonic_rs::from_str::<sonic_rs::Value>(&line) {
if let Some(did) = value.get("did").and_then(|v| v.as_str()) {
// ...
}
}
// ❌ WRONG - Don't use serde_json
use serde_json::Value;
let value: Value = serde_json::from_str(&line)?;
```
## Testing
**IMPORTANT**: Always test compilation with all features enabled.
When implementing features:
- **Always run**: `cargo check --all-features` or `cargo build --all-features` before considering work complete
- This ensures code compiles with both `cli` and `server` features enabled
- It's very common that code compiles with default features but fails with all features
- Fix any compilation errors that appear with all features enabled
If writing actual tests:
- Test through the public API
- Don't test internal implementation details
- CLI tests should use actual `BundleManager` instances
## CLI Output Formatting
### Bundle Numbers in Human-Readable Output
- ✅ **ALWAYS** display bundle numbers **without leading zeros** in CLI output for humans
- Use `{}` format, NOT `{:06}` format
- Example: `"Bundle: 123"` not `"Bundle: 000123"`
- This applies to all `println!`, `eprintln!`, and `log::*!` statements that display bundle numbers to users
- Exception: File paths and internal identifiers may use zero-padded format if needed
**Examples:**
```rust
// ✅ CORRECT - Human-readable output
println!(" Last bundle: {}", bundle_num);
log::info!("Bundle {} processed", bundle_num);
// ❌ WRONG - Don't use leading zeros for humans
println!(" Last bundle: {:06}", bundle_num);
log::info!("Bundle {:06} processed", bundle_num);
```
## Command File Structure
All command files (`src/cli/cmd_*.rs`) must follow a consistent structure:
1. **Imports** - All `use` statements at the top
2. **Command Definitions** - All `#[derive(Args)]`, `#[derive(Subcommand)]` structs and enums
3. **Trait Implementations** - Any `impl` blocks for command structs (e.g., `impl HasGlobalFlags`)
4. **Run Function** - The main `pub fn run()` function
5. **Helper Functions** - All private helper functions at the end
**Exception**: Helper functions that are referenced in struct definitions (e.g., `value_parser = parse_duration`) must be defined before the struct that uses them.
**Examples:**
**✅ CORRECT - Proper structure:**
```rust
use anyhow::Result;
use clap::Args;
use plcbundle::BundleManager;
use std::path::PathBuf;
#[derive(Args)]
#[command(about = "Example command")]
pub struct ExampleCommand {
// ...
}
impl HasGlobalFlags for ExampleCommand {
// ...
}
pub fn run(cmd: ExampleCommand, dir: PathBuf) -> Result<()> {
// ...
}
fn helper_function() {
// ...
}
```
**❌ WRONG - Helper function before command struct:**
```rust
use anyhow::Result;
use clap::Args;
fn helper_function() {
// ...
}
#[derive(Args)]
pub struct ExampleCommand {
// ...
}
```
## Questions?
If you're implementing a feature and need file access:
1. Check if `BundleManager` already has the method
2. If not, add it to `BundleManager` first
3. Update `docs/API.md` with the new method
4. Then use it from the CLI or server
**Remember: The CLI and server are just thin wrappers around `BundleManager`!**