Skip to main content

grammarbot/
lib.rs

1//! # grammarbot-rs
2//!
3//! An API client for GrammarBot - a free grammar checking API.
4//!
5//! # Usage
6//!
7//! A [`Client`](Client) is the primary way to interact with the API.
8//!
9//! ```no_run
10//! use grammarbot::Client;
11//! let client = Client::new("your_api_key");
12//! ```
13//!
14//! Use [`check`](Client::check) to check your texts.
15//!
16//! ```no_run
17//! # use grammarbot::Client;
18//! # let client = Client::new("your_api_key");
19//! let response = client.check("I can't remember how to go their.");
20//! ```
21//!
22//! The [`Response`](Response) struct stores all information
23//! in the fields that you can access directly:
24//!
25//! ```no_run
26//! # use grammarbot::Client;
27//! # let client = Client::new("your_api_key");
28//! # let response = client.check("I can't remember how to go their.")?;
29//! assert_eq!("CONFUSION_RULE", response.matches[0].rule.id);
30//! # Ok::<(), grammarbot::Error>(())
31//! ```
32
33extern crate reqwest;
34extern crate serde;
35extern crate snafu;
36
37use serde::Deserialize;
38use snafu::{ResultExt, Snafu};
39
40/// All the entities used in responses from the API.
41pub mod types {
42    use serde::Deserialize;
43
44    #[derive(Debug, Deserialize)]
45    #[serde(rename_all = "camelCase")]
46    pub struct Software {
47        pub name: String,
48        pub version: String,
49        pub api_version: u8,
50        pub premium: bool,
51        pub premium_hint: String,
52        pub status: String,
53    }
54
55    #[derive(Debug, Deserialize)]
56    #[serde(rename_all = "camelCase")]
57    pub struct Warnings {
58        pub incomplete_results: bool,
59    }
60
61    #[derive(Debug, Deserialize)]
62    #[serde(rename_all = "camelCase")]
63    pub struct Language {
64        pub name: String,
65        pub code: String,
66        pub detected_language: DetectedLanguage,
67    }
68
69    #[derive(Debug, Deserialize)]
70    #[serde(rename_all = "camelCase")]
71    pub struct DetectedLanguage {
72        pub name: String,
73        pub code: String,
74    }
75
76    #[derive(Debug, Deserialize)]
77    #[serde(rename_all = "camelCase")]
78    pub struct Match {
79        pub message: String,
80        pub short_message: String,
81        pub replacements: Vec<Replacement>,
82        pub offset: u32,
83        pub length: u32,
84        pub context: Context,
85        pub sentence: String,
86        pub r#type: Type,
87        pub rule: Rule,
88    }
89
90    #[derive(Debug, Deserialize)]
91    #[serde(rename_all = "camelCase")]
92    pub struct Replacement {
93        pub value: String,
94    }
95
96    #[derive(Debug, Deserialize)]
97    #[serde(rename_all = "camelCase")]
98    pub struct Context {
99        pub text: String,
100        pub offset: u32,
101        pub length: u32,
102    }
103
104    #[derive(Debug, Deserialize)]
105    #[serde(rename_all = "camelCase")]
106    pub struct Type {
107        pub type_name: String,
108    }
109
110    #[derive(Debug, Deserialize)]
111    #[serde(rename_all = "camelCase")]
112    pub struct Rule {
113        pub id: String,
114        pub description: String,
115        pub issue_type: String,
116        pub category: Category,
117    }
118
119    #[derive(Debug, Deserialize)]
120    #[serde(rename_all = "camelCase")]
121    pub struct Category {
122        /// Category ID. `"TYPOS"`
123        pub id: String,
124        /// Category name. `"Possible Typo"`
125        pub name: String,
126    }
127}
128
129/// A typed representation of the JSON response.
130#[derive(Debug, Deserialize)]
131pub struct Response {
132    pub software: types::Software,
133    pub warnings: types::Warnings,
134    pub language: types::Language,
135    pub matches: Vec<types::Match>,
136}
137
138/// The primary way to interact with the API.
139pub struct Client {
140    api_key: String,
141    language: String,
142    base: reqwest::Url,
143    client: reqwest::Client,
144}
145
146impl Client {
147    /// Create a new `Client`.
148    ///
149    /// Currently it requires an API key.
150    /// If you need one, [sign up](https://www.grammarbot.io/signup)
151    /// with GrammarBot.
152    ///
153    /// ```
154    /// # use grammarbot::Client;
155    /// let api_key = "your_api_key";
156    /// let client = Client::new(api_key);
157    /// ```
158    pub fn new(api_key: &str) -> Self {
159        Self {
160            api_key: api_key.to_string(),
161            language: "en-US".to_string(),
162            base: reqwest::Url::parse("http://api.grammarbot.io").unwrap(),
163            client: reqwest::Client::new(),
164        }
165    }
166
167    pub fn check(&self, text: &str) -> Result<Response> {
168        self.request(reqwest::Method::GET, "/v2/check", &[("text", text)])?
169            .json()
170            .context(InvalidJSON)
171    }
172
173    /// Set the API key for the client.
174    ///
175    /// ```no_run
176    /// # use grammarbot::Client;
177    /// # let api_key = "test";
178    /// # let other_api_key = "test2";
179    /// let client = Client::new(api_key).api_key(other_api_key);
180    /// ```
181    pub fn api_key(&mut self, api_key: &str) -> &mut Self {
182        self.api_key = api_key.to_string();
183        self
184    }
185
186    /// Set the language for the client.
187    ///
188    /// ```no_run
189    /// # use grammarbot::Client;
190    /// # let api_key = "test";
191    /// let client = Client::new(api_key).language("en-UK");
192    /// ```
193    pub fn language(&mut self, language: &str) -> &mut Self {
194        self.language = language.to_string();
195        self
196    }
197
198    /// Set the base URL for the client.
199    ///
200    /// ```no_run
201    /// # use grammarbot::Client;
202    /// # let api_key = "test";
203    /// let client = Client::new(api_key).base("http://pro.grammarbot.io");
204    /// ```
205    pub fn base(&mut self, base: &str) -> Result<&mut Self> {
206        self.base = reqwest::Url::parse(base).context(InvalidUrl)?;
207        Ok(self)
208    }
209
210    fn request(
211        &self,
212        method: reqwest::Method,
213        path: &str,
214        query: &[(&str, &str)],
215    ) -> Result<reqwest::Response> {
216        self.client
217            .request(method, self.base.join(path).context(InvalidUrl)?)
218            .query(&query)
219            .send()
220            .context(RequestFailed)
221    }
222}
223
224/// A domain-specific error type.
225#[derive(Debug, Snafu)]
226pub enum Error {
227    /// A request failed to execute; there is no valid response.
228    #[snafu(display("request failed: {}", source))]
229    RequestFailed {
230        /// A source error from `reqwest`.
231        source: reqwest::Error,
232    },
233    /// An invalid URL was supplied to [`parse`](reqwest::Url::parse).
234    #[snafu(display("invalid URL: {}", source))]
235    InvalidUrl {
236        /// A source error from `reqwest`.
237        source: reqwest::UrlError,
238    },
239    /// Response returned invalid JSON.
240    #[snafu(display("invalid JSON: {}", source))]
241    InvalidJSON {
242        /// A source error from `reqwest`.
243        source: reqwest::Error,
244    },
245}
246
247/// A short alias for a [`Result`] with [`Error`].
248///
249/// [Result]: std::result::Result
250pub type Result<T, E = Error> = std::result::Result<T, E>;