swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/ra-mcp-domain/src
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-19 15:49:41 -0400
committermain <main@swarm.moe>2026-03-19 15:49:41 -0400
commitfa1bd32800b65aab31ea732dd240261b4047522c (patch)
tree2fd08af6f36b8beb3c7c941990becc1a0a091d62 /crates/ra-mcp-domain/src
downloadadequate-rust-mcp-fa1bd32800b65aab31ea732dd240261b4047522c.zip
Release adequate-rust-mcp 1.0.0v1.0.0
Diffstat (limited to 'crates/ra-mcp-domain/src')
-rw-r--r--crates/ra-mcp-domain/src/fault.rs129
-rw-r--r--crates/ra-mcp-domain/src/lib.rs5
-rw-r--r--crates/ra-mcp-domain/src/lifecycle.rs259
-rw-r--r--crates/ra-mcp-domain/src/types.rs460
4 files changed, 853 insertions, 0 deletions
diff --git a/crates/ra-mcp-domain/src/fault.rs b/crates/ra-mcp-domain/src/fault.rs
new file mode 100644
index 0000000..6d404ab
--- /dev/null
+++ b/crates/ra-mcp-domain/src/fault.rs
@@ -0,0 +1,129 @@
+//! Fault taxonomy and recovery guidance.
+
+use crate::types::Generation;
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
+
+/// Logical fault class.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum FaultClass {
+ /// Underlying I/O or transport channel failure.
+ Transport,
+ /// Child process startup/liveness/exiting failures.
+ Process,
+ /// Malformed or unexpected protocol payloads.
+ Protocol,
+ /// Deadline exceeded.
+ Timeout,
+ /// Internal resource budget exhaustion.
+ Resource,
+}
+
+/// Fine-grained fault code.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum FaultCode {
+ /// Pipe write failed with `EPIPE`.
+ BrokenPipe,
+ /// Pipe reached EOF.
+ UnexpectedEof,
+ /// Child process exited unexpectedly.
+ ChildExited,
+ /// Child process failed to spawn.
+ SpawnFailed,
+ /// Startup sequence exceeded deadline.
+ StartupTimedOut,
+ /// Request exceeded deadline.
+ RequestTimedOut,
+ /// Received an invalid protocol frame.
+ InvalidFrame,
+ /// Received invalid JSON.
+ InvalidJson,
+ /// Response could not be correlated with a pending request.
+ UnknownResponseId,
+}
+
+/// Recovery strategy for a fault.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum RecoveryDirective {
+ /// Retry the request on the same process.
+ RetryInPlace,
+ /// Restart the worker process and retry once.
+ RestartAndReplay,
+ /// Fail-fast and bubble to the caller.
+ AbortRequest,
+}
+
+/// Structured fault event.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Fault {
+ /// Generation in which this fault happened.
+ pub generation: Generation,
+ /// Broad fault class.
+ pub class: FaultClass,
+ /// Specific fault code.
+ pub code: FaultCode,
+ /// Caller-facing context.
+ pub detail: FaultDetail,
+}
+
+impl Fault {
+ /// Constructs a new fault.
+ #[must_use]
+ pub fn new(
+ generation: Generation,
+ class: FaultClass,
+ code: FaultCode,
+ detail: FaultDetail,
+ ) -> Self {
+ Self {
+ generation,
+ class,
+ code,
+ detail,
+ }
+ }
+
+ /// Returns the default recovery directive for this fault.
+ #[must_use]
+ pub fn directive(&self) -> RecoveryDirective {
+ match (self.class, self.code) {
+ (FaultClass::Transport, FaultCode::BrokenPipe)
+ | (FaultClass::Transport, FaultCode::UnexpectedEof)
+ | (FaultClass::Process, FaultCode::ChildExited)
+ | (FaultClass::Process, FaultCode::SpawnFailed)
+ | (FaultClass::Timeout, FaultCode::StartupTimedOut) => {
+ RecoveryDirective::RestartAndReplay
+ }
+ (FaultClass::Timeout, FaultCode::RequestTimedOut) => {
+ RecoveryDirective::RestartAndReplay
+ }
+ (FaultClass::Protocol, FaultCode::UnknownResponseId) => RecoveryDirective::RetryInPlace,
+ (FaultClass::Protocol, FaultCode::InvalidFrame)
+ | (FaultClass::Protocol, FaultCode::InvalidJson)
+ | (FaultClass::Resource, _) => RecoveryDirective::AbortRequest,
+ _ => RecoveryDirective::AbortRequest,
+ }
+ }
+}
+
+/// Typed detail payload for a fault.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct FaultDetail {
+ /// Human-consumable context.
+ pub message: String,
+}
+
+impl FaultDetail {
+ /// Creates a new detail message.
+ #[must_use]
+ pub fn new(message: impl Into<String>) -> Self {
+ Self {
+ message: message.into(),
+ }
+ }
+}
+
+/// Domain fault conversion error.
+#[derive(Debug, Error)]
+#[error("fault conversion failure: {0}")]
+pub struct FaultConversionError(String);
diff --git a/crates/ra-mcp-domain/src/lib.rs b/crates/ra-mcp-domain/src/lib.rs
new file mode 100644
index 0000000..90285f0
--- /dev/null
+++ b/crates/ra-mcp-domain/src/lib.rs
@@ -0,0 +1,5 @@
+//! Domain model for the Adequate Rust MCP server.
+
+pub mod fault;
+pub mod lifecycle;
+pub mod types;
diff --git a/crates/ra-mcp-domain/src/lifecycle.rs b/crates/ra-mcp-domain/src/lifecycle.rs
new file mode 100644
index 0000000..91007ac
--- /dev/null
+++ b/crates/ra-mcp-domain/src/lifecycle.rs
@@ -0,0 +1,259 @@
+//! Typestate machine for worker lifecycle.
+
+use crate::{
+ fault::Fault,
+ types::{Generation, InvariantViolation},
+};
+use serde::{Deserialize, Serialize};
+
+/// A worker in cold state (no process).
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Cold;
+
+/// A worker in startup handshake.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Starting;
+
+/// A healthy worker ready to serve requests.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Ready;
+
+/// A worker currently recovering from failure.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Recovering {
+ last_fault: Fault,
+}
+
+impl Recovering {
+ /// Constructs recovering state from the most recent fault.
+ #[must_use]
+ pub fn new(last_fault: Fault) -> Self {
+ Self { last_fault }
+ }
+
+ /// Returns the most recent fault.
+ #[must_use]
+ pub fn last_fault(&self) -> &Fault {
+ &self.last_fault
+ }
+}
+
+/// Lifecycle state with a typestate payload.
+#[derive(Debug, Clone)]
+pub struct Lifecycle<S> {
+ generation: Generation,
+ state: S,
+}
+
+impl Lifecycle<Cold> {
+ /// Constructs a cold lifecycle.
+ #[must_use]
+ pub fn cold() -> Self {
+ Self {
+ generation: Generation::genesis(),
+ state: Cold,
+ }
+ }
+
+ /// Begins startup sequence.
+ #[must_use]
+ pub fn ignite(self) -> Lifecycle<Starting> {
+ Lifecycle {
+ generation: self.generation,
+ state: Starting,
+ }
+ }
+}
+
+impl Lifecycle<Starting> {
+ /// Marks startup as successful.
+ #[must_use]
+ pub fn arm(self) -> Lifecycle<Ready> {
+ Lifecycle {
+ generation: self.generation,
+ state: Ready,
+ }
+ }
+
+ /// Marks startup as failed and enters recovery.
+ #[must_use]
+ pub fn fracture(self, fault: Fault) -> Lifecycle<Recovering> {
+ Lifecycle {
+ generation: self.generation,
+ state: Recovering::new(fault),
+ }
+ }
+}
+
+impl Lifecycle<Ready> {
+ /// Moves from ready to recovering after a fault.
+ #[must_use]
+ pub fn fracture(self, fault: Fault) -> Lifecycle<Recovering> {
+ Lifecycle {
+ generation: self.generation,
+ state: Recovering::new(fault),
+ }
+ }
+}
+
+impl Lifecycle<Recovering> {
+ /// Advances generation and retries startup.
+ #[must_use]
+ pub fn respawn(self) -> Lifecycle<Starting> {
+ Lifecycle {
+ generation: self.generation.next(),
+ state: Starting,
+ }
+ }
+
+ /// Returns the most recent fault.
+ #[must_use]
+ pub fn last_fault(&self) -> &Fault {
+ self.state.last_fault()
+ }
+}
+
+impl<S> Lifecycle<S> {
+ /// Returns the active generation.
+ #[must_use]
+ pub fn generation(&self) -> Generation {
+ self.generation
+ }
+
+ /// Returns the typestate payload.
+ #[must_use]
+ pub fn state(&self) -> &S {
+ &self.state
+ }
+}
+
+/// Serializable lifecycle snapshot for diagnostics.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum LifecycleSnapshot {
+ /// No worker is currently running.
+ Cold {
+ /// Current generation counter.
+ generation: Generation,
+ },
+ /// Worker startup is in progress.
+ Starting {
+ /// Current generation counter.
+ generation: Generation,
+ },
+ /// Worker is ready for requests.
+ Ready {
+ /// Current generation counter.
+ generation: Generation,
+ },
+ /// Worker is recovering after a fault.
+ Recovering {
+ /// Current generation counter.
+ generation: Generation,
+ /// Most recent fault.
+ last_fault: Fault,
+ },
+}
+
+/// Dynamically typed lifecycle state for runtime storage.
+#[derive(Debug, Clone)]
+pub enum DynamicLifecycle {
+ /// Cold typestate wrapper.
+ Cold(Lifecycle<Cold>),
+ /// Starting typestate wrapper.
+ Starting(Lifecycle<Starting>),
+ /// Ready typestate wrapper.
+ Ready(Lifecycle<Ready>),
+ /// Recovering typestate wrapper.
+ Recovering(Lifecycle<Recovering>),
+}
+
+impl DynamicLifecycle {
+ /// Creates a cold dynamic lifecycle.
+ #[must_use]
+ pub fn cold() -> Self {
+ Self::Cold(Lifecycle::cold())
+ }
+
+ /// Returns the serializable snapshot.
+ #[must_use]
+ pub fn snapshot(&self) -> LifecycleSnapshot {
+ match self {
+ Self::Cold(state) => LifecycleSnapshot::Cold {
+ generation: state.generation(),
+ },
+ Self::Starting(state) => LifecycleSnapshot::Starting {
+ generation: state.generation(),
+ },
+ Self::Ready(state) => LifecycleSnapshot::Ready {
+ generation: state.generation(),
+ },
+ Self::Recovering(state) => LifecycleSnapshot::Recovering {
+ generation: state.generation(),
+ last_fault: state.last_fault().clone(),
+ },
+ }
+ }
+
+ /// Enters startup from cold or recovering.
+ pub fn begin_startup(self) -> Result<Self, InvariantViolation> {
+ match self {
+ Self::Cold(state) => Ok(Self::Starting(state.ignite())),
+ Self::Recovering(state) => Ok(Self::Starting(state.respawn())),
+ Self::Starting(_) | Self::Ready(_) => Err(InvariantViolation::new(
+ "invalid lifecycle transition to starting",
+ )),
+ }
+ }
+
+ /// Marks startup as complete.
+ pub fn complete_startup(self) -> Result<Self, InvariantViolation> {
+ match self {
+ Self::Starting(state) => Ok(Self::Ready(state.arm())),
+ _ => Err(InvariantViolation::new(
+ "invalid lifecycle transition to ready",
+ )),
+ }
+ }
+
+ /// Records a fault and enters recovering state.
+ pub fn fracture(self, fault: Fault) -> Result<Self, InvariantViolation> {
+ match self {
+ Self::Starting(state) => Ok(Self::Recovering(state.fracture(fault))),
+ Self::Ready(state) => Ok(Self::Recovering(state.fracture(fault))),
+ Self::Recovering(state) => Ok(Self::Recovering(Lifecycle {
+ generation: state.generation(),
+ state: Recovering::new(fault),
+ })),
+ Self::Cold(_) => Err(InvariantViolation::new("cannot fracture cold lifecycle")),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{DynamicLifecycle, Lifecycle, LifecycleSnapshot};
+ use crate::fault::{Fault, FaultClass, FaultCode, FaultDetail};
+
+ #[test]
+ fn typestate_chain_advances_generation_on_recovery() {
+ let cold = Lifecycle::cold();
+ let starting = cold.ignite();
+ let ready = starting.arm();
+ let ready_generation = ready.generation();
+ let fault = Fault::new(
+ ready_generation,
+ FaultClass::Transport,
+ FaultCode::BrokenPipe,
+ FaultDetail::new("broken pipe"),
+ );
+ let recovering = ready.fracture(fault);
+ let restarted = recovering.respawn();
+ assert!(restarted.generation() > ready_generation);
+ }
+
+ #[test]
+ fn dynamic_snapshot_of_recovering_is_infallible() {
+ let cold = DynamicLifecycle::cold();
+ assert!(matches!(cold.snapshot(), LifecycleSnapshot::Cold { .. }));
+ }
+}
diff --git a/crates/ra-mcp-domain/src/types.rs b/crates/ra-mcp-domain/src/types.rs
new file mode 100644
index 0000000..db709d6
--- /dev/null
+++ b/crates/ra-mcp-domain/src/types.rs
@@ -0,0 +1,460 @@
+//! Fundamental domain types.
+
+use serde::{Deserialize, Deserializer, Serialize};
+use std::{
+ num::NonZeroU64,
+ path::{Path, PathBuf},
+};
+use thiserror::Error;
+
+/// A value that failed a domain-level invariant.
+#[derive(Debug, Clone, PartialEq, Eq, Error)]
+#[error("domain invariant violated: {detail}")]
+pub struct InvariantViolation {
+ detail: &'static str,
+}
+
+impl InvariantViolation {
+ /// Creates a new invariant violation.
+ #[must_use]
+ pub fn new(detail: &'static str) -> Self {
+ Self { detail }
+ }
+}
+
+/// Process generation for a rust-analyzer worker.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct Generation(NonZeroU64);
+
+impl Generation {
+ /// Returns the first generation.
+ #[must_use]
+ pub fn genesis() -> Self {
+ Self(NonZeroU64::MIN)
+ }
+
+ /// Returns the inner integer value.
+ #[must_use]
+ pub fn get(self) -> u64 {
+ self.0.get()
+ }
+
+ /// Advances to the next generation.
+ #[must_use]
+ pub fn next(self) -> Self {
+ let next = self.get().saturating_add(1);
+ let non_zero = NonZeroU64::new(next).map_or(NonZeroU64::MAX, |value| value);
+ Self(non_zero)
+ }
+}
+
+/// A non-empty absolute workspace root.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct WorkspaceRoot(PathBuf);
+
+impl WorkspaceRoot {
+ /// Constructs a validated workspace root.
+ pub fn try_new(path: PathBuf) -> Result<Self, InvariantViolation> {
+ if !path.is_absolute() {
+ return Err(InvariantViolation::new("workspace root must be absolute"));
+ }
+ if path.as_os_str().is_empty() {
+ return Err(InvariantViolation::new("workspace root must be non-empty"));
+ }
+ Ok(Self(path))
+ }
+
+ /// Returns the root path.
+ #[must_use]
+ pub fn as_path(&self) -> &Path {
+ self.0.as_path()
+ }
+}
+
+/// A non-empty absolute source file path.
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
+#[serde(transparent)]
+pub struct SourceFilePath(PathBuf);
+
+impl SourceFilePath {
+ /// Constructs a validated source file path.
+ pub fn try_new(path: PathBuf) -> Result<Self, InvariantViolation> {
+ if !path.is_absolute() {
+ return Err(InvariantViolation::new("source file path must be absolute"));
+ }
+ if path.as_os_str().is_empty() {
+ return Err(InvariantViolation::new(
+ "source file path must be non-empty",
+ ));
+ }
+ Ok(Self(path))
+ }
+
+ /// Returns the underlying path.
+ #[must_use]
+ pub fn as_path(&self) -> &Path {
+ self.0.as_path()
+ }
+}
+
+impl<'de> Deserialize<'de> for SourceFilePath {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let path = PathBuf::deserialize(deserializer)?;
+ Self::try_new(path).map_err(serde::de::Error::custom)
+ }
+}
+
+/// One-indexed source line number.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct OneIndexedLine(NonZeroU64);
+
+impl OneIndexedLine {
+ /// Constructs a one-indexed line.
+ pub fn try_new(raw: u64) -> Result<Self, InvariantViolation> {
+ let line = NonZeroU64::new(raw).ok_or(InvariantViolation::new("line must be >= 1"))?;
+ Ok(Self(line))
+ }
+
+ /// Returns the one-indexed value.
+ #[must_use]
+ pub fn get(self) -> u64 {
+ self.0.get()
+ }
+
+ /// Returns the corresponding zero-indexed value for LSP.
+ #[must_use]
+ pub fn to_zero_indexed(self) -> u32 {
+ let raw = self.get().saturating_sub(1);
+ u32::try_from(raw).unwrap_or(u32::MAX)
+ }
+}
+
+/// One-indexed source column number.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct OneIndexedColumn(NonZeroU64);
+
+impl OneIndexedColumn {
+ /// Constructs a one-indexed column.
+ pub fn try_new(raw: u64) -> Result<Self, InvariantViolation> {
+ let column = NonZeroU64::new(raw).ok_or(InvariantViolation::new("column must be >= 1"))?;
+ Ok(Self(column))
+ }
+
+ /// Returns the one-indexed value.
+ #[must_use]
+ pub fn get(self) -> u64 {
+ self.0.get()
+ }
+
+ /// Returns the corresponding zero-indexed value for LSP.
+ #[must_use]
+ pub fn to_zero_indexed(self) -> u32 {
+ let raw = self.get().saturating_sub(1);
+ u32::try_from(raw).unwrap_or(u32::MAX)
+ }
+}
+
+/// A file-local source point.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub struct SourcePoint {
+ line: OneIndexedLine,
+ column: OneIndexedColumn,
+}
+
+impl SourcePoint {
+ /// Constructs a file-local source point.
+ #[must_use]
+ pub fn new(line: OneIndexedLine, column: OneIndexedColumn) -> Self {
+ Self { line, column }
+ }
+
+ /// Returns the line component.
+ #[must_use]
+ pub fn line(self) -> OneIndexedLine {
+ self.line
+ }
+
+ /// Returns the column component.
+ #[must_use]
+ pub fn column(self) -> OneIndexedColumn {
+ self.column
+ }
+}
+
+/// Request position in a source file.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct SourcePosition {
+ file_path: SourceFilePath,
+ #[serde(flatten)]
+ point: SourcePoint,
+}
+
+impl SourcePosition {
+ /// Constructs a request position.
+ #[must_use]
+ pub fn new(file_path: SourceFilePath, point: SourcePoint) -> Self {
+ Self { file_path, point }
+ }
+
+ /// Returns the source file path.
+ #[must_use]
+ pub fn file_path(&self) -> &SourceFilePath {
+ &self.file_path
+ }
+
+ /// Returns the file-local point.
+ #[must_use]
+ pub fn point(&self) -> SourcePoint {
+ self.point
+ }
+
+ /// Returns the one-indexed line.
+ #[must_use]
+ pub fn line(&self) -> OneIndexedLine {
+ self.point.line()
+ }
+
+ /// Returns the one-indexed column.
+ #[must_use]
+ pub fn column(&self) -> OneIndexedColumn {
+ self.point.column()
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct SourcePositionWire {
+ file_path: SourceFilePath,
+ #[serde(flatten)]
+ point: SourcePoint,
+}
+
+impl<'de> Deserialize<'de> for SourcePosition {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let SourcePositionWire { file_path, point } =
+ SourcePositionWire::deserialize(deserializer)?;
+ Ok(Self::new(file_path, point))
+ }
+}
+
+/// A concrete source location.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct SourceLocation {
+ file_path: SourceFilePath,
+ #[serde(flatten)]
+ point: SourcePoint,
+}
+
+impl SourceLocation {
+ /// Constructs a source location.
+ #[must_use]
+ pub fn new(file_path: SourceFilePath, point: SourcePoint) -> Self {
+ Self { file_path, point }
+ }
+
+ /// Returns the source file path.
+ #[must_use]
+ pub fn file_path(&self) -> &SourceFilePath {
+ &self.file_path
+ }
+
+ /// Returns the file-local point.
+ #[must_use]
+ pub fn point(&self) -> SourcePoint {
+ self.point
+ }
+
+ /// Returns the one-indexed line.
+ #[must_use]
+ pub fn line(&self) -> OneIndexedLine {
+ self.point.line()
+ }
+
+ /// Returns the one-indexed column.
+ #[must_use]
+ pub fn column(&self) -> OneIndexedColumn {
+ self.point.column()
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct SourceLocationWire {
+ file_path: SourceFilePath,
+ #[serde(flatten)]
+ point: SourcePoint,
+}
+
+impl<'de> Deserialize<'de> for SourceLocation {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let SourceLocationWire { file_path, point } =
+ SourceLocationWire::deserialize(deserializer)?;
+ Ok(Self::new(file_path, point))
+ }
+}
+
+/// A source range in a specific file.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+pub struct SourceRange {
+ file_path: SourceFilePath,
+ start: SourcePoint,
+ end: SourcePoint,
+}
+
+impl SourceRange {
+ /// Constructs a validated source range.
+ pub fn try_new(
+ file_path: SourceFilePath,
+ start: SourcePoint,
+ end: SourcePoint,
+ ) -> Result<Self, InvariantViolation> {
+ if end < start {
+ return Err(InvariantViolation::new(
+ "source range end must not precede start",
+ ));
+ }
+ Ok(Self {
+ file_path,
+ start,
+ end,
+ })
+ }
+
+ /// Returns the source file path.
+ #[must_use]
+ pub fn file_path(&self) -> &SourceFilePath {
+ &self.file_path
+ }
+
+ /// Returns the start point.
+ #[must_use]
+ pub fn start(&self) -> SourcePoint {
+ self.start
+ }
+
+ /// Returns the end point.
+ #[must_use]
+ pub fn end(&self) -> SourcePoint {
+ self.end
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct SourceRangeWire {
+ file_path: SourceFilePath,
+ start: SourcePoint,
+ end: SourcePoint,
+}
+
+impl<'de> Deserialize<'de> for SourceRange {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let SourceRangeWire {
+ file_path,
+ start,
+ end,
+ } = SourceRangeWire::deserialize(deserializer)?;
+ Self::try_new(file_path, start, end).map_err(serde::de::Error::custom)
+ }
+}
+
+/// A monotonically increasing request sequence.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct RequestSequence(NonZeroU64);
+
+impl RequestSequence {
+ /// Starts a fresh sequence.
+ #[must_use]
+ pub fn genesis() -> Self {
+ Self(NonZeroU64::MIN)
+ }
+
+ /// Returns the current integer value.
+ #[must_use]
+ pub fn get(self) -> u64 {
+ self.0.get()
+ }
+
+ /// Consumes and returns the next sequence.
+ #[must_use]
+ pub fn next(self) -> Self {
+ let next = self.get().saturating_add(1);
+ let non_zero = NonZeroU64::new(next).map_or(NonZeroU64::MAX, |value| value);
+ Self(non_zero)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ Generation, InvariantViolation, OneIndexedColumn, OneIndexedLine, RequestSequence,
+ SourceFilePath, SourcePoint, SourceRange,
+ };
+ use assert_matches::assert_matches;
+ use std::{num::NonZeroU64, path::PathBuf};
+
+ #[test]
+ fn generation_advances_monotonically() {
+ let first = Generation::genesis();
+ let second = first.next();
+ let third = second.next();
+ assert!(first < second);
+ assert!(second < third);
+ }
+
+ #[test]
+ fn generation_saturates_at_maximum() {
+ let max = Generation(NonZeroU64::MAX);
+ assert_eq!(max.next(), max);
+ }
+
+ #[test]
+ fn line_must_be_one_or_greater() {
+ assert_matches!(OneIndexedLine::try_new(0), Err(InvariantViolation { .. }));
+ }
+
+ #[test]
+ fn column_must_be_one_or_greater() {
+ assert_matches!(OneIndexedColumn::try_new(0), Err(InvariantViolation { .. }));
+ }
+
+ #[test]
+ fn source_range_rejects_reversed_points() {
+ let file_path = SourceFilePath::try_new(PathBuf::from("/tmp/range.rs"));
+ assert!(file_path.is_ok());
+ let file_path = match file_path {
+ Ok(value) => value,
+ Err(_) => return,
+ };
+ let start = SourcePoint::new(
+ OneIndexedLine::try_new(4).unwrap_or(OneIndexedLine(NonZeroU64::MIN)),
+ OneIndexedColumn::try_new(3).unwrap_or(OneIndexedColumn(NonZeroU64::MIN)),
+ );
+ let end = SourcePoint::new(
+ OneIndexedLine::try_new(2).unwrap_or(OneIndexedLine(NonZeroU64::MIN)),
+ OneIndexedColumn::try_new(1).unwrap_or(OneIndexedColumn(NonZeroU64::MIN)),
+ );
+ assert_matches!(
+ SourceRange::try_new(file_path, start, end),
+ Err(InvariantViolation { .. })
+ );
+ }
+
+ #[test]
+ fn request_sequence_saturates_at_maximum() {
+ let max = RequestSequence(NonZeroU64::MAX);
+ assert_eq!(max.next().get(), u64::MAX);
+ }
+}