spaceapi-server 0.8.0

A library that allows you to easily implement a SpaceAPI server.
Documentation
//! The SpaceAPI server struct.

use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::time::Duration;

use iron::Iron;
use log::debug;
use redis::{ConnectionInfo, IntoConnectionInfo};
use router::Router;

use serde_json::map::Map;
use serde_json::Value;

mod handlers;

use crate::api;

use crate::errors::SpaceapiServerError;
use crate::modifiers;
use crate::sensors;
use crate::types::RedisPool;

enum RedisInfo {
    None,
    Pool(r2d2::Pool<redis::Client>),
    ConnectionInfo(ConnectionInfo),
    Err(SpaceapiServerError),
}

/// Builder to create a new [`SpaceapiServer`](struct.SpaceapiServer.html)
/// instance.
pub struct SpaceapiServerBuilder {
    status: api::Status,
    redis_info: RedisInfo,
    sensor_specs: Vec<sensors::SensorSpec>,
    status_modifiers: Vec<Box<dyn modifiers::StatusModifier>>,
}

impl SpaceapiServerBuilder {
    /// Create a new builder instance based on the provided static status data.
    pub fn new(mut status: api::Status) -> SpaceapiServerBuilder {
        // Instantiate versions object
        let mut versions = Map::new();
        versions.insert("spaceapi-rs".into(), api::get_version().into());
        versions.insert("spaceapi-server-rs".into(), crate::get_version().into());

        // Add to extensions
        status
            .extensions
            .insert("versions".into(), Value::Object(versions));

        SpaceapiServerBuilder {
            status,
            redis_info: RedisInfo::None,
            sensor_specs: vec![],
            status_modifiers: vec![],
        }
    }

    /// Specify a Redis connection string.
    ///
    /// This can be any object that implements
    /// [`redis::IntoConnectionInfo`](../redis/trait.IntoConnectionInfo.html),
    /// e.g. a connection string:
    ///
    /// ```ignore
    /// ...
    /// .redis_connection_info("redis://127.0.0.1/")
    /// ...
    /// ```
    pub fn redis_connection_info<R: IntoConnectionInfo>(mut self, redis_connection_info: R) -> Self {
        self.redis_info = match redis_connection_info.into_connection_info() {
            Ok(ci) => RedisInfo::ConnectionInfo(ci),
            Err(e) => RedisInfo::Err(e.into()),
        };
        self
    }

    /// Use this as an alternative to
    /// [`redis_connection_info`](struct.SpaceapiServerBuilder.html#method.redis_connection_info)
    /// if you want to initialize the Redis connection pool yourself, to have
    /// full control over the connection parameters.
    ///
    /// See
    /// [`examples/with_custom_redis_pool.rs`](https://github.com/spaceapi-community/spaceapi-server-rs/blob/master/examples/with_custom_redis_pool.rs)
    /// for a real example.
    pub fn redis_pool(mut self, redis_pool: r2d2::Pool<redis::Client>) -> Self {
        self.redis_info = RedisInfo::Pool(redis_pool);
        self
    }

    /// Add a status modifier, that modifies the status dynamically per
    /// request.
    ///
    /// This can be an instance of
    /// [`modifiers::StateFromPeopleNowPresent`](modifiers/struct.StateFromPeopleNowPresent.html),
    /// or your own implementation that uses the dynamic sensor data and/or
    /// external data.
    pub fn add_status_modifier<M: modifiers::StatusModifier + 'static>(mut self, modifier: M) -> Self {
        self.status_modifiers.push(Box::new(modifier));
        self
    }

    /// Add a new sensor.
    ///
    /// The first argument is a ``api::SensorTemplate`` instance containing all static data.
    /// The second argument specifies how to get the actual sensor value from Redis.
    pub fn add_sensor<T: api::sensors::SensorTemplate + 'static>(
        mut self,
        template: T,
        data_key: String,
    ) -> Self {
        self.sensor_specs.push(sensors::SensorSpec {
            template: Box::new(template),
            data_key,
        });
        self
    }

    /// Build a server instance.
    ///
    /// This can fail if not all required data has been provided.
    pub fn build(self) -> Result<SpaceapiServer, SpaceapiServerError> {
        let pool = match self.redis_info {
            RedisInfo::None => Err("No redis connection defined".into()),
            RedisInfo::Err(e) => Err(e),
            RedisInfo::Pool(p) => Ok(p),
            RedisInfo::ConnectionInfo(ci) => {
                // Log some useful debug information
                debug!("Connecting to redis database {} at {:?}", ci.redis.db, ci.addr);

                let client: redis::Client = redis::Client::open(ci)?;

                let redis_pool: r2d2::Pool<redis::Client> = r2d2::Pool::builder()
                    // Provide up to 6 connections in connection pool
                    .max_size(6)
                    // At least 1 connection must be active
                    .min_idle(Some(2))
                    // Try to get a connection for max 1 second
                    .connection_timeout(Duration::from_secs(1))
                    // Don't log errors directly.
                    // They can get quite verbose, and we're already catching and
                    // logging the corresponding results anyways.
                    .error_handler(Box::new(r2d2::NopErrorHandler))
                    // Initialize connection pool lazily. This allows the SpaceAPI
                    // server to work even without a database connection.
                    .build_unchecked(client);
                Ok(redis_pool)
            }
        };

        Ok(SpaceapiServer {
            status: self.status,
            redis_pool: pool?,
            sensor_specs: Arc::new(self.sensor_specs),
            status_modifiers: self.status_modifiers,
        })
    }
}

/// A SpaceAPI server instance.
///
/// You can create a new instance using the ``new`` constructor method by
/// passing it the host, the port, the ``Status`` object and a redis connection info object.
///
/// The ``SpaceapiServer`` includes a web server through
/// [Hyper](http://hyper.rs/hyper/hyper/server/index.html). Simply call the ``serve`` method.
pub struct SpaceapiServer {
    status: api::Status,
    redis_pool: RedisPool,
    sensor_specs: sensors::SafeSensorSpecs,
    status_modifiers: Vec<Box<dyn modifiers::StatusModifier>>,
}

impl SpaceapiServer {
    /// Create and return a Router instance.
    fn route(self) -> Router {
        let mut router = Router::new();

        router.get(
            "/",
            handlers::ReadHandler::new(
                self.status.clone(),
                self.redis_pool.clone(),
                self.sensor_specs.clone(),
                self.status_modifiers,
            ),
            "root",
        );

        router.put(
            "/sensors/:sensor/",
            handlers::UpdateHandler::new(self.redis_pool.clone(), self.sensor_specs),
            "sensors",
        );

        router
    }

    /// Start a HTTP server listening on ``self.host:self.port``.
    ///
    /// The call returns an `HttpResult<Listening>` object, see
    /// http://ironframework.io/doc/hyper/server/struct.Listening.html
    /// for more information.
    pub fn serve<S: ToSocketAddrs>(self, socket_addr: S) -> crate::HttpResult<crate::Listening> {
        // Launch server process
        let router = self.route();
        println!("Starting HTTP server on:");
        for a in socket_addr.to_socket_addrs()? {
            println!("\thttp://{}", a);
        }
        Iron::new(router).http(socket_addr)
    }
}