Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d50e50f
feat(adaptive_card): add horizontal alignment, separator, and spacing…
mistastn Dec 18, 2025
613c888
chore: update dependencies and toolchain to latest versions
mistastn Dec 18, 2025
2c25b27
chore: add .idea to gitignore
mistastn Dec 18, 2025
17fa880
refactor: fix high-priority code quality and security issues
mistastn Dec 18, 2025
86fda0a
chore: optimize dependencies and reduce binary footprint
mistastn Dec 18, 2025
fd6b68b
refactor: consolidate error handling and add comprehensive documentation
mistastn Dec 18, 2025
d64b68d
refactor: clean up verbose error logging
mistastn Dec 18, 2025
bc2cb8f
test: add 29 new unit tests for easy-to-test functions
mistastn Dec 18, 2025
02518f9
refactor(adaptive_card): split monolithic file into modular structure
mistastn Dec 18, 2025
e6fcdfa
refactor(types): split monolithic file into modular structure
mistastn Dec 18, 2025
39f3a00
refactor(client): split lib.rs into client module structure
mistastn Dec 18, 2025
1f7a26d
fix: restore Clone trait to Webex struct for backward compatibility
mistastn Dec 18, 2025
3f96e96
ci: improve GitHub Actions workflow with comprehensive checks
mistastn Dec 18, 2025
c890f06
chore: bump version to 0.11.0 and update documentation
mistastn Dec 18, 2025
bc1329f
fix: resolve all clippy warnings for strict pedantic and nursery lints
mistastn Dec 18, 2025
aebbbd5
fix: restore context-aware error logging lost during refactoring
mistastn Dec 19, 2025
2dd6b44
style: fix cargo fmt formatting issues
mistastn Dec 19, 2025
c5dc0b8
feat: add pre-commit hook for automatic code formatting
mistastn Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,47 @@ on:

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

jobs:
build:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: cargo test --verbose

clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run clippy (warnings only)
run: cargo clippy --all-targets --all-features

fmt:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check formatting
run: cargo fmt -- --check

build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Build crate
run: cargo build --verbose
- name: Buld examples
run: cargo build --examples
- name: Build examples
run: cargo build --examples --verbose

doc:
name: Documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check documentation
run: cargo doc --no-deps --document-private-items
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@

Cargo.lock
.claude/settings.local.json

# IDE
.idea/
34 changes: 26 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,21 @@ This is `webex-rust`, an asynchronous Rust library providing a minimal interface
- `cargo clippy --all-targets --all-features` - Full clippy check
- `cargo build --all-targets` - Build everything including examples

### Git Hooks
- `./hooks/install.sh` - Install pre-commit hooks that automatically run cargo fmt
- Pre-commit hook ensures code is formatted before each commit

## Architecture

### Core Components

- **`Webex` struct** (`src/lib.rs:92-100`) - Main API client with token-based authentication
- **`WebexEventStream`** (`src/lib.rs:102-108`) - WebSocket event stream handler for real-time events
- **`RestClient`** (`src/lib.rs:247-251`) - Low-level HTTP client wrapper
- **Types module** (`src/types.rs`) - All API data structures and serialization
- **AdaptiveCard module** (`src/adaptive_card.rs`) - Support for interactive cards
- **Auth module** (`src/auth.rs`) - Device authentication flows
- **`Webex` struct** (`src/client/mod.rs`) - Main API client with token-based authentication
- **`WebexEventStream`** (`src/client/websocket.rs`) - WebSocket event stream handler for real-time events
- **`RestClient`** (`src/client/rest.rs`) - Low-level HTTP client wrapper with flexible authentication
- **Client module** (`src/client/`) - Client implementation split into modular components
- **Types module** (`src/types/`) - All API data structures organized by resource type
- **AdaptiveCard module** (`src/adaptive_card/`) - Support for interactive cards with builders
- **Auth module** (`src/auth.rs`) - Device authentication flows (OAuth device grant)
- **Error module** (`src/error.rs`) - Comprehensive error handling

### Key Patterns
Expand All @@ -54,8 +59,21 @@ This is `webex-rust`, an asynchronous Rust library providing a minimal interface

## Important Notes

- Uses Rust 1.76 toolchain (see `rust-toolchain.toml`)
- Uses Rust 1.92 toolchain (see `rust-toolchain.toml`)
- Very strict clippy configuration with pedantic and nursery lints enabled
- All public APIs must have documentation (`#![deny(missing_docs)]`)
- WebSocket connections require device registration and token authentication
- Mercury URL caching reduces API calls for device discovery
- Mercury URL caching reduces API calls for device discovery
- Comprehensive CI workflow with tests, clippy, fmt, build, and doc checks
- Git pre-commit hooks available in `hooks/` directory to auto-format code

## Recent Refactoring (v0.11.0)

- **Module organization**: Refactored large files into logical modules
- `src/lib.rs` reduced from 1532 lines to 54 lines (thin orchestrator)
- `src/client/` module split into `mod.rs`, `rest.rs`, and `websocket.rs`
- `src/types/` module organized by resource type (message, room, person, etc.)
- `src/adaptive_card/` module split into elements, containers, and styles
- **Backward compatibility**: All public APIs maintained, including Clone trait on Webex struct
- **Test coverage**: 37 unit tests ensuring functionality after refactoring
- **Documentation**: Fixed broken doc links, cargo doc builds with zero warnings
11 changes: 5 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "webex"
version = "0.10.0"
version = "0.11.0"
authors = [
"Scott Hutton <shutton@pobox.com>",
"Milan Stastny <milan@stastnej.ch>",
Expand All @@ -20,12 +20,11 @@ futures = "0.3.30"
futures-util = "0.3.30"
log = "0.4"
serde_json = "1.0"
tungstenite = "0.23.0"
tungstenite = "0.28"
url = "2.5"
lazy_static = "1.5.0"
serde_html_form = "0.2.6"
serde_with = { version = "3.9.0", features = ["macros"] }
thiserror = "1.0.63"
thiserror = "2.0"
reqwest = { version = "0.12.5", features = ["json"] }

[dependencies.chrono]
Expand All @@ -38,10 +37,10 @@ features = ["derive"]

[dependencies.tokio]
version = "1.39"
features = ["full"]
features = ["macros", "net", "time", "rt-multi-thread"]

[dependencies.tokio-tungstenite]
version = "0.23.1"
version = "0.28"
features = ["connect", "native-tls"]

[dependencies.uuid]
Expand Down
6 changes: 3 additions & 3 deletions examples/adaptivecard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,10 @@ async fn respond_to_message(webex: &webex::Webex, config: &Config, event: &webex
// Send event card
reply.text = Some("Welcome to Adaptivecard Tester Bot".into());
let mut body = CardElement::container();
body.add_element(CardElement::text_block(
let _ = body.add_element(CardElement::text_block(
"Welcome to Adaptivecard Tester Bot!",
));
body.add_element(
let _ = body.add_element(
CardElement::column_set()
.add_column(
webex::adaptive_card::Column::new()
Expand All @@ -154,7 +154,7 @@ async fn respond_to_message(webex: &webex::Webex, config: &Config, event: &webex
.add_element(CardElement::input_text("input2", None::<&'static str>)),
),
);
body.add_element(CardElement::action_set().add_action_to_set(
let _ = body.add_element(CardElement::action_set().add_action_to_set(
webex::adaptive_card::Action::Submit {
data: Some(HashMap::from([("id".into(), "init".into())])),
title: Some("Submit".into()),
Expand Down
50 changes: 50 additions & 0 deletions hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Git Hooks for webex-rust

This directory contains git hooks to maintain code quality and consistency.

## Available Hooks

### pre-commit

Automatically runs `cargo fmt` before each commit to ensure all code is properly formatted.

**What it does:**
- Checks if code formatting is required
- Runs `cargo fmt --all` if needed
- Automatically adds formatted files to the commit
- Prevents commits with formatting issues

## Installation

To install the hooks, run:

```bash
./hooks/install.sh
```

Or manually:

```bash
cp hooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
```

## Uninstalling

To remove the hooks:

```bash
rm .git/hooks/pre-commit
```

## Bypassing Hooks

If you need to bypass the hooks temporarily (not recommended):

```bash
git commit --no-verify
```

## CI/CD

The CI pipeline in `.github/workflows/` runs the same checks, so even if hooks are bypassed locally, the CI will catch formatting issues.
23 changes: 23 additions & 0 deletions hooks/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh
# Install git hooks for webex-rust development

set -e

HOOKS_DIR="$(cd "$(dirname "$0")" && pwd)"
GIT_HOOKS_DIR="$(git rev-parse --git-dir)/hooks"

echo "Installing git hooks..."

# Install pre-commit hook
if [ -f "$HOOKS_DIR/pre-commit" ]; then
cp "$HOOKS_DIR/pre-commit" "$GIT_HOOKS_DIR/pre-commit"
chmod +x "$GIT_HOOKS_DIR/pre-commit"
echo "✓ Installed pre-commit hook"
else
echo "✗ pre-commit hook not found"
exit 1
fi

echo ""
echo "Git hooks installed successfully!"
echo "The pre-commit hook will automatically format your code with 'cargo fmt' before each commit."
21 changes: 21 additions & 0 deletions hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/sh
# Pre-commit hook for webex-rust
# Automatically formats Rust code with cargo fmt before committing

set -e

echo "Running cargo fmt..."

# Run cargo fmt on all Rust files
if ! cargo fmt --all --check &>/dev/null; then
echo "Code formatting issues found. Running cargo fmt..."
cargo fmt --all

# Add the formatted files to the commit
git add -u

echo "✓ Code formatted successfully"
fi

echo "✓ Pre-commit checks passed"
exit 0
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[toolchain]
# The default profile includes rustc, rust-std, cargo, rust-docs, rustfmt and clippy.
profile = "default"
channel = "1.88"
channel = "stable"
119 changes: 119 additions & 0 deletions src/adaptive_card/containers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//! Container structures for organizing Adaptive Card content.

use serde::{Deserialize, Serialize};

use super::elements::CardElement;
use super::{Action, ContainerStyle, Spacing, VerticalContentAlignment};

/// Describes a choice for use in a `ChoiceSet`.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Choice {
/// Text to display.
pub title: String,
/// The raw value for the choice. **NOTE:** do not use a , in the value, since a `ChoiceSet` with isMultiSelect set to true returns a comma-delimited string of choice values.
pub value: String,
}

/// Describes a Fact in a `FactSet` as a key/value pair.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Fact {
/// The title of the fact.
pub title: String,
/// The value of the fact.
pub value: String,
}

/// Column in a `ColumnSet`
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Column {
/// The card elements to render inside the Column.
#[serde(default)]
pub items: Vec<CardElement>,
/// An Action that will be invoked when the Column is tapped or selected.
#[serde(rename = "selectAction", skip_serializing_if = "Option::is_none")]
select_action: Option<Action>,
/// Style hint for Column.
#[serde(skip_serializing_if = "Option::is_none")]
style: Option<ContainerStyle>,
/// Defines how the content should be aligned vertically within the column.
#[serde(
rename = "verticalContentAlignment",
skip_serializing_if = "Option::is_none"
)]
vertical_content_alignment: Option<VerticalContentAlignment>,
/// When true, draw a separating line between this column and the previous column.
#[serde(skip_serializing_if = "Option::is_none")]
separator: Option<bool>,
/// Controls the amount of spacing between this column and the preceding column.
#[serde(skip_serializing_if = "Option::is_none")]
spacing: Option<Spacing>,
/// "auto", "stretch", a number representing relative width of the column in the column group, or in version 1.1 and higher, a specific pixel width, like "50px".
#[serde(skip_serializing_if = "Option::is_none")]
width: Option<serde_json::Value>,
/// A unique identifier associated with the item.
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
}

impl From<&Self> for Column {
fn from(item: &Self) -> Self {
item.clone()
}
}

impl From<&mut Self> for Column {
fn from(item: &mut Self) -> Self {
item.clone()
}
}

impl Column {
/// Creates new Column
#[must_use]
pub const fn new() -> Self {
Self {
items: vec![],
select_action: None,
style: None,
vertical_content_alignment: None,
separator: None,
spacing: None,
width: None,
id: None,
}
}

/// Adds element to column
#[must_use]
pub fn add_element(&mut self, item: CardElement) -> Self {
self.items.push(item);
self.into()
}

/// Sets separator
#[must_use]
pub fn set_separator(&mut self, s: bool) -> Self {
self.separator = Some(s);
self.into()
}

/// Sets `VerticalContentAlignment`
#[must_use]
pub fn set_vertical_alignment(&mut self, s: VerticalContentAlignment) -> Self {
self.vertical_content_alignment = Some(s);
self.into()
}

/// Sets width
#[must_use]
pub fn set_width<T: Into<String>>(&mut self, s: T) -> Self {
self.width = Some(serde_json::Value::String(s.into()));
self.into()
}
}

impl Default for Column {
fn default() -> Self {
Self::new()
}
}
Loading