diff --git a/Cargo.toml b/Cargo.toml index 3672027..b19da7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "ggemini" -version = "0.13.1" +version = "0.14.0" edition = "2021" license = "MIT" readme = "README.md" description = "Glib/Gio-oriented network API for Gemini protocol" -keywords = ["gemini", "gemini-protocol", "gtk", "gio", "client"] +keywords = ["gemini", "titan", "glib", "gio", "client"] categories = ["development-tools", "network-programming", "parsing"] repository = "https://github.com/YGGverse/ggemini" diff --git a/README.md b/README.md index 1c455fb..f49ce36 100644 --- a/README.md +++ b/README.md @@ -25,20 +25,25 @@ cargo add ggemini ### Example ``` rust -use gtk::gio::*; -use gtk::glib::*; +use gio::*; +use glib::*; use ggemini::client::{ connection::{ - response::meta::{Mime, Status}, - Response, + Request, Response, + request::Gemini, + response::meta::{Mime, Status} }, Client, Error, }; fn main() -> ExitCode { Client::new().request_async( - Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), + Request::Gemini( + Gemini::build( + Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap() + ) + ), Priority::DEFAULT, Cancellable::new(), None, // optional `GTlsCertificate` @@ -63,6 +68,8 @@ fn main() -> ExitCode { } ``` -## See also +* to send requests using Titan protocol, see also `titan_request_async` implementation + +## Other crates * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API \ No newline at end of file diff --git a/src/client.rs b/src/client.rs index 77026e9..c92077a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,11 +4,11 @@ pub mod connection; pub mod error; -pub use connection::Connection; +pub use connection::{Connection, Request, Response}; pub use error::Error; use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate}; -use glib::{Priority, Uri}; +use glib::Priority; // Defaults @@ -56,16 +56,22 @@ impl Client { /// * compatible with user (certificate) and guest (certificate-less) connection types pub fn request_async( &self, - uri: Uri, + request: Request, priority: Priority, cancellable: Cancellable, certificate: Option, - callback: impl Fn(Result) + 'static, + callback: impl Fn(Result) + 'static, ) { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid // [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) - match crate::gio::network_address::from_uri(&uri, crate::DEFAULT_PORT) { + match crate::gio::network_address::from_uri( + &match request { + Request::Gemini(ref request) => request.uri.clone(), + Request::Titan(ref request) => request.uri.clone(), + }, + crate::DEFAULT_PORT, + ) { Ok(network_address) => { self.socket .connect_async(&network_address.clone(), Some(&cancellable.clone()), { @@ -78,15 +84,27 @@ impl Client { Some(network_address), is_session_resumption, ) { - Ok(connection) => connection.request_async( - uri.to_string(), - priority, - cancellable, - move |result| match result { - Ok(response) => callback(Ok(response)), - Err(e) => callback(Err(Error::Connection(e))), - }, - ), + Ok(connection) => match request { + Request::Gemini(request) => connection + .gemini_request_async( + request, + priority, + cancellable, + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), + }, + ), + Request::Titan(request) => connection.titan_request_async( + request, + priority, + cancellable, + move |result| match result { + Ok(response) => callback(Ok(response)), + Err(e) => callback(Err(Error::Connection(e))), + }, + ), + }, Err(e) => callback(Err(Error::Connection(e))), } } diff --git a/src/client/connection.rs b/src/client/connection.rs index d5588bf..380a115 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -1,7 +1,9 @@ pub mod error; +pub mod request; pub mod response; pub use error::Error; +pub use request::{Gemini, Request, Titan}; pub use response::Response; use gio::{ @@ -46,18 +48,69 @@ impl Connection { // Actions - /// Make new request to `Self` connection - /// * callback with new `Response` on success or `Error` on failure + /// Send new `Request` to `Self` connection using + /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or + /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol pub fn request_async( self, - query: String, + request: Request, + priority: Priority, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, + ) { + match request { + Request::Gemini(request) => { + self.gemini_request_async(request, priority, cancellable, callback) + } + Request::Titan(request) => { + self.titan_request_async(request, priority, cancellable, callback) + } + } + } + + /// Make new request to `Self` connection using + /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol + /// * callback with new `Response` on success or `Error` on failure + /// * see also `request_async` method to send multi-protocol requests + pub fn gemini_request_async( + self, + request: Gemini, + priority: Priority, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, + ) { + self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); + } + + /// Make new request to `Self` connection using + /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol + /// * callback with new `Response` on success or `Error` on failure + /// * see also `request_async` method to send multi-protocol requests + pub fn titan_request_async( + self, + request: Titan, + priority: Priority, + cancellable: Cancellable, + callback: impl Fn(Result) + 'static, + ) { + self.bytes_request_async(&request.to_bytes(), priority, cancellable, callback); + } + + /// Low-level shared method to send raw bytes array over + /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) or + /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol + /// * bytes array should include formatted header according to protocol selected + /// * for high-level requests see `gemini_request_async` and `titan_request_async` methods + /// * to construct multi-protocol request with single function, use `request_async` method + pub fn bytes_request_async( + self, + request: &Bytes, priority: Priority, cancellable: Cancellable, callback: impl Fn(Result) + 'static, ) { - // Send request self.stream().output_stream().write_bytes_async( - &Bytes::from(format!("{query}\r\n").as_bytes()), + request, priority, Some(&cancellable.clone()), move |result| match result { diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs new file mode 100644 index 0000000..5986f7b --- /dev/null +++ b/src/client/connection/request.rs @@ -0,0 +1,10 @@ +pub mod gemini; +pub mod titan; + +pub use gemini::Gemini; +pub use titan::Titan; + +pub enum Request { + Gemini(Gemini), + Titan(Titan), +} diff --git a/src/client/connection/request/gemini.rs b/src/client/connection/request/gemini.rs new file mode 100644 index 0000000..4887499 --- /dev/null +++ b/src/client/connection/request/gemini.rs @@ -0,0 +1,22 @@ +use glib::{Bytes, Uri}; + +/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) protocol enum object for `Request` +pub struct Gemini { + pub uri: Uri, +} + +impl Gemini { + // Constructors + + /// Build valid new `Self` + pub fn build(uri: Uri) -> Self { + Self { uri } // @TODO validate + } + + // Getters + + /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + pub fn to_bytes(&self) -> Bytes { + Bytes::from(format!("{}\r\n", self.uri).as_bytes()) + } +} diff --git a/src/client/connection/request/titan.rs b/src/client/connection/request/titan.rs new file mode 100644 index 0000000..0affd3f --- /dev/null +++ b/src/client/connection/request/titan.rs @@ -0,0 +1,53 @@ +use glib::{Bytes, Uri}; + +/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) protocol enum object for `Request` +pub struct Titan { + pub uri: Uri, + pub size: usize, + pub mime: String, + pub token: Option, + pub data: Vec, +} + +impl Titan { + // Constructors + + /// Build valid new `Self` + pub fn build( + uri: Uri, + size: usize, + mime: String, + token: Option, + data: Vec, + ) -> Self { + Self { + uri, + size, + mime, + token, + data, + } // @TODO validate + } + + // Getters + + /// Copy `Self` to [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) + pub fn to_bytes(&self) -> Bytes { + // Build header + let mut header = format!("{};size={};mime={}", self.uri, self.size, self.mime); + if let Some(ref token) = self.token { + header.push_str(&format!(";token={token}")); + } + header.push_str("\r\n"); + + let header_bytes = header.into_bytes(); + + // Build request + let mut bytes: Vec = Vec::with_capacity(self.size + header_bytes.len()); + bytes.extend(header_bytes); + bytes.extend(&self.data); + + // Wrap result + Bytes::from(&bytes) + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 1673a59..aa0aaaa 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1 +1,15 @@ +use gio::*; +use glib::*; + +use ggemini::client::connection::request::Gemini; + +#[test] +fn client_connection_request_gemini_build() { + const REQUEST: &str = "gemini://geminiprotocol.net/"; + + let request = Gemini::build(Uri::parse(REQUEST, UriFlags::NONE).unwrap()); + + assert_eq!(&request.uri.to_string(), REQUEST); +} + // @TODO