web api glue for updating signals

This is very lightly tested, but it at least sometimes works.
This commit is contained in:
Scott Lamb 2019-06-14 16:11:12 -07:00
parent 7fe9d34655
commit 6d4b06f7d2
4 changed files with 122 additions and 38 deletions

View File

@ -1801,7 +1801,7 @@ impl LockedDatabase {
}
pub fn update_signals(
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
-> Result<(), Error> {
-> Result<(), base::Error> {
self.signal.update_signals(when, signals, states)
}
}

View File

@ -255,7 +255,7 @@ impl State {
pub fn update_signals(
&mut self, when: Range<recording::Time>, signals: &[u32], states: &[u16])
-> Result<(), Error> {
-> Result<(), base::Error> {
// Do input validation before any mutation.
self.update_signals_validate(signals, states)?;
@ -274,7 +274,7 @@ impl State {
}
/// Helper for `update_signals` to do validation.
fn update_signals_validate(&self, signals: &[u32], states: &[u16]) -> Result<(), Error> {
fn update_signals_validate(&self, signals: &[u32], states: &[u16]) -> Result<(), base::Error> {
if signals.len() != states.len() {
bail_t!(InvalidArgument, "signals and states must have same length");
}

View File

@ -30,7 +30,7 @@
use db::auth::SessionHash;
use failure::{Error, format_err};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use serde::ser::{Error as _, SerializeMap, SerializeSeq, Serializer};
use std::collections::BTreeMap;
use std::ops::Not;
@ -111,6 +111,29 @@ pub struct Signal<'a> {
pub short_name: &'a str,
}
#[derive(Deserialize)]
#[serde(rename_all="camelCase")]
pub enum PostSignalsEndBase {
Epoch,
Now,
}
#[derive(Deserialize)]
#[serde(rename_all="camelCase")]
pub struct PostSignalsRequest {
pub signal_ids: Vec<u32>,
pub states: Vec<u16>,
pub start_time_90k: Option<i64>,
pub end_base: PostSignalsEndBase,
pub rel_end_time_90k: Option<i64>,
}
#[derive(Serialize)]
#[serde(rename_all="camelCase")]
pub struct PostSignalsResponse {
pub time_90k: i64,
}
#[derive(Default, Serialize)]
#[serde(rename_all="camelCase")]
pub struct Signals {

View File

@ -629,7 +629,26 @@ impl ServiceInner {
Ok(res)
}
fn signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
fn post_signals(&self, req: &Request<hyper::Body>, body: hyper::Chunk) -> ResponseResult {
let r: json::PostSignalsRequest = serde_json::from_slice(&body)
.map_err(|e| bad_req(e.to_string()))?;
let mut l = self.db.lock();
let now = recording::Time::new(self.db.clocks().realtime());
let start = r.start_time_90k.map(recording::Time).unwrap_or(now);
let end = match r.end_base {
json::PostSignalsEndBase::Epoch => recording::Time(r.rel_end_time_90k.ok_or_else(
|| bad_req("must specify rel_end_time_90k when end_base is epoch"))?),
json::PostSignalsEndBase::Now => {
now + recording::Duration(r.rel_end_time_90k.unwrap_or(0))
},
};
l.update_signals(start .. end, &r.signal_ids, &r.states).map_err(from_base_error)?;
serve_json(req, &json::PostSignalsResponse {
time_90k: now.0,
})
}
fn get_signals(&self, req: &Request<hyper::Body>) -> ResponseResult {
let mut time = recording::Time::min_value() .. recording::Time::max_value();
if let Some(q) = req.uri().query() {
for (key, value) in form_urlencoded::parse(q.as_bytes()) {
@ -704,6 +723,63 @@ fn extract_sid(req: &Request<hyper::Body>) -> Option<auth::RawSessionId> {
None
}
/// Returns a future separating the request from its form body.
///
/// If this is not a `POST` or the body's `Content-Type` is not
/// `application/x-www-form-urlencoded`, returns an appropriate error response instead.
///
/// Use with `and_then` to chain logic which consumes the form body.
fn with_form_body(mut req: Request<hyper::Body>)
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
Error = Response<Body>> +
Send + 'static> {
if *req.method() != http::method::Method::POST {
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
"POST expected")));
}
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
Some(t) if t == "application/x-www-form-urlencoded" => true,
Some(t) if t == "application/x-www-form-urlencoded; charset=UTF-8" => true,
_ => false,
};
if !correct_mime_type {
return Box::new(future::err(bad_req(
"expected application/x-www-form-urlencoded request body")));
}
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
Box::new(b.concat2()
.map(|b| (req, b))
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
e))))
}
// TODO: remove redundancy with above. Probably better to just always expect requests in json
// format rather than using the form style for login/logout.
fn with_json_body(mut req: Request<hyper::Body>)
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
Error = Response<Body>> +
Send + 'static> {
if *req.method() != http::method::Method::POST {
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
"POST expected")));
}
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
Some(t) if t == "application/json" => true,
Some(t) if t == "application/json; charset=UTF-8" => true,
_ => false,
};
if !correct_mime_type {
return Box::new(future::err(bad_req(
"expected application/json request body")));
}
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
Box::new(b.concat2()
.map(|b| (req, b))
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
e))))
}
pub struct Config<'a> {
pub db: Arc<db::Database>,
pub ui_dir: Option<&'a str>,
@ -791,36 +867,6 @@ impl Service {
}
}
/// Returns a future separating the request from its form body.
///
/// If this is not a `POST` or the body's `Content-Type` is not
/// `application/x-www-form-urlencoded`, returns an appropriate error response instead.
///
/// Use with `and_then` to chain logic which consumes the form body.
fn with_form_body(&self, mut req: Request<hyper::Body>)
-> Box<dyn Future<Item = (Request<hyper::Body>, hyper::Chunk),
Error = Response<Body>> +
Send + 'static> {
if *req.method() != http::method::Method::POST {
return Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
"POST expected")));
}
let correct_mime_type = match req.headers().get(header::CONTENT_TYPE) {
Some(t) if t == "application/x-www-form-urlencoded" => true,
Some(t) if t == "application/x-www-form-urlencoded; charset=UTF-8" => true,
_ => false,
};
if !correct_mime_type {
return Box::new(future::err(bad_req(
"expected application/x-www-form-urlencoded request body")));
}
let b = ::std::mem::replace(req.body_mut(), hyper::Body::empty());
Box::new(b.concat2()
.map(|b| (req, b))
.map_err(|e| internal_server_err(format_err!("unable to read request body: {}",
e))))
}
fn stream_live_m4s(&self, _req: &Request<::hyper::Body>, uuid: Uuid,
stream_type: db::StreamType) -> ResponseResult {
let stream_id;
@ -905,6 +951,21 @@ impl Service {
.body(body)
.unwrap())
}
fn signals(&self, req: Request<hyper::Body>)
-> Box<dyn Future<Item = Response<Body>, Error = Response<Body>> + Send + 'static> {
use http::method::Method;
match *req.method() {
Method::POST => Box::new(with_json_body(req)
.and_then({
let s = self.0.clone();
move |(req, b)| s.post_signals(&req, b)
})),
Method::GET | Method::HEAD => Box::new(future::result(self.0.get_signals(&req))),
_ => Box::new(future::err(plain_response(StatusCode::METHOD_NOT_ALLOWED,
"POST, GET, or HEAD expected"))),
}
}
}
impl ::hyper::service::Service for Service {
@ -963,15 +1024,15 @@ impl ::hyper::service::Service for Service {
wrap_r(true, self.stream_live_m4s(&req, uuid, type_))
},
Path::NotFound => wrap(true, future::err(not_found("path not understood"))),
Path::Login => wrap(true, self.with_form_body(req).and_then({
Path::Login => wrap(true, with_form_body(req).and_then({
let s = self.clone();
move |(req, b)| { s.0.login(&req, b) }
})),
Path::Logout => wrap(true, self.with_form_body(req).and_then({
Path::Logout => wrap(true, with_form_body(req).and_then({
let s = self.clone();
move |(req, b)| { s.0.logout(&req, b) }
})),
Path::Signals => wrap_r(true, self.0.signals(&req)),
Path::Signals => wrap(true, self.signals(req)),
Path::Static => wrap_r(false, self.0.static_file(&req, req.uri().path())),
}
}