toolforge/lib.rs
1/*
2Small library for common tasks on Wikimedia Toolforge
3Copyright (C) 2013, 2017, 2020, 2022 Kunal Mehta <legoktm@debian.org>
4
5This program is free software: you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation, either version 3 of the License, or
8(at your option) any later version.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18//! The `toolforge` crate provides helper functions for various tasks on
19//! [Toolforge](https://toolforge.org/).
20//!
21//! ## Constructing a User-Agent
22//! The `user_agent!` macro generates a tool-specific `User-Agent` header in
23//! compliance with [Wikimedia's User-Agent policy](https://meta.wikimedia.org/wiki/User-Agent_policy).
24//! The first and only required parameter is the tool's name. Optional second
25//! and third parameters can be overrides to the url and email, respectively.
26//!
27//! ### Example
28//! ```rust
29//! const USER_AGENT: &str = toolforge::user_agent!("mycooltool");
30//!
31//! assert_eq!(
32//! USER_AGENT,
33//! "https://mycooltool.toolforge.org/ tools.mycooltool@toolforge.org"
34//! );
35//! ```
36//!
37//! ## WikiReplica connection helpers
38//! The `connection_info!` macro builds the URL string to connect to [Wiki Replicas](https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database)
39//! for use with the [`mysql_async`](https://docs.rs/mysql_async) crate. It
40//! should also work with the [`mysql`](https://docs.rs/mysql/) crate.
41//! Specifically, it follows the [Toolforge connection handling policy](https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database#Connection_handling_policy).
42//!
43//! The first argument is the database name, with or without a `_p` suffix.
44//! The second (optional) argument is which cluster to connect to, either
45//! `WEB` (default) or `ANALYTICS`.
46//!
47//! ### Example
48//! ```rust
49//! # #[cfg(feature = "mysql")]
50//! # {
51//! use mysql_async::Pool;
52//! # async fn demo() -> toolforge::Result<()> {
53//! let pool = Pool::new(
54//! toolforge::connection_info!("enwiki", WEB)
55//! .expect("unable to load db config")
56//! .to_string()
57//! .as_str(),
58//! );
59//! let mut conn = pool.get_conn().await?;
60//! # Ok(())
61//! # }
62//! # }
63//! ```
64//!
65//! ### Local development
66//! Copy your tool's `replica.my.cnf` to your local machine, saving it to
67//! `~/replica.my.cnf` and add a `local='true'` flag. For example:
68//! ```text
69//! [client]
70//! user='u####'
71//! password='...'
72//! local='true'
73//! ```
74//!
75//! Then open a [SSH tunnel](https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database#SSH_tunneling_for_local_testing_which_makes_use_of_Wiki_Replica_databases)
76//! on port 3306 to the specific wiki replicas database you want to access.
77//! The `toolforge-tunnel` helper described below can simplify this.
78//!
79//! ## `WikiPool` feature
80//! `WikiPool` is a wrapper around `mysql_async::Pool` that given a database
81//! name, will connect to the appropriate backend server. It is smart enough to
82//! know which databases are on the same server so connections can be optimally
83//! reused for better performance. This is behind the `wikipool` feature.
84//!
85//! ## `toolforge-tunnel` helper
86//! The `toolforge-tunnel` tool simplifies the process of opening SSH tunnels
87//! to wiki replicas. Example usage: `toolforge-tunnel enwiki`.
88//!
89//! It can be installed via cargo: `cargo install toolforge --features=cli`.
90//! Pre-built binaries can be downloaded from [GitLab](https://gitlab.wikimedia.org/repos/mwbot-rs/toolforge/-/releases).
91//!
92//! ## Contributing
93//! `toolforge` is a part of the [`mwbot-rs` project](https://www.mediawiki.org/wiki/Mwbot-rs).
94//! We're always looking for new contributors, please [reach out](https://www.mediawiki.org/wiki/Mwbot-rs#Contributing)
95//! if you're interested!
96
97#![cfg_attr(docsrs, feature(doc_cfg))]
98#![deny(clippy::all)]
99#![deny(rustdoc::all)]
100
101#[cfg(feature = "mysql")]
102#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
103pub mod db;
104#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
105#[cfg(feature = "mysql")]
106mod error;
107#[cfg_attr(docsrs, doc(cfg(feature = "wikipool")))]
108#[cfg(feature = "wikipool")]
109pub mod pool;
110
111#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
112#[cfg(feature = "mysql")]
113pub use error::Error;
114#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
115#[cfg(feature = "mysql")]
116pub type Result<A> = std::result::Result<A, Error>;
117
118/// Generates a Tool-specific `User-Agent` header in compliance with
119/// [Wikimedia's User-Agent policy](https://meta.wikimedia.org/wiki/User-Agent_policy).
120/// The first and only required parameter is the tool's name. Optional second
121/// and third parameters can be overrides to the url and email, respectively.
122///
123/// # Example
124/// ```rust
125/// const USER_AGENT: &str = toolforge::user_agent!("mycooltool");
126///
127/// assert_eq!(
128/// USER_AGENT,
129/// "https://mycooltool.toolforge.org/ tools.mycooltool@toolforge.org"
130/// );
131/// ```
132#[macro_export]
133macro_rules! user_agent {
134 ( $tool:expr ) => {
135 concat!(
136 "https://",
137 $tool,
138 ".toolforge.org/ tools.",
139 $tool,
140 "@toolforge.org"
141 )
142 };
143 ( $tool:expr, $url:expr ) => {
144 concat!($url, " tools.", $tool, "@toolforge.org")
145 };
146 ( $tool:expr, $url:expr, $email:expr ) => {
147 concat!($url, " ", $email)
148 };
149}
150
151/// Get connection information for the [Toolforge databases](https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database).
152///
153/// The first argument is the database name, with or without a `_p` suffix.
154/// The second (optional) argument is which cluster to connect to, either
155/// `WEB` (default) or `ANALYTICS`.
156///
157/// You can also connect to the the [`meta_p`](https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database#Metadata_database)
158/// database this way.
159///
160/// ```rust
161/// # fn demo() -> toolforge::Result<()> {
162/// assert_eq!(
163/// "mysql://user:pass@enwiki.web.db.svc.eqiad.wmflabs:3306/enwiki_p".to_string(),
164/// toolforge::connection_info!("enwiki")?.to_string()
165/// );
166/// assert_eq!(
167/// "mysql://user:pass@enwiki.analytics.db.svc.eqiad.wmflabs:3306/enwiki_p".to_string(),
168/// toolforge::connection_info!(
169/// "enwiki",
170/// ANALYTICS
171/// )?.to_string()
172/// );
173/// # Ok(())
174/// # }
175/// ```
176///
177/// ## Local development
178/// It is possible to set up a SSH tunnel to the wiki replicas to allow for
179/// local testing of MySQL connections. Copy your `replica.my.cnf` to your
180/// machine, and add `local='true'` to it. With that set, this library
181/// will automatically connect to port 3306 on your local machine.
182#[cfg(feature = "mysql")]
183#[cfg_attr(docsrs, doc(cfg(feature = "mysql")))]
184#[macro_export]
185macro_rules! connection_info {
186 ( $dbname:expr ) => {
187 $crate::db::get_db_connection_info(
188 $dbname,
189 $crate::db::Cluster::WEB,
190 None,
191 )
192 };
193 ( $dbname:expr, $cluster:ident ) => {
194 $crate::db::get_db_connection_info(
195 $dbname,
196 $crate::db::Cluster::$cluster,
197 None,
198 )
199 };
200}
201
202#[cfg(test)]
203mod tests {
204 use super::user_agent;
205
206 const USER_AGENT: &str = user_agent!("foo");
207
208 #[test]
209 fn test_const_user_agent() {
210 assert_eq!(
211 USER_AGENT,
212 "https://foo.toolforge.org/ tools.foo@toolforge.org"
213 );
214 }
215
216 #[test]
217 fn test_user_agent() {
218 assert_eq!(
219 user_agent!("foo"),
220 "https://foo.toolforge.org/ tools.foo@toolforge.org"
221 );
222 assert_eq!(
223 user_agent!("foo", "https://example.org/"),
224 "https://example.org/ tools.foo@toolforge.org"
225 );
226 assert_eq!(
227 user_agent!("foo", "https://example.org/", "tool@example.org"),
228 "https://example.org/ tool@example.org"
229 );
230 }
231}