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
124
//!# MPESA Environment
//!
//! Code related to setting up the desired Safaricom API environment. Environment can be either
//! sandbox or production.
//! you will need environment specific credentials (`CLIENT_KEY` AND `CLIENT_SECRET`) when creating
//! an instance of the `Mpesa` client struct. Note that you cannot use sandbox credentials in
//! production and vice versa.
//!
//! Based on selected environment. You are able to access environment specific data such as the `base_url`
//! and the `public key` an X509 certificate used for encrypting initiator passwords. You can read more about that from
//! the Safaricom API [docs](https://developer.safaricom.co.ke/docs?javascript#security-credentials).

use std::convert::TryFrom;
use std::str::FromStr;

use crate::MpesaError;

#[derive(Debug, Clone)]
/// Enum to map to desired environment so as to access certificate
/// and the base url
/// Required to construct a new `Mpesa` struct
pub enum Environment {
    /// Production environment
    Production,
    /// Sandbox environment: for testing and development purposes
    Sandbox,
}

/// Expected behavior of an `Mpesa` client environment
/// This abstraction exists to make it possible to mock the MPESA api server for tests
pub trait ApiEnvironment: Clone {
    fn base_url(&self) -> &str;
    fn get_certificate(&self) -> &str;
}

macro_rules! environment_from_string {
    ($v:expr) => {
        match $v {
            "production" => Ok(Self::Production),
            "sandbox" => Ok(Self::Sandbox),
            _ => Err(MpesaError::Message(
                "Could not parse the provided environment name",
            )),
        }
    };
}

impl FromStr for Environment {
    type Err = MpesaError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        environment_from_string!(s.to_lowercase().as_str())
    }
}

impl TryFrom<&str> for Environment {
    type Error = MpesaError;

    fn try_from(v: &str) -> Result<Self, Self::Error> {
        environment_from_string!(v.to_lowercase().as_str())
    }
}

impl TryFrom<String> for Environment {
    type Error = MpesaError;

    fn try_from(v: String) -> Result<Self, Self::Error> {
        environment_from_string!(v.to_lowercase().as_str())
    }
}

impl ApiEnvironment for Environment {
    /// Matches to base_url based on `Environment` variant
    fn base_url(&self) -> &str {
        match self {
            Environment::Production => "https://api.safaricom.co.ke",
            Environment::Sandbox => "https://sandbox.safaricom.co.ke",
        }
    }

    /// Match to X509 public key certificate based on `Environment`
    fn get_certificate(&self) -> &str {
        match self {
            Environment::Production => include_str!("./certificates/production"),
            Environment::Sandbox => include_str!("./certificates/sandbox"),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::convert::TryInto;

    use super::*;

    #[test]
    fn test_valid_string_is_parsed_as_environment() {
        let accepted_production_values =
            vec!["production", "Production", "PRODUCTION", "prODUctIoN"];
        let accepted_sandbox_values = vec!["sandbox", "Sandbox", "SANDBOX", "sanDBoX"];
        accepted_production_values.into_iter().for_each(|v| {
            let environment: Environment = v.parse().unwrap();
            assert_eq!(environment.base_url(), "https://api.safaricom.co.ke");
            assert_eq!(
                environment.get_certificate(),
                include_str!("./certificates/production")
            )
        });
        accepted_sandbox_values.into_iter().for_each(|v| {
            let environment: Environment = v.try_into().unwrap();
            assert_eq!(environment.base_url(), "https://sandbox.safaricom.co.ke");
            assert_eq!(
                environment.get_certificate(),
                include_str!("./certificates/sandbox")
            )
        })
    }

    #[test]
    #[should_panic]
    fn test_invalid_string_panics() {
        let _: Environment = "foo_bar".try_into().unwrap();
    }
}