base_endpoint/lib.rs
1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Reusable descriptions of how to **connect** and **bind**, inspired by Ruby's [IO::Endpoint](https://github.com/socketry/io-endpoint). Application code depends on endpoint contracts; endpoint implementations handle resolution, socket creation and establishment through explicit `base-scheduler` capabilities. The API is experimental.
5//!
6//! [`ConnectEndpoint<S>`](ConnectEndpoint) returns an associated `Connection`; [`BindEndpoint<S>`](BindEndpoint) returns a collection of associated `Listener` values. Implementations choose these representations, so consumers can abstract over establishment without hard-coding TCP or a particular runtime. The traits are separate so a connecting consumer does not need binding capabilities. The built-in TCP implementations return the scheduler's native socket type for use with its existing accept and I/O APIs.
7//!
8//! ```rust
9//! use base_endpoint::{BindEndpoint, BindOptions, ConnectEndpoint, HostEndpoint};
10//! use base_scheduler::{Cancellation, SocketAccept, SocketClose, SocketLocalAddress};
11//! use base_scheduler_native::NativeScheduler;
12//!
13//! let scheduler = NativeScheduler::new()?;
14//! scheduler.block_on(async {
15//! let cancellation = Cancellation::never();
16//!
17//! let listeners = HostEndpoint::new("127.0.0.1", 0)
18//! .bind(&scheduler, BindOptions::default(), &cancellation).await?;
19//! let port = scheduler.socket_local_address(&listeners[0])?.port();
20//! let client = HostEndpoint::new("127.0.0.1", port)
21//! .connect(&scheduler, &cancellation).await?;
22//! let (accepted, _peer) = scheduler.socket_accept(&listeners[0], &cancellation).await?;
23//!
24//! // Use scheduler-bound stream readers/writers here. Retire and drop them
25//! // before closing their sockets.
26//! scheduler.socket_close(client).await?;
27//! scheduler.socket_close(accepted).await?;
28//! for listener in listeners {
29//! scheduler.socket_close(listener).await?;
30//! }
31//! scheduler.shutdown().await;
32//! Ok::<(), base_scheduler::OperationError>(())
33//! })?;
34//! # Ok::<(), base_scheduler::OperationError>(())
35//! ```
36//!
37//! A consumer can work entirely through the connection contract:
38//!
39//! ```rust
40//! use base_endpoint::ConnectEndpoint;
41//! use base_scheduler::{Cancellation, OperationResult};
42//!
43//! async fn establish<S: Sync, E: ConnectEndpoint<S>>(
44//! scheduler: &S,
45//! endpoint: &E,
46//! cancellation: &Cancellation,
47//! ) -> OperationResult<E::Connection> {
48//! endpoint.connect(scheduler, cancellation).await
49//! }
50//! ```
51//!
52//! # TCP endpoints
53//!
54//! [`AddressEndpoint::new(socket_address)`](AddressEndpoint::new) describes a numeric TCP address and needs no resolver. [`HostEndpoint::new(hostname, port)`](HostEndpoint::new) resolves afresh for each operation. [`HostEndpoint::wildcard(port)`](HostEndpoint::wildcard) binds local interfaces and resolves loopback addresses when connecting. [`with_domain(SocketDomain::Ipv4)`](HostEndpoint::with_domain) or `Ipv6` restricts resolution. [`resolve`](HostEndpoint::resolve) exposes numeric endpoints when callers need to inspect candidates.
55//!
56//! # Composition and fallback
57//!
58//! [`CompositeEndpoint::new(vec![...])`](CompositeEndpoint::new) connects to the first successful child, preserving order and returning the last failure if all attempts fail. Bind creates all child listeners; a later failure closes earlier listeners before returning. Host endpoints use the same policy for resolved addresses. Cancellation stops fallback, and empty candidate sets fail with `AddrNotAvailable`. Binding composition currently requires listeners that the supplied scheduler can close; connection composition supports arbitrary connection representations.
59//!
60//! # Binding
61//!
62//! Binding includes listening with the requested backlog. All candidates must bind successfully; there is no best-effort family fallback. Resolver ordering and duplicates are preserved. Wildcard IPv4/IPv6 overlap follows the operating system's dual-stack policy and may cause binding to fail; select one family when needed. With port zero each listener receives its own OS-selected port, available through `SocketLocalAddress`.
63//!
64//! # Ownership and cancellation
65//!
66//! Endpoints retain no scheduler and spawn no tasks. Pass cancellation explicitly and await the operation to finish rollback before scheduler shutdown. Failed connection attempts close their sockets before fallback; failed binds close every socket created by that operation. Cleanup is awaited without the cancelled signal, and the primary establishment error is retained if closing also reports an error. Dropping a pending future provides ordinary destruction only, without an asynchronous join. Successful sockets and their cleanup belong to the caller.
67//!
68//! # Scope
69//!
70//! The first concrete transport covers TCP over IPv4/IPv6. TCP uses OS option defaults. UNIX sockets, UDP, URI parsing, named endpoints, socket-option policies, local source binding, Happy Eyeballs and built-in timeouts are not provided. The scheduler currently exposes only IP socket creation/address operations, so UNIX sockets need a scheduler extension. No concrete runtime is a production dependency; rustls is optional.
71//!
72//! The [`tls`] module composes these endpoints with provider-neutral TLS, including an optional rustls provider. The [`stream`] module describes plaintext byte I/O for established transports.
73
74#![forbid(unsafe_code)]
75#![warn(missing_docs)]
76
77/// Compiles and runs the readme examples as doctests.
78#[cfg(doctest)]
79#[doc = include_str!("../readme.md")]
80pub struct ReadmeDoctests;
81
82mod address_endpoint;
83mod bind_endpoint;
84mod composite_endpoint;
85mod connect_endpoint;
86mod host_endpoint;
87
88pub use address_endpoint::AddressEndpoint;
89pub use bind_endpoint::{BindEndpoint, BindOptions};
90pub use composite_endpoint::CompositeEndpoint;
91pub use connect_endpoint::ConnectEndpoint;
92pub use host_endpoint::HostEndpoint;
93
94pub mod stream;
95pub mod tls;
96
97use base_scheduler::{OperationError, SocketClose};
98use std::io;
99
100fn no_addresses() -> OperationError {
101 io::Error::new(io::ErrorKind::AddrNotAvailable, "endpoint has no addresses").into()
102}
103
104// Preserve the establishment error, but await cleanup of every owned socket before returning it. SocketClose consumes ownership even when closing reports an error.
105async fn rollback<S: SocketClose + ?Sized>(scheduler: &S, sockets: Vec<S::Socket>) {
106 for socket in sockets {
107 let _ = scheduler.socket_close(socket).await;
108 }
109}