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}