spaceapi_server/server/
mod.rs

1//! The SpaceAPI server struct.
2
3use std::net::ToSocketAddrs;
4use std::sync::Arc;
5use std::time::Duration;
6
7use iron::Iron;
8use log::debug;
9use redis::{ConnectionInfo, IntoConnectionInfo};
10use router::Router;
11
12use serde_json::map::Map;
13use serde_json::Value;
14
15mod handlers;
16
17use crate::api;
18
19use crate::errors::SpaceapiServerError;
20use crate::modifiers;
21use crate::sensors;
22use crate::types::RedisPool;
23
24enum RedisInfo {
25    None,
26    Pool(r2d2::Pool<redis::Client>),
27    ConnectionInfo(ConnectionInfo),
28    Err(SpaceapiServerError),
29}
30
31/// Builder to create a new [`SpaceapiServer`](struct.SpaceapiServer.html)
32/// instance.
33pub struct SpaceapiServerBuilder {
34    status: api::Status,
35    redis_info: RedisInfo,
36    sensor_specs: Vec<sensors::SensorSpec>,
37    status_modifiers: Vec<Box<dyn modifiers::StatusModifier>>,
38}
39
40impl SpaceapiServerBuilder {
41    /// Create a new builder instance based on the provided static status data.
42    pub fn new(mut status: api::Status) -> SpaceapiServerBuilder {
43        // Instantiate versions object
44        let mut versions = Map::new();
45        versions.insert("spaceapi-rs".into(), api::get_version().into());
46        versions.insert("spaceapi-server-rs".into(), crate::get_version().into());
47
48        // Add to extensions
49        status
50            .extensions
51            .insert("versions".into(), Value::Object(versions));
52
53        SpaceapiServerBuilder {
54            status,
55            redis_info: RedisInfo::None,
56            sensor_specs: vec![],
57            status_modifiers: vec![],
58        }
59    }
60
61    /// Specify a Redis connection string.
62    ///
63    /// This can be any object that implements
64    /// [`redis::IntoConnectionInfo`](../redis/trait.IntoConnectionInfo.html),
65    /// e.g. a connection string:
66    ///
67    /// ```ignore
68    /// ...
69    /// .redis_connection_info("redis://127.0.0.1/")
70    /// ...
71    /// ```
72    pub fn redis_connection_info<R: IntoConnectionInfo>(mut self, redis_connection_info: R) -> Self {
73        self.redis_info = match redis_connection_info.into_connection_info() {
74            Ok(ci) => RedisInfo::ConnectionInfo(ci),
75            Err(e) => RedisInfo::Err(e.into()),
76        };
77        self
78    }
79
80    /// Use this as an alternative to
81    /// [`redis_connection_info`](struct.SpaceapiServerBuilder.html#method.redis_connection_info)
82    /// if you want to initialize the Redis connection pool yourself, to have
83    /// full control over the connection parameters.
84    ///
85    /// See
86    /// [`examples/with_custom_redis_pool.rs`](https://github.com/spaceapi-community/spaceapi-server-rs/blob/master/examples/with_custom_redis_pool.rs)
87    /// for a real example.
88    pub fn redis_pool(mut self, redis_pool: r2d2::Pool<redis::Client>) -> Self {
89        self.redis_info = RedisInfo::Pool(redis_pool);
90        self
91    }
92
93    /// Add a status modifier, that modifies the status dynamically per
94    /// request.
95    ///
96    /// This can be an instance of
97    /// [`modifiers::StateFromPeopleNowPresent`](modifiers/struct.StateFromPeopleNowPresent.html),
98    /// or your own implementation that uses the dynamic sensor data and/or
99    /// external data.
100    pub fn add_status_modifier<M: modifiers::StatusModifier + 'static>(mut self, modifier: M) -> Self {
101        self.status_modifiers.push(Box::new(modifier));
102        self
103    }
104
105    /// Add a new sensor.
106    ///
107    /// The first argument is a ``api::SensorTemplate`` instance containing all static data.
108    /// The second argument specifies how to get the actual sensor value from Redis.
109    pub fn add_sensor<T: api::sensors::SensorTemplate + 'static>(
110        mut self,
111        template: T,
112        data_key: String,
113    ) -> Self {
114        self.sensor_specs.push(sensors::SensorSpec {
115            template: Box::new(template),
116            data_key,
117        });
118        self
119    }
120
121    /// Build a server instance.
122    ///
123    /// This can fail if not all required data has been provided.
124    pub fn build(self) -> Result<SpaceapiServer, SpaceapiServerError> {
125        let pool = match self.redis_info {
126            RedisInfo::None => Err("No redis connection defined".into()),
127            RedisInfo::Err(e) => Err(e),
128            RedisInfo::Pool(p) => Ok(p),
129            RedisInfo::ConnectionInfo(ci) => {
130                // Log some useful debug information
131                debug!("Connecting to redis database {} at {:?}", ci.redis.db, ci.addr);
132
133                let client: redis::Client = redis::Client::open(ci)?;
134
135                let redis_pool: r2d2::Pool<redis::Client> = r2d2::Pool::builder()
136                    // Provide up to 6 connections in connection pool
137                    .max_size(6)
138                    // At least 1 connection must be active
139                    .min_idle(Some(2))
140                    // Try to get a connection for max 1 second
141                    .connection_timeout(Duration::from_secs(1))
142                    // Don't log errors directly.
143                    // They can get quite verbose, and we're already catching and
144                    // logging the corresponding results anyways.
145                    .error_handler(Box::new(r2d2::NopErrorHandler))
146                    // Initialize connection pool lazily. This allows the SpaceAPI
147                    // server to work even without a database connection.
148                    .build_unchecked(client);
149                Ok(redis_pool)
150            }
151        };
152
153        Ok(SpaceapiServer {
154            status: self.status,
155            redis_pool: pool?,
156            sensor_specs: Arc::new(self.sensor_specs),
157            status_modifiers: self.status_modifiers,
158        })
159    }
160}
161
162/// A SpaceAPI server instance.
163///
164/// You can create a new instance using the ``new`` constructor method by
165/// passing it the host, the port, the ``Status`` object and a redis connection info object.
166///
167/// The ``SpaceapiServer`` includes a web server through
168/// [Hyper](http://hyper.rs/hyper/hyper/server/index.html). Simply call the ``serve`` method.
169pub struct SpaceapiServer {
170    status: api::Status,
171    redis_pool: RedisPool,
172    sensor_specs: sensors::SafeSensorSpecs,
173    status_modifiers: Vec<Box<dyn modifiers::StatusModifier>>,
174}
175
176impl SpaceapiServer {
177    /// Create and return a Router instance.
178    fn route(self) -> Router {
179        let mut router = Router::new();
180
181        router.get(
182            "/",
183            handlers::ReadHandler::new(
184                self.status.clone(),
185                self.redis_pool.clone(),
186                self.sensor_specs.clone(),
187                self.status_modifiers,
188            ),
189            "root",
190        );
191
192        router.put(
193            "/sensors/:sensor/",
194            handlers::UpdateHandler::new(self.redis_pool.clone(), self.sensor_specs),
195            "sensors",
196        );
197
198        router
199    }
200
201    /// Start a HTTP server listening on ``self.host:self.port``.
202    ///
203    /// The call returns an `HttpResult<Listening>` object, see
204    /// http://ironframework.io/doc/hyper/server/struct.Listening.html
205    /// for more information.
206    pub fn serve<S: ToSocketAddrs>(self, socket_addr: S) -> crate::HttpResult<crate::Listening> {
207        // Launch server process
208        let router = self.route();
209        println!("Starting HTTP server on:");
210        for a in socket_addr.to_socket_addrs()? {
211            println!("\thttp://{}", a);
212        }
213        Iron::new(router).http(socket_addr)
214    }
215}