hubcaps/
hooks.rs

1//! Hooks interface
2//!
3//! See the [github docs](https://developer.github.com/v3/repos/hooks/) for more information
4use std::collections::BTreeMap;
5use std::fmt;
6
7use serde::{Deserialize, Serialize};
8
9use crate::{Future, Github};
10
11/// Content-Type web hooks will receive
12/// deliveries in
13#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
14pub enum WebHookContentType {
15    /// application/json
16    #[serde(rename = "json")]
17    Json,
18    /// application/x-form-url-encoded
19    #[serde(rename = "form")]
20    Form,
21}
22
23impl Default for WebHookContentType {
24    fn default() -> WebHookContentType {
25        WebHookContentType::Form
26    }
27}
28
29impl fmt::Display for WebHookContentType {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match *self {
32            WebHookContentType::Form => "form",
33            WebHookContentType::Json => "json",
34        }
35        .fmt(f)
36    }
37}
38
39/// Interface for managing repository hooks
40pub struct Hooks {
41    github: Github,
42    owner: String,
43    repo: String,
44}
45
46impl Hooks {
47    #[doc(hidden)]
48    pub fn new<O, R>(github: Github, owner: O, repo: R) -> Self
49    where
50        O: Into<String>,
51        R: Into<String>,
52    {
53        Hooks {
54            github,
55            owner: owner.into(),
56            repo: repo.into(),
57        }
58    }
59
60    /// lists hook associated with a repository
61    pub fn list(&self) -> Future<Vec<Hook>> {
62        self.github
63            .get(&format!("/repos/{}/{}/hooks", self.owner, self.repo))
64    }
65
66    /// creates a new repository hook
67    /// Repository service hooks (like email or Campfire) can have at most one configured at a time.
68    /// Creating hooks for a service that already has one configured will update the existing hook.
69    /// see [github docs](https://developer.github.com/v3/repos/hooks/)
70    /// for more information
71    pub fn create(&self, options: &HookCreateOptions) -> Future<Hook> {
72        self.github.post(
73            &format!("/repos/{}/{}/hooks", self.owner, self.repo),
74            json!(options),
75        )
76    }
77
78    /// edits an existing repository hook
79    pub fn edit(&self, id: u64, options: &HookEditOptions) -> Future<Hook> {
80        self.github.patch(
81            &format!("/repos/{}/{}/hooks/{}", self.owner, self.repo, id),
82            json!(options),
83        )
84    }
85
86    /// deletes a repository hook by id
87    pub fn delete(&self, id: u64) -> Future<()> {
88        self.github
89            .delete(&format!("/repos/{}/{}/hooks/{}", self.owner, self.repo, id))
90    }
91}
92
93// representations
94
95/// options for creating a repository hook
96/// see [this](https://developer.github.com/v3/repos/hooks/#create-a-hook)
97/// for githubs official documentation
98#[derive(Debug, Default, Serialize)]
99pub struct HookCreateOptions {
100    name: String,
101    config: BTreeMap<String, ::serde_json::Value>,
102    events: Vec<String>,
103    active: bool,
104}
105
106impl HookCreateOptions {
107    /// creates a new builder instance with a hook name
108    /// care should be taken with respect to the hook name as you can only
109    /// use "web" or a valid service name listed [here](https://api.github.com/hooks)
110    pub fn builder<N>(name: N) -> HookCreateOptionsBuilder
111    where
112        N: Into<String>,
113    {
114        HookCreateOptionsBuilder::new(name)
115    }
116
117    /// use this for creating a builder for webhook options
118    pub fn web() -> HookCreateOptionsBuilder {
119        Self::builder("web")
120    }
121}
122
123pub struct HookCreateOptionsBuilder(HookCreateOptions);
124
125impl HookCreateOptionsBuilder {
126    #[doc(hidden)]
127    pub(crate) fn new<N>(name: N) -> Self
128    where
129        N: Into<String>,
130    {
131        HookCreateOptionsBuilder(HookCreateOptions {
132            name: name.into(),
133            active: true,
134            ..Default::default()
135        })
136    }
137
138    pub fn active(&mut self, active: bool) -> &mut Self {
139        self.0.active = active;
140        self
141    }
142
143    /// a list of github events this hook should receive deliveries for
144    /// the default is "push". for a full list, see
145    /// the [Github api docs](https://developer.github.com/webhooks/#events)
146    pub fn events<E>(&mut self, events: Vec<E>) -> &mut Self
147    where
148        E: Into<String>,
149    {
150        self.0.events = events.into_iter().map(|e| e.into()).collect::<Vec<_>>();
151        self
152    }
153
154    /// web hooks must have an associated url
155    pub fn url<U>(&mut self, url: U) -> &mut Self
156    where
157        U: Into<String>,
158    {
159        self.config_entry("url".to_owned(), ::serde_json::Value::String(url.into()))
160    }
161
162    /// web hooks can optionally specify a content_type of "form" or "json"
163    /// which indicates the type of payload they will expect to receive
164    pub fn content_type(&mut self, content_type: WebHookContentType) -> &mut Self {
165        self.config_str_entry("content_type", content_type.to_string());
166        self
167    }
168
169    /// web hooks can optionally provide a secret used to sign deliveries
170    /// to identify that their source was indeed github
171    pub fn secret<S>(&mut self, sec: S) -> &mut Self
172    where
173        S: Into<String>,
174    {
175        self.config_str_entry("secret", sec);
176        self
177    }
178
179    pub fn config_str_entry<K, V>(&mut self, k: K, v: V) -> &mut Self
180    where
181        K: Into<String>,
182        V: Into<String>,
183    {
184        self.config_entry(k.into(), ::serde_json::Value::String(v.into()));
185        self
186    }
187
188    pub fn config_entry<N>(&mut self, name: N, value: ::serde_json::Value) -> &mut Self
189    where
190        N: Into<String>,
191    {
192        self.0.config.insert(name.into(), value);
193        self
194    }
195
196    pub fn build(&self) -> HookCreateOptions {
197        HookCreateOptions {
198            name: self.0.name.clone(),
199            config: self.0.config.clone(),
200            events: self.0.events.clone(),
201            active: self.0.active,
202        }
203    }
204}
205
206/// options for editing a repository hook
207/// see [this](https://developer.github.com/v3/repos/hooks/#edit-a-hook)
208/// for githubs official documentation
209#[derive(Debug, Default, Serialize)]
210pub struct HookEditOptions {
211    config: BTreeMap<String, ::serde_json::Value>,
212    events: Vec<String>,
213    add_events: Vec<String>,
214    remove_events: Vec<String>,
215    active: bool,
216}
217
218impl HookEditOptions {
219    /// creates a new builder instance
220    pub fn builder() -> HookEditOptionsBuilder {
221        HookEditOptionsBuilder::default()
222    }
223}
224
225#[derive(Default)]
226pub struct HookEditOptionsBuilder(HookEditOptions);
227
228impl HookEditOptionsBuilder {
229    pub fn active(&mut self, active: bool) -> &mut Self {
230        self.0.active = active;
231        self
232    }
233
234    /// a list of github events this hook should receive deliveries for
235    /// the default is "push". for a full list, see
236    /// the [Github api docs](https://developer.github.com/webhooks/#events)
237    pub fn events<E>(&mut self, events: Vec<E>) -> &mut Self
238    where
239        E: Into<String>,
240    {
241        self.0.events = events.into_iter().map(|e| e.into()).collect::<Vec<_>>();
242        self
243    }
244
245    /// web hooks must have an associated url
246    pub fn url<U>(&mut self, url: U) -> &mut Self
247    where
248        U: Into<String>,
249    {
250        self.config_entry("url".to_owned(), ::serde_json::Value::String(url.into()))
251    }
252
253    /// web hooks can optionally specify a content_type of "form" or "json"
254    /// which indicates the type of payload they will expect to receive
255    pub fn content_type(&mut self, content_type: WebHookContentType) -> &mut Self {
256        self.config_str_entry("content_type", content_type.to_string());
257        self
258    }
259
260    /// web hooks can optionally provide a secret used to sign deliveries
261    /// to identify that their source was indeed github
262    pub fn secret<S>(&mut self, sec: S) -> &mut Self
263    where
264        S: Into<String>,
265    {
266        self.config_str_entry("secret", sec);
267        self
268    }
269
270    pub fn config_str_entry<K, V>(&mut self, k: K, v: V) -> &mut Self
271    where
272        K: Into<String>,
273        V: Into<String>,
274    {
275        self.config_entry(k.into(), ::serde_json::Value::String(v.into()));
276        self
277    }
278
279    pub fn config_entry<N>(&mut self, name: N, value: ::serde_json::Value) -> &mut Self
280    where
281        N: Into<String>,
282    {
283        self.0.config.insert(name.into(), value);
284        self
285    }
286
287    pub fn build(&self) -> HookEditOptions {
288        HookEditOptions {
289            config: self.0.config.clone(),
290            events: self.0.events.clone(),
291            add_events: self.0.add_events.clone(),
292            remove_events: self.0.remove_events.clone(),
293            active: self.0.active,
294        }
295    }
296}
297
298#[derive(Debug, Deserialize)]
299pub struct Hook {
300    pub id: u64,
301    pub url: String,
302    pub test_url: String,
303    pub ping_url: String,
304    pub name: String,
305    pub events: Vec<String>,
306    pub config: ::serde_json::Value,
307    pub created_at: String,
308    pub updated_at: String,
309    pub active: bool,
310}
311
312impl Hook {
313    pub fn config_value(&self, name: &str) -> Option<&::serde_json::Value> {
314        self.config.pointer(&format!("/{}", name))
315    }
316
317    pub fn config_string(&self, name: &str) -> Option<String> {
318        self.config_value(name).and_then(|value| match *value {
319            ::serde_json::Value::String(ref val) => Some(val.clone()),
320            _ => None,
321        })
322    }
323
324    pub fn url(&self) -> Option<String> {
325        self.config_string("url")
326    }
327
328    pub fn content_type(&self) -> Option<String> {
329        self.config_string("content_type")
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::WebHookContentType;
336
337    #[test]
338    fn webhook_content_type_display() {
339        for (ct, expect) in &[
340            (WebHookContentType::Form, "form"),
341            (WebHookContentType::Json, "json"),
342        ] {
343            assert_eq!(ct.to_string(), *expect)
344        }
345    }
346
347    #[test]
348    fn webhook_content_type_default() {
349        let default: WebHookContentType = Default::default();
350        assert_eq!(default, WebHookContentType::Form)
351    }
352}