1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! This is a simple example of how you'd implement a worker.
//! Here we spawn jobs before we create a worker, but that would generally be done on other machines.
//!
//! By default, `cwab` will look for configuration in the following order:
//! - Attempts to load the file `cwab.toml`, or a file passed in with `--config`
//! - Looks for ENV vars like `REDIS_URL`
//! - Fallback to using `redis://127.0.0.1:6379`
//!
//! The config format is as follows:
//! ```toml
//! version = 1
//! redis_url = "redis://127.0.0.1:6379"
//! ```
//!
//! NOTE: This doesn't include running `cwab librarian start`, which is a separate binary this library provides, which handles bookkeeping work not covered by workers (Scheduled job polling, retry scheduling, etc...).
//! You should probably only run one instance of `cwab` somewhere, since it doesn't need much resources, but could put a lot of load on your redis instance otherwise.
//!```ignore
//! use anyhow::Result;
//! use cwab::prelude::*;
//! use async_trait::async_trait;
//! use serde::{Serialize, Deserialize};
//!
//! #[derive(Serialize, Deserialize, Copy, Clone, Debug)]
//! pub struct HelloJob;
//!
//! #[async_trait]
//! impl Job for HelloJob {
//!     fn name(&self) -> &'static str {
//!         "HelloJob"
//!     }
//!
//!     // Note that input is an optional arbitrary string.
//!     // You could pass in JSON and parse it in your job.
//!     async fn perform(&self, input: Option<String>) -> Result<Option<String>, anyhow::Error> {
//!         let to_print = if let Some(i) = input {
//!             format!("Hello {:?}", i)
//!         } else {
//!             format!("Hello World")
//!         };
//!         println!("{}", to_print);
//!         Ok(None)
//!     }
//! }
//!
//! #[tokio::main]
//! async fn main() -> Result<()> {
//!     let config = Config::new(None)?;
//!     let cwab = Cwab::new(&config)?;
//!     let mut worker = cwab.worker();
//!     worker.register(HelloJob);
//!
//!     cwab.perform_async(HelloJob, None)
//!         .await?;
//!     cwab.perform_async(HelloJob, Some("Bob".to_string()))
//!         .await?;
//!
//!     worker.start_working().await?;
//!     Ok(())
//! }
//!```

use serde::{Deserialize, Serialize};
use std::time::Duration;

pub(crate) mod client;
pub(crate) mod cwab;
pub(crate) mod job;
pub(crate) mod worker;
pub(crate) const MAX_WAIT: Duration = Duration::from_secs(5);

/// This is what you'll import to get started. Click in for more documentation.
pub mod prelude;

/// Configuration used to (de)serialize a TOML configuration file.
/// Used to customize various aspects of the runtime.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Config {
    /// Used to version the configuration format
    pub version: usize,
    /// A `redis://...` url
    pub redis_url: String,
    /// A secret used to encrypt job inputs
    secret: Option<String>,
    /// The queue name spaces to watch
    pub namespaces: Option<Vec<String>>,
}

impl Config {
    pub fn new(config_path: Option<&str>) -> Result<Config, anyhow::Error> {
        let path_str = config_path.unwrap_or("cwab.toml");
        let path = std::path::Path::new(path_str);
        let config = if path.exists() {
            toml::from_str::<Config>(std::str::from_utf8(&std::fs::read(path_str)?)?)?
        } else {
            println!("No config file found, falling back to ENV vars");
            let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| {
                println!("REDIS_URL is not set, falling back to redis://127.0.0.1:6379");
                "redis://127.0.0.1:6379".to_string()
            });

            let secret = std::env::var("CWAB_SECRET").ok();
            if secret.is_none() {
                println!("CWAB_SECRET is not set, disabling encryption");
            }

            let namespaces_str = std::env::var("CWAB_NAMESPACES").ok();
            let namespaces: Option<Vec<String>> = namespaces_str.map(|namespaces| {
                namespaces
                    .split(',')
                    .map(|x| x.trim().to_string())
                    .collect()
            });

            Config {
                version: 1,
                redis_url,
                secret,
                namespaces,
            }
        };
        Ok(config)
    }
}