yupdates/
lib.rs

1//! # Yupdates Rust SDK
2//!
3//! The Yupdates Rust SDK lets you easily use the Yupdates API from your own software and scripts.
4//!
5//! The code is [hosted on GitHub](https://github.com/yupdates/yupdates-sdk-rs). Also see the
6//! [Yupdates Python SDK](https://github.com/yupdates/yupdates-sdk-py).
7//!
8//! The [api] module provides a low-level functions that wrap calls to the HTTP+JSON API,
9//! serializing and deserializing the requests and responses.
10//!
11//! The [clients] module provides an `async` client that is more convenient, and [clients::sync]
12//! provides a synchronous version of the client that hides any need to set up an async runtime.
13//!
14//! The following examples require setting the `YUPDATES_API_TOKEN` environment variable.
15//!
16//! Synchronous client example:
17//! ```rust
18//! use yupdates::api::YupdatesV0;
19//! use yupdates::clients::sync::new_sync_client;
20//! use yupdates::errors::Error;
21//!
22//! fn main() -> Result<(), Error> {
23//!     let feed_id = "02fb24a4478462a4491067224b66d9a8b2338ddca2737";
24//!     let yup = new_sync_client()?;
25//!     for item in yup.read_items(feed_id)? {
26//!         println!("Title: {}", item.title);
27//!     }
28//!     Ok(())
29//! }
30//! ```
31//!
32//! The following asynchronous client example requires adding `tokio` to your `Cargo.toml`.
33//! For example: `tokio = { version = "1", features = ["macros"] }`
34//! ```rust
35//! use yupdates::clients::new_async_client;
36//! use yupdates::errors::Error;
37//!
38//! #[tokio::main]
39//! async fn main() -> Result<(), Error> {
40//!     let feed_id = "02fb24a4478462a4491067224b66d9a8b2338ddca2737";
41//!     let yup = new_async_client()?;
42//!     for item in yup.read_items(feed_id).await? {
43//!         println!("Title: {}", item.title);
44//!     }
45//!     Ok(())
46//! }
47//! ```
48//!
49//! See the [README](https://github.com/yupdates/yupdates-sdk-rs/blob/main/README.md).
50//! The SDK is distributed under the MIT license, see [LICENSE](https://github.com/yupdates/yupdates-sdk-rs/blob/main/LICENSE).
51
52pub mod api;
53pub mod clients;
54pub mod errors;
55pub mod models;
56
57use crate::errors::{Error, Kind, Result};
58
59use std::env;
60use std::env::VarError;
61
62/// The HTTP header we need on every API call
63pub const X_AUTH_TOKEN_HEADER: &str = "X-Auth-Token";
64/// Environment variable to consult for the API token (you can bypass this by passing the token
65/// directly to certain functions)
66pub const YUPDATES_API_TOKEN: &str = "YUPDATES_API_TOKEN";
67/// Environment variable to consult for the base API URL. It's not usually needed: you would
68/// typically only use this to exercise against an alternative API endpoint, or if you wanted
69/// to downgrade API versions in the future (right now, there is only `/api/v0/`).
70pub const YUPDATES_API_URL: &str = "YUPDATES_API_URL";
71/// The default base URL
72pub const YUPDATES_DEFAULT_API_URL: &str = "https://feeds.yupdates.com/api/v0/";
73
74/// Retrieve the API URL from the environment or use the default.
75///
76/// You can override by bypassing the default setup methods. You can instantiate your own
77/// `AsyncYupdatesClient` or use the functions in the `api` module directly.
78pub fn env_or_default_url() -> Result<String> {
79    match env::var(YUPDATES_API_URL) {
80        Ok(s) => {
81            if s.ends_with('/') {
82                Ok(s)
83            } else {
84                Ok(format!("{}/", s))
85            }
86        }
87        Err(e) => match e {
88            VarError::NotPresent => Ok(YUPDATES_DEFAULT_API_URL.to_string()),
89            VarError::NotUnicode(_) => Err(Error {
90                kind: Kind::Config(format!("{} is not valid unicode", YUPDATES_API_URL)),
91            }),
92        },
93    }
94}
95
96/// Retrieve the API token from the environment.
97///
98/// This is the default source; you can override by bypassing the default setup methods. You can
99/// instantiate your own `AsyncYupdatesClient` or use the functions in the `api` module directly.
100pub fn api_token() -> Result<String> {
101    match env::var(YUPDATES_API_TOKEN) {
102        Ok(s) => Ok(s),
103        Err(e) => {
104            let err = match e {
105                VarError::NotPresent => {
106                    format!("API token is missing, set {}", YUPDATES_API_TOKEN)
107                }
108                VarError::NotUnicode(_) => {
109                    format!("{} is not valid unicode", YUPDATES_API_TOKEN)
110                }
111            };
112            Err(Error {
113                kind: Kind::Config(err),
114            })
115        }
116    }
117}
118
119/// Accept many forms of item time, validate it, and return a normalized version.
120///
121/// An item time is a unix ms from 0 to 9_999_999_999_999. It has an optional 5 digit suffix.
122/// Valid inputs: "1234", "1661564013555", "1661564013555.00003", "123456.789"
123pub fn normalize_item_time<S>(item_time: S) -> Result<String>
124where
125    S: AsRef<str>,
126{
127    let it = item_time.as_ref();
128    let parts = it.split('.').collect::<Vec<&str>>();
129    let (base_str, slot_str) = match parts.len() {
130        1 => (it, "0"),
131        2 => (parts[0], parts[1]),
132        _ => {
133            return Err(Error {
134                kind: Kind::Deserialization(format!("invalid item time: '{}'", it)),
135            });
136        }
137    };
138    let base_ms = parse_bounded_int(base_str, "base ms", 9_999_999_999_999)?;
139    let slot = parse_bounded_int(slot_str, "suffix", 99_999)?;
140    Ok(format!("{:0>13}.{:0>5}", base_ms, slot))
141}
142
143/// This is [normalize_item_time] for when you are using integer timestamps.
144pub fn normalize_item_time_ms(item_time_ms: u64) -> Result<String> {
145    normalize_item_time(item_time_ms.to_string())
146}
147
148fn parse_bounded_int(int_str: &str, name: &str, upper_bound: u64) -> Result<u64> {
149    let parsed = int_str.parse::<u64>().map_err(|_| Error {
150        kind: Kind::IllegalParameter(format!("invalid u64: '{}'", int_str)),
151    })?;
152    if parsed > upper_bound {
153        return Err(Error {
154            kind: Kind::IllegalParameter(format!(
155                "item time {} may not be larger than {}: '{}'",
156                name, upper_bound, parsed
157            )),
158        });
159    }
160    Ok(parsed)
161}