diff --git a/.lock b/.lock new file mode 100644 index 00000000..e69de29b diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/borderlands_launcher/all.html b/borderlands_launcher/all.html new file mode 100644 index 00000000..e80957ae --- /dev/null +++ b/borderlands_launcher/all.html @@ -0,0 +1 @@ +
pub enum Error {
+ Reqwest(Error),
+ Io(Error),
+ ProstDecode(DecodeError),
+}
Library error type
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub enum Game {
+ Borderlands,
+ Borderlands2,
+ BorderlandsPresequel,
+}
The game type.
+Borderlands 3 does not follow this protocol, so it is not included.
+clone_to_uninit
)clone_to_uninit
)Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read moreRedirecting to ../../borderlands_launcher/struct.AvailableData.html...
+ + + \ No newline at end of file diff --git a/borderlands_launcher/protos/struct.DynamicContentUpdateInfo.html b/borderlands_launcher/protos/struct.DynamicContentUpdateInfo.html new file mode 100644 index 00000000..6e90334b --- /dev/null +++ b/borderlands_launcher/protos/struct.DynamicContentUpdateInfo.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../borderlands_launcher/struct.DynamicContentUpdateInfo.html...
+ + + \ No newline at end of file diff --git a/borderlands_launcher/protos/struct.NewsStory.html b/borderlands_launcher/protos/struct.NewsStory.html new file mode 100644 index 00000000..b80c93c1 --- /dev/null +++ b/borderlands_launcher/protos/struct.NewsStory.html @@ -0,0 +1,11 @@ + + + + +Redirecting to ../../borderlands_launcher/struct.NewsStory.html...
+ + + \ No newline at end of file diff --git a/borderlands_launcher/sidebar-items.js b/borderlands_launcher/sidebar-items.js new file mode 100644 index 00000000..758401a5 --- /dev/null +++ b/borderlands_launcher/sidebar-items.js @@ -0,0 +1 @@ +window.SIDEBAR_ITEMS = {"enum":["Error","Game"],"struct":["AvailableData","Client","DynamicContentUpdateInfo","NewsStory"]}; \ No newline at end of file diff --git a/borderlands_launcher/struct.AvailableData.html b/borderlands_launcher/struct.AvailableData.html new file mode 100644 index 00000000..deca48c2 --- /dev/null +++ b/borderlands_launcher/struct.AvailableData.html @@ -0,0 +1,36 @@ +pub struct AvailableData {
+ pub news_info: Option<DynamicContentUpdateInfo>,
+ pub background_info: Option<DynamicContentUpdateInfo>,
+ pub news_stories: Vec<NewsStory>,
+}
news_info: Option<DynamicContentUpdateInfo>
§background_info: Option<DynamicContentUpdateInfo>
§news_stories: Vec<NewsStory>
source
. Read moreself
. Read moreself
.self
and other
values to be equal, and is used
+by ==
.clone_to_uninit
)Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct Client {
+ pub client: Client,
+}
An API client for the borderlands launcher apis
+client: Client
The http client
+Get launcher content.
+lang_code
parameterlang_code
must be lowercase.
Valid options are:
+Specifically for Borderlands 2,
+Specifically for Borderlands Presequel,
+is_qa
parameterThis argument with toggle whether a request will be made to a different url, presumable the Q/A server. +This server is not active or accessible, so this parameter should be false. +This parameter has no effect for Borderlands.
+Currently, only the Borderlands 2 game api still is functional.
+clone_to_uninit
)Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct DynamicContentUpdateInfo {
+ pub last_updated: i32,
+ pub data_url: String,
+}
last_updated: i32
§data_url: String
source
. Read moreself
. Read moreself
.self
and other
values to be equal, and is used
+by ==
.clone_to_uninit
)Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct NewsStory {
+ pub headline: String,
+ pub date: i32,
+ pub url: String,
+ pub full_story: String,
+}
headline: String
§date: i32
§url: String
§full_story: String
self
. Read moreself
.clone_to_uninit
)Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read moreU::from(self)
.\nCalls U::from(self)
.\nCalls U::from(self)
.\nCalls U::from(self)
.\nCalls U::from(self)
.\nCalls U::from(self)
.\nMake a new client")
\ No newline at end of file
diff --git a/search.desc/shift_client/shift_client-desc-0-.js b/search.desc/shift_client/shift_client-desc-0-.js
new file mode 100644
index 00000000..f6f3df2a
--- /dev/null
+++ b/search.desc/shift_client/shift_client-desc-0-.js
@@ -0,0 +1 @@
+searchState.loadedDescShard("shift_client", 0, "Login the client\nRead a string from the console.\nRead a (Y/N) from the console.")
\ No newline at end of file
diff --git a/search.desc/shift_orcz/shift_orcz-desc-0-.js b/search.desc/shift_orcz/shift_orcz-desc-0-.js
new file mode 100644
index 00000000..2f756477
--- /dev/null
+++ b/search.desc/shift_orcz/shift_orcz-desc-0-.js
@@ -0,0 +1 @@
+searchState.loadedDescShard("shift_orcz", 0, "Client\nContains the error value\nBorderlands Games\nContains the success value\nLibrary Error Type\nLibrary Result Type\nReqwest HTTP Error\nError Parsing a Table\na tokio task failed\nReturns the argument unchanged.\nReturns the argument unchanged.\nReturns the argument unchanged.\nGet the shift codes for a given game\nCalls U::from(self)
.\nCalls U::from(self)
.\nCalls U::from(self)
.\nCheck whether this game is bl\nCheck whether this game is bl2\nCheck whether this game is bl3\nMake a new Client
.\nA Shift Code\nAn invalid code\nError that may occur while parsing a Code from an element\nMissing code\nA valid code\nGet this code as a mutable string\nGet this code as a string\nReturns the argument unchanged.\nReturns the argument unchanged.\nCalls U::from(self)
.\nCalls U::from(self)
.\nIs this code expired?\nIs this code valid?\nError that may occur while parsing a ShiftCode from an …\nInvalid code\nThe final parsed date is invalid\nInvalid day\nFailed to parse an issue date\nAn error occured while parsing an issue date.\nInvalid month\nInvalid month integer\nInvalid month string\nInvalid year\nMissing day\nMissing expiration\nMissing Issue Date\nMissing month\nMissing PC Code\nMissing Playstaion Code\nMissing rewards\nMissing the source\nMissing PC Code\nMissing the year\nA shift code table entry wrapper\nFailed to parse as it was an unknown format.\nReturns the argument unchanged.\nReturns the argument unchanged.\nReturns the argument unchanged.\nCalls U::from(self)
.\nCalls U::from(self)
.\nCalls U::from(self)
.\nThe issue date.\nThe pc code\nThe ps code\nThe rewards\nThe source\nThe xbox code\nThe date that could not be parsed\nThe issue date error")
\ No newline at end of file
diff --git a/settings.html b/settings.html
new file mode 100644
index 00000000..870cc4f8
--- /dev/null
+++ b/settings.html
@@ -0,0 +1 @@
+pub(crate) async fn async_main() -> Result<()>
pub(crate) async fn auto_loop(client: &Client)
pub(crate) async fn login_client() -> Client
Login the client
+pub(crate) fn main() -> Result<()>
pub(crate) async fn manual_loop(client: &Client)
pub(crate) async fn redeem_code(
+ client: &Client,
+ rewards_page: &RewardsPage,
+ code: &str,
+) -> Result<()>
pub(crate) async fn redeem_form(
+ client: &Client,
+ form: &RewardForm,
+) -> Result<()>
pub fn input() -> String
Read a string from the console.
+pub fn input_yn() -> bool
Read a (Y/N) from the console.
+Redirecting to ../../shift_orcz/struct.Client.html...
+ + + \ No newline at end of file diff --git a/shift_orcz/code/enum.Code.html b/shift_orcz/code/enum.Code.html new file mode 100644 index 00000000..746f7dda --- /dev/null +++ b/shift_orcz/code/enum.Code.html @@ -0,0 +1,33 @@ +pub enum Code {
+ Valid(String),
+ Expired(String),
+}
A Shift Code
+clone_to_uninit
)Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub enum FromElementError {
+ MissingCode,
+}
Error that may occur while parsing a Code from an element
+Missing code
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub enum Game {
+ Borderlands,
+ Borderlands2,
+ BorderlandsPreSequel,
+ Borderlands3,
+}
Borderlands Games
+clone_to_uninit
)clone_to_uninit
)Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub enum OrczError {
+ Reqwest(Error),
+ TableParse(ExtractShiftCodesError),
+ TokioJoin(JoinError),
+}
Library Error Type
+Reqwest HTTP Error
+Error Parsing a Table
+This is usually a library error; update this lib.
+a tokio task failed
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read moreRedirecting to ../../shift_orcz/enum.Game.html...
+ + + \ No newline at end of file diff --git a/shift_orcz/index.html b/shift_orcz/index.html new file mode 100644 index 00000000..e750bddf --- /dev/null +++ b/shift_orcz/index.html @@ -0,0 +1 @@ +pub const PC_CODE_INDEX: usize = 0;
pub const PLAYSTATION_CODE_INDEX: usize = 1;
pub const XBOX_CODE_INDEX: usize = 2;
pub enum FromElementError {
+ MissingSource,
+ MissingRewards,
+ MissingIssueDate,
+ MissingExpiration,
+ MissingPcCode,
+ MissingPlaystationCode,
+ MissingXboxCode,
+ InvalidIssueDate {
+ date: Box<str>,
+ error: InvalidIssueDateError,
+ },
+ InvalidCode(FromElementError),
+}
Error that may occur while parsing a ShiftCode from an element
+Missing the source
+Missing rewards
+Missing Issue Date
+Missing expiration
+Missing PC Code
+Missing Playstaion Code
+Missing PC Code
+Failed to parse an issue date
+error: InvalidIssueDateError
The issue date error
+Invalid code
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub enum InvalidIssueDateError {
+ UnknownFormat,
+ MissingYear,
+ MissingMonth,
+ MissingDay,
+ InvalidYear(ParseIntError),
+ InvalidMonthInteger(ParseIntError),
+ InvalidMonth(ComponentRange),
+ InvalidMonthString(Box<str>),
+ InvalidDay(ParseIntError),
+ InvalidDate(ComponentRange),
+}
An error occured while parsing an issue date.
+Failed to parse as it was an unknown format.
+Missing the year
+Missing month
+Missing day
+Invalid year
+Invalid month integer
+Invalid month
+Invalid month string
+Invalid day
+The final parsed date is invalid
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct ShiftCode {
+ pub source: String,
+ pub issue_date: Option<Date>,
+ pub rewards: String,
+ pub pc: Code,
+ pub playstation: Code,
+ pub xbox: Code,
+}
A shift code table entry wrapper
+TODO: Needs overhaul to properly support bl3 instead of mashing bl3 into bl2-like stats
+source: String
The source
+issue_date: Option<Date>
The issue date.
+If None, it is unknown.
+rewards: String
The rewards
+pc: Code
The pc code
+playstation: Code
The ps code
+xbox: Code
The xbox code
+Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read morepub struct Client { /* private fields */ }
Client
+clone_to_uninit
)Subscriber
to this type, returning a
+[WithDispatch
] wrapper. Read more1 +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 +
#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct AvailableData {
+ #[prost(message, optional, tag = "1")]
+ pub news_info: ::core::option::Option<DynamicContentUpdateInfo>,
+ #[prost(message, optional, tag = "2")]
+ pub background_info: ::core::option::Option<DynamicContentUpdateInfo>,
+ #[prost(message, repeated, tag = "3")]
+ pub news_stories: ::prost::alloc::vec::Vec<NewsStory>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct DynamicContentUpdateInfo {
+ #[prost(int32, tag = "1")]
+ pub last_updated: i32,
+ #[prost(string, tag = "2")]
+ pub data_url: ::prost::alloc::string::String,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct NewsStory {
+ #[prost(string, tag = "1")]
+ pub headline: ::prost::alloc::string::String,
+ #[prost(int32, tag = "2")]
+ pub date: i32,
+ #[prost(string, tag = "3")]
+ pub url: ::prost::alloc::string::String,
+ #[prost(string, tag = "4")]
+ pub full_story: ::prost::alloc::string::String,
+}
+
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 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +
/// Generated protobuf structs
+mod protos {
+ include!("generated/launcher.rs");
+}
+
+pub use self::protos::*;
+use async_compression::tokio::bufread::GzipDecoder;
+use bytes::BytesMut;
+use futures_util::TryStreamExt;
+use prost::Message;
+use tokio::io::AsyncReadExt;
+use tokio_util::io::StreamReader;
+
+/// Library error type
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// Reqwest Error
+ #[error(transparent)]
+ Reqwest(#[from] reqwest::Error),
+
+ /// I/O Error
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+
+ /// Protobuf decode error
+ #[error(transparent)]
+ ProstDecode(#[from] prost::DecodeError),
+}
+
+/// The game type.
+///
+/// Borderlands 3 does not follow this protocol, so it is not included.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Game {
+ Borderlands,
+ Borderlands2,
+ BorderlandsPresequel,
+}
+
+/// An API client for the borderlands launcher apis
+#[derive(Debug, Clone)]
+pub struct Client {
+ /// The http client
+ pub client: reqwest::Client,
+}
+
+impl Client {
+ /// Make a new client
+ pub fn new() -> Self {
+ Self {
+ client: reqwest::Client::new(),
+ }
+ }
+
+ /// Get launcher content.
+ ///
+ /// # The `lang_code` parameter
+ /// `lang_code` must be lowercase.
+ ///
+ /// Valid options are:
+ /// * "int" for english. This is the default.
+ /// * "deu" for germany
+ /// * "esn" for spanish
+ /// * "fra" for france
+ /// * "ita" for italy
+ /// * "jpn" for japan
+ ///
+ ///
+ /// Specifically for Borderlands 2,
+ /// * "twn" for chinese. Note that the chinese code, "cht", is mapped to "twn", or taiwan. Cute.
+ /// * "kor" for korean
+ ///
+ /// Specifically for Borderlands Presequel,
+ /// * "rus" for russian
+ /// * "kor" for korean
+ ///
+ /// # The `is_qa` parameter
+ /// This argument with toggle whether a request will be made to a different url, presumable the Q/A server.
+ /// This server is not active or accessible, so this parameter should be false.
+ /// This parameter has no effect for Borderlands.
+ ///
+ /// # Supported Games
+ /// Currently, only the Borderlands 2 game api still is functional.
+ pub async fn get_launcher_content(
+ &self,
+ game: Game,
+ is_qa: bool,
+ lang_code: &str,
+ ) -> Result<AvailableData, Error> {
+ let qa_str = if is_qa { "-qa" } else { "" };
+
+ let url = match game {
+ Game::Borderlands => {
+ format!("http://gbx.parnic.com/launcher/LauncherContent.{lang_code}.wpb")
+ }
+ Game::Borderlands2 => {
+ format!("https://cdn{qa_str}.services.gearboxsoftware.com/sparktms/willow2/pc/steam/launcher/LauncherContent.{lang_code}.wpb")
+ }
+ Game::BorderlandsPresequel => {
+ format!("http://cdn{qa_str}.services.gearboxsoftware.com/sparktms/cork/pc/steam/launcher/LauncherContent.{lang_code}.wpb")
+ }
+ };
+
+ let response = self.client.get(url).send().await?.error_for_status()?;
+ let stream = response
+ .bytes_stream()
+ .map_err(|error| std::io::Error::new(std::io::ErrorKind::Other, error));
+ let reader = StreamReader::new(stream);
+ let mut decoder = GzipDecoder::new(reader);
+
+ let mut buffer = BytesMut::new();
+ while decoder.read_buf(&mut buffer).await? != 0 {}
+
+ Ok(AvailableData::decode(buffer)?)
+ }
+}
+
+impl Default for Client {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const LANG_CODES: &[&str] = &["int", "deu", "esn", "ita", "jpn", "kor", "twn", "rus"];
+
+ /// Appears to have been taken down
+ #[tokio::test]
+ #[ignore]
+ async fn get_launcher_content_bl() {
+ let client = Client::new();
+ for lang_code in LANG_CODES {
+ dbg!(&lang_code);
+ let available_data = client
+ .get_launcher_content(Game::Borderlands, false, lang_code)
+ .await
+ .expect("failed to get launcher content");
+
+ dbg!(available_data);
+ }
+ }
+
+ #[tokio::test]
+ async fn get_launcher_content_bl2() {
+ let client = Client::new();
+
+ for lang_code in LANG_CODES {
+ dbg!(&lang_code);
+
+ if *lang_code == "rus" {
+ continue;
+ }
+
+ let available_data = client
+ .get_launcher_content(Game::Borderlands2, false, lang_code)
+ .await
+ .expect("failed to get launcher content");
+ dbg!(available_data);
+ }
+ }
+
+ /// Appears to have been taken down
+ #[tokio::test]
+ #[ignore]
+ async fn get_launcher_content_blps() {
+ let client = Client::new();
+ for lang_code in LANG_CODES {
+ dbg!(&lang_code);
+ let available_data = client
+ .get_launcher_content(Game::BorderlandsPresequel, false, lang_code)
+ .await
+ .expect("failed to get launcher content");
+
+ dbg!(available_data);
+ }
+ }
+}
+
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 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +
use crate::{
+ error::{ShiftError, ShiftResult},
+ types::{
+ rewards::{AlertNotice, CodeRedemptionJson, CodeRedemptionPage, RewardForm, RewardsPage},
+ AccountPage, HomePage,
+ },
+};
+use scraper::Html;
+use std::{
+ sync::{Arc, RwLock},
+ time::Duration,
+};
+
+const HOME_URL: &str = "https://shift.gearboxsoftware.com/home";
+const SESSIONS_URL: &str = "https://shift.gearboxsoftware.com/sessions";
+const CODE_REDEMPTIONS_URL: &str = "https://shift.gearboxsoftware.com/code_redemptions";
+const REWARDS_URL: &str = "https://shift.gearboxsoftware.com/rewards";
+const ENTITLEMENT_OFFER_CODES_URL: &str =
+ "https://shift.gearboxsoftware.com/entitlement_offer_codes";
+
+#[derive(Clone)]
+pub struct Client {
+ client: reqwest::Client,
+ client_data: Arc<RwLock<ClientData>>,
+}
+
+impl Client {
+ /// Make a new shift client, not logged in
+ pub fn new(email: String, password: String) -> Self {
+ Self {
+ client: reqwest::Client::builder()
+ .cookie_store(true)
+ .build()
+ .expect("failed to build reqwest client"),
+
+ client_data: Arc::new(RwLock::new(ClientData { email, password })),
+ }
+ }
+
+ /// Get the home page. Does not need authentication.
+ async fn get_home_page(&self) -> ShiftResult<HomePage> {
+ let res = self.client.get(HOME_URL).send().await?;
+ let home_page = res_to_html_transform(res, |html| Ok(HomePage::from_html(&html)?)).await?;
+ Ok(home_page)
+ }
+
+ /// Logs in and allows making other requests
+ pub async fn login(&self) -> ShiftResult<AccountPage> {
+ let home_page = self.get_home_page().await?;
+
+ let req = {
+ let lock = self.client_data.read().expect("client data poisoned");
+ self.client.post(SESSIONS_URL).form(&[
+ ("utf8", "✓"),
+ ("authenticity_token", &home_page.csrf_token),
+ ("user[email]", &lock.email),
+ ("user[password]", &lock.password),
+ ("commit", "SIGN IN"),
+ ])
+ };
+ let res = req.send().await?;
+
+ match res.url().as_str() {
+ "https://shift.gearboxsoftware.com/home?redirect_to=false" => {
+ return Err(ShiftError::IncorrectEmailOrPassword);
+ }
+ "https://shift.gearboxsoftware.com/account" => {}
+ url => {
+ return Err(ShiftError::InvalidRedirect(url.into()));
+ }
+ }
+
+ let account_page =
+ res_to_html_transform(res, |html| Ok(AccountPage::from_html(&html)?)).await?;
+ Ok(account_page)
+ }
+
+ /// Get the [`RewardsPage`]
+ pub async fn get_rewards_page(&self) -> ShiftResult<RewardsPage> {
+ let res = self.client.get(REWARDS_URL).send().await?;
+ let page = res_to_html_transform(res, |html| Ok(RewardsPage::from_html(&html)?)).await?;
+ Ok(page)
+ }
+
+ pub async fn get_reward_forms(
+ &self,
+ rewards_page: &RewardsPage,
+ code: &str,
+ ) -> ShiftResult<Vec<RewardForm>> {
+ let res = self
+ .client
+ .get(ENTITLEMENT_OFFER_CODES_URL)
+ .query(&[("code", code)])
+ .header("X-CSRF-Token", &rewards_page.csrf_token)
+ .header("X-Requested-With", "XMLHttpRequest")
+ .send()
+ .await?
+ .error_for_status()?;
+
+ let body = res.text().await?;
+
+ match body.as_str().trim() {
+ "This SHiFT code has expired" => return Err(ShiftError::ExpiredShiftCode),
+ "This SHiFT code does not exist" => return Err(ShiftError::NonExistentShiftCode),
+ "This code is not available for your account" => {
+ return Err(ShiftError::UnavailableShiftCode)
+ }
+ _ => {}
+ }
+
+ let forms = tokio::task::spawn_blocking(move || {
+ let html = Html::parse_document(body.as_str());
+ RewardForm::from_html(&html)
+ })
+ .await??;
+
+ Ok(forms)
+ }
+
+ /// Redeem a code
+ pub async fn redeem(&self, form: &RewardForm) -> ShiftResult<Option<CodeRedemptionJson>> {
+ let res = self
+ .client
+ .post(CODE_REDEMPTIONS_URL)
+ .form(&form)
+ .send()
+ .await?;
+
+ let url = res.url().as_str();
+ if url.starts_with(REWARDS_URL) {
+ let page =
+ res_to_html_transform(res, |html| Ok(RewardsPage::from_html(&html)?)).await?;
+ let alert_notice = page.alert_notice.ok_or(ShiftError::MissingAlertNotice)?;
+ match alert_notice {
+ AlertNotice::ShiftCodeAlreadyRedeemed => {
+ return Err(ShiftError::ShiftCodeAlreadyRedeemed);
+ }
+ AlertNotice::LaunchShiftGame => {
+ return Err(ShiftError::LaunchShiftGame);
+ }
+ AlertNotice::ShiftCodeRedeemed => {
+ return Ok(None);
+ }
+ AlertNotice::ShiftCodeRedeemFail => {
+ return Err(ShiftError::ShiftCodeRedeemFail);
+ }
+ }
+ }
+
+ if !url.starts_with(CODE_REDEMPTIONS_URL) {
+ return Err(ShiftError::InvalidRedirect(url.into()));
+ }
+
+ let page =
+ res_to_html_transform(res, |html| Ok(CodeRedemptionPage::from_html(&html)?)).await?;
+
+ let res = loop {
+ let json: CodeRedemptionJson = self
+ .client
+ .get(&page.check_redemption_status_url)
+ .header("X-CSRF-Token", &page.csrf_token)
+ .header("X-Requested-With", "XMLHttpRequest")
+ .send()
+ .await?
+ .error_for_status()?
+ .json()
+ .await?;
+
+ tokio::time::sleep(Duration::from_secs(2)).await;
+
+ if !json.in_progress() {
+ break json;
+ }
+ };
+
+ Ok(Some(res))
+ }
+}
+
+/// Client data
+struct ClientData {
+ email: String,
+ password: String,
+}
+
+/// Convert a response to html, then feed it to the given transform function
+async fn res_to_html_transform<F, T>(res: reqwest::Response, f: F) -> ShiftResult<T>
+where
+ F: Fn(Html) -> ShiftResult<T> + Send + 'static,
+ T: Send + 'static,
+{
+ let text = res.error_for_status()?.text().await?;
+ let ret = tokio::task::spawn_blocking(move || f(Html::parse_document(text.as_str()))).await??;
+ Ok(ret)
+}
+
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 +
/// Library result type
+pub type ShiftResult<T> = Result<T, ShiftError>;
+
+pub type RewardsPageError = crate::types::rewards::rewards_page::FromHtmlError;
+pub type InvalidHomePageError = crate::types::home_page::FromHtmlError;
+pub type RewardFormError = crate::types::rewards::reward_form::FromHtmlError;
+pub type InvalidCodeRedemptionPageError =
+ crate::types::rewards::code_redemption_page::FromHtmlError;
+pub type InvalidAccountPage = crate::types::account_page::FromHtmlError;
+
+/// The library error type
+#[derive(Debug, thiserror::Error)]
+pub enum ShiftError {
+ /// Reqwest HTTP error
+ #[error(transparent)]
+ Reqwest(#[from] reqwest::Error),
+
+ /// The password or email was incorrect
+ #[error("incorrect email or password")]
+ IncorrectEmailOrPassword,
+
+ /// Invalid HTTP Redirect
+ #[error("invalid http redirect '{0}'")]
+ InvalidRedirect(String),
+
+ /// Json Error
+ #[error(transparent)]
+ Json(#[from] serde_json::Error),
+
+ /// Invalid Rewards page
+ #[error("invalid rewards page")]
+ InvalidRewardsPage(#[from] RewardsPageError),
+ /// Invalid Home page
+ #[error("invalid home page")]
+ InvalidHomePage(#[from] InvalidHomePageError),
+ /// Invalid RewardForm
+ #[error("invalid reward form")]
+ InvalidRewardForm(#[from] RewardFormError),
+ /// Invalid code redemption page
+ #[error("invalid code redemption page")]
+ InvalidCodeRedemptionPage(#[from] InvalidCodeRedemptionPageError),
+ /// Invalid Account page
+ #[error("invalid account page")]
+ InvalidAccountPage(#[from] InvalidAccountPage),
+
+ /// Missing alert notice
+ #[error("missing alert notice")]
+ MissingAlertNotice,
+
+ /// NonExistentShiftCode
+ #[error("non-existent shift code")]
+ NonExistentShiftCode,
+ /// Expired ShiftCode
+ #[error("expired shift code")]
+ ExpiredShiftCode,
+ /// Unavailable ShiftCode
+ #[error("unavailable shift code")]
+ UnavailableShiftCode,
+
+ /// Shift Code already redeemed
+ #[error("shift code already redeemed")]
+ ShiftCodeAlreadyRedeemed,
+ /// Launch shift game
+ #[error("launch a shift game to redeem the code")]
+ LaunchShiftGame,
+ /// ShiftCode Redeem Fail
+ #[error("failed to redeem shift code")]
+ ShiftCodeRedeemFail,
+
+ /// Failed to join tokio task
+ #[error("tokio task join error")]
+ TokioJoin(#[from] tokio::task::JoinError),
+}
+
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 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +
mod util;
+
+use crate::util::input;
+use crate::util::input_yn;
+use anyhow::{ensure, Context};
+use reqwest::StatusCode;
+use shift_client::{types::RewardsPage, Client, RewardForm, ShiftError};
+use shift_orcz::Game;
+use std::time::Duration;
+
+async fn manual_loop(client: &Client) {
+ let rewards_page = match client.get_rewards_page().await {
+ Ok(p) => p,
+ Err(error) => {
+ eprintln!("Failed to get rewards page, got error: {error:#?}");
+ return;
+ }
+ };
+
+ loop {
+ print!("Enter a shift code, or type 'exit' to exit: ");
+ let code = input();
+
+ if code.to_lowercase() == "exit" {
+ println!("Exiting...");
+ break;
+ }
+
+ if let Err(error) = redeem_code(client, &rewards_page, code.trim()).await {
+ eprintln!("{error:?}");
+ eprintln!();
+ }
+ }
+}
+
+async fn auto_loop(client: &Client) {
+ let orcz_client = shift_orcz::Client::new();
+
+ let rewards_page = match client.get_rewards_page().await {
+ Ok(p) => p,
+ Err(error) => {
+ eprintln!("Failed to get rewards page, got error: {error:#?}");
+ return;
+ }
+ };
+
+ let game = loop {
+ println!("What game do you want to target? (bl, bl2, blps, bl3)");
+ let choice = input().to_lowercase();
+ println!();
+
+ match choice.as_str() {
+ "bl" => break Game::Borderlands,
+ "bl2" => break Game::Borderlands2,
+ "blps" => break Game::BorderlandsPreSequel,
+ "bl3" => break Game::Borderlands3,
+ data => {
+ eprintln!("\"{data}\" is not a valid option");
+ eprintln!();
+ }
+ }
+ };
+ println!("Targeting game: {game:?}");
+
+ let codes = match orcz_client
+ .get_shift_codes(game)
+ .await
+ .context("Failed to get shift codes")
+ {
+ Ok(codes) => codes,
+ Err(error) => {
+ eprintln!("{error:?}");
+ eprintln!();
+ return;
+ }
+ };
+
+ for shift_code in codes {
+ for code in shift_code
+ .get_code_array()
+ .iter()
+ .filter(|code| code.is_valid())
+ .take(1)
+ // IDK how other platforms redeem, seems buggy so I'll focus on PC
+ {
+ println!("Code: {}", code.as_str());
+ println!("Reward: {}", shift_code.rewards);
+ println!(
+ "Issue Date: {}",
+ shift_code
+ .issue_date
+ .map(|date| format!("{date}"))
+ .unwrap_or_else(|| "Unknown".into())
+ );
+ println!("Source: {}", shift_code.source);
+ println!();
+
+ println!("Redeeming code...");
+ loop {
+ match redeem_code(client, &rewards_page, code.as_str().trim()).await {
+ Ok(()) => {
+ break;
+ }
+ Err(error) => {
+ if let Some(ShiftError::Reqwest(e)) = error.downcast_ref::<ShiftError>() {
+ if let Some(StatusCode::TOO_MANY_REQUESTS) = e.status() {
+ eprintln!("Encountered 429, backing off for 60 seconds...");
+ tokio::time::sleep(Duration::from_secs(60)).await;
+ continue;
+ }
+ }
+
+ eprintln!("{error:?}");
+ eprintln!();
+
+ break;
+ }
+ }
+ }
+ println!();
+ }
+ }
+}
+
+async fn redeem_code(
+ client: &Client,
+ rewards_page: &RewardsPage,
+ code: &str,
+) -> anyhow::Result<()> {
+ let forms = client
+ .get_reward_forms(rewards_page, code.trim())
+ .await
+ .context("failed to get code")?;
+
+ ensure!(!forms.is_empty(), "No forms retrieved for code");
+
+ for form in forms {
+ match redeem_form(client, &form).await {
+ Ok(()) => {}
+ Err(error) => {
+ eprintln!("{error:?}");
+ eprintln!();
+ }
+ }
+ }
+
+ Ok(())
+}
+
+async fn redeem_form(client: &Client, form: &RewardForm) -> anyhow::Result<()> {
+ let redeem_response = client.redeem(form).await.context("Failed to redeem code")?;
+ println!("Redeemed code!");
+
+ if let Some(redeem_response) = redeem_response {
+ if let Some(text) = redeem_response.text {
+ println!("Response: {text}");
+ } else {
+ eprintln!("Unknown Redeem Response: {redeem_response:#?}");
+ }
+ }
+
+ Ok(())
+}
+
+fn main() -> anyhow::Result<()> {
+ let tokio_rt = tokio::runtime::Builder::new_multi_thread()
+ .enable_all()
+ .build()?;
+ tokio_rt.block_on(async_main())
+}
+
+async fn async_main() -> anyhow::Result<()> {
+ let client = login_client().await;
+
+ println!("Would you like to use manual mode? (Y/N)");
+ if input_yn() {
+ println!("Using manual mode...");
+ manual_loop(&client).await;
+ } else {
+ println!("Using auto mode...");
+ auto_loop(&client).await;
+ }
+
+ Ok(())
+}
+
+/// Login the client
+async fn login_client() -> Client {
+ loop {
+ // get credentials
+ print!("Email: ");
+ let email = input();
+ print!("Password: ");
+ let password = input();
+ println!();
+
+ // make client
+ let client = Client::new(email.trim().into(), password.trim().into());
+
+ // try to log in
+ match client.login().await.context("Login failed") {
+ Ok(page) => {
+ println!("Logged in!");
+ println!();
+
+ println!("Email: {}", page.email);
+ println!("Display Name: {}", page.display_name);
+ println!("First Name: {}", page.first_name);
+ println!();
+
+ break client;
+ }
+ Err(error) => {
+ eprintln!("{error:?}");
+ eprintln!();
+ }
+ }
+ }
+}
+
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 +
use crate::util::extract_csrf_token;
+use scraper::{Html, Selector};
+
+/// Error that may occur while parsing an [`AccountPage`].
+#[derive(Debug, thiserror::Error)]
+pub enum FromHtmlError {
+ /// Missing csrf token
+ #[error("missing csrf token")]
+ MissingCsrfToken,
+ /// Missing email
+ #[error("missing email")]
+ MissingEmail,
+ /// Missing display name
+ #[error("missing display name")]
+ MissingDisplayName,
+ /// Missing first name
+ #[error("missing first name")]
+ MissingFirstName,
+}
+
+/// The account page
+#[derive(Debug)]
+pub struct AccountPage {
+ /// The csrf token
+ pub csrf_token: String,
+ /// The email
+ pub email: String,
+ /// The display name
+ pub display_name: String,
+ /// The first name
+ pub first_name: String,
+}
+
+impl AccountPage {
+ /// Parse an [`AccountPage`] from html
+ pub(crate) fn from_html(html: &Html) -> Result<Self, FromHtmlError> {
+ let csrf_token = extract_csrf_token(html)
+ .ok_or(FromHtmlError::MissingCsrfToken)?
+ .to_string();
+
+ let email = get_text_by_id(html, "current_email")
+ .ok_or(FromHtmlError::MissingEmail)?
+ .to_string();
+
+ let display_name = get_text_by_id(html, "current_display_name")
+ .ok_or(FromHtmlError::MissingDisplayName)?
+ .to_string();
+
+ let first_name = get_text_by_id(html, "current_first_name")
+ .ok_or(FromHtmlError::MissingFirstName)?
+ .to_string();
+
+ Ok(Self {
+ csrf_token,
+ email,
+ display_name,
+ first_name,
+ })
+ }
+}
+
+fn get_text_by_id<'a>(html: &'a Html, id: &str) -> Option<&'a str> {
+ let selector = Selector::parse(&format!("#{id}")).ok()?;
+ let element = html.select(&selector).next()?;
+ element.text().next()
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const SAMPLE_1: &str = include_str!("../../test_data/account.html");
+
+ #[test]
+ fn sample_1() {
+ let html = Html::parse_document(SAMPLE_1);
+ let page = AccountPage::from_html(&html).expect("invalid account page");
+ dbg!(page);
+ }
+}
+
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 +
use crate::util::extract_csrf_token;
+use scraper::Html;
+
+/// Error that may occur while parsing a [`HomePage`].
+#[derive(Debug, thiserror::Error)]
+pub enum FromHtmlError {
+ /// Missing csrf token
+ #[error("missing csrf token")]
+ MissingCsrfToken,
+}
+
+/// The home page
+#[derive(Debug)]
+pub struct HomePage {
+ /// The csrf token
+ pub csrf_token: String,
+}
+
+impl HomePage {
+ /// Parse a [`HomePage`] from html
+ pub(crate) fn from_html(html: &Html) -> Result<Self, FromHtmlError> {
+ let csrf_token = extract_csrf_token(html)
+ .ok_or(FromHtmlError::MissingCsrfToken)?
+ .to_string();
+
+ Ok(Self { csrf_token })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const SAMPLE_1: &str = include_str!("../../test_data/home.html");
+
+ #[test]
+ fn sample_1() {
+ let html = Html::parse_document(SAMPLE_1);
+ let _page = HomePage::from_html(&html).expect("failed to parse home page");
+ }
+}
+
pub mod code_redemption_json;
+pub mod code_redemption_page;
+pub mod reward_form;
+pub mod rewards_page;
+
+pub use self::{
+ code_redemption_json::CodeRedemptionJson,
+ code_redemption_page::CodeRedemptionPage,
+ reward_form::RewardForm,
+ rewards_page::{AlertNotice, RewardsPage},
+};
+
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 +
use std::collections::HashMap;
+
+#[derive(Debug, serde::Deserialize)]
+pub struct CodeRedemptionJson {
+ pub in_progress: Option<bool>,
+ pub text: Option<String>,
+ pub url: Option<String>,
+
+ #[serde(flatten)]
+ pub unknown: HashMap<String, serde_json::Value>,
+}
+
+impl CodeRedemptionJson {
+ pub fn in_progress(&self) -> bool {
+ self.in_progress.unwrap_or(false)
+ }
+
+ pub fn is_success(&self) -> bool {
+ match self.text.as_deref() {
+ Some("Failed to redeem your SHiFT code") => false,
+ Some("Your code was successfully redeemed") => true,
+ Some(_s) => false,
+ None => false,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const SAMPLE_1: &str = include_str!("../../../test_data/code_redemption_json_1.json");
+ const SAMPLE_2: &str = include_str!("../../../test_data/code_redemption_json_2.json");
+
+ #[test]
+ fn sample_1() {
+ let _json: CodeRedemptionJson = serde_json::from_str(SAMPLE_1).unwrap();
+ }
+
+ #[test]
+ fn sample_2() {
+ let _json: CodeRedemptionJson = serde_json::from_str(SAMPLE_2).unwrap();
+ }
+}
+
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 +
use crate::util::extract_csrf_token;
+use scraper::{Html, Selector};
+
+/// Error that may occur while parsing a [`CodeRedemptionPage`]
+#[derive(Debug, thiserror::Error)]
+pub enum FromHtmlError {
+ /// Missing csrf token
+ #[error("missing csrf token")]
+ MissingCsrfToken,
+
+ /// Missing CheckRedemptionStatusUrl
+ #[error("missing check redemption status")]
+ MissingCheckRedemptionStatusUrl,
+}
+
+/// A code redemption page
+#[derive(Debug)]
+pub struct CodeRedemptionPage {
+ /// The csrf token
+ pub csrf_token: String,
+
+ /// The check_redemption_status_url
+ pub check_redemption_status_url: String,
+}
+
+impl CodeRedemptionPage {
+ pub(crate) fn from_html(html: &Html) -> Result<Self, FromHtmlError> {
+ let csrf_token = extract_csrf_token(html)
+ .ok_or(FromHtmlError::MissingCsrfToken)?
+ .to_string();
+
+ let check_redemption_status_url_selector =
+ Selector::parse("#check_redemption_status[data-url]")
+ .expect("invalid check_redemption_status_url selector");
+ let check_redemption_status_url = html
+ .select(&check_redemption_status_url_selector)
+ .next()
+ .and_then(|element| element.value().attr("data-url"))
+ .map(|url| format!("https://shift.gearboxsoftware.com{url}"))
+ .ok_or(FromHtmlError::MissingCheckRedemptionStatusUrl)?;
+
+ Ok(Self {
+ csrf_token,
+ check_redemption_status_url,
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const SAMPLE_1: &str = include_str!("../../../test_data/code_redemption_page_html.html");
+
+ #[test]
+ fn sample_1() {
+ let html = Html::parse_document(SAMPLE_1);
+ let _page = CodeRedemptionPage::from_html(&html).expect("invalid code redemption page");
+ }
+}
+
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 +
use crate::util::extract_by_name;
+use scraper::{ElementRef, Html, Selector};
+
+/// Error that may occur while parsing a [`RewardForm`].
+pub type FromHtmlError = FromElementError;
+
+/// Error that may occur while parsing a [`RewardForm`].
+#[derive(Debug, thiserror::Error)]
+pub enum FromElementError {
+ /// ?
+ #[error("missing utf8")]
+ MissingUtf8,
+ /// Missing auth token
+ #[error("missing auth token")]
+ MissingAuthToken,
+ /// Missing code
+ #[error("missing code")]
+ MissingCode,
+ /// Missing check
+ #[error("missing check")]
+ MissingCheck,
+ /// Missing service
+ #[error("missing service")]
+ MissingService,
+ /// Missing title
+ #[error("missing title")]
+ MissingTitle,
+ /// Missing commit
+ #[error("missing commit")]
+ MissingCommit,
+}
+
+/// The reward form
+#[derive(Debug, serde::Serialize)]
+pub struct RewardForm {
+ utf8: String,
+ authenticity_token: String,
+
+ #[serde(rename = "archway_code_redemption[code]")]
+ archway_code_redemption_code: String,
+
+ #[serde(rename = "archway_code_redemption[check]")]
+ archway_code_redemption_check: String,
+
+ #[serde(rename = "archway_code_redemption[service]")]
+ archway_code_redemption_service: String,
+
+ #[serde(rename = "archway_code_redemption[title]")]
+ archway_code_redemption_title: String,
+
+ commit: String,
+}
+
+impl RewardForm {
+ /// Parse a [`RewardForm`] from html
+ pub(crate) fn from_html(html: &Html) -> Result<Vec<Self>, FromHtmlError> {
+ let form_selector = Selector::parse("form").expect("invalid form selector");
+ html.select(&form_selector)
+ .map(RewardForm::from_element)
+ .collect()
+ }
+
+ /// Parse a [`RewardForm`] from a node.
+ pub(crate) fn from_element(element: ElementRef) -> Result<Self, FromElementError> {
+ let utf8 = extract_by_name(element, "utf8")
+ .ok_or(FromElementError::MissingUtf8)?
+ .to_string();
+
+ let authenticity_token = extract_by_name(element, "authenticity_token")
+ .ok_or(FromElementError::MissingAuthToken)?
+ .to_string();
+
+ let archway_code_redemption_code =
+ extract_by_name(element, "archway_code_redemption[code]")
+ .ok_or(FromElementError::MissingCode)?
+ .to_string();
+
+ let archway_code_redemption_check =
+ extract_by_name(element, "archway_code_redemption[check]")
+ .ok_or(FromElementError::MissingCheck)?
+ .to_string();
+
+ let archway_code_redemption_service =
+ extract_by_name(element, "archway_code_redemption[service]")
+ .ok_or(FromElementError::MissingService)?
+ .to_string();
+
+ let archway_code_redemption_title =
+ extract_by_name(element, "archway_code_redemption[title]")
+ .ok_or(FromElementError::MissingTitle)?
+ .to_string();
+
+ let commit = extract_by_name(element, "commit")
+ .ok_or(FromElementError::MissingCommit)?
+ .to_string();
+
+ Ok(Self {
+ utf8,
+ authenticity_token,
+ archway_code_redemption_code,
+ archway_code_redemption_check,
+ archway_code_redemption_service,
+ archway_code_redemption_title,
+ commit,
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ const SAMPLE_1: &str = include_str!("../../../test_data/reward_form_1.html");
+
+ #[test]
+ fn sampe_1() {
+ let doc = Html::parse_document(SAMPLE_1);
+ let _form = RewardForm::from_html(&doc).expect("Failed to parse reward form");
+ }
+}
+
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 +
use crate::util::extract_csrf_token;
+use once_cell::sync::Lazy;
+use scraper::{ElementRef, Html, Selector};
+
+static ALERT_NOTICE_SELECTOR: Lazy<Selector> =
+ Lazy::new(|| Selector::parse(".alert.notice p").expect("invalid ALERT_NOTICE_SELECTOR"));
+
+/// Error that may occur while parsing a [`RewardsPage`].
+#[derive(Debug, thiserror::Error)]
+pub enum FromHtmlError {
+ /// MissingCsrfToken
+ #[error("missing csrf token")]
+ MissingCsrfToken,
+
+ /// Invalid alert notice
+ #[error("invalid alert notice")]
+ InvalidAlertNotice(#[from] FromElementError),
+}
+
+/// The rewards page
+#[derive(Debug)]
+pub struct RewardsPage {
+ /// The csrf token
+ pub csrf_token: String,
+
+ /// An alert notice
+ pub alert_notice: Option<AlertNotice>,
+}
+
+impl RewardsPage {
+ /// Parse a [`RewardsPage`] from html
+ pub(crate) fn from_html(html: &Html) -> Result<Self, FromHtmlError> {
+ let csrf_token = extract_csrf_token(html)
+ .ok_or(FromHtmlError::MissingCsrfToken)?
+ .to_string();
+
+ let alert_notice = html
+ .select(&ALERT_NOTICE_SELECTOR)
+ .next()
+ .map(AlertNotice::from_element)
+ .transpose()?;
+
+ Ok(Self {
+ csrf_token,
+ alert_notice,
+ })
+ }
+}
+
+/// An error that may occur while parsing an AlertNotice
+#[derive(Debug, thiserror::Error)]
+pub enum FromElementError {
+ /// Missing text
+ #[error("missing text")]
+ MissingText,
+
+ #[error("unknown text '{0}'")]
+ UnknownText(String),
+}
+
+#[derive(Debug)]
+pub enum AlertNotice {
+ /// A shift code was already redeemed
+ ShiftCodeAlreadyRedeemed,
+
+ /// Launch a shift game to redeem codes
+ LaunchShiftGame,
+
+ /// Redeemed a shift code
+ ShiftCodeRedeemed,
+
+ /// Redeem failed
+ ShiftCodeRedeemFail,
+}
+
+impl AlertNotice {
+ /// Parse an alert notice from an element
+ fn from_element(el: ElementRef) -> Result<Self, FromElementError> {
+ let text = el.text().next().ok_or(FromElementError::MissingText)?;
+
+ match text {
+ "This SHiFT code has already been redeemed" => Ok(Self::ShiftCodeAlreadyRedeemed),
+ "To continue to redeem SHiFT codes, please launch a SHiFT-enabled title first!" => {
+ Ok(Self::LaunchShiftGame)
+ }
+ "Your code was successfully redeemed" => Ok(Self::ShiftCodeRedeemed),
+ "Failed to redeem your SHiFT code" => Ok(Self::ShiftCodeRedeemFail),
+ _ => Err(FromElementError::UnknownText(text.to_string())),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const SAMPLE_1: &str = include_str!("../../../test_data/rewards_page_1.html");
+
+ #[test]
+ fn sample_1() {
+ let html = Html::parse_document(SAMPLE_1);
+ let _page = RewardsPage::from_html(&html).unwrap();
+ }
+}
+
use std::io::Write;
+
+/// Read a string from the console.
+pub fn input() -> String {
+ let mut s = String::new();
+ let _ = std::io::stdout().flush();
+ let _ = std::io::stdin().read_line(&mut s);
+ if let Some('\n') = s.chars().next_back() {
+ s.pop();
+ }
+ if let Some('\r') = s.chars().next_back() {
+ s.pop();
+ }
+ s
+}
+
+/// Read a (Y/N) from the console.
+pub fn input_yn() -> bool {
+ matches!(input().chars().next(), Some('Y') | Some('y'))
+}
+
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 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +
use crate::{Game, OrczResult, ShiftCode};
+use once_cell::sync::Lazy;
+use scraper::{Html, Selector};
+use time::Date;
+
+/// Client
+#[derive(Default, Clone)]
+pub struct Client {
+ client: reqwest::Client,
+}
+
+impl Client {
+ /// Make a new [`Client`].
+ pub fn new() -> Self {
+ Client {
+ client: reqwest::Client::new(),
+ }
+ }
+
+ /// Make a [`Html`] from the provided url str, processing it with f on a threadpool.
+ ///
+ /// # Errors
+ /// Returns an error if the website could not be fetched
+ async fn get_html<F, T>(&self, url: &str, f: F) -> OrczResult<T>
+ where
+ F: Fn(Html) -> OrczResult<T> + Send + 'static,
+ T: Send + 'static,
+ {
+ let text = self
+ .client
+ .get(url)
+ .send()
+ .await?
+ .error_for_status()?
+ .text()
+ .await?;
+ tokio::task::spawn_blocking(move || f(Html::parse_document(text.as_str()))).await?
+ }
+
+ /// Get the shift codes for a given game
+ pub async fn get_shift_codes(&self, game: Game) -> OrczResult<Vec<ShiftCode>> {
+ self.get_html(game.page_url(), move |html| {
+ Ok(extract_shift_codes(&html, game)?)
+ })
+ .await
+ }
+}
+
+/// Error that may occur while extracting shift codes from html
+#[derive(Debug, thiserror::Error)]
+pub enum ExtractShiftCodesError {
+ /// Missing table
+ #[error("missing table")]
+ MissingTable,
+
+ /// Missing table body
+ #[error("missing table body")]
+ MissingTableBody,
+
+ /// Invalid lookup date
+ #[error("invalid lookup date `{0}`")]
+ InvalidLookupDate(String, #[source] time::error::Parse),
+
+ /// Invalid shift code
+ #[error("invalid shift code")]
+ InvalidShiftCode(#[from] crate::shift_code::FromElementError),
+
+ /// Unknown error occured
+ #[error("unknown error")]
+ Unknown,
+}
+
+/// Extract shift codes from html
+fn extract_shift_codes(html: &Html, game: Game) -> Result<Vec<ShiftCode>, ExtractShiftCodesError> {
+ static TABLE_BODY_ROW_SELECTOR: Lazy<Selector> =
+ Lazy::new(|| Selector::parse("table tbody tr").expect("invalid TABLE_BODY_ROW_SELECTOR"));
+ const SAME_CODE_AS_FORMAT: &[time::format_description::FormatItem<'static>] =
+ time::macros::format_description!("[ month padding:none ]/[ day ]/[ year ]");
+
+ let mut ret = if game.is_bl3() {
+ html.select(&TABLE_BODY_ROW_SELECTOR)
+ .skip(1) // Skip title
+ .map(ShiftCode::from_element_bl3)
+ .collect::<Result<Vec<_>, _>>()?
+ } else {
+ html.select(&TABLE_BODY_ROW_SELECTOR)
+ .skip(1) // Skip title
+ .map(ShiftCode::from_element)
+ .collect::<Result<Vec<_>, _>>()?
+ };
+
+ // I hate this
+ for i in 0..ret.len() {
+ for code_index in 0..ret[i].get_code_array().len() {
+ // Fix "Same as {date}" entries...
+ {
+ let code_str = ret[i].get_code_array()[code_index].as_str();
+ if let Some(code_str) = code_str.strip_prefix("Same code as ") {
+ let lookup_date = Date::parse(code_str, SAME_CODE_AS_FORMAT).map_err(|e| {
+ ExtractShiftCodesError::InvalidLookupDate(code_str.to_string(), e)
+ })?;
+
+ let mut resolved_code = ret
+ .iter()
+ .find(|el| el.issue_date == Some(lookup_date))
+ .ok_or(ExtractShiftCodesError::Unknown)?
+ .get_code(code_index)
+ .ok_or(ExtractShiftCodesError::Unknown)?
+ .as_str()
+ .to_string();
+
+ std::mem::swap(
+ ret[i]
+ .get_code_mut(code_index)
+ .ok_or(ExtractShiftCodesError::Unknown)?
+ .as_mut_string(),
+ &mut resolved_code,
+ );
+ }
+ }
+
+ // Fix "See Key Above" entries
+ {
+ let code_str = ret[i].get_code_array()[code_index].as_str();
+ if code_str.starts_with("See Key Above") {
+ let mut resolved_code = ret[i - 1]
+ .get_code(code_index)
+ .ok_or(ExtractShiftCodesError::Unknown)?
+ .as_str()
+ .to_string();
+
+ std::mem::swap(
+ ret[i]
+ .get_code_mut(code_index)
+ .ok_or(ExtractShiftCodesError::Unknown)?
+ .as_mut_string(),
+ &mut resolved_code,
+ );
+ }
+ }
+ }
+ }
+
+ Ok(ret)
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const BL_DOC: &str = include_str!("../test_data/bl-keys.html");
+ const BL2_DOC: &str = include_str!("../test_data/bl2-keys.html");
+ const BLPS_DOC: &str = include_str!("../test_data/blps-keys.html");
+ const BL3_DOC: &str = include_str!("../test_data/bl3-keys.html");
+
+ #[test]
+ fn parse_bl() {
+ let html = Html::parse_document(BL_DOC);
+ let codes = extract_shift_codes(&html, Game::Borderlands).expect("bl parse failed");
+ assert!(!codes.is_empty());
+ dbg!(codes);
+ }
+
+ #[test]
+ fn parse_bl2() {
+ let html = Html::parse_document(BL2_DOC);
+ let codes = extract_shift_codes(&html, Game::Borderlands2).expect("bl2 parse failed");
+ assert!(!codes.is_empty());
+ dbg!(codes);
+ }
+
+ #[test]
+ fn parse_blps() {
+ let html = Html::parse_document(BLPS_DOC);
+ let codes =
+ extract_shift_codes(&html, Game::BorderlandsPreSequel).expect("blps parse failed");
+ assert!(!codes.is_empty());
+ dbg!(codes);
+ }
+
+ #[test]
+ fn parse_bl3() {
+ let html = Html::parse_document(BL3_DOC);
+ let codes = extract_shift_codes(&html, Game::Borderlands3).expect("bl3 parse failed");
+ assert!(!codes.is_empty());
+ dbg!(&codes);
+ }
+}
+
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 +
use once_cell::sync::Lazy;
+use scraper::{ElementRef, Selector};
+
+static SPAN_SELECTOR: Lazy<Selector> =
+ Lazy::new(|| Selector::parse("span[style=\"color:red\"]").expect("invalid span selector"));
+
+/// Error that may occur while parsing a Code from an element
+#[derive(Debug, thiserror::Error)]
+pub enum FromElementError {
+ /// Missing code
+ #[error("missing code")]
+ MissingCode,
+}
+
+/// A Shift Code
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum Code {
+ /// A valid code
+ Valid(String),
+
+ /// An invalid code
+ Expired(String),
+}
+
+impl Code {
+ /// Is this code valid?
+ pub fn is_valid(&self) -> bool {
+ match self {
+ Self::Valid(_) => true,
+ Self::Expired(_) => false,
+ }
+ }
+
+ /// Is this code expired?
+ pub fn is_expired(&self) -> bool {
+ match self {
+ Self::Valid(_) => false,
+ Self::Expired(_) => true,
+ }
+ }
+
+ /// Get this code as a string
+ pub fn as_str(&self) -> &str {
+ match self {
+ Self::Valid(s) => s.as_str(),
+ Self::Expired(s) => s.as_str(),
+ }
+ }
+
+ /// Get this code as a mutable string
+ pub fn as_mut_string(&mut self) -> &mut String {
+ match self {
+ Self::Valid(s) => s,
+ Self::Expired(s) => s,
+ }
+ }
+
+ /// Parse this code from an element
+ pub(crate) fn from_element(element: ElementRef) -> Result<Self, FromElementError> {
+ let maybe_span = element.select(&SPAN_SELECTOR).next();
+
+ match maybe_span {
+ Some(el) => {
+ let code = el
+ .text()
+ .next()
+ .ok_or(FromElementError::MissingCode)?
+ .trim();
+ Ok(Self::Expired(code.into()))
+ }
+ None => {
+ let code = element
+ .text()
+ .next()
+ .ok_or(FromElementError::MissingCode)?
+ .trim();
+ Ok(Self::Valid(code.into()))
+ }
+ }
+ }
+}
+
+impl std::fmt::Display for Code {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}", self.as_str())
+ }
+}
+
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 +
/// Borderlands Games
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum Game {
+ Borderlands,
+ Borderlands2,
+ BorderlandsPreSequel,
+ Borderlands3,
+}
+
+impl Game {
+ /// Get the orcz page url
+ pub(crate) fn page_url(&self) -> &'static str {
+ match self {
+ Self::Borderlands => "http://orcz.com/Borderlands:_Golden_Key",
+ Self::Borderlands2 => "http://orcz.com/borderlands_2:_Golden_Key",
+ Self::BorderlandsPreSequel => "http://orcz.com/Borderlands_Pre-Sequel:_Shift_Codes",
+ Self::Borderlands3 => "http://orcz.com/Borderlands_3:_Shift_Codes",
+ }
+ }
+
+ /// Check whether this game is bl
+ pub fn is_bl(self) -> bool {
+ matches!(self, Self::Borderlands)
+ }
+
+ /// Check whether this game is bl2
+ pub fn is_bl2(self) -> bool {
+ matches!(self, Self::Borderlands2)
+ }
+
+ /// Check whether this game is bl3
+ pub fn is_bl3(self) -> bool {
+ matches!(self, Self::Borderlands3)
+ }
+}
+
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 +
mod client;
+pub mod code;
+mod game;
+pub mod shift_code;
+
+pub use crate::{client::Client, code::Code, game::Game, shift_code::ShiftCode};
+
+/// Library Result Type
+pub type OrczResult<T> = Result<T, OrczError>;
+
+/// Library Error Type
+#[derive(Debug, thiserror::Error)]
+pub enum OrczError {
+ /// Reqwest HTTP Error
+ #[error("reqwest http error")]
+ Reqwest(#[from] reqwest::Error),
+
+ /// Error Parsing a Table
+ ///
+ /// This is usually a library error; update this lib.
+ #[error("invalid table")]
+ TableParse(#[from] self::client::ExtractShiftCodesError),
+
+ /// a tokio task failed
+ #[error("tokio task join failed")]
+ TokioJoin(#[from] tokio::task::JoinError),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[tokio::test]
+ async fn it_works_bl() {
+ let client = Client::new();
+ let codes = client.get_shift_codes(Game::Borderlands).await.unwrap();
+ dbg!(codes);
+ }
+
+ #[tokio::test]
+ async fn it_works_bl2() {
+ let client = Client::new();
+ let codes = client.get_shift_codes(Game::Borderlands2).await.unwrap();
+ dbg!(codes);
+ }
+
+ #[tokio::test]
+ async fn it_works_blps() {
+ let client = Client::new();
+ let codes = client
+ .get_shift_codes(Game::BorderlandsPreSequel)
+ .await
+ .unwrap();
+ dbg!(codes);
+ }
+
+ #[tokio::test]
+ async fn it_works_bl3() {
+ let client = Client::new();
+ let codes = client.get_shift_codes(Game::Borderlands3).await.unwrap();
+ dbg!(codes);
+ }
+}
+
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 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +
use crate::code::Code;
+use once_cell::sync::Lazy;
+use regex::Regex;
+use scraper::ElementRef;
+use scraper::Selector;
+use time::Date;
+
+pub const PC_CODE_INDEX: usize = 0;
+pub const PLAYSTATION_CODE_INDEX: usize = 1;
+pub const XBOX_CODE_INDEX: usize = 2;
+
+static TD_SELECTOR: Lazy<Selector> = Lazy::new(|| Selector::parse("td").unwrap());
+static DATE_REGEX: Lazy<Regex> = Lazy::new(|| {
+ Regex::new(r"((?P<year_1>\d{4}).(?P<month_1>\d{2}).(?P<day_1>\d{1,2}))|((?P<month_1_1>\d{1,2}).(?P<day_1_1>\d{2}).(?P<year_1_1>\d{4}))|((?P<month_2>[[:alpha:]]*?) *(?P<day_2>\d{1,2})(th|nd)? ?,? (?P<year_2>\d{4}))").unwrap()
+});
+
+/// Error that may occur while parsing a ShiftCode from an element
+#[derive(Debug, thiserror::Error)]
+pub enum FromElementError {
+ /// Missing the source
+ #[error("missing source")]
+ MissingSource,
+
+ /// Missing rewards
+ #[error("missing rewards")]
+ MissingRewards,
+
+ /// Missing Issue Date
+ #[error("missing issue date")]
+ MissingIssueDate,
+
+ /// Missing expiration
+ #[error("missing expiration")]
+ MissingExpiration,
+
+ /// Missing PC Code
+ #[error("missing pc code")]
+ MissingPcCode,
+
+ /// Missing Playstaion Code
+ #[error("missing playstation code")]
+ MissingPlaystationCode,
+
+ /// Missing PC Code
+ #[error("missing xbox code")]
+ MissingXboxCode,
+
+ /// Failed to parse an issue date
+ #[error("invalid issue date")]
+ InvalidIssueDate {
+ /// The date that could not be parsed
+ date: Box<str>,
+
+ /// The issue date error
+ #[source]
+ error: InvalidIssueDateError,
+ },
+
+ /// Invalid code
+ #[error("invalid code")]
+ InvalidCode(#[from] crate::code::FromElementError),
+}
+
+/// An error occured while parsing an issue date.
+#[derive(Debug, thiserror::Error)]
+pub enum InvalidIssueDateError {
+ /// Failed to parse as it was an unknown format.
+ #[error("unknown format")]
+ UnknownFormat,
+
+ /// Missing the year
+ #[error("missing year")]
+ MissingYear,
+
+ /// Missing month
+ #[error("missing month")]
+ MissingMonth,
+
+ /// Missing day
+ #[error("missing day")]
+ MissingDay,
+
+ /// Invalid year
+ #[error("invalid year")]
+ InvalidYear(#[source] std::num::ParseIntError),
+
+ /// Invalid month integer
+ #[error("invalid month integer")]
+ InvalidMonthInteger(#[source] std::num::ParseIntError),
+
+ /// Invalid month
+ #[error("invalid month")]
+ InvalidMonth(#[source] time::error::ComponentRange),
+
+ /// Invalid month string
+ #[error("invalid month \"{0}\"")]
+ InvalidMonthString(Box<str>),
+
+ /// Invalid day
+ #[error("invalid day")]
+ InvalidDay(#[source] std::num::ParseIntError),
+
+ /// The final parsed date is invalid
+ #[error("invalid date")]
+ InvalidDate(#[source] time::error::ComponentRange),
+}
+
+/// A shift code table entry wrapper
+///
+/// TODO: Needs overhaul to properly support bl3 instead of mashing bl3 into bl2-like stats
+#[derive(Debug)]
+pub struct ShiftCode {
+ /// The source
+ pub source: String,
+
+ /// The issue date.
+ ///
+ /// If None, it is unknown.
+ pub issue_date: Option<Date>,
+
+ /// The rewards
+ pub rewards: String,
+
+ /// The pc code
+ pub pc: Code,
+
+ /// The ps code
+ pub playstation: Code,
+
+ /// The xbox code
+ pub xbox: Code,
+}
+
+impl ShiftCode {
+ /// Parse a [`ShiftCode`] from a non-bl3 element.
+ pub(crate) fn from_element(row: ElementRef) -> Result<Self, FromElementError> {
+ let mut iter = row.select(&TD_SELECTOR);
+
+ let source = iter
+ .next()
+ .and_then(|el| el.text().next())
+ .ok_or(FromElementError::MissingSource)?
+ .trim()
+ .to_string();
+
+ let rewards = process_rewards_node(iter.next().ok_or(FromElementError::MissingRewards)?);
+
+ let issue_date_str = iter
+ .next()
+ .and_then(|el| el.text().next())
+ .ok_or(FromElementError::MissingIssueDate)?
+ .trim();
+ let issue_date = parse_issue_date_str(issue_date_str).map_err(|error| {
+ FromElementError::InvalidIssueDate {
+ date: issue_date_str.into(),
+ error,
+ }
+ })?;
+ let _expiration = iter.next().ok_or(FromElementError::MissingExpiration)?;
+
+ let pc = Code::from_element(iter.next().ok_or(FromElementError::MissingPcCode)?)?;
+ let playstation = Code::from_element(
+ iter.next()
+ .ok_or(FromElementError::MissingPlaystationCode)?,
+ )?;
+ let xbox = Code::from_element(iter.next().ok_or(FromElementError::MissingXboxCode)?)?;
+
+ Ok(ShiftCode {
+ source,
+ issue_date,
+ rewards,
+
+ pc,
+ playstation,
+ xbox,
+ })
+ }
+
+ /// Parse a [`ShiftCode`] from a bl3 element.
+ pub(crate) fn from_element_bl3(row: ElementRef) -> Result<Self, FromElementError> {
+ let mut iter = row.select(&TD_SELECTOR);
+
+ let source = iter
+ .next()
+ .and_then(|el| el.text().next())
+ .ok_or(FromElementError::MissingSource)?
+ .trim()
+ .to_string();
+
+ let rewards = process_rewards_node(iter.next().ok_or(FromElementError::MissingRewards)?);
+
+ let issue_date_str = iter
+ .next()
+ .and_then(|el| el.text().next())
+ .ok_or(FromElementError::MissingIssueDate)?
+ .trim()
+ .replace("??", "1"); // TODO: Consider making day optional
+ let issue_date = parse_issue_date_str(&issue_date_str).map_err(|error| {
+ FromElementError::InvalidIssueDate {
+ date: issue_date_str.into(),
+ error,
+ }
+ })?;
+
+ let _expiration = iter.next().ok_or(FromElementError::MissingExpiration)?;
+
+ // Kinda hacky, maybe introduce a new error type
+ let code = Code::from_element(iter.next().ok_or(FromElementError::MissingPcCode)?)?;
+
+ let pc = code.clone();
+ let playstation = code.clone();
+ let xbox = code;
+
+ Ok(ShiftCode {
+ source,
+ issue_date,
+ rewards,
+
+ pc,
+ playstation,
+ xbox,
+ })
+ }
+
+ pub fn get_code_array(&self) -> [&Code; 3] {
+ [&self.pc, &self.playstation, &self.xbox]
+ }
+
+ pub fn get_code_array_mut(&mut self) -> [&mut Code; 3] {
+ [&mut self.pc, &mut self.playstation, &mut self.xbox]
+ }
+
+ pub fn get_code(&self, index: usize) -> Option<&Code> {
+ Some(self.get_code_array()[index])
+ }
+
+ pub fn get_code_mut(&mut self, index: usize) -> Option<&mut Code> {
+ Some(self.get_code_array_mut()[index])
+ }
+}
+
+fn process_rewards_node(element: ElementRef) -> String {
+ let mut ret =
+ element
+ .text()
+ .filter(|text| !text.trim().is_empty())
+ .fold(String::new(), |mut ret, el| {
+ ret += el;
+ ret += " ";
+ ret
+ });
+
+ while ret.chars().next_back().map_or(false, |c| c.is_whitespace()) {
+ ret.pop();
+ }
+
+ ret
+}
+
+fn parse_issue_date_str(issue_date_str: &str) -> Result<Option<time::Date>, InvalidIssueDateError> {
+ if issue_date_str == "Unknown" {
+ return Ok(None);
+ }
+
+ let captures = DATE_REGEX
+ .captures(issue_date_str)
+ .ok_or(InvalidIssueDateError::UnknownFormat)?;
+ let y = captures
+ .name("year_1")
+ .or_else(|| captures.name("year_1_1"))
+ .or_else(|| captures.name("year_2"))
+ .ok_or(InvalidIssueDateError::MissingYear)?
+ .as_str()
+ .parse::<i32>()
+ .map_err(InvalidIssueDateError::InvalidYear)?;
+ let m = captures
+ .name("month_1")
+ .or_else(|| captures.name("month_1_1"))
+ .map(|month| {
+ let month = month
+ .as_str()
+ .parse::<u8>()
+ .map_err(InvalidIssueDateError::InvalidMonthInteger)?;
+ let month: time::Month = month
+ .try_into()
+ .map_err(InvalidIssueDateError::InvalidMonth)?;
+ Ok(month)
+ })
+ .or_else(|| {
+ captures.name("month_2").map(|month| match month.as_str() {
+ "January" | "Jan" => Ok(time::Month::January),
+ "February" | "Feb" => Ok(time::Month::February),
+ "March" | "Mar" => Ok(time::Month::March),
+ "April" | "Apr" => Ok(time::Month::April),
+ "May" => Ok(time::Month::May),
+ "June" | "Jun" => Ok(time::Month::June),
+ "July" | "Jul" => Ok(time::Month::July),
+ "August" | "Aug" => Ok(time::Month::August),
+ "September" | "Sep" | "Sept" => Ok(time::Month::September),
+ "October" | "Oct" => Ok(time::Month::October),
+ "November" | "Nov" => Ok(time::Month::November),
+ "December" | "Dec" => Ok(time::Month::December),
+ month => Err(InvalidIssueDateError::InvalidMonthString(month.into())),
+ })
+ })
+ .ok_or(InvalidIssueDateError::MissingMonth)??;
+ let d = captures
+ .name("day_1")
+ .or_else(|| captures.name("day_1_1"))
+ .or_else(|| captures.name("day_2"))
+ .ok_or(InvalidIssueDateError::MissingDay)?
+ .as_str()
+ .parse::<u8>()
+ .map_err(InvalidIssueDateError::InvalidDay)?;
+
+ let date = Date::from_calendar_date(y, m, d).map_err(InvalidIssueDateError::InvalidDate)?;
+
+ Ok(Some(date))
+}
+
fn:
) to \
+ restrict the search to a given item kind.","Accepted kinds are: fn
, mod
, struct
, \
+ enum
, trait
, type
, macro
, \
+ and const
.","Search functions by type signature (e.g., vec -> usize
or \
+ -> vec
or String, enum:Cow -> bool
)","You can look for items with an exact name by putting double quotes around \
+ your request: \"string\"
","Look for functions that accept or return \
+ slices and \
+ arrays by writing \
+ square brackets (e.g., -> [u8]
or [] -> Option
)","Look for items inside another one by searching for a path: vec::Vec
",].map(x=>""+x+"
").join("");const div_infos=document.createElement("div");addClass(div_infos,"infos");div_infos.innerHTML="${value.replaceAll(" ", " ")}
`}else{error[index]=value}});output+=`Takes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
occur, a\ncontainer with the values of each Result
is returned.
Here is an example which increments every integer in a vector,\nchecking for overflow:
\n\nlet v = vec![1, 2];\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32|\n x.checked_add(1).ok_or(\"Overflow!\")\n).collect();\nassert_eq!(res, Ok(vec![2, 3]));
Here is another example that tries to subtract one from another list\nof integers, this time checking for underflow:
\n\nlet v = vec![1, 2, 0];\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32|\n x.checked_sub(1).ok_or(\"Underflow!\")\n).collect();\nassert_eq!(res, Err(\"Underflow!\"));
Here is a variation on the previous example, showing that no\nfurther elements are taken from iter
after the first Err
.
let v = vec![3, 2, 1, 10];\nlet mut shared = 0;\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32| {\n shared += x;\n x.checked_sub(2).ok_or(\"Underflow!\")\n}).collect();\nassert_eq!(res, Err(\"Underflow!\"));\nassert_eq!(shared, 6);
Since the third element caused an underflow, no further elements were taken,\nso the final value of shared
is 6 (= 3 + 2 + 1
), not 16.
try_trait_v2
)Residual
type. Read moreReturns a consuming iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let x: Result<u32, &str> = Ok(5);\nlet v: Vec<u32> = x.into_iter().collect();\nassert_eq!(v, [5]);\n\nlet x: Result<u32, &str> = Err(\"nothing!\");\nlet v: Vec<u32> = x.into_iter().collect();\nassert_eq!(v, []);
self
and other
) and is used by the <=
\noperator. Read moreTakes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
\noccur, the product of all elements is returned.
This multiplies each number in a vector of strings,\nif a string could not be parsed the operation returns Err
:
let nums = vec![\"5\", \"10\", \"1\", \"2\"];\nlet total: Result<usize, _> = nums.iter().map(|w| w.parse::<usize>()).product();\nassert_eq!(total, Ok(100));\nlet nums = vec![\"5\", \"10\", \"one\", \"2\"];\nlet total: Result<usize, _> = nums.iter().map(|w| w.parse::<usize>()).product();\nassert!(total.is_err());
Maps a Result<&mut T, E>
to a Result<T, E>
by copying the contents of the\nOk
part.
let mut val = 12;\nlet x: Result<&mut i32, i32> = Ok(&mut val);\nassert_eq!(x, Ok(&mut 12));\nlet copied = x.copied();\nassert_eq!(copied, Ok(12));
Maps a Result<&mut T, E>
to a Result<T, E>
by cloning the contents of the\nOk
part.
let mut val = 12;\nlet x: Result<&mut i32, i32> = Ok(&mut val);\nassert_eq!(x, Ok(&mut 12));\nlet cloned = x.cloned();\nassert_eq!(cloned, Ok(12));
Transposes a Result
of an Option
into an Option
of a Result
.
Ok(None)
will be mapped to None
.\nOk(Some(_))
and Err(_)
will be mapped to Some(Ok(_))
and Some(Err(_))
.
#[derive(Debug, Eq, PartialEq)]\nstruct SomeErr;\n\nlet x: Result<Option<i32>, SomeErr> = Ok(Some(5));\nlet y: Option<Result<i32, SomeErr>> = Some(Ok(5));\nassert_eq!(x.transpose(), y);
result_flattening
)Converts from Result<Result<T, E>, E>
to Result<T, E>
#![feature(result_flattening)]\nlet x: Result<Result<&'static str, u32>, u32> = Ok(Ok(\"hello\"));\nassert_eq!(Ok(\"hello\"), x.flatten());\n\nlet x: Result<Result<&'static str, u32>, u32> = Ok(Err(6));\nassert_eq!(Err(6), x.flatten());\n\nlet x: Result<Result<&'static str, u32>, u32> = Err(6);\nassert_eq!(Err(6), x.flatten());
Flattening only removes one level of nesting at a time:
\n\n#![feature(result_flattening)]\nlet x: Result<Result<Result<&'static str, u32>, u32>, u32> = Ok(Ok(Ok(\"hello\")));\nassert_eq!(Ok(Ok(\"hello\")), x.flatten());\nassert_eq!(Ok(\"hello\"), x.flatten().flatten());
Returns true
if the result is Ok
and the value inside of it matches a predicate.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.is_ok_and(|x| x > 1), true);\n\nlet x: Result<u32, &str> = Ok(0);\nassert_eq!(x.is_ok_and(|x| x > 1), false);\n\nlet x: Result<u32, &str> = Err(\"hey\");\nassert_eq!(x.is_ok_and(|x| x > 1), false);
Returns true
if the result is Err
and the value inside of it matches a predicate.
use std::io::{Error, ErrorKind};\n\nlet x: Result<u32, Error> = Err(Error::new(ErrorKind::NotFound, \"!\"));\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), true);\n\nlet x: Result<u32, Error> = Err(Error::new(ErrorKind::PermissionDenied, \"!\"));\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), false);\n\nlet x: Result<u32, Error> = Ok(123);\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), false);
Converts from Result<T, E>
to Option<E>
.
Converts self
into an Option<E>
, consuming self
,\nand discarding the success value, if any.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.err(), None);\n\nlet x: Result<u32, &str> = Err(\"Nothing here\");\nassert_eq!(x.err(), Some(\"Nothing here\"));
Converts from &Result<T, E>
to Result<&T, &E>
.
Produces a new Result
, containing a reference\ninto the original, leaving the original in place.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.as_ref(), Ok(&2));\n\nlet x: Result<u32, &str> = Err(\"Error\");\nassert_eq!(x.as_ref(), Err(&\"Error\"));
Converts from &mut Result<T, E>
to Result<&mut T, &mut E>
.
fn mutate(r: &mut Result<i32, i32>) {\n match r.as_mut() {\n Ok(v) => *v = 42,\n Err(e) => *e = 0,\n }\n}\n\nlet mut x: Result<i32, i32> = Ok(2);\nmutate(&mut x);\nassert_eq!(x.unwrap(), 42);\n\nlet mut x: Result<i32, i32> = Err(13);\nmutate(&mut x);\nassert_eq!(x.unwrap_err(), 0);
Maps a Result<T, E>
to Result<U, E>
by applying a function to a\ncontained Ok
value, leaving an Err
value untouched.
This function can be used to compose the results of two functions.
\nPrint the numbers on each line of a string multiplied by two.
\n\nlet line = \"1\\n2\\n3\\n4\\n\";\n\nfor num in line.lines() {\n match num.parse::<i32>().map(|i| i * 2) {\n Ok(n) => println!(\"{n}\"),\n Err(..) => {}\n }\n}
Returns the provided default (if Err
), or\napplies a function to the contained value (if Ok
).
Arguments passed to map_or
are eagerly evaluated; if you are passing\nthe result of a function call, it is recommended to use map_or_else
,\nwhich is lazily evaluated.
let x: Result<_, &str> = Ok(\"foo\");\nassert_eq!(x.map_or(42, |v| v.len()), 3);\n\nlet x: Result<&str, _> = Err(\"bar\");\nassert_eq!(x.map_or(42, |v| v.len()), 42);
Maps a Result<T, E>
to U
by applying fallback function default
to\na contained Err
value, or function f
to a contained Ok
value.
This function can be used to unpack a successful result\nwhile handling an error.
\nlet k = 21;\n\nlet x : Result<_, &str> = Ok(\"foo\");\nassert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 3);\n\nlet x : Result<&str, _> = Err(\"bar\");\nassert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 42);
Maps a Result<T, E>
to Result<T, F>
by applying a function to a\ncontained Err
value, leaving an Ok
value untouched.
This function can be used to pass through a successful result while handling\nan error.
\nfn stringify(x: u32) -> String { format!(\"error code: {x}\") }\n\nlet x: Result<u32, u32> = Ok(2);\nassert_eq!(x.map_err(stringify), Ok(2));\n\nlet x: Result<u32, u32> = Err(13);\nassert_eq!(x.map_err(stringify), Err(\"error code: 13\".to_string()));
Converts from Result<T, E>
(or &Result<T, E>
) to Result<&<T as Deref>::Target, &E>
.
Coerces the Ok
variant of the original Result
via Deref
\nand returns the new Result
.
let x: Result<String, u32> = Ok(\"hello\".to_string());\nlet y: Result<&str, &u32> = Ok(\"hello\");\nassert_eq!(x.as_deref(), y);\n\nlet x: Result<String, u32> = Err(42);\nlet y: Result<&str, &u32> = Err(&42);\nassert_eq!(x.as_deref(), y);
Converts from Result<T, E>
(or &mut Result<T, E>
) to Result<&mut <T as DerefMut>::Target, &mut E>
.
Coerces the Ok
variant of the original Result
via DerefMut
\nand returns the new Result
.
let mut s = \"HELLO\".to_string();\nlet mut x: Result<String, u32> = Ok(\"hello\".to_string());\nlet y: Result<&mut str, &mut u32> = Ok(&mut s);\nassert_eq!(x.as_deref_mut().map(|x| { x.make_ascii_uppercase(); x }), y);\n\nlet mut i = 42;\nlet mut x: Result<String, u32> = Err(42);\nlet y: Result<&mut str, &mut u32> = Err(&mut i);\nassert_eq!(x.as_deref_mut().map(|x| { x.make_ascii_uppercase(); x }), y);
Returns an iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let x: Result<u32, &str> = Ok(7);\nassert_eq!(x.iter().next(), Some(&7));\n\nlet x: Result<u32, &str> = Err(\"nothing!\");\nassert_eq!(x.iter().next(), None);
Returns a mutable iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let mut x: Result<u32, &str> = Ok(7);\nmatch x.iter_mut().next() {\n Some(v) => *v = 40,\n None => {},\n}\nassert_eq!(x, Ok(40));\n\nlet mut x: Result<u32, &str> = Err(\"nothing!\");\nassert_eq!(x.iter_mut().next(), None);
Returns the contained Ok
value, consuming the self
value.
Because this function may panic, its use is generally discouraged.\nInstead, prefer to use pattern matching and handle the Err
\ncase explicitly, or call unwrap_or
, unwrap_or_else
, or\nunwrap_or_default
.
Panics if the value is an Err
, with a panic message including the\npassed message, and the content of the Err
.
let x: Result<u32, &str> = Err(\"emergency failure\");\nx.expect(\"Testing expect\"); // panics with `Testing expect: emergency failure`
We recommend that expect
messages are used to describe the reason you\nexpect the Result
should be Ok
.
let path = std::env::var(\"IMPORTANT_PATH\")\n .expect(\"env variable `IMPORTANT_PATH` should be set by `wrapper_script.sh`\");
Hint: If you’re having trouble remembering how to phrase expect\nerror messages remember to focus on the word “should” as in “env\nvariable should be set by blah” or “the given binary should be available\nand executable by the current user”.
\nFor more detail on expect message styles and the reasoning behind our recommendation please\nrefer to the section on “Common Message\nStyles” in the\nstd::error
module docs.
Returns the contained Ok
value, consuming the self
value.
Because this function may panic, its use is generally discouraged.\nInstead, prefer to use pattern matching and handle the Err
\ncase explicitly, or call unwrap_or
, unwrap_or_else
, or\nunwrap_or_default
.
Panics if the value is an Err
, with a panic message provided by the\nErr
’s value.
Basic usage:
\n\nlet x: Result<u32, &str> = Ok(2);\nassert_eq!(x.unwrap(), 2);
let x: Result<u32, &str> = Err(\"emergency failure\");\nx.unwrap(); // panics with `emergency failure`
Returns the contained Ok
value or a default
Consumes the self
argument then, if Ok
, returns the contained\nvalue, otherwise if Err
, returns the default value for that\ntype.
Converts a string to an integer, turning poorly-formed strings\ninto 0 (the default value for integers). parse
converts\na string to any other type that implements FromStr
, returning an\nErr
on error.
let good_year_from_input = \"1909\";\nlet bad_year_from_input = \"190blarg\";\nlet good_year = good_year_from_input.parse().unwrap_or_default();\nlet bad_year = bad_year_from_input.parse().unwrap_or_default();\n\nassert_eq!(1909, good_year);\nassert_eq!(0, bad_year);
Returns the contained Err
value, consuming the self
value.
Panics if the value is an Ok
, with a panic message including the\npassed message, and the content of the Ok
.
let x: Result<u32, &str> = Ok(10);\nx.expect_err(\"Testing expect_err\"); // panics with `Testing expect_err: 10`
Returns the contained Err
value, consuming the self
value.
Panics if the value is an Ok
, with a custom panic message provided\nby the Ok
’s value.
let x: Result<u32, &str> = Ok(2);\nx.unwrap_err(); // panics with `2`
let x: Result<u32, &str> = Err(\"emergency failure\");\nassert_eq!(x.unwrap_err(), \"emergency failure\");
unwrap_infallible
)Returns the contained Ok
value, but never panics.
Unlike unwrap
, this method is known to never panic on the\nresult types it is implemented for. Therefore, it can be used\ninstead of unwrap
as a maintainability safeguard that will fail\nto compile if the error type of the Result
is later changed\nto an error that can actually occur.
\nfn only_good_news() -> Result<String, !> {\n Ok(\"this is fine\".into())\n}\n\nlet s: String = only_good_news().into_ok();\nprintln!(\"{s}\");
unwrap_infallible
)Returns the contained Err
value, but never panics.
Unlike unwrap_err
, this method is known to never panic on the\nresult types it is implemented for. Therefore, it can be used\ninstead of unwrap_err
as a maintainability safeguard that will fail\nto compile if the ok type of the Result
is later changed\nto a type that can actually occur.
\nfn only_bad_news() -> Result<!, String> {\n Err(\"Oops, it failed\".into())\n}\n\nlet error: String = only_bad_news().into_err();\nprintln!(\"{error}\");
Returns res
if the result is Ok
, otherwise returns the Err
value of self
.
Arguments passed to and
are eagerly evaluated; if you are passing the\nresult of a function call, it is recommended to use and_then
, which is\nlazily evaluated.
let x: Result<u32, &str> = Ok(2);\nlet y: Result<&str, &str> = Err(\"late error\");\nassert_eq!(x.and(y), Err(\"late error\"));\n\nlet x: Result<u32, &str> = Err(\"early error\");\nlet y: Result<&str, &str> = Ok(\"foo\");\nassert_eq!(x.and(y), Err(\"early error\"));\n\nlet x: Result<u32, &str> = Err(\"not a 2\");\nlet y: Result<&str, &str> = Err(\"late error\");\nassert_eq!(x.and(y), Err(\"not a 2\"));\n\nlet x: Result<u32, &str> = Ok(2);\nlet y: Result<&str, &str> = Ok(\"different result type\");\nassert_eq!(x.and(y), Ok(\"different result type\"));
Calls op
if the result is Ok
, otherwise returns the Err
value of self
.
This function can be used for control flow based on Result
values.
fn sq_then_to_string(x: u32) -> Result<String, &'static str> {\n x.checked_mul(x).map(|sq| sq.to_string()).ok_or(\"overflowed\")\n}\n\nassert_eq!(Ok(2).and_then(sq_then_to_string), Ok(4.to_string()));\nassert_eq!(Ok(1_000_000).and_then(sq_then_to_string), Err(\"overflowed\"));\nassert_eq!(Err(\"not a number\").and_then(sq_then_to_string), Err(\"not a number\"));
Often used to chain fallible operations that may return Err
.
use std::{io::ErrorKind, path::Path};\n\n// Note: on Windows \"/\" maps to \"C:\\\"\nlet root_modified_time = Path::new(\"/\").metadata().and_then(|md| md.modified());\nassert!(root_modified_time.is_ok());\n\nlet should_fail = Path::new(\"/bad/path\").metadata().and_then(|md| md.modified());\nassert!(should_fail.is_err());\nassert_eq!(should_fail.unwrap_err().kind(), ErrorKind::NotFound);
Returns res
if the result is Err
, otherwise returns the Ok
value of self
.
Arguments passed to or
are eagerly evaluated; if you are passing the\nresult of a function call, it is recommended to use or_else
, which is\nlazily evaluated.
let x: Result<u32, &str> = Ok(2);\nlet y: Result<u32, &str> = Err(\"late error\");\nassert_eq!(x.or(y), Ok(2));\n\nlet x: Result<u32, &str> = Err(\"early error\");\nlet y: Result<u32, &str> = Ok(2);\nassert_eq!(x.or(y), Ok(2));\n\nlet x: Result<u32, &str> = Err(\"not a 2\");\nlet y: Result<u32, &str> = Err(\"late error\");\nassert_eq!(x.or(y), Err(\"late error\"));\n\nlet x: Result<u32, &str> = Ok(2);\nlet y: Result<u32, &str> = Ok(100);\nassert_eq!(x.or(y), Ok(2));
Calls op
if the result is Err
, otherwise returns the Ok
value of self
.
This function can be used for control flow based on result values.
\nfn sq(x: u32) -> Result<u32, u32> { Ok(x * x) }\nfn err(x: u32) -> Result<u32, u32> { Err(x) }\n\nassert_eq!(Ok(2).or_else(sq).or_else(sq), Ok(2));\nassert_eq!(Ok(2).or_else(err).or_else(sq), Ok(2));\nassert_eq!(Err(3).or_else(sq).or_else(err), Ok(9));\nassert_eq!(Err(3).or_else(err).or_else(err), Err(3));
Returns the contained Ok
value or a provided default.
Arguments passed to unwrap_or
are eagerly evaluated; if you are passing\nthe result of a function call, it is recommended to use unwrap_or_else
,\nwhich is lazily evaluated.
let default = 2;\nlet x: Result<u32, &str> = Ok(9);\nassert_eq!(x.unwrap_or(default), 9);\n\nlet x: Result<u32, &str> = Err(\"error\");\nassert_eq!(x.unwrap_or(default), default);
Returns the contained Ok
value, consuming the self
value,\nwithout checking that the value is not an Err
.
Calling this method on an Err
is undefined behavior.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(unsafe { x.unwrap_unchecked() }, 2);
let x: Result<u32, &str> = Err(\"emergency failure\");\nunsafe { x.unwrap_unchecked(); } // Undefined behavior!
Returns the contained Err
value, consuming the self
value,\nwithout checking that the value is not an Ok
.
Calling this method on an Ok
is undefined behavior.
let x: Result<u32, &str> = Ok(2);\nunsafe { x.unwrap_err_unchecked() }; // Undefined behavior!
let x: Result<u32, &str> = Err(\"emergency failure\");\nassert_eq!(unsafe { x.unwrap_err_unchecked() }, \"emergency failure\");
Takes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
\noccur, the sum of all elements is returned.
This sums up every integer in a vector, rejecting the sum if a negative\nelement is encountered:
\n\nlet f = |&x: &i32| if x < 0 { Err(\"Negative element found\") } else { Ok(x) };\nlet v = vec![1, 2];\nlet res: Result<i32, _> = v.iter().map(f).sum();\nassert_eq!(res, Ok(3));\nlet v = vec![1, -2];\nlet res: Result<i32, _> = v.iter().map(f).sum();\nassert_eq!(res, Err(\"Negative element found\"));
try_trait_v2
)?
when not short-circuiting.try_trait_v2
)FromResidual::from_residual
\nas part of ?
when short-circuiting. Read moretry_trait_v2
)Output
type. Read moretry_trait_v2
)?
to decide whether the operator should produce a value\n(because this returned ControlFlow::Continue
)\nor propagate a value back to the caller\n(because this returned ControlFlow::Break
). Read moreTakes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
occur, a\ncontainer with the values of each Result
is returned.
Here is an example which increments every integer in a vector,\nchecking for overflow:
\n\nlet v = vec![1, 2];\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32|\n x.checked_add(1).ok_or(\"Overflow!\")\n).collect();\nassert_eq!(res, Ok(vec![2, 3]));
Here is another example that tries to subtract one from another list\nof integers, this time checking for underflow:
\n\nlet v = vec![1, 2, 0];\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32|\n x.checked_sub(1).ok_or(\"Underflow!\")\n).collect();\nassert_eq!(res, Err(\"Underflow!\"));
Here is a variation on the previous example, showing that no\nfurther elements are taken from iter
after the first Err
.
let v = vec![3, 2, 1, 10];\nlet mut shared = 0;\nlet res: Result<Vec<u32>, &'static str> = v.iter().map(|x: &u32| {\n shared += x;\n x.checked_sub(2).ok_or(\"Underflow!\")\n}).collect();\nassert_eq!(res, Err(\"Underflow!\"));\nassert_eq!(shared, 6);
Since the third element caused an underflow, no further elements were taken,\nso the final value of shared
is 6 (= 3 + 2 + 1
), not 16.
try_trait_v2
)Residual
type. Read moreReturns a consuming iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let x: Result<u32, &str> = Ok(5);\nlet v: Vec<u32> = x.into_iter().collect();\nassert_eq!(v, [5]);\n\nlet x: Result<u32, &str> = Err(\"nothing!\");\nlet v: Vec<u32> = x.into_iter().collect();\nassert_eq!(v, []);
self
and other
) and is used by the <=
\noperator. Read moreTakes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
\noccur, the product of all elements is returned.
This multiplies each number in a vector of strings,\nif a string could not be parsed the operation returns Err
:
let nums = vec![\"5\", \"10\", \"1\", \"2\"];\nlet total: Result<usize, _> = nums.iter().map(|w| w.parse::<usize>()).product();\nassert_eq!(total, Ok(100));\nlet nums = vec![\"5\", \"10\", \"one\", \"2\"];\nlet total: Result<usize, _> = nums.iter().map(|w| w.parse::<usize>()).product();\nassert!(total.is_err());
Maps a Result<&mut T, E>
to a Result<T, E>
by copying the contents of the\nOk
part.
let mut val = 12;\nlet x: Result<&mut i32, i32> = Ok(&mut val);\nassert_eq!(x, Ok(&mut 12));\nlet copied = x.copied();\nassert_eq!(copied, Ok(12));
Maps a Result<&mut T, E>
to a Result<T, E>
by cloning the contents of the\nOk
part.
let mut val = 12;\nlet x: Result<&mut i32, i32> = Ok(&mut val);\nassert_eq!(x, Ok(&mut 12));\nlet cloned = x.cloned();\nassert_eq!(cloned, Ok(12));
Transposes a Result
of an Option
into an Option
of a Result
.
Ok(None)
will be mapped to None
.\nOk(Some(_))
and Err(_)
will be mapped to Some(Ok(_))
and Some(Err(_))
.
#[derive(Debug, Eq, PartialEq)]\nstruct SomeErr;\n\nlet x: Result<Option<i32>, SomeErr> = Ok(Some(5));\nlet y: Option<Result<i32, SomeErr>> = Some(Ok(5));\nassert_eq!(x.transpose(), y);
result_flattening
)Converts from Result<Result<T, E>, E>
to Result<T, E>
#![feature(result_flattening)]\nlet x: Result<Result<&'static str, u32>, u32> = Ok(Ok(\"hello\"));\nassert_eq!(Ok(\"hello\"), x.flatten());\n\nlet x: Result<Result<&'static str, u32>, u32> = Ok(Err(6));\nassert_eq!(Err(6), x.flatten());\n\nlet x: Result<Result<&'static str, u32>, u32> = Err(6);\nassert_eq!(Err(6), x.flatten());
Flattening only removes one level of nesting at a time:
\n\n#![feature(result_flattening)]\nlet x: Result<Result<Result<&'static str, u32>, u32>, u32> = Ok(Ok(Ok(\"hello\")));\nassert_eq!(Ok(Ok(\"hello\")), x.flatten());\nassert_eq!(Ok(\"hello\"), x.flatten().flatten());
Returns true
if the result is Ok
and the value inside of it matches a predicate.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.is_ok_and(|x| x > 1), true);\n\nlet x: Result<u32, &str> = Ok(0);\nassert_eq!(x.is_ok_and(|x| x > 1), false);\n\nlet x: Result<u32, &str> = Err(\"hey\");\nassert_eq!(x.is_ok_and(|x| x > 1), false);
Returns true
if the result is Err
and the value inside of it matches a predicate.
use std::io::{Error, ErrorKind};\n\nlet x: Result<u32, Error> = Err(Error::new(ErrorKind::NotFound, \"!\"));\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), true);\n\nlet x: Result<u32, Error> = Err(Error::new(ErrorKind::PermissionDenied, \"!\"));\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), false);\n\nlet x: Result<u32, Error> = Ok(123);\nassert_eq!(x.is_err_and(|x| x.kind() == ErrorKind::NotFound), false);
Converts from Result<T, E>
to Option<E>
.
Converts self
into an Option<E>
, consuming self
,\nand discarding the success value, if any.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.err(), None);\n\nlet x: Result<u32, &str> = Err(\"Nothing here\");\nassert_eq!(x.err(), Some(\"Nothing here\"));
Converts from &Result<T, E>
to Result<&T, &E>
.
Produces a new Result
, containing a reference\ninto the original, leaving the original in place.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(x.as_ref(), Ok(&2));\n\nlet x: Result<u32, &str> = Err(\"Error\");\nassert_eq!(x.as_ref(), Err(&\"Error\"));
Converts from &mut Result<T, E>
to Result<&mut T, &mut E>
.
fn mutate(r: &mut Result<i32, i32>) {\n match r.as_mut() {\n Ok(v) => *v = 42,\n Err(e) => *e = 0,\n }\n}\n\nlet mut x: Result<i32, i32> = Ok(2);\nmutate(&mut x);\nassert_eq!(x.unwrap(), 42);\n\nlet mut x: Result<i32, i32> = Err(13);\nmutate(&mut x);\nassert_eq!(x.unwrap_err(), 0);
Maps a Result<T, E>
to Result<U, E>
by applying a function to a\ncontained Ok
value, leaving an Err
value untouched.
This function can be used to compose the results of two functions.
\nPrint the numbers on each line of a string multiplied by two.
\n\nlet line = \"1\\n2\\n3\\n4\\n\";\n\nfor num in line.lines() {\n match num.parse::<i32>().map(|i| i * 2) {\n Ok(n) => println!(\"{n}\"),\n Err(..) => {}\n }\n}
Returns the provided default (if Err
), or\napplies a function to the contained value (if Ok
).
Arguments passed to map_or
are eagerly evaluated; if you are passing\nthe result of a function call, it is recommended to use map_or_else
,\nwhich is lazily evaluated.
let x: Result<_, &str> = Ok(\"foo\");\nassert_eq!(x.map_or(42, |v| v.len()), 3);\n\nlet x: Result<&str, _> = Err(\"bar\");\nassert_eq!(x.map_or(42, |v| v.len()), 42);
Maps a Result<T, E>
to U
by applying fallback function default
to\na contained Err
value, or function f
to a contained Ok
value.
This function can be used to unpack a successful result\nwhile handling an error.
\nlet k = 21;\n\nlet x : Result<_, &str> = Ok(\"foo\");\nassert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 3);\n\nlet x : Result<&str, _> = Err(\"bar\");\nassert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 42);
Maps a Result<T, E>
to Result<T, F>
by applying a function to a\ncontained Err
value, leaving an Ok
value untouched.
This function can be used to pass through a successful result while handling\nan error.
\nfn stringify(x: u32) -> String { format!(\"error code: {x}\") }\n\nlet x: Result<u32, u32> = Ok(2);\nassert_eq!(x.map_err(stringify), Ok(2));\n\nlet x: Result<u32, u32> = Err(13);\nassert_eq!(x.map_err(stringify), Err(\"error code: 13\".to_string()));
Converts from Result<T, E>
(or &Result<T, E>
) to Result<&<T as Deref>::Target, &E>
.
Coerces the Ok
variant of the original Result
via Deref
\nand returns the new Result
.
let x: Result<String, u32> = Ok(\"hello\".to_string());\nlet y: Result<&str, &u32> = Ok(\"hello\");\nassert_eq!(x.as_deref(), y);\n\nlet x: Result<String, u32> = Err(42);\nlet y: Result<&str, &u32> = Err(&42);\nassert_eq!(x.as_deref(), y);
Converts from Result<T, E>
(or &mut Result<T, E>
) to Result<&mut <T as DerefMut>::Target, &mut E>
.
Coerces the Ok
variant of the original Result
via DerefMut
\nand returns the new Result
.
let mut s = \"HELLO\".to_string();\nlet mut x: Result<String, u32> = Ok(\"hello\".to_string());\nlet y: Result<&mut str, &mut u32> = Ok(&mut s);\nassert_eq!(x.as_deref_mut().map(|x| { x.make_ascii_uppercase(); x }), y);\n\nlet mut i = 42;\nlet mut x: Result<String, u32> = Err(42);\nlet y: Result<&mut str, &mut u32> = Err(&mut i);\nassert_eq!(x.as_deref_mut().map(|x| { x.make_ascii_uppercase(); x }), y);
Returns an iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let x: Result<u32, &str> = Ok(7);\nassert_eq!(x.iter().next(), Some(&7));\n\nlet x: Result<u32, &str> = Err(\"nothing!\");\nassert_eq!(x.iter().next(), None);
Returns a mutable iterator over the possibly contained value.
\nThe iterator yields one value if the result is Result::Ok
, otherwise none.
let mut x: Result<u32, &str> = Ok(7);\nmatch x.iter_mut().next() {\n Some(v) => *v = 40,\n None => {},\n}\nassert_eq!(x, Ok(40));\n\nlet mut x: Result<u32, &str> = Err(\"nothing!\");\nassert_eq!(x.iter_mut().next(), None);
Returns the contained Ok
value, consuming the self
value.
Because this function may panic, its use is generally discouraged.\nInstead, prefer to use pattern matching and handle the Err
\ncase explicitly, or call unwrap_or
, unwrap_or_else
, or\nunwrap_or_default
.
Panics if the value is an Err
, with a panic message including the\npassed message, and the content of the Err
.
let x: Result<u32, &str> = Err(\"emergency failure\");\nx.expect(\"Testing expect\"); // panics with `Testing expect: emergency failure`
We recommend that expect
messages are used to describe the reason you\nexpect the Result
should be Ok
.
let path = std::env::var(\"IMPORTANT_PATH\")\n .expect(\"env variable `IMPORTANT_PATH` should be set by `wrapper_script.sh`\");
Hint: If you’re having trouble remembering how to phrase expect\nerror messages remember to focus on the word “should” as in “env\nvariable should be set by blah” or “the given binary should be available\nand executable by the current user”.
\nFor more detail on expect message styles and the reasoning behind our recommendation please\nrefer to the section on “Common Message\nStyles” in the\nstd::error
module docs.
Returns the contained Ok
value, consuming the self
value.
Because this function may panic, its use is generally discouraged.\nInstead, prefer to use pattern matching and handle the Err
\ncase explicitly, or call unwrap_or
, unwrap_or_else
, or\nunwrap_or_default
.
Panics if the value is an Err
, with a panic message provided by the\nErr
’s value.
Basic usage:
\n\nlet x: Result<u32, &str> = Ok(2);\nassert_eq!(x.unwrap(), 2);
let x: Result<u32, &str> = Err(\"emergency failure\");\nx.unwrap(); // panics with `emergency failure`
Returns the contained Ok
value or a default
Consumes the self
argument then, if Ok
, returns the contained\nvalue, otherwise if Err
, returns the default value for that\ntype.
Converts a string to an integer, turning poorly-formed strings\ninto 0 (the default value for integers). parse
converts\na string to any other type that implements FromStr
, returning an\nErr
on error.
let good_year_from_input = \"1909\";\nlet bad_year_from_input = \"190blarg\";\nlet good_year = good_year_from_input.parse().unwrap_or_default();\nlet bad_year = bad_year_from_input.parse().unwrap_or_default();\n\nassert_eq!(1909, good_year);\nassert_eq!(0, bad_year);
Returns the contained Err
value, consuming the self
value.
Panics if the value is an Ok
, with a panic message including the\npassed message, and the content of the Ok
.
let x: Result<u32, &str> = Ok(10);\nx.expect_err(\"Testing expect_err\"); // panics with `Testing expect_err: 10`
Returns the contained Err
value, consuming the self
value.
Panics if the value is an Ok
, with a custom panic message provided\nby the Ok
’s value.
let x: Result<u32, &str> = Ok(2);\nx.unwrap_err(); // panics with `2`
let x: Result<u32, &str> = Err(\"emergency failure\");\nassert_eq!(x.unwrap_err(), \"emergency failure\");
unwrap_infallible
)Returns the contained Ok
value, but never panics.
Unlike unwrap
, this method is known to never panic on the\nresult types it is implemented for. Therefore, it can be used\ninstead of unwrap
as a maintainability safeguard that will fail\nto compile if the error type of the Result
is later changed\nto an error that can actually occur.
\nfn only_good_news() -> Result<String, !> {\n Ok(\"this is fine\".into())\n}\n\nlet s: String = only_good_news().into_ok();\nprintln!(\"{s}\");
unwrap_infallible
)Returns the contained Err
value, but never panics.
Unlike unwrap_err
, this method is known to never panic on the\nresult types it is implemented for. Therefore, it can be used\ninstead of unwrap_err
as a maintainability safeguard that will fail\nto compile if the ok type of the Result
is later changed\nto a type that can actually occur.
\nfn only_bad_news() -> Result<!, String> {\n Err(\"Oops, it failed\".into())\n}\n\nlet error: String = only_bad_news().into_err();\nprintln!(\"{error}\");
Returns res
if the result is Ok
, otherwise returns the Err
value of self
.
Arguments passed to and
are eagerly evaluated; if you are passing the\nresult of a function call, it is recommended to use and_then
, which is\nlazily evaluated.
let x: Result<u32, &str> = Ok(2);\nlet y: Result<&str, &str> = Err(\"late error\");\nassert_eq!(x.and(y), Err(\"late error\"));\n\nlet x: Result<u32, &str> = Err(\"early error\");\nlet y: Result<&str, &str> = Ok(\"foo\");\nassert_eq!(x.and(y), Err(\"early error\"));\n\nlet x: Result<u32, &str> = Err(\"not a 2\");\nlet y: Result<&str, &str> = Err(\"late error\");\nassert_eq!(x.and(y), Err(\"not a 2\"));\n\nlet x: Result<u32, &str> = Ok(2);\nlet y: Result<&str, &str> = Ok(\"different result type\");\nassert_eq!(x.and(y), Ok(\"different result type\"));
Calls op
if the result is Ok
, otherwise returns the Err
value of self
.
This function can be used for control flow based on Result
values.
fn sq_then_to_string(x: u32) -> Result<String, &'static str> {\n x.checked_mul(x).map(|sq| sq.to_string()).ok_or(\"overflowed\")\n}\n\nassert_eq!(Ok(2).and_then(sq_then_to_string), Ok(4.to_string()));\nassert_eq!(Ok(1_000_000).and_then(sq_then_to_string), Err(\"overflowed\"));\nassert_eq!(Err(\"not a number\").and_then(sq_then_to_string), Err(\"not a number\"));
Often used to chain fallible operations that may return Err
.
use std::{io::ErrorKind, path::Path};\n\n// Note: on Windows \"/\" maps to \"C:\\\"\nlet root_modified_time = Path::new(\"/\").metadata().and_then(|md| md.modified());\nassert!(root_modified_time.is_ok());\n\nlet should_fail = Path::new(\"/bad/path\").metadata().and_then(|md| md.modified());\nassert!(should_fail.is_err());\nassert_eq!(should_fail.unwrap_err().kind(), ErrorKind::NotFound);
Returns res
if the result is Err
, otherwise returns the Ok
value of self
.
Arguments passed to or
are eagerly evaluated; if you are passing the\nresult of a function call, it is recommended to use or_else
, which is\nlazily evaluated.
let x: Result<u32, &str> = Ok(2);\nlet y: Result<u32, &str> = Err(\"late error\");\nassert_eq!(x.or(y), Ok(2));\n\nlet x: Result<u32, &str> = Err(\"early error\");\nlet y: Result<u32, &str> = Ok(2);\nassert_eq!(x.or(y), Ok(2));\n\nlet x: Result<u32, &str> = Err(\"not a 2\");\nlet y: Result<u32, &str> = Err(\"late error\");\nassert_eq!(x.or(y), Err(\"late error\"));\n\nlet x: Result<u32, &str> = Ok(2);\nlet y: Result<u32, &str> = Ok(100);\nassert_eq!(x.or(y), Ok(2));
Calls op
if the result is Err
, otherwise returns the Ok
value of self
.
This function can be used for control flow based on result values.
\nfn sq(x: u32) -> Result<u32, u32> { Ok(x * x) }\nfn err(x: u32) -> Result<u32, u32> { Err(x) }\n\nassert_eq!(Ok(2).or_else(sq).or_else(sq), Ok(2));\nassert_eq!(Ok(2).or_else(err).or_else(sq), Ok(2));\nassert_eq!(Err(3).or_else(sq).or_else(err), Ok(9));\nassert_eq!(Err(3).or_else(err).or_else(err), Err(3));
Returns the contained Ok
value or a provided default.
Arguments passed to unwrap_or
are eagerly evaluated; if you are passing\nthe result of a function call, it is recommended to use unwrap_or_else
,\nwhich is lazily evaluated.
let default = 2;\nlet x: Result<u32, &str> = Ok(9);\nassert_eq!(x.unwrap_or(default), 9);\n\nlet x: Result<u32, &str> = Err(\"error\");\nassert_eq!(x.unwrap_or(default), default);
Returns the contained Ok
value, consuming the self
value,\nwithout checking that the value is not an Err
.
Calling this method on an Err
is undefined behavior.
let x: Result<u32, &str> = Ok(2);\nassert_eq!(unsafe { x.unwrap_unchecked() }, 2);
let x: Result<u32, &str> = Err(\"emergency failure\");\nunsafe { x.unwrap_unchecked(); } // Undefined behavior!
Returns the contained Err
value, consuming the self
value,\nwithout checking that the value is not an Ok
.
Calling this method on an Ok
is undefined behavior.
let x: Result<u32, &str> = Ok(2);\nunsafe { x.unwrap_err_unchecked() }; // Undefined behavior!
let x: Result<u32, &str> = Err(\"emergency failure\");\nassert_eq!(unsafe { x.unwrap_err_unchecked() }, \"emergency failure\");
Takes each element in the Iterator
: if it is an Err
, no further\nelements are taken, and the Err
is returned. Should no Err
\noccur, the sum of all elements is returned.
This sums up every integer in a vector, rejecting the sum if a negative\nelement is encountered:
\n\nlet f = |&x: &i32| if x < 0 { Err(\"Negative element found\") } else { Ok(x) };\nlet v = vec![1, 2];\nlet res: Result<i32, _> = v.iter().map(f).sum();\nassert_eq!(res, Ok(3));\nlet v = vec![1, -2];\nlet res: Result<i32, _> = v.iter().map(f).sum();\nassert_eq!(res, Err(\"Negative element found\"));
try_trait_v2
)?
when not short-circuiting.try_trait_v2
)FromResidual::from_residual
\nas part of ?
when short-circuiting. Read moretry_trait_v2
)Output
type. Read moretry_trait_v2
)?
to decide whether the operator should produce a value\n(because this returned ControlFlow::Continue
)\nor propagate a value back to the caller\n(because this returned ControlFlow::Break
). Read more