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}