Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 5 additions & 20 deletions rsworkspace/crates/acp-nats/src/acp_prefix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
//! malformed dots (consecutive, leading, trailing). Max 128 bytes. Validity is guaranteed at
//! construction.

use std::sync::Arc;

use crate::constants::MAX_PREFIX_LENGTH;
use crate::nats::token;
use crate::subject_token_violation::SubjectTokenViolation;
use trogon_nats::SubjectTokenViolation;
use trogon_nats::DottedNatsToken;

/// Error returned when [`AcpPrefix`] validation fails.
#[derive(Debug, Clone, PartialEq)]
Expand All @@ -35,28 +32,16 @@ impl std::error::Error for AcpPrefixError {}

/// NATS-safe ACP prefix. Guarantees validity at construction—invalid instances are unrepresentable.
#[derive(Clone, Debug)]
pub struct AcpPrefix(Arc<str>);
pub struct AcpPrefix(DottedNatsToken);

impl AcpPrefix {
pub fn new(s: impl Into<String>) -> Result<Self, AcpPrefixError> {
let s = s.into();
if s.is_empty() {
return Err(AcpPrefixError(SubjectTokenViolation::Empty));
}
if let Some(ch) = token::has_wildcards_or_whitespace(&s) {
return Err(AcpPrefixError(SubjectTokenViolation::InvalidCharacter(ch)));
}
if token::has_consecutive_or_boundary_dots(&s) {
return Err(AcpPrefixError(SubjectTokenViolation::InvalidCharacter('.')));
}
if s.len() > MAX_PREFIX_LENGTH {
return Err(AcpPrefixError(SubjectTokenViolation::TooLong(s.len())));
}
Ok(Self(s.into()))
DottedNatsToken::new(s).map(Self).map_err(AcpPrefixError)
}

pub fn as_str(&self) -> &str {
&self.0
self.0.as_str()
}
}

Expand Down
4 changes: 0 additions & 4 deletions rsworkspace/crates/acp-nats/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ pub const SESSION_READY_DELAY: Duration = Duration::from_millis(100);
pub const PROMPT_TIMEOUT_WARNING_SUPPRESSION_WINDOW: Duration = Duration::from_secs(5);
pub const TEST_PROMPT_TIMEOUT: Duration = Duration::from_secs(5);

pub const MAX_PREFIX_LENGTH: usize = 128;
pub const MAX_SESSION_ID_LENGTH: usize = 128;
pub const MAX_METHOD_NAME_LENGTH: usize = 128;

pub const AGENT_UNAVAILABLE: i32 = -32001;

pub const SESSION_PREFIX: &str = ".session.";
Expand Down
32 changes: 7 additions & 25 deletions rsworkspace/crates/acp-nats/src/ext_method_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
//! rejects `*`, `>`, whitespace; allows dotted namespaces (e.g. `vendor.operation`) but rejects
//! malformed dots (consecutive, leading, trailing). Validity is guaranteed at construction.

use std::sync::Arc;

use crate::constants::MAX_METHOD_NAME_LENGTH;
use crate::nats::token;
use crate::subject_token_violation::SubjectTokenViolation;
use trogon_nats::SubjectTokenViolation;
use trogon_nats::DottedNatsToken;

/// Error returned when [`ExtMethodName`] validation fails.
#[derive(Debug, Clone, PartialEq)]
Expand All @@ -35,32 +32,17 @@ impl std::error::Error for ExtMethodNameError {}
///
/// Rejects empty, too-long, wildcard, whitespace, and malformed dotted names.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ExtMethodName(Arc<str>);
pub struct ExtMethodName(DottedNatsToken);

impl ExtMethodName {
pub fn new(method: impl AsRef<str>) -> Result<Self, ExtMethodNameError> {
let s = method.as_ref();
if s.is_empty() {
return Err(ExtMethodNameError(SubjectTokenViolation::Empty));
}
if s.len() > MAX_METHOD_NAME_LENGTH {
return Err(ExtMethodNameError(SubjectTokenViolation::TooLong(s.len())));
}
if let Some(ch) = token::has_wildcards_or_whitespace(s) {
return Err(ExtMethodNameError(SubjectTokenViolation::InvalidCharacter(
ch,
)));
}
if token::has_consecutive_or_boundary_dots(s) {
return Err(ExtMethodNameError(SubjectTokenViolation::InvalidCharacter(
'.',
)));
}
Ok(Self(s.into()))
DottedNatsToken::new(method)
.map(Self)
.map_err(ExtMethodNameError)
}

pub fn as_str(&self) -> &str {
&self.0
self.0.as_str()
}
}

Expand Down
1 change: 0 additions & 1 deletion rsworkspace/crates/acp-nats/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ pub(crate) mod jsonrpc;
pub mod nats;
pub(crate) mod pending_prompt_waiters;
pub mod session_id;
pub mod subject_token_violation;
pub(crate) mod telemetry;

pub use acp_prefix::{AcpPrefix, AcpPrefixError};
Expand Down
1 change: 0 additions & 1 deletion rsworkspace/crates/acp-nats/src/nats/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
mod extensions;
pub mod parsing;
mod subjects;
pub(crate) mod token;

use serde::Serialize;
use serde::de::DeserializeOwned;
Expand Down
30 changes: 5 additions & 25 deletions rsworkspace/crates/acp-nats/src/session_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@
//! Validation follows [NATS subject naming](https://docs.nats.io/nats-concepts/subjects#characters-allowed-and-recommended-for-subject-names):
//! ASCII only (recommended), rejecting `.` `*` `>` and whitespace (forbidden). Validity is
//! guaranteed at construction.
//!
//! TODO: Consider extracting to `trogon-nats` as a generic `NatsSubject` (or `NatsToken`) type
//! so prefix, session_id, and other subject tokens share the same validation.

use crate::constants::MAX_SESSION_ID_LENGTH;
use crate::subject_token_violation::SubjectTokenViolation;
use trogon_nats::SubjectTokenViolation;
use trogon_nats::NatsToken;

/// Error returned when [`AcpSessionId`] validation fails.
#[derive(Debug, Clone, PartialEq)]
Expand All @@ -36,32 +33,15 @@ impl std::error::Error for SessionIdError {}
/// Follows [NATS subject naming](https://docs.nats.io/nats-concepts/subjects#characters-allowed-and-recommended-for-subject-names):
/// ASCII only; rejects `.`, `*`, `>`, and whitespace. Max 128 characters.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AcpSessionId(std::sync::Arc<str>);
pub struct AcpSessionId(NatsToken);

impl AcpSessionId {
pub fn new(s: impl AsRef<str>) -> Result<Self, SessionIdError> {
let s = s.as_ref();
if s.is_empty() {
return Err(SessionIdError(SubjectTokenViolation::Empty));
}
let mut char_count = 0;
for ch in s.chars() {
char_count += 1;
if char_count > MAX_SESSION_ID_LENGTH {
return Err(SessionIdError(SubjectTokenViolation::TooLong(char_count)));
}
if !ch.is_ascii() {
return Err(SessionIdError(SubjectTokenViolation::InvalidCharacter(ch)));
}
if ch == '.' || ch == '*' || ch == '>' || ch.is_whitespace() {
return Err(SessionIdError(SubjectTokenViolation::InvalidCharacter(ch)));
}
}
Ok(Self(s.into()))
NatsToken::new(s).map(Self).map_err(SessionIdError)
}

pub fn as_str(&self) -> &str {
&self.0
self.0.as_str()
}
}

Expand Down
2 changes: 2 additions & 0 deletions rsworkspace/crates/trogon-nats/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(30);

pub const REQ_ID_HEADER: &str = "X-Req-Id";

pub const MAX_NATS_TOKEN_LENGTH: usize = 128;
5 changes: 5 additions & 0 deletions rsworkspace/crates/trogon-nats/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub mod connect;
pub mod constants;
pub mod jetstream;
pub mod messaging;
pub mod nats_token;
pub mod subject_token_violation;
pub(crate) mod token;

#[cfg(feature = "test-support")]
pub mod mocks;
Expand All @@ -56,6 +59,8 @@ pub use messaging::{
RetryPolicy, build_request_headers, headers_with_trace_context, inject_trace_context, publish,
request, request_with_timeout,
};
pub use nats_token::{DottedNatsToken, NatsToken};
pub use subject_token_violation::SubjectTokenViolation;

#[cfg(feature = "test-support")]
pub use mocks::{AdvancedMockNatsClient, MockNatsClient};
Loading