webhook-router 0.2.2

Classifies and routes webhooks according to rules and webhook contents.
Documentation
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::collections::hash_map::HashMap;
use std::fs::File;
use std::io;
use std::path::Path;

use serde::Deserialize;
use serde_json::Value;
use thiserror::Error;

use crate::handler::Handler;

/// Configuration for routing webhooks.
#[derive(Deserialize, Debug)]
pub struct Config {
    #[serde(default)]
    pub(crate) post_paths: HashMap<String, Handler>,
    secrets: Option<String>,
}

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("deserialization error: {}", source)]
    Deserialize {
        #[source]
        source: serde_json::Error,
    },
    #[error("failed to read file: {}", source)]
    Read {
        #[source]
        source: io::Error,
    },
}

impl Config {
    /// Read the configuration from a path.
    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
        let fin = File::open(path.as_ref()).map_err(|source| {
            ConfigError::Read {
                source,
            }
        })?;
        serde_json::from_reader(fin).map_err(|source| {
            ConfigError::Deserialize {
                source,
            }
        })
    }

    pub(crate) fn secrets(&self) -> Result<Value, ConfigError> {
        if let Some(ref path) = self.secrets {
            let fin = File::open(path).map_err(|source| {
                ConfigError::Read {
                    source,
                }
            })?;
            serde_json::from_reader(fin).map_err(|source| {
                ConfigError::Deserialize {
                    source,
                }
            })
        } else {
            Ok(Value::Null)
        }
    }

    #[cfg(test)]
    pub(crate) fn secrets_path(&self) -> Option<&String> {
        self.secrets.as_ref()
    }
}

#[cfg(test)]
mod test {
    use std::fs::File;
    use std::io::Write;

    use serde_json::{json, Value};

    use crate::config::{Config, ConfigError};
    use crate::test_utils;

    #[test]
    fn test_config_empty_paths() {
        let tempdir = test_utils::create_tempdir();
        let path = test_utils::write_config(tempdir.path(), json!({}));
        let config = Config::from_path(path).unwrap();

        assert!(config.post_paths.is_empty());
        assert_eq!(config.secrets, None);
        assert_eq!(config.secrets().unwrap(), Value::Null);
    }

    #[test]
    fn test_config_unreadable() {
        let path = {
            let tempdir = test_utils::create_tempdir();
            test_utils::write_config(tempdir.path(), json!({}))
        };
        let err = Config::from_path(path).unwrap_err();

        if let ConfigError::Read {
            ..
        } = err
        {
            // expected error
        } else {
            panic!("unexpected error: {:?}", err);
        }
    }

    #[test]
    fn test_config_unparseable() {
        let tempdir = test_utils::create_tempdir();
        let path = tempdir.path().join("config.json");
        {
            let mut fout = File::create(&path).unwrap();
            fout.write_all(b"not json\n").unwrap();
        }
        let err = Config::from_path(path).unwrap_err();

        if let ConfigError::Deserialize {
            ..
        } = err
        {
            // expected error
        } else {
            panic!("unexpected error: {:?}", err);
        }
    }

    #[test]
    fn test_secrets_unreadable() {
        let config = {
            let tempdir = test_utils::create_tempdir();
            let (path, _) = test_utils::write_config_secrets(tempdir.path(), json!({}), json!({}));
            Config::from_path(path).unwrap()
        };
        let err = config.secrets().unwrap_err();

        if let ConfigError::Read {
            ..
        } = err
        {
            // expected error
        } else {
            panic!("unexpected error: {:?}", err);
        }
    }

    #[test]
    fn test_secrets_unparseable() {
        let tempdir = test_utils::create_tempdir();
        let secrets_path = tempdir.path().join("secrets.json");
        let path = test_utils::write_config(
            tempdir.path(),
            json!({
                "secrets": secrets_path.to_str().unwrap(),
            }),
        );
        {
            let mut fout = File::create(&secrets_path).unwrap();
            fout.write_all(b"not json\n").unwrap();
        }
        let config = Config::from_path(path).unwrap();
        let err = config.secrets().unwrap_err();

        if let ConfigError::Deserialize {
            ..
        } = err
        {
            // expected error
        } else {
            panic!("unexpected error: {:?}", err);
        }
    }

    #[test]
    fn test_config_parse() {
        let tempdir = test_utils::create_tempdir();
        let path = test_utils::write_config(
            tempdir.path(),
            json!({
                "post_paths": {
                    "hostname.example": {
                        "path": "path",
                        "filters": [
                            {
                                "kind": "blah",
                            },
                        ],
                        "header_name": "X-WebHook-Type",
                    },
                },
            }),
        );
        let config = Config::from_path(path).unwrap();

        assert_eq!(config.post_paths.len(), 1);
    }
}