shodan-client 0.2.0

Shodan client
Documentation
use std::collections::HashMap;

#[derive(Default)]
pub struct SslFilterBuilder {
    filters: HashMap<String, Vec<String>>,
}

impl SslFilterBuilder {
    pub fn build(self) -> HashMap<String, Vec<String>> {
        self.filters
    }

    pub fn cert_subject_cn(mut self, value: impl ToString) -> Self {
        let values = self.filters.entry("cert.subject.cn".into()).or_default();

        values.push(value.to_string());

        self
    }
}

#[derive(Default)]
pub struct SearchQueryBuilder {
    query: String,
    filters: HashMap<String, Vec<String>>,
}

impl SearchQueryBuilder {
    pub fn build(self) -> String {
        let mut query = vec![];
        if !self.query.is_empty() {
            query.push(self.query);
        }

        for (filter, values) in self.filters {
            query.push(format!("{}:{}", filter, values.join(",")));
        }

        query.join(" ")
    }

    pub fn query(mut self, query: impl ToString) -> Self {
        self.query = query.to_string();

        self
    }

    pub fn port(mut self, value: impl ToString) -> Self {
        let values = self.filters.entry("port".into()).or_default();

        values.push(value.to_string());

        self
    }

    pub fn product(mut self, filter: impl ToString) -> Self {
        let values = self.filters.entry("product".into()).or_default();

        values.push(filter.to_string());

        self
    }

    pub fn ssl(mut self, closure: fn(SslFilterBuilder) -> SslFilterBuilder) -> Self {
        let filters = closure(SslFilterBuilder::default()).build();

        self.filters.extend(filters);

        self
    }
}

#[cfg(test)]
mod tests {
    use crate::*;

    #[test]
    fn can_build_query() {
        let query = SearchQueryBuilder::default()
            .query("apache")
            .port("69")
            .port(420)
            .product("Apache")
            .product("Ngnix")
            .build();

        assert!(query.contains("port:69,420"), "query was: {query}");
        assert!(query.contains("product:Apache,Ngnix"), "query was: {query}");
        assert!(query.starts_with("apache"), "query was: {query}");
    }

    #[test]
    fn can_build_without_query() {
        let query = SearchQueryBuilder::default()
            .port("69")
            .port(420)
            .product("Apache")
            .product("Ngnix")
            .build();

        assert!(query.contains("port:69,420"), "query was: {query}");
        assert!(query.contains("product:Apache,Ngnix"), "query was: {query}");
    }

    #[test]
    fn can_build_with_ssl() {
        let query = SearchQueryBuilder::default()
            .port("69")
            .port(420)
            .product("Apache")
            .product("Ngnix")
            .ssl(|ssl_builder| {
                ssl_builder
                    .cert_subject_cn("google.com")
                    .cert_subject_cn("bing.com")
            })
            .build();

        assert!(query.contains("port:69,420"), "query was: {query}");
        assert!(query.contains("product:Apache,Ngnix"), "query was: {query}");
        assert!(
            query.contains("cert.subject.cn:google.com,bing.com"),
            "query was: {query}"
        );
    }
}