feat: Add comprehensive edit screen to new listing wizard
- Replace individual state structs with unified ListingDraft struct - Add EditingListing state with field selection interface - Implement individual field editing states (Title, Description, Price, Slots, StartTime, Duration) - Add field-specific keyboards with Back buttons and Clear functionality for description - Update all handlers to use ListingDraft instead of separate state structs - Rename Confirming to ViewingDraft for clarity - Add proper validation and error handling for all field edits - Enable seamless navigation between edit screen and confirmation - Maintain all existing functionality while adding edit capabilities
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1548,12 +1548,16 @@ dependencies = [
|
||||
"chrono",
|
||||
"dotenvy",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rstest",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"teloxide",
|
||||
"teloxide-core",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -13,13 +13,17 @@ sqlx = { version = "0.8.6", features = [
|
||||
"macros",
|
||||
"rust_decimal",
|
||||
] }
|
||||
rust_decimal = { version = "1.33", features = ["serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
rust_decimal = { version = "1.33" }
|
||||
chrono = { version = "0.4" }
|
||||
log = "0.4"
|
||||
env_logger = "0.11.8"
|
||||
anyhow = "1.0"
|
||||
dotenvy = "0.15"
|
||||
lazy_static = "1.4"
|
||||
serde = "1.0.219"
|
||||
futures = "0.3.31"
|
||||
thiserror = "2.0.16"
|
||||
teloxide-core = "0.13.0"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.26.1"
|
||||
|
||||
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
use teloxide::{prelude::*, types::Message, utils::command::BotCommands, Bot};
|
||||
|
||||
use crate::Command;
|
||||
use crate::{Command, HandlerResult};
|
||||
|
||||
pub async fn handle_help(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
pub async fn handle_help(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let help_message = format!(
|
||||
"📋 Available Commands:\n\n{}\n\n\
|
||||
📧 Support: Contact @admin for help\n\
|
||||
|
||||
@@ -9,6 +9,10 @@ pub mod start;
|
||||
pub use help::handle_help;
|
||||
pub use my_bids::handle_my_bids;
|
||||
pub use my_listings::handle_my_listings;
|
||||
pub use new_listing::handle_new_listing;
|
||||
pub use new_listing::{
|
||||
handle_new_listing, new_listing_callback_handler, new_listing_dialogue_handler,
|
||||
};
|
||||
pub use settings::handle_settings;
|
||||
pub use start::handle_start;
|
||||
|
||||
// Note: Text message handling is now handled by the dialogue system
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_my_bids(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub async fn handle_my_bids(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let response = "🎯 My Bids (Coming Soon)\n\n\
|
||||
Here you'll be able to view:\n\
|
||||
• Your active bids\n\
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_my_listings(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub async fn handle_my_listings(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let response = "📊 My Listings and Auctions (Coming Soon)\n\n\
|
||||
Here you'll be able to view and manage:\n\
|
||||
• Your active listings and auctions\n\
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_settings(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub async fn handle_settings(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let response = "⚙️ Settings (Coming Soon)\n\n\
|
||||
Here you'll be able to configure:\n\
|
||||
• Notification preferences\n\
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, types::Message, Bot};
|
||||
|
||||
pub async fn handle_start(bot: Bot, msg: Message) -> ResponseResult<()> {
|
||||
use crate::HandlerResult;
|
||||
|
||||
pub async fn handle_start(bot: Bot, msg: Message) -> HandlerResult {
|
||||
let welcome_message = "🎯 Welcome to Pawctioneer Bot! 🎯\n\n\
|
||||
This bot helps you participate in various types of auctions:\n\
|
||||
• Standard auctions with anti-sniping protection\n\
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::db::money_amount::MoneyAmount;
|
||||
|
||||
/// Actual bids placed on listings
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct Bid {
|
||||
pub id: i64,
|
||||
pub listing_id: i64,
|
||||
|
||||
@@ -13,6 +13,13 @@ use super::listing_type::ListingType;
|
||||
use crate::db::{listing_id::ListingId, money_amount::MoneyAmount, user_id::UserId};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
/// Main listing/auction entity
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Listing {
|
||||
pub base: ListingBase,
|
||||
pub fields: ListingFields,
|
||||
}
|
||||
|
||||
/// Common fields shared by all listing types
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ListingBase {
|
||||
@@ -26,13 +33,6 @@ pub struct ListingBase {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Main listing/auction entity enum
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Listing {
|
||||
pub base: ListingBase,
|
||||
pub fields: ListingFields,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ListingFields {
|
||||
BasicAuction {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
/// Media attachments for listings
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct ListingMedia {
|
||||
pub id: i64,
|
||||
pub listing_id: i64,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Types of listings supported by the platform
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, sqlx::Type)]
|
||||
#[sqlx(type_name = "TEXT")]
|
||||
#[sqlx(rename_all = "snake_case")]
|
||||
pub enum ListingType {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::db::money_amount::MoneyAmount;
|
||||
|
||||
/// Proxy bid strategies (automatic bidding settings)
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct ProxyBid {
|
||||
pub id: i64,
|
||||
pub listing_id: i64,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::db::{TelegramUserId, UserId};
|
||||
|
||||
/// Core user information
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub telegram_id: TelegramUserId,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
/// User preferences and settings
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct UserSettings {
|
||||
pub user_id: i64,
|
||||
pub language_code: String,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
|
||||
/// Currency types supported by the platform
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CurrencyType {
|
||||
#[serde(rename = "USD")]
|
||||
USD,
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
//! This newtype prevents accidentally mixing up listing IDs with other ID types
|
||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
/// Type-safe wrapper for listing IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListingId(i64);
|
||||
|
||||
impl ListingId {
|
||||
|
||||
@@ -45,6 +45,12 @@ impl MoneyAmount {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MoneyAmount {
|
||||
fn default() -> Self {
|
||||
Self::zero()
|
||||
}
|
||||
}
|
||||
|
||||
// Allow easy conversion from Decimal
|
||||
impl From<Decimal> for MoneyAmount {
|
||||
fn from(decimal: Decimal) -> Self {
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
//! This newtype prevents accidentally mixing up user IDs with other ID types
|
||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
/// Type-safe wrapper for user IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct TelegramUserId(teloxide::types::UserId);
|
||||
|
||||
impl TelegramUserId {
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
//! This newtype prevents accidentally mixing up user IDs with other ID types
|
||||
//! while maintaining compatibility with the database layer through SQLx traits.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
encode::IsNull, error::BoxDynError, sqlite::SqliteTypeInfo, Decode, Encode, Sqlite, Type,
|
||||
};
|
||||
use std::fmt;
|
||||
|
||||
/// Type-safe wrapper for user IDs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UserId(i64);
|
||||
|
||||
impl UserId {
|
||||
|
||||
37
src/main.rs
37
src/main.rs
@@ -1,16 +1,24 @@
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use teloxide::{prelude::*, utils::command::BotCommands};
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod db;
|
||||
mod message_utils;
|
||||
mod sqlite_storage;
|
||||
mod wizard_utils;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::info;
|
||||
use teloxide::dispatching::dialogue::serializer::Json;
|
||||
use teloxide::{prelude::*, types::CallbackQuery, utils::command::BotCommands};
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
|
||||
use commands::new_listing::ListingWizardState;
|
||||
use commands::*;
|
||||
use config::Config;
|
||||
|
||||
use crate::sqlite_storage::SqliteStorage;
|
||||
|
||||
pub type HandlerResult = anyhow::Result<()>;
|
||||
|
||||
#[derive(BotCommands, Clone)]
|
||||
#[command(rename_rule = "lowercase", description = "Auction Bot Commands")]
|
||||
pub enum Command {
|
||||
@@ -39,10 +47,16 @@ async fn main() -> Result<()> {
|
||||
info!("Starting Pawctioneer Bot...");
|
||||
let bot = Bot::new(&config.telegram_token);
|
||||
|
||||
// Create dispatcher with direct command routing
|
||||
let dialog_storage = SqliteStorage::new(db_pool.clone(), Json).await?;
|
||||
|
||||
// Create dispatcher with dialogue system
|
||||
Dispatcher::builder(
|
||||
bot,
|
||||
Update::filter_message().branch(
|
||||
dptree::entry()
|
||||
.branch(
|
||||
Update::filter_message()
|
||||
.enter_dialogue::<Message, SqliteStorage<Json>, ListingWizardState>()
|
||||
.branch(
|
||||
dptree::entry()
|
||||
.filter_command::<Command>()
|
||||
.branch(dptree::case![Command::Start].endpoint(handle_start))
|
||||
@@ -51,9 +65,16 @@ async fn main() -> Result<()> {
|
||||
.branch(dptree::case![Command::MyListings].endpoint(handle_my_listings))
|
||||
.branch(dptree::case![Command::MyBids].endpoint(handle_my_bids))
|
||||
.branch(dptree::case![Command::Settings].endpoint(handle_settings)),
|
||||
)
|
||||
.branch(new_listing_dialogue_handler()),
|
||||
)
|
||||
.branch(
|
||||
Update::filter_callback_query()
|
||||
.enter_dialogue::<CallbackQuery, SqliteStorage<Json>, ListingWizardState>()
|
||||
.branch(new_listing_callback_handler()),
|
||||
),
|
||||
)
|
||||
.dependencies(dptree::deps![db_pool])
|
||||
.dependencies(dptree::deps![db_pool, dialog_storage])
|
||||
.enable_ctrlc_handler()
|
||||
.build()
|
||||
.dispatch()
|
||||
|
||||
40
src/message_utils.rs
Normal file
40
src/message_utils.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use teloxide::types::{Chat, User};
|
||||
|
||||
pub struct UserHandleAndId<'s> {
|
||||
pub handle: Option<&'s str>,
|
||||
pub id: Option<i64>,
|
||||
}
|
||||
impl<'s> Display for UserHandleAndId<'s> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} ({})",
|
||||
self.handle.unwrap_or("unknown"),
|
||||
self.id.unwrap_or(-1)
|
||||
)
|
||||
}
|
||||
}
|
||||
impl<'s> UserHandleAndId<'s> {
|
||||
pub fn from_chat(chat: &'s Chat) -> Self {
|
||||
Self {
|
||||
handle: chat.username(),
|
||||
id: Some(chat.id.0),
|
||||
}
|
||||
}
|
||||
pub fn from_user(user: &'s User) -> Self {
|
||||
Self {
|
||||
handle: user.username.as_deref(),
|
||||
id: Some(user.id.0 as i64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_cancel_or_no(text: &str) -> bool {
|
||||
is_cancel(text) || text.eq_ignore_ascii_case("no")
|
||||
}
|
||||
|
||||
pub fn is_cancel(text: &str) -> bool {
|
||||
text.eq_ignore_ascii_case("/cancel")
|
||||
}
|
||||
150
src/sqlite_storage.rs
Normal file
150
src/sqlite_storage.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use futures::future::BoxFuture;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use sqlx::{sqlite::SqlitePool, Executor};
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
fmt::{Debug, Display},
|
||||
str,
|
||||
sync::Arc,
|
||||
};
|
||||
use teloxide::dispatching::dialogue::{serializer::Serializer, Storage};
|
||||
use teloxide_core::types::ChatId;
|
||||
use thiserror::Error;
|
||||
|
||||
/// A persistent dialogue storage based on [SQLite](https://www.sqlite.org/).
|
||||
pub struct SqliteStorage<S> {
|
||||
pool: SqlitePool,
|
||||
serializer: S,
|
||||
}
|
||||
|
||||
/// An error returned from [`SqliteStorage`].
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SqliteStorageError<SE>
|
||||
where
|
||||
SE: Debug + Display,
|
||||
{
|
||||
#[error("dialogue serialization error: {0}")]
|
||||
SerdeError(SE),
|
||||
|
||||
#[error("sqlite error: {0}")]
|
||||
SqliteError(#[from] sqlx::Error),
|
||||
|
||||
/// Returned from [`SqliteStorage::remove_dialogue`].
|
||||
#[error("row not found")]
|
||||
DialogueNotFound,
|
||||
}
|
||||
|
||||
impl<S> SqliteStorage<S> {
|
||||
pub async fn new(
|
||||
pool: SqlitePool,
|
||||
serializer: S,
|
||||
) -> Result<Arc<Self>, SqliteStorageError<Infallible>> {
|
||||
sqlx::query(
|
||||
"
|
||||
CREATE TABLE IF NOT EXISTS teloxide_dialogues (
|
||||
chat_id BIGINT PRIMARY KEY,
|
||||
dialogue BLOB NOT NULL
|
||||
);
|
||||
",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(Arc::new(Self { pool, serializer }))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, D> Storage<D> for SqliteStorage<S>
|
||||
where
|
||||
S: Send + Sync + Serializer<D> + 'static,
|
||||
D: Send + Serialize + DeserializeOwned + 'static,
|
||||
<S as Serializer<D>>::Error: Debug + Display,
|
||||
{
|
||||
type Error = SqliteStorageError<<S as Serializer<D>>::Error>;
|
||||
|
||||
/// Returns [`sqlx::Error::RowNotFound`] if a dialogue does not exist.
|
||||
fn remove_dialogue(
|
||||
self: Arc<Self>,
|
||||
ChatId(chat_id): ChatId,
|
||||
) -> BoxFuture<'static, Result<(), Self::Error>> {
|
||||
Box::pin(async move {
|
||||
let deleted_rows_count =
|
||||
sqlx::query("DELETE FROM teloxide_dialogues WHERE chat_id = ?")
|
||||
.bind(chat_id)
|
||||
.execute(&self.pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
|
||||
if deleted_rows_count == 0 {
|
||||
return Err(SqliteStorageError::DialogueNotFound);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn update_dialogue(
|
||||
self: Arc<Self>,
|
||||
ChatId(chat_id): ChatId,
|
||||
dialogue: D,
|
||||
) -> BoxFuture<'static, Result<(), Self::Error>> {
|
||||
Box::pin(async move {
|
||||
let d = self
|
||||
.serializer
|
||||
.serialize(&dialogue)
|
||||
.map_err(SqliteStorageError::SerdeError)?;
|
||||
|
||||
self.pool
|
||||
.acquire()
|
||||
.await?
|
||||
.execute(
|
||||
sqlx::query(
|
||||
"
|
||||
INSERT INTO teloxide_dialogues VALUES (?, ?)
|
||||
ON CONFLICT(chat_id) DO UPDATE SET dialogue=excluded.dialogue
|
||||
",
|
||||
)
|
||||
.bind(chat_id)
|
||||
.bind(d),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn get_dialogue(
|
||||
self: Arc<Self>,
|
||||
chat_id: ChatId,
|
||||
) -> BoxFuture<'static, Result<Option<D>, Self::Error>> {
|
||||
Box::pin(async move {
|
||||
get_dialogue(&self.pool, chat_id)
|
||||
.await?
|
||||
.map(|d| {
|
||||
self.serializer
|
||||
.deserialize(&d)
|
||||
.map_err(SqliteStorageError::SerdeError)
|
||||
})
|
||||
.transpose()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_dialogue(
|
||||
pool: &SqlitePool,
|
||||
ChatId(chat_id): ChatId,
|
||||
) -> Result<Option<Vec<u8>>, sqlx::Error> {
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct DialogueDbRow {
|
||||
dialogue: Vec<u8>,
|
||||
}
|
||||
|
||||
let bytes = sqlx::query_as::<_, DialogueDbRow>(
|
||||
"SELECT dialogue FROM teloxide_dialogues WHERE chat_id = ?",
|
||||
)
|
||||
.bind(chat_id)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.map(|r| r.dialogue);
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
64
src/wizard_utils.rs
Normal file
64
src/wizard_utils.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
/// Define a set of states for a wizard.
|
||||
/// Example:
|
||||
/// ```
|
||||
/// wizard_step_states! {
|
||||
/// struct AwaitingDescription {
|
||||
/// title: String,
|
||||
/// }
|
||||
/// struct AwaitingPrice {
|
||||
/// title: String,
|
||||
/// description: Option<String>,
|
||||
/// },
|
||||
/// struct AwaitingSlots {
|
||||
/// title: String,
|
||||
/// description: Option<String>,
|
||||
/// price: Option<MoneyAmount>,
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// Constructs structs for each step of the wizard, and
|
||||
/// generators for converting one step into the next.
|
||||
/// The above example would generate the following structs:
|
||||
/// ```
|
||||
/// struct AwaitingDescription {
|
||||
/// title: String,
|
||||
/// }
|
||||
/// struct AwaitingPrice {
|
||||
/// title: String,
|
||||
/// description: Option<String>,
|
||||
/// }
|
||||
/// struct AwaitingSlots {
|
||||
/// title: String,
|
||||
/// description: Option<String>,
|
||||
/// price: Option<MoneyAmount>,
|
||||
/// }
|
||||
/// ```
|
||||
/// And the following implementations:
|
||||
/// ```
|
||||
/// impl AwaitingDescription {
|
||||
/// fn add_description(self, description: Option<String>) -> AwaitingPrice {
|
||||
/// AwaitingPrice {
|
||||
/// title: self.title,
|
||||
/// description,
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// impl AwaitingPrice {
|
||||
/// fn add_buy_now_price(self, buy_now_price: MoneyAmount) -> AwaitingSlots {
|
||||
/// AwaitingSlots {
|
||||
/// title: self.title,
|
||||
/// description: self.description,
|
||||
/// buy_now_price,
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! wizard_step_states {
|
||||
($($vis:vis struct $name:ident { $($field_vis:vis $field:ident: $ty:ty),* $(,)? }),*) => {
|
||||
$(
|
||||
$vis struct $name {
|
||||
$($field_vis $field: $ty),*
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user