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
// TODO: https://wiki.factorio.com/Web_authentication_API
// TODO: Download: https://mods.factorio.com/{download_url}?username={username}&token={token}

pub mod api;

use api::FullModSpec;
use elsa::FrozenMap;
use thiserror::Error;
use tracing::info;

/// A simple caching client for the [Factorio mod portal
/// API](https://wiki.factorio.com/Mod_portal_API).
pub struct ModPortalClient {
    client: reqwest::Client,
    specs: FrozenMap<String, Box<FullModSpec>>,
}

impl ModPortalClient {
    /// Creates a new client with default configuration.
    pub fn new() -> Result<ModPortalClient> {
        ModPortalClient::with_client(reqwest::Client::builder().build()?)
    }

    /// Creates a new client with a pre-configured `reqwest::Client`.
    pub fn with_client(client: reqwest::Client) -> Result<ModPortalClient> {
        Ok(ModPortalClient { client, specs: FrozenMap::new() })
    }

    /// Get the full spec of a Factorio mod. Request results are cached in memory.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use factorio_mod_api::ModPortalClient;
    /// use futures::TryFutureExt;
    ///
    /// let client = ModPortalClient::new()?;
    /// client.get_mod_spec("Warehousing")
    ///     .map_ok(|spec| println!("{}", spec.created_at));
    ///
    /// # Ok::<(), factorio_mod_api::FactorioModApiError>(())
    /// ```
    pub async fn get_mod_spec(&self, name: &str) -> Result<&FullModSpec> {
        Ok(if let Some(spec) = self.specs.get(name) {
            spec
        } else {
            info!("requesting mod spec for '{name}'");
            let url = format!("https://mods.factorio.com/api/mods/{name}/full");
            let response = self.client.get(url).send().await?.json().await?;
            self.specs.insert(name.into(), Box::new(response))
        })
    }
}

/// Main result type used throughout factorio-mod-api
pub type Result<T> = std::result::Result<T, FactorioModApiError>;

/// Main error type used throughout factorio-mod-api
#[derive(Error, Debug)]
pub enum FactorioModApiError {
    // Error that is raised if a mod dependency has an invalid format.
    #[error("Invalid mod dependency: '{dep}'")]
    InvalidModDependency { dep: String },

    /// Error that is raised if a request to the mod portal failed.
    #[error("Error while talking to the API Server")]
    RequestError(#[from] reqwest::Error),

    /// Error that is raised if parsing of a SemVer version number failed.
    #[error("Error while parsing a version number")]
    VersionError(#[from] semver::Error),

    /// Error that is raised if deserialization from JSON failed.
    #[error("failed to parse JSON")]
    JsonParsingError(#[from] serde_json::Error),
}