mirror of
https://github.com/teloxide/teloxide.git
synced 2025-01-03 09:49:07 +01:00
refactor attributes once again
This commit is contained in:
parent
25f9bff97a
commit
c8514823d7
8 changed files with 302 additions and 129 deletions
217
src/attr.rs
217
src/attr.rs
|
@ -1,96 +1,153 @@
|
||||||
use crate::Result;
|
use crate::{error::compile_error_at, Result};
|
||||||
|
|
||||||
|
use proc_macro2::Span;
|
||||||
use syn::{
|
use syn::{
|
||||||
parse::{Parse, ParseBuffer, ParseStream},
|
parse::{Parse, ParseBuffer, ParseStream},
|
||||||
Attribute, LitStr, Token,
|
spanned::Spanned,
|
||||||
|
Attribute, Ident, Lit, Path, Token,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) enum CommandAttrName {
|
pub(crate) fn fold_attrs<A, R>(
|
||||||
Prefix,
|
attrs: &[Attribute],
|
||||||
Description,
|
filter: fn(&Attribute) -> bool,
|
||||||
Rename,
|
parse: impl Fn(Attr) -> Result<R>,
|
||||||
ParseWith,
|
init: A,
|
||||||
Separator,
|
f: impl Fn(A, R) -> Result<A>,
|
||||||
|
) -> Result<A> {
|
||||||
|
attrs
|
||||||
|
.iter()
|
||||||
|
.filter(|&a| filter(a))
|
||||||
|
.flat_map(|attribute| {
|
||||||
|
// FIXME: don't allocate here
|
||||||
|
let attrs =
|
||||||
|
match attribute.parse_args_with(|input: &ParseBuffer| {
|
||||||
|
input.parse_terminated::<_, Token![,]>(Attr::parse)
|
||||||
|
}) {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(err) => return vec![Err(err.into())],
|
||||||
|
};
|
||||||
|
|
||||||
|
attrs.into_iter().map(&parse).collect()
|
||||||
|
})
|
||||||
|
.try_fold(init, |acc, r| r.and_then(|r| f(acc, r)))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parse for CommandAttrName {
|
/// An attribute key-value pair.
|
||||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
///
|
||||||
let name_arg: syn::Ident = input.parse()?;
|
/// For example:
|
||||||
|
/// ```text
|
||||||
|
/// #[blahblah(key = "puff", value = 12, nope)]
|
||||||
|
/// ^^^^^^^^^^^^ ^^^^^^^^^^ ^^^^
|
||||||
|
/// ```
|
||||||
|
pub(crate) struct Attr {
|
||||||
|
pub key: Ident,
|
||||||
|
pub value: AttrValue,
|
||||||
|
}
|
||||||
|
|
||||||
match name_arg.to_string().as_str() {
|
/// Value of an attribute.
|
||||||
"prefix" => Ok(CommandAttrName::Prefix),
|
///
|
||||||
"description" => Ok(CommandAttrName::Description),
|
/// For example:
|
||||||
"rename" => Ok(CommandAttrName::Rename),
|
/// ```text
|
||||||
"parse_with" => Ok(CommandAttrName::ParseWith),
|
/// #[blahblah(key = "puff", value = 12, nope)]
|
||||||
"separator" => Ok(CommandAttrName::Separator),
|
/// ^^^^^^ ^^ ^-- (None pseudo-value)
|
||||||
_ => Err(syn::Error::new(
|
/// ```
|
||||||
name_arg.span(),
|
pub(crate) enum AttrValue {
|
||||||
"unexpected attribute name (expected one of `prefix`, \
|
Path(Path),
|
||||||
`description`, `rename`, `parse_with`, `separator`",
|
Lit(Lit),
|
||||||
)),
|
None(Span),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Attr {
|
||||||
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
|
let key = input.parse::<Ident>()?;
|
||||||
|
|
||||||
|
let value = match input.peek(Token![=]) {
|
||||||
|
true => {
|
||||||
|
input.parse::<Token![=]>()?;
|
||||||
|
input.parse::<AttrValue>()?
|
||||||
|
}
|
||||||
|
false => AttrValue::None(input.span()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { key, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attr {
|
||||||
|
pub(crate) fn span(&self) -> Span {
|
||||||
|
self.key.span().join(self.value.span()).unwrap_or(self.key.span())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrValue {
|
||||||
|
/// Unwraps this value if it's a string literal.
|
||||||
|
pub fn expect_string(self) -> Result<String> {
|
||||||
|
self.expect("a string", |this| match this {
|
||||||
|
AttrValue::Lit(Lit::Str(s)) => Ok(s.value()),
|
||||||
|
_ => Err(this),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// /// Unwraps this value if it's a path.
|
||||||
|
// pub fn expect_path(self) -> Result<Path> {
|
||||||
|
// self.expect("a path", |this| match this {
|
||||||
|
// AttrValue::Path(p) => Ok(p),
|
||||||
|
// _ => Err(this),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn expect<T>(
|
||||||
|
self,
|
||||||
|
expected: &str,
|
||||||
|
f: impl FnOnce(Self) -> Result<T, Self>,
|
||||||
|
) -> Result<T> {
|
||||||
|
f(self).map_err(|this| {
|
||||||
|
compile_error_at(
|
||||||
|
&format!("expected {expected}, found {}", this.descr()),
|
||||||
|
this.span(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descr(&self) -> &'static str {
|
||||||
|
use Lit::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::None(_) => "nothing",
|
||||||
|
Self::Lit(l) => match l {
|
||||||
|
Str(_) | ByteStr(_) => "a string",
|
||||||
|
Char(_) => "a character",
|
||||||
|
Byte(_) | Int(_) => "an integer",
|
||||||
|
Float(_) => "a floating point integer",
|
||||||
|
Bool(_) => "a boolean",
|
||||||
|
Verbatim(_) => ":shrug:",
|
||||||
|
},
|
||||||
|
Self::Path(_) => "a path",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns span of the value
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// #[blahblah(key = "puff", value = 12, nope )]
|
||||||
|
/// ^^^^^^ ^^ ^
|
||||||
|
/// ```
|
||||||
|
fn span(&self) -> Span {
|
||||||
|
match self {
|
||||||
|
Self::Path(p) => p.span(),
|
||||||
|
Self::Lit(l) => l.span(),
|
||||||
|
Self::None(sp) => *sp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct CommandAttr {
|
impl Parse for AttrValue {
|
||||||
pub name: CommandAttrName,
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parse for CommandAttr {
|
|
||||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||||
let name = input.parse::<CommandAttrName>()?;
|
let this = match input.peek(Lit) {
|
||||||
|
true => Self::Lit(input.parse()?),
|
||||||
|
false => Self::Path(input.parse()?),
|
||||||
|
};
|
||||||
|
|
||||||
// FIXME: this should support value-less attrs, as well as
|
Ok(this)
|
||||||
// non-string-literal values
|
|
||||||
input.parse::<Token![=]>()?;
|
|
||||||
let value = input.parse::<LitStr>()?.value();
|
|
||||||
|
|
||||||
Ok(Self { name, value })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct CommandAttrs(Vec<CommandAttr>);
|
|
||||||
|
|
||||||
impl CommandAttrs {
|
|
||||||
pub fn from_attributes(attributes: &[Attribute]) -> Result<Self> {
|
|
||||||
let mut attrs = Vec::new();
|
|
||||||
|
|
||||||
for attribute in attributes.iter().filter(is_command_attribute) {
|
|
||||||
let attrs_ = attribute.parse_args_with(|input: &ParseBuffer| {
|
|
||||||
input.parse_terminated::<_, Token![,]>(CommandAttr::parse)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
attrs.extend(attrs_);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self(attrs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoIterator for &'a CommandAttrs {
|
|
||||||
type Item = &'a CommandAttr;
|
|
||||||
|
|
||||||
type IntoIter = std::slice::Iter<'a, CommandAttr>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.0.iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for CommandAttrs {
|
|
||||||
type Item = CommandAttr;
|
|
||||||
|
|
||||||
type IntoIter = std::vec::IntoIter<CommandAttr>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.0.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_command_attribute(a: &&Attribute) -> bool {
|
|
||||||
match a.path.get_ident() {
|
|
||||||
Some(ident) => ident == "command",
|
|
||||||
_ => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
attr::CommandAttrs, command::Command, command_enum::CommandEnum,
|
command::Command, command_attr::CommandAttrs, command_enum::CommandEnum,
|
||||||
compile_error, fields_parse::impl_parse_args, unzip::Unzip, Result,
|
compile_error, fields_parse::impl_parse_args, unzip::Unzip, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
attr::{self, CommandAttr, CommandAttrName},
|
command_attr::CommandAttrs, command_enum::CommandEnum,
|
||||||
command_enum::CommandEnum,
|
fields_parse::ParserType, rename_rules::RenameRule, Result,
|
||||||
fields_parse::ParserType,
|
|
||||||
rename_rules::RenameRule,
|
|
||||||
Result,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) struct Command {
|
pub(crate) struct Command {
|
||||||
|
@ -14,8 +11,7 @@ pub(crate) struct Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Command {
|
impl Command {
|
||||||
pub fn try_from(attrs: attr::CommandAttrs, name: &str) -> Result<Self> {
|
pub fn try_from(attrs: CommandAttrs, name: &str) -> Result<Self> {
|
||||||
let attrs = parse_attrs(attrs)?;
|
|
||||||
let CommandAttrs {
|
let CommandAttrs {
|
||||||
prefix,
|
prefix,
|
||||||
description,
|
description,
|
||||||
|
@ -24,7 +20,7 @@ impl Command {
|
||||||
separator: _,
|
separator: _,
|
||||||
} = attrs;
|
} = attrs;
|
||||||
|
|
||||||
let name = rename_rule.apply(name);
|
let name = rename_rule.unwrap_or(RenameRule::Identity).apply(name);
|
||||||
|
|
||||||
Ok(Self { prefix, description, parser, name })
|
Ok(Self { prefix, description, parser, name })
|
||||||
}
|
}
|
||||||
|
@ -60,33 +56,3 @@ impl Command {
|
||||||
self.description != Some("off".to_owned())
|
self.description != Some("off".to_owned())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct CommandAttrs {
|
|
||||||
pub prefix: Option<String>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub rename_rule: RenameRule,
|
|
||||||
pub parser: Option<ParserType>,
|
|
||||||
pub separator: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_attrs(attrs: attr::CommandAttrs) -> Result<CommandAttrs> {
|
|
||||||
let mut prefix = None;
|
|
||||||
let mut description = None;
|
|
||||||
let mut rename_rule = RenameRule::Identity;
|
|
||||||
let mut parser = None;
|
|
||||||
let mut separator = None;
|
|
||||||
|
|
||||||
for CommandAttr { name, value } in attrs {
|
|
||||||
match name {
|
|
||||||
CommandAttrName::Prefix => prefix = Some(value),
|
|
||||||
CommandAttrName::Description => description = Some(value),
|
|
||||||
CommandAttrName::Rename => rename_rule = RenameRule::parse(&value)?,
|
|
||||||
CommandAttrName::ParseWith => {
|
|
||||||
parser = Some(ParserType::parse(&value))
|
|
||||||
}
|
|
||||||
CommandAttrName::Separator => separator = Some(value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(CommandAttrs { prefix, description, rename_rule, parser, separator })
|
|
||||||
}
|
|
||||||
|
|
115
src/command_attr.rs
Normal file
115
src/command_attr.rs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
use crate::{
|
||||||
|
attr::{fold_attrs, Attr},
|
||||||
|
error::compile_error_at,
|
||||||
|
fields_parse::ParserType,
|
||||||
|
rename_rules::RenameRule,
|
||||||
|
Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
use proc_macro2::Span;
|
||||||
|
use syn::Attribute;
|
||||||
|
|
||||||
|
/// Attributes for `BotCommands` derive macro.
|
||||||
|
pub(crate) struct CommandAttrs {
|
||||||
|
pub prefix: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub rename_rule: Option<RenameRule>,
|
||||||
|
pub parser: Option<ParserType>,
|
||||||
|
pub separator: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An attribute for `BotCommands` derive macro.
|
||||||
|
pub(crate) struct CommandAttr {
|
||||||
|
kind: CommandAttrKind,
|
||||||
|
sp: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) enum CommandAttrKind {
|
||||||
|
Prefix(String),
|
||||||
|
Description(String),
|
||||||
|
Rename(RenameRule),
|
||||||
|
ParseWith(ParserType),
|
||||||
|
Separator(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandAttrs {
|
||||||
|
pub fn from_attributes(attributes: &[Attribute]) -> Result<Self> {
|
||||||
|
use CommandAttrKind::*;
|
||||||
|
|
||||||
|
fold_attrs(
|
||||||
|
attributes,
|
||||||
|
is_command_attribute,
|
||||||
|
CommandAttr::parse,
|
||||||
|
Self {
|
||||||
|
prefix: None,
|
||||||
|
description: None,
|
||||||
|
rename_rule: None,
|
||||||
|
parser: None,
|
||||||
|
separator: None,
|
||||||
|
},
|
||||||
|
|mut this, attr| {
|
||||||
|
fn insert<T>(
|
||||||
|
opt: &mut Option<T>,
|
||||||
|
x: T,
|
||||||
|
sp: Span,
|
||||||
|
) -> Result<()> {
|
||||||
|
match opt {
|
||||||
|
slot @ None => {
|
||||||
|
*slot = Some(x);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
Err(compile_error_at("duplicate attribute", sp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match attr.kind {
|
||||||
|
Prefix(p) => insert(&mut this.prefix, p, attr.sp),
|
||||||
|
Description(d) => insert(&mut this.description, d, attr.sp),
|
||||||
|
Rename(r) => insert(&mut this.rename_rule, r, attr.sp),
|
||||||
|
ParseWith(p) => insert(&mut this.parser, p, attr.sp),
|
||||||
|
Separator(s) => insert(&mut this.separator, s, attr.sp),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandAttr {
|
||||||
|
fn parse(attr: Attr) -> Result<Self> {
|
||||||
|
use CommandAttrKind::*;
|
||||||
|
|
||||||
|
let sp = attr.span();
|
||||||
|
let Attr { key, value } = attr;
|
||||||
|
let kind = match &*key.to_string() {
|
||||||
|
"prefix" => Prefix(value.expect_string()?),
|
||||||
|
"description" => Description(value.expect_string()?),
|
||||||
|
"rename" => Rename(
|
||||||
|
value.expect_string().and_then(|r| RenameRule::parse(&r))?,
|
||||||
|
),
|
||||||
|
"parse_with" => {
|
||||||
|
ParseWith(value.expect_string().map(|p| ParserType::parse(&p))?)
|
||||||
|
}
|
||||||
|
"separator" => Separator(value.expect_string()?),
|
||||||
|
_ => {
|
||||||
|
return Err(compile_error_at(
|
||||||
|
"unexpected attribute name (expected one of `prefix`, \
|
||||||
|
`description`, `rename`, `parse_with` and `separator`",
|
||||||
|
key.span(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { kind, sp })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_command_attribute(a: &Attribute) -> bool {
|
||||||
|
match a.path.get_ident() {
|
||||||
|
Some(ident) => ident == "command",
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
attr, command::parse_attrs, fields_parse::ParserType,
|
command_attr::CommandAttrs, fields_parse::ParserType,
|
||||||
rename_rules::RenameRule, Result,
|
rename_rules::RenameRule, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,14 +12,17 @@ pub(crate) struct CommandEnum {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommandEnum {
|
impl CommandEnum {
|
||||||
pub fn try_from(attrs: attr::CommandAttrs) -> Result<Self> {
|
pub fn try_from(attrs: CommandAttrs) -> Result<Self> {
|
||||||
let attrs = parse_attrs(attrs)?;
|
let CommandAttrs {
|
||||||
|
prefix,
|
||||||
|
description,
|
||||||
|
rename_rule,
|
||||||
|
parser,
|
||||||
|
separator,
|
||||||
|
} = attrs;
|
||||||
|
let mut parser = parser.unwrap_or(ParserType::Default);
|
||||||
|
|
||||||
let prefix = attrs.prefix;
|
// FIXME: Error on unused separator
|
||||||
let description = attrs.description;
|
|
||||||
let rename = attrs.rename_rule;
|
|
||||||
let separator = attrs.separator;
|
|
||||||
let mut parser = attrs.parser.unwrap_or(ParserType::Default);
|
|
||||||
if let (ParserType::Split { separator }, Some(s)) =
|
if let (ParserType::Split { separator }, Some(s)) =
|
||||||
(&mut parser, &separator)
|
(&mut parser, &separator)
|
||||||
{
|
{
|
||||||
|
@ -28,7 +31,7 @@ impl CommandEnum {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
prefix,
|
prefix,
|
||||||
description,
|
description,
|
||||||
rename_rule: rename,
|
rename_rule: rename_rule.unwrap_or(RenameRule::Identity),
|
||||||
parser_type: parser,
|
parser_type: parser,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
32
src/error.rs
32
src/error.rs
|
@ -1,4 +1,4 @@
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::{quote, ToTokens};
|
use quote::{quote, ToTokens};
|
||||||
|
|
||||||
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
|
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
@ -13,6 +13,36 @@ where
|
||||||
Error(quote! { compile_error! { #data } })
|
Error(quote! { compile_error! { #data } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compile_error_at(msg: &str, sp: Span) -> Error {
|
||||||
|
use proc_macro2::{
|
||||||
|
Delimiter, Group, Ident, Literal, Punct, Spacing, TokenTree,
|
||||||
|
};
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
// compile_error! { $msg }
|
||||||
|
let ts = TokenStream::from_iter(vec![
|
||||||
|
TokenTree::Ident(Ident::new("compile_error", sp)),
|
||||||
|
TokenTree::Punct({
|
||||||
|
let mut punct = Punct::new('!', Spacing::Alone);
|
||||||
|
punct.set_span(sp);
|
||||||
|
punct
|
||||||
|
}),
|
||||||
|
TokenTree::Group({
|
||||||
|
let mut group = Group::new(Delimiter::Brace, {
|
||||||
|
TokenStream::from_iter(vec![TokenTree::Literal({
|
||||||
|
let mut string = Literal::string(msg);
|
||||||
|
string.set_span(sp);
|
||||||
|
string
|
||||||
|
})])
|
||||||
|
});
|
||||||
|
group.set_span(sp);
|
||||||
|
group
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Error(ts)
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Error> for proc_macro2::TokenStream {
|
impl From<Error> for proc_macro2::TokenStream {
|
||||||
fn from(Error(e): Error) -> Self {
|
fn from(Error(e): Error) -> Self {
|
||||||
e
|
e
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub(crate) enum ParserType {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParserType {
|
impl ParserType {
|
||||||
|
// FIXME: use path for custom
|
||||||
pub fn parse(data: &str) -> Self {
|
pub fn parse(data: &str) -> Self {
|
||||||
match data {
|
match data {
|
||||||
"default" => ParserType::Default,
|
"default" => ParserType::Default,
|
||||||
|
|
|
@ -5,6 +5,7 @@ extern crate proc_macro;
|
||||||
mod attr;
|
mod attr;
|
||||||
mod bot_commands;
|
mod bot_commands;
|
||||||
mod command;
|
mod command;
|
||||||
|
mod command_attr;
|
||||||
mod command_enum;
|
mod command_enum;
|
||||||
mod error;
|
mod error;
|
||||||
mod fields_parse;
|
mod fields_parse;
|
||||||
|
|
Loading…
Reference in a new issue