377 lines
12 KiB
Rust

pub extern crate chrono;
pub extern crate eyre;
use {
crate::entities::{market, ticker},
async_trait::async_trait,
chrono::{DateTime, Local},
eyre::Result,
std::collections::HashMap,
};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum RustlerStatus {
Connecting,
Connected,
Disconnecting,
#[default]
Disconnected,
}
#[derive(Debug, Clone)]
pub enum MarketHourType {
Pre,
Regular,
Post,
Extended,
}
#[derive(Debug, Clone)]
pub struct Quote {
pub id: String,
pub market: String,
pub price: f64,
pub change_percent: Option<f64>,
pub time: Option<i64>,
pub market_hours: Option<MarketHourType>,
}
#[derive(Debug, Clone)]
pub struct RustlerOpts {
pub connect_on_start: bool,
pub connect_on_add: bool,
}
impl Default for RustlerOpts {
fn default() -> Self {
Self {
connect_on_start: true,
connect_on_add: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ScrapperCallbacks {
pub on_connected: Option<fn() -> Result<()>>,
pub on_disconnected: Option<fn() -> Result<()>>,
pub on_message: Option<fn(message: Quote) -> Result<()>>,
}
/// 🤠 » a scruct representing a ticker
///
/// in `rustler` a ticker is the union between a symbol (stock identifier) and its market
///
/// they `key` of a ticker is the concatenation of the market and the symbol separated by a colon
///
/// e.g. `AAPL` in the `NASDAQ` market would have the key `NASDAQ:AAPL`
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Ticker {
pub symbol: String,
pub market: String,
}
impl Ticker {
pub fn from(t: &ticker::Model, m: &market::Model) -> Self {
Self {
symbol: t.symbol.clone(),
market: m.short_name.clone(),
}
}
pub fn many_from(tickers: &[ticker::Model], market: &market::Model) -> Vec<Self> {
tickers.iter().map(|t| Self::from(t, market)).collect()
}
/// 🤠 » returns the key of the ticker
pub fn key(&self) -> String {
format!("{}:{}", self.market, self.symbol)
}
}
pub trait RustlerAccessor {
// #region fields g&s
fn name(&self) -> String;
fn static_name() -> String
where
Self: Sized;
fn status(&self) -> &RustlerStatus;
fn set_status(&mut self, status: RustlerStatus) -> Result<()>;
fn next_run(&self) -> &DateTime<Local>;
fn set_next_run(&mut self, next_run: DateTime<Local>);
fn next_stop(&self) -> &Option<DateTime<Local>>;
fn set_next_stop(&mut self, next_stop: Option<DateTime<Local>>);
fn last_run(&self) -> &Option<DateTime<Local>>;
fn set_last_run(&mut self, last_run: Option<DateTime<Local>>);
fn last_stop(&self) -> &Option<DateTime<Local>>;
fn set_last_stop(&mut self, last_stop: Option<DateTime<Local>>);
fn last_update(&self) -> &Option<DateTime<Local>>;
fn set_last_update(&mut self, last_update: Option<DateTime<Local>>);
fn opts(&self) -> &RustlerOpts;
fn set_opts(&mut self, opts: RustlerOpts);
fn tickers(&self) -> &HashMap<String, Ticker>;
fn tickers_mut(&mut self) -> &mut HashMap<String, Ticker>;
fn set_tickers(&mut self, tickers: HashMap<String, Ticker>);
fn callbacks(&self) -> &Option<ScrapperCallbacks>;
fn set_callbacks(&mut self, callbacks: Option<ScrapperCallbacks>);
// #endregion
}
#[async_trait]
pub trait Rustler: RustlerAccessor + Send + Sync {
// #region Unimplemented trait functions
/// 🤠 » fn called after tickers are added to the rustler
fn on_add(&mut self, tickers: &[Ticker]) -> Result<()>;
/// 🤠 » fn called after tickers are deleted from the rustler
fn on_delete(&mut self, tickers: &[Ticker]) -> Result<()>;
/// 🤠 » connects the rustler to the data source
async fn connect(&mut self) -> Result<()>;
/// 🤠 » disconnects the rustler from the data source
async fn disconnect(&mut self) -> Result<()>;
// #endregion
/// 🤠 » starts the rustler
async fn start(&mut self) -> Result<()> {
let opts = self.opts();
if opts.connect_on_start {
self.connect().await?;
}
Ok(())
}
/// 🤠 » updates last stop and last run times and calls the appropriate callback
///
/// should be called after the status of the rustler changes
fn handle_status_change(&mut self) -> Result<()> {
match self.status() {
RustlerStatus::Disconnected => {
self.set_last_stop(Some(Local::now()));
if let Some(callbacks) = self.callbacks() {
if let Some(on_disconnected) = callbacks.on_disconnected {
on_disconnected()?;
}
}
}
RustlerStatus::Connected => {
self.set_last_run(Some(Local::now()));
if let Some(callbacks) = self.callbacks() {
if let Some(on_connected) = callbacks.on_connected {
on_connected()?;
}
}
}
_ => {}
};
Ok(())
}
/// adds new tickers to the rustler
async fn add(&mut self, new_tickers: &Vec<Ticker>) -> Result<()> {
let tickers = self.tickers_mut();
for new_ticker in new_tickers {
// if the ticker already exists in the tickers map, skip it
if tickers.contains_key(&new_ticker.key()) {
continue;
}
tickers.insert(new_ticker.key(), new_ticker.clone());
}
if self.opts().connect_on_add {
// if disconnected, then connect the rustler
if self.status() == &RustlerStatus::Disconnected {
self.connect().await?;
}
}
self.on_add(new_tickers)?;
Ok(())
}
/// deletes tickers from the rustler
async fn delete(&mut self, new_tickers: &Vec<Ticker>) -> Result<()> {
let tickers = self.tickers_mut();
for new_ticker in new_tickers {
tickers.remove(&new_ticker.key());
}
// if after deleting the tickers the tickers map is
// empty, disconnect the rustler
if tickers.is_empty() {
self.disconnect().await?;
}
self.on_delete(new_tickers)?;
Ok(())
}
/// registers a new quote by passing it to the on_message callback
fn register_quote(&self, quote: Quote) -> Result<()> {
if let Some(callbacks) = self.callbacks() {
if let Some(on_message) = callbacks.on_message {
on_message(quote)?;
}
}
Ok(())
}
}
#[macro_export]
macro_rules! rustler_accessors {
(
$name:ident
) => {
fn name(&self) -> String {
stringify!($name).to_string()
}
fn static_name() -> String {
stringify!($name).to_string()
}
fn status(&self) -> &$crate::rustlers::RustlerStatus {
&self.status
}
fn set_status(
&mut self,
status: $crate::rustlers::RustlerStatus,
) -> $crate::rustlers::eyre::Result<()> {
self.status = status;
self.handle_status_change()?;
lool::logger::info!(
"Rustler {} status changed to {:?}",
self.name(),
self.status()
);
Ok(())
}
fn next_run(&self) -> &$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local> {
&self.next_run
}
fn set_next_run(
&mut self,
next_run: $crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>,
) {
self.next_run = next_run;
}
fn next_stop(
&self,
) -> &Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>> {
&self.next_stop
}
fn set_next_stop(
&mut self,
next_stop: Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>>,
) {
self.next_stop = next_stop;
}
fn last_run(
&self,
) -> &Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>> {
&self.last_run
}
fn set_last_run(
&mut self,
last_run: Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>>,
) {
self.last_run = last_run;
}
fn last_stop(
&self,
) -> &Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>> {
&self.last_stop
}
fn set_last_stop(
&mut self,
last_stop: Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>>,
) {
self.last_stop = last_stop;
}
fn last_update(
&self,
) -> &Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>> {
&self.last_update
}
fn set_last_update(
&mut self,
last_update: Option<
$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>,
>,
) {
self.last_update = last_update;
}
fn opts(&self) -> &$crate::rustlers::RustlerOpts {
&self.opts
}
fn set_opts(&mut self, opts: $crate::rustlers::RustlerOpts) {
self.opts = opts;
}
fn tickers(&self) -> &HashMap<String, $crate::rustlers::Ticker> {
&self.tickers
}
fn tickers_mut(&mut self) -> &mut HashMap<String, $crate::rustlers::Ticker> {
&mut self.tickers
}
fn set_tickers(&mut self, tickers: HashMap<String, $crate::rustlers::Ticker>) {
self.tickers = tickers;
}
fn callbacks(&self) -> &Option<$crate::rustlers::ScrapperCallbacks> {
&self.callbacks
}
fn set_callbacks(&mut self, callbacks: Option<$crate::rustlers::ScrapperCallbacks>) {
self.callbacks = callbacks;
}
};
}
/// **🤠 » rustler builder macro**
///
/// The `rustler!` macro is used to define a new `Rustler` struct, expanding the struct definition
/// with the required fields and derives, and implementing the `RustlerAccessor` trait for the
/// struct.
#[macro_export]
macro_rules! rustler {
// Entry point for the macro, takes the struct definition
(
$(#[$outer:meta])*
$vis:vis struct $name:ident { $($fields:tt)* }
) => {
// Expand to the struct with derives and the fields
$(#[$outer])*
#[derive(Debug, Clone, Default)]
$vis struct $name {
status: $crate::rustlers::RustlerStatus,
next_run: $crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>,
next_stop: Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>>,
last_run: Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>>,
last_stop: Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>>,
last_update: Option<$crate::rustlers::chrono::DateTime<$crate::rustlers::chrono::Local>>,
opts: $crate::rustlers::RustlerOpts,
tickers: HashMap<String, $crate::rustlers::Ticker>,
callbacks: Option<$crate::rustlers::ScrapperCallbacks>,
$($fields)*
}
// Implement the RustlerAccessor trait for the struct
impl $crate::rustlers::RustlerAccessor for $name {
$crate::rustler_accessors!($name);
}
};
}