draft new api version

This commit is contained in:
yggverse 2024-10-28 02:27:30 +02:00
parent dccff1e111
commit 9152528790
17 changed files with 299 additions and 317 deletions

View file

@ -1,14 +1,13 @@
# ggemini # ggemini
Glib/Gio-oriented network library for [Gemini protocol](https://geminiprotocol.net/) Glib-oriented client for [Gemini protocol](https://geminiprotocol.net/)
> [!IMPORTANT] > [!IMPORTANT]
> Project in development! > Project in development!
> >
GGemini (or G-Gemini) initially created as the client extension for [Yoda Browser](https://github.com/YGGverse/Yoda), This library mostly written as the network extension for [Yoda](https://github.com/YGGverse/Yoda) - GTK Browser for Gemini Protocol,
also could be useful for any other integration as depends of it also could be useful for any other integrations as depend of [glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`2.66+`) crates only.
[glib](https://crates.io/crates/glib) and [gio](https://crates.io/crates/gio) (`v2_66`) crates only.
## Install ## Install
@ -20,23 +19,16 @@ cargo add ggemini
### `client` ### `client`
Gio API already includes powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html), [Gio](https://docs.gtk.org/gio/) API already provide powerful [SocketClient](https://docs.gtk.org/gio/class.SocketClient.html).
`ggemini::client` just extends some features a bit, to simplify interaction with socket over Gemini Protocol. This library just extend some minimal features wanted for Gemini Protocol
It also contain some children components/mods bellow for low-level access any feature directly.
#### `client::buffer`
#### `client::response` #### `client::response`
Response parser for [InputStream](https://docs.gtk.org/gio/class.InputStream.html) Response parser, currently includes low-level interaction API for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
#### `client::response::Response`
#### `client::response::header` #### `client::response::header`
#### `client::response::body` #### `client::response::body`
https://docs.gtk.org/glib/struct.Bytes.html
## See also ## See also
* [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API * [ggemtext](https://github.com/YGGverse/ggemtext) - Glib-oriented Gemtext API

View file

@ -1,7 +1 @@
pub mod buffer;
pub mod error;
pub mod response; pub mod response;
pub use buffer::Buffer;
pub use error::Error;
pub use response::Response;

View file

@ -1,164 +0,0 @@
pub mod error;
pub use error::Error;
use gio::{
prelude::{IOStreamExt, InputStreamExt},
Cancellable, SocketConnection,
};
use glib::{Bytes, Priority};
pub const DEFAULT_CAPACITY: usize = 0x400;
pub const DEFAULT_MAX_SIZE: usize = 0xfffff;
/// Dynamically allocated [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) buffer
/// with configurable `capacity` and `max_size` limits
pub struct Buffer {
buffer: Vec<Bytes>,
max_size: usize,
}
impl Buffer {
// Constructors
/// Create new `Self` with default `capacity` and `max_size` preset
pub fn new() -> Self {
Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE))
}
/// Create new `Self` with options
///
/// Options:
/// * `capacity` initial bytes request to reduce extra memory reallocation (`DEFAULT_CAPACITY` if `None`)
/// * `max_size` max bytes to prevent memory overflow by unknown stream source (`DEFAULT_MAX_SIZE` if `None`)
pub fn new_with_options(capacity: Option<usize>, max_size: Option<usize>) -> Self {
Self {
buffer: Vec::with_capacity(match capacity {
Some(value) => value,
None => DEFAULT_CAPACITY,
}),
max_size: match max_size {
Some(value) => value,
None => DEFAULT_MAX_SIZE,
},
}
}
// Intentable constructors
/// Simplest way to create `Self` buffer from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
///
/// Options:
/// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from
/// * `callback` function to apply on all async operations complete, return `Result<Self, (Error, Option<&str>)>`
pub fn from_connection_async(
connection: SocketConnection,
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
) {
Self::read_all_async(Self::new(), connection, None, None, None, callback);
}
// Actions
/// Asynchronously read all [Bytes](https://docs.gtk.org/glib/struct.Bytes.html)
/// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to `Self.buffer`
///
/// Useful to grab entire stream without risk of memory overflow (according to `Self.max_size`),
/// reduce extra memory reallocations by `capacity` option.
///
/// **Notes**
///
/// We are using entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) reference
/// instead of [InputStream](https://docs.gtk.org/gio/class.InputStream.html) directly just to keep main connection alive in the async context
///
/// **Options**
/// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from
/// * `cancellable` - [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) or `None::<&Cancellable>` by default
/// * `priority` - [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default
/// * `chunk` optional bytes count to read per chunk (`0x100` by default)
/// * `callback` function to apply on all async operations complete, return `Result<Self, (Error, Option<&str>)>`
pub fn read_all_async(
mut self,
connection: SocketConnection,
cancelable: Option<Cancellable>,
priority: Option<Priority>,
chunk: Option<usize>,
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
) {
connection.input_stream().read_bytes_async(
match chunk {
Some(value) => value,
None => 0x100,
},
match priority {
Some(value) => value,
None => Priority::DEFAULT,
},
match cancelable.clone() {
Some(value) => Some(value),
None => None::<Cancellable>,
}
.as_ref(),
move |result| match result {
Ok(bytes) => {
// No bytes were read, end of stream
if bytes.len() == 0 {
return callback(Ok(self));
}
// Save chunk to buffer
if let Err(reason) = self.push(bytes) {
return callback(Err((reason, None)));
};
// Continue bytes read..
self.read_all_async(connection, cancelable, priority, chunk, callback);
}
Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))),
},
);
}
/// Push [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to `Self.buffer`
///
/// Return `Error::Overflow` on `max_size` reached
pub fn push(&mut self, bytes: Bytes) -> Result<usize, Error> {
// Calculate new size value
let total = self.buffer.len() + bytes.len();
// Validate overflow
if total > self.max_size {
return Err(Error::Overflow);
}
// Success
self.buffer.push(bytes);
Ok(total)
}
// Setters
/// Set new `max_size` value, `DEFAULT_MAX_SIZE` if `None`
pub fn set_max_size(&mut self, value: Option<usize>) {
self.max_size = match value {
Some(size) => size,
None => DEFAULT_MAX_SIZE,
}
}
// Getters
/// Get reference to bytes collected
pub fn buffer(&self) -> &Vec<Bytes> {
&self.buffer
}
/// Return copy of bytes as UTF-8 vector
pub fn to_utf8(&self) -> Vec<u8> {
self.buffer
.iter()
.flat_map(|byte| byte.iter())
.cloned()
.collect()
}
}

View file

@ -1,4 +0,0 @@
pub enum Error {
InputStream,
Overflow,
}

View file

@ -1,6 +0,0 @@
pub enum Error {
Close,
Connection,
Request,
Response,
}

View file

@ -1,48 +1,5 @@
pub mod body; pub mod body;
pub mod error;
pub mod header; pub mod header;
pub use body::Body; pub use body::Body;
pub use error::Error;
pub use header::Header; pub use header::Header;
use glib::Bytes;
pub struct Response {
header: Header,
body: Body,
}
impl Response {
/// Create new `Self`
pub fn new(header: Header, body: Body) -> Self {
Self { header, body }
}
/// Construct from [Bytes](https://docs.gtk.org/glib/struct.Bytes.html)
///
/// Useful for [Gio::InputStream](https://docs.gtk.org/gio/class.InputStream.html):
/// * [read_bytes](https://docs.gtk.org/gio/method.InputStream.read_bytes.html)
/// * [read_bytes_async](https://docs.gtk.org/gio/method.InputStream.read_bytes_async.html)
pub fn from(bytes: &Bytes) -> Result<Self, Error> {
let header = match Header::from_response(bytes) {
Ok(result) => result,
Err(_) => return Err(Error::Header),
};
let body = match Body::from_response(bytes) {
Ok(result) => result,
Err(_) => return Err(Error::Body),
};
Ok(Self::new(header, body))
}
pub fn header(&self) -> &Header {
&self.header
}
pub fn body(&self) -> &Body {
&self.body
}
}

View file

@ -1,46 +1,187 @@
pub mod error; pub mod error;
pub use error::Error; pub use error::Error;
use glib::{Bytes, GString}; use gio::{
prelude::{IOStreamExt, InputStreamExt},
Cancellable, SocketConnection,
};
use glib::{Bytes, GString, Priority};
pub const DEFAULT_CAPACITY: usize = 0x400;
pub const DEFAULT_MAX_SIZE: usize = 0xfffff;
/// Body container with memory-overflow-safe, dynamically allocated [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) buffer
///
/// **Features**
///
/// * configurable `capacity` and `max_size` options
/// * build-in [InputStream](https://docs.gtk.org/gio/class.InputStream.html) parser
///
/// **Notice**
///
/// * Recommended for gemtext documents
/// * For media types, use native stream processors (e.g. [Pixbuf](https://docs.gtk.org/gdk-pixbuf/ctor.Pixbuf.new_from_stream.html) for images)
pub struct Body { pub struct Body {
buffer: Vec<u8>, buffer: Vec<Bytes>,
max_size: usize,
} }
impl Body { impl Body {
// Constructors // Constructors
pub fn from_response(bytes: &Bytes) -> Result<Self, Error> {
let start = Self::start(bytes)?;
let buffer = match bytes.get(start..) { /// Create new empty `Self` with default `capacity` and `max_size` preset
Some(result) => result, pub fn new() -> Self {
None => return Err(Error::Buffer), Self::new_with_options(Some(DEFAULT_CAPACITY), Some(DEFAULT_MAX_SIZE))
}
/// Create new new `Self` with options
///
/// Options:
/// * `capacity` initial bytes request to reduce extra memory reallocation (`DEFAULT_CAPACITY` if `None`)
/// * `max_size` max bytes to prevent memory overflow by unknown stream source (`DEFAULT_MAX_SIZE` if `None`)
pub fn new_with_options(capacity: Option<usize>, max_size: Option<usize>) -> Self {
Self {
buffer: Vec::with_capacity(match capacity {
Some(value) => value,
None => DEFAULT_CAPACITY,
}),
max_size: match max_size {
Some(value) => value,
None => DEFAULT_MAX_SIZE,
},
}
}
/// Simple way to create `Self` buffer from active [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
///
/// **Options**
/// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from
/// * `callback` function to apply on async operations complete, return `Result<Self, (Error, Option<&str>)>`
///
/// **Notes**
///
/// * method requires entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html),
/// not just [InputStream](https://docs.gtk.org/gio/class.InputStream.html) because of async features;
/// * use this method after `Header` bytes taken from input stream connected (otherwise, take a look on high-level `Response` parser)
pub fn from_socket_connection_async(
connection: SocketConnection,
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
) {
Self::read_all_async(Self::new(), connection, None, None, None, callback);
}
// Actions
/// Asynchronously read all [Bytes](https://docs.gtk.org/glib/struct.Bytes.html)
/// from [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to `Self.buffer` by `chunk`
///
/// Useful to grab entire stream without risk of memory overflow (according to `Self.max_size`),
/// reduce extra memory reallocations by `capacity` option.
///
/// **Notes**
///
/// We are using entire [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) reference
/// instead of [InputStream](https://docs.gtk.org/gio/class.InputStream.html) just to keep main connection alive in the async chunks context
///
/// **Options**
/// * `connection` - [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) to read bytes from
/// * `cancellable` - [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html) or `None::<&Cancellable>` by default
/// * `priority` - [Priority::DEFAULT](https://docs.gtk.org/glib/const.PRIORITY_DEFAULT.html) by default
/// * `chunk` optional bytes count to read per chunk (`0x100` by default)
/// * `callback` function to apply on all async operations complete, return `Result<Self, (Error, Option<&str>)>`
pub fn read_all_async(
mut self,
connection: SocketConnection,
cancelable: Option<Cancellable>,
priority: Option<Priority>,
chunk: Option<usize>,
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
) {
connection.input_stream().read_bytes_async(
match chunk {
Some(value) => value,
None => 0x100,
},
match priority {
Some(value) => value,
None => Priority::DEFAULT,
},
match cancelable.clone() {
Some(value) => Some(value),
None => None::<Cancellable>,
}
.as_ref(),
move |result| match result {
Ok(bytes) => {
// No bytes were read, end of stream
if bytes.len() == 0 {
return callback(Ok(self));
}
// Save chunk to buffer
if let Err(reason) = self.push(bytes) {
return callback(Err((reason, None)));
}; };
Ok(Self { // Continue bytes read..
buffer: Vec::from(buffer), self.read_all_async(connection, cancelable, priority, chunk, callback);
}) }
Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))),
},
);
}
/// Push [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to `Self.buffer`
///
/// Return `Error::Overflow` on `max_size` reached
pub fn push(&mut self, bytes: Bytes) -> Result<usize, Error> {
// Calculate new size value
let total = self.buffer.len() + bytes.len();
// Validate overflow
if total > self.max_size {
return Err(Error::Overflow);
}
// Success
self.buffer.push(bytes);
Ok(total)
}
// Setters
/// Set new `max_size` value, `DEFAULT_MAX_SIZE` if `None`
pub fn set_max_size(&mut self, value: Option<usize>) {
self.max_size = match value {
Some(size) => size,
None => DEFAULT_MAX_SIZE,
}
} }
// Getters // Getters
pub fn buffer(&self) -> &[u8] {
/// Get reference to `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) collected
pub fn buffer(&self) -> &Vec<Bytes> {
&self.buffer &self.buffer
} }
/// Return copy of `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) as UTF-8 vector
pub fn to_utf8(&self) -> Vec<u8> {
self.buffer
.iter()
.flat_map(|byte| byte.iter())
.cloned()
.collect()
}
// Intentable getters
/// Try convert `Self.buffer` [Bytes](https://docs.gtk.org/glib/struct.Bytes.html) to GString
pub fn to_gstring(&self) -> Result<GString, Error> { pub fn to_gstring(&self) -> Result<GString, Error> {
match GString::from_utf8(self.buffer.to_vec()) { match GString::from_utf8(self.to_utf8()) {
Ok(result) => Ok(result), Ok(result) => Ok(result),
Err(_) => Err(Error::Decode), Err(_) => Err(Error::Decode),
} }
} }
// Tools
fn start(buffer: &[u8]) -> Result<usize, Error> {
for (offset, &byte) in buffer.iter().enumerate() {
if byte == b'\n' {
return Ok(offset + 1);
}
}
Err(Error::Format)
}
} }

View file

@ -2,5 +2,6 @@ pub enum Error {
Buffer, Buffer,
Decode, Decode,
Format, Format,
Status, InputStream,
Overflow,
} }

View file

@ -1,4 +0,0 @@
pub enum Error {
Header,
Body,
}

View file

@ -8,7 +8,11 @@ pub use meta::Meta;
pub use mime::Mime; pub use mime::Mime;
pub use status::Status; pub use status::Status;
use glib::Bytes; use gio::{
prelude::{IOStreamExt, InputStreamExt},
Cancellable, SocketConnection,
};
use glib::{Bytes, Priority};
pub struct Header { pub struct Header {
status: Status, status: Status,
@ -21,39 +25,58 @@ pub struct Header {
impl Header { impl Header {
// Constructors // Constructors
pub fn from_response(bytes: &Bytes) -> Result<Self, Error> {
// Get header slice of bytes
let end = Self::end(bytes)?;
let bytes = Bytes::from(match bytes.get(..end) {
Some(buffer) => buffer,
None => return Err(Error::Buffer),
});
pub fn from_socket_connection_async(
socket_connection: SocketConnection,
priority: Option<Priority>,
cancellable: Option<Cancellable>,
callback: impl FnOnce(Result<Self, (Error, Option<&str>)>) + 'static,
) {
// Take header buffer from input stream
Self::read_from_socket_connection_async(
Vec::with_capacity(1024),
socket_connection,
match cancellable {
Some(value) => Some(value),
None => None::<Cancellable>,
},
match priority {
Some(value) => value,
None => Priority::DEFAULT,
},
|result| {
callback(match result {
Ok(buffer) => {
// Status is required, parse to continue // Status is required, parse to continue
let status = match Status::from_header(&bytes) { match Status::from_header(&buffer) {
Ok(status) => Ok(status), Ok(status) => Ok(Self {
Err(reason) => Err(match reason {
status::Error::Decode => Error::StatusDecode,
status::Error::Undefined => Error::StatusUndefined,
}),
}?;
// Done
Ok(Self {
status, status,
meta: match Meta::from_header(&bytes) { meta: match Meta::from_header(&buffer) {
Ok(meta) => Some(meta), Ok(meta) => Some(meta),
Err(_) => None, Err(_) => None,
}, },
mime: match Mime::from_header(&bytes) { mime: match Mime::from_header(&buffer) {
Ok(mime) => Some(mime), Ok(mime) => Some(mime),
Err(_) => None, Err(_) => None,
}, },
}),
Err(reason) => Err((
match reason {
status::Error::Decode => Error::StatusDecode,
status::Error::Undefined => Error::StatusUndefined,
},
None,
)),
}
}
Err(error) => Err(error),
}) })
},
);
} }
// Getters // Getters
pub fn status(&self) -> &Status { pub fn status(&self) -> &Status {
&self.status &self.status
} }
@ -68,13 +91,58 @@ impl Header {
// Tools // Tools
/// Get last header byte (until \r) pub fn read_from_socket_connection_async(
fn end(bytes: &Bytes) -> Result<usize, Error> { mut buffer: Vec<Bytes>,
for (offset, &byte) in bytes.iter().enumerate() { connection: SocketConnection,
if byte == b'\r' { cancellable: Option<Cancellable>,
return Ok(offset); priority: Priority,
} callback: impl FnOnce(Result<Vec<u8>, (Error, Option<&str>)>) + 'static,
} ) {
Err(Error::Format) connection.input_stream().read_bytes_async(
1, // do not change!
priority,
cancellable.clone().as_ref(),
move |result| match result {
Ok(bytes) => {
// Expect valid header length
if bytes.len() == 0 || buffer.len() + 1 > 1024 {
return callback(Err((Error::Protocol, None)));
}
// Read next byte without buffer record
if bytes.contains(&b'\r') {
return Self::read_from_socket_connection_async(
buffer,
connection,
cancellable,
priority,
callback,
);
}
// Complete without buffer record
if bytes.contains(&b'\n') {
return callback(Ok(buffer
.iter()
.flat_map(|byte| byte.iter())
.cloned()
.collect())); // convert to UTF-8
}
// Record
buffer.push(bytes);
// Continue
Self::read_from_socket_connection_async(
buffer,
connection,
cancellable,
priority,
callback,
);
}
Err(reason) => callback(Err((Error::InputStream, Some(reason.message())))),
},
);
} }
} }

View file

@ -1,6 +1,8 @@
#[derive(Debug)]
pub enum Error { pub enum Error {
Buffer, Buffer,
Format, InputStream,
Protocol,
StatusDecode, StatusDecode,
StatusUndefined, StatusUndefined,
} }

View file

@ -1,7 +1,7 @@
pub mod error; pub mod error;
pub use error::Error; pub use error::Error;
use glib::{Bytes, GString}; use glib::GString;
/// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes). /// Entire meta buffer, but [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes).
/// ///
@ -11,13 +11,13 @@ pub struct Meta {
} }
impl Meta { impl Meta {
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> { pub fn from_header(buffer: &[u8]) -> Result<Self, Error> {
let buffer = match bytes.get(3..) { match buffer.get(3..) {
Some(bytes) => bytes.to_vec(), Some(value) => Ok(Self {
buffer: value.to_vec(),
}),
None => return Err(Error::Undefined), None => return Err(Error::Undefined),
}; }
Ok(Self { buffer })
} }
pub fn to_gstring(&self) -> Result<GString, Error> { pub fn to_gstring(&self) -> Result<GString, Error> {

View file

@ -1,3 +1,4 @@
#[derive(Debug)]
pub enum Error { pub enum Error {
Decode, Decode,
Undefined, Undefined,

View file

@ -1,10 +1,11 @@
pub mod error; pub mod error;
pub use error::Error; pub use error::Error;
use glib::{Bytes, GString, Uri}; use glib::{GString, Uri};
use std::path::Path; use std::path::Path;
/// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters /// https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters
#[derive(Debug)]
pub enum Mime { pub enum Mime {
TextGemini, TextGemini,
TextPlain, TextPlain,
@ -15,9 +16,9 @@ pub enum Mime {
} // @TODO } // @TODO
impl Mime { impl Mime {
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> { pub fn from_header(buffer: &[u8]) -> Result<Self, Error> {
match bytes.get(..) { match buffer.get(..) {
Some(bytes) => match GString::from_utf8(bytes.to_vec()) { Some(value) => match GString::from_utf8(value.to_vec()) {
Ok(string) => Self::from_string(string.as_str()), Ok(string) => Self::from_string(string.as_str()),
Err(_) => Err(Error::Decode), Err(_) => Err(Error::Decode),
}, },

View file

@ -1,3 +1,4 @@
#[derive(Debug)]
pub enum Error { pub enum Error {
Decode, Decode,
Undefined, Undefined,

View file

@ -1,9 +1,10 @@
pub mod error; pub mod error;
pub use error::Error; pub use error::Error;
use glib::{Bytes, GString}; use glib::GString;
/// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes
#[derive(Debug)]
pub enum Status { pub enum Status {
Input, Input,
SensitiveInput, SensitiveInput,
@ -12,9 +13,9 @@ pub enum Status {
} // @TODO } // @TODO
impl Status { impl Status {
pub fn from_header(bytes: &Bytes) -> Result<Self, Error> { pub fn from_header(buffer: &[u8]) -> Result<Self, Error> {
match bytes.get(0..2) { match buffer.get(0..2) {
Some(bytes) => match GString::from_utf8(bytes.to_vec()) { Some(value) => match GString::from_utf8(value.to_vec()) {
Ok(string) => Self::from_string(string.as_str()), Ok(string) => Self::from_string(string.as_str()),
Err(_) => Err(Error::Decode), Err(_) => Err(Error::Decode),
}, },

View file

@ -1,3 +1,4 @@
#[derive(Debug)]
pub enum Error { pub enum Error {
Decode, Decode,
Undefined, Undefined,