-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c72c78d
commit b126dc1
Showing
25 changed files
with
600 additions
and
47 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/** | ||
* A list cursor is a string that contains two values: | ||
* - the last created_at value, | ||
* - the last uuid value. | ||
* | ||
* It is used to paginate the list of sensors. | ||
* | ||
* It is NOT a secret. But it doesn't have to be human readable. | ||
* | ||
* For simplicity, we use use a string serialisation, and the | ||
* ASCIIÂ UNIT SEPARATOR (US) character to separate the two values. | ||
* The string is then rot13 and base64+url encoded. Rot13Â for fun. | ||
*/ | ||
use anyhow::{bail, Result}; | ||
|
||
#[derive(Debug)] | ||
pub struct ListCursor { | ||
pub next_created_at: String, | ||
pub next_uuid: String, | ||
} | ||
|
||
impl ListCursor { | ||
pub fn new(next_created_at: String, next_uuid: String) -> Self { | ||
Self { | ||
next_created_at, | ||
next_uuid, | ||
} | ||
} | ||
|
||
/// Parse a list cursor from a string. | ||
/// | ||
/// The string must be in the format: | ||
/// - last_created_at | ||
/// - last_uuid | ||
/// | ||
/// The string is then rot13 and base64+url decoded. | ||
pub fn parse(cursor: &str) -> Result<Self> { | ||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; | ||
let base64_data = rot13::rot13(cursor); | ||
let data = URL_SAFE_NO_PAD.decode(base64_data)?; | ||
let data_string = String::from_utf8(data)?; | ||
// split on the separator | ||
let parts: Vec<&str> = data_string.split('\u{001F}').collect(); | ||
if parts.len() != 2 { | ||
bail!("Invalid cursor: must contain two parts"); | ||
} | ||
Ok(Self { | ||
next_created_at: parts[0].to_string(), | ||
next_uuid: parts[1].to_string(), | ||
}) | ||
} | ||
|
||
/// Convert the list cursor to a string. | ||
/// | ||
/// The string is then rot13 and base64+url encoded. | ||
#[allow(clippy::inherent_to_string)] | ||
pub fn to_string(&self) -> String { | ||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; | ||
let data_string = format!("{}\u{001F}{}", self.next_created_at, self.next_uuid); | ||
let data = data_string.as_bytes(); | ||
let base64_data = URL_SAFE_NO_PAD.encode(data); | ||
rot13::rot13(&base64_data) | ||
} | ||
} | ||
|
||
impl Default for ListCursor { | ||
fn default() -> Self { | ||
Self::new( | ||
"-1".to_string(), | ||
"00000000-0000-0000-0000-000000000000".to_string(), | ||
) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_list_cursor() { | ||
let cursor = ListCursor::new( | ||
"2023-01-01T00:00:00Z".to_string(), | ||
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".to_string(), | ||
); | ||
let string = cursor.to_string(); | ||
|
||
assert_eq!( | ||
string, | ||
"ZwNlZl0jZF0jZIDjZQbjZQbjZSbsLJSuLJSuLJRgLJSuLF1uLJSuYJSuLJRgLJSuLJSuLJSuLJSu" | ||
); | ||
|
||
let parsed = ListCursor::parse(&string).unwrap(); | ||
assert_eq!(parsed.next_created_at, "2023-01-01T00:00:00Z"); | ||
assert_eq!(parsed.next_uuid, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); | ||
} | ||
|
||
#[test] | ||
fn test_list_cursor_default() { | ||
let cursor = ListCursor::default(); | ||
assert_eq!(cursor.next_created_at, "-1"); | ||
assert_eq!(cursor.next_uuid, "00000000-0000-0000-0000-000000000000"); | ||
} | ||
|
||
#[test] | ||
fn test_parsing_failures() { | ||
assert!(ListCursor::parse("").is_err()); | ||
assert!( | ||
ListCursor::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\u{001F}") | ||
.is_err() | ||
); | ||
assert!(ListCursor::parse("aaa\u{001F}aa\u{001F}aa").is_err()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod list_cursor; | ||
pub mod viewmodel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pub mod sensor_viewmodel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
use std::collections::BTreeMap; | ||
|
||
use serde::{Deserialize, Serialize}; | ||
use uuid::Uuid; | ||
|
||
#[derive(Debug, Serialize, Deserialize)] | ||
pub struct SensorViewModel { | ||
pub uuid: Uuid, | ||
pub name: String, | ||
pub created_at: Option<String>, | ||
pub sensor_type: String, | ||
#[serde(skip_serializing_if = "Option::is_none")] | ||
pub unit: Option<String>, | ||
pub labels: BTreeMap<String, String>, | ||
} | ||
|
||
// From Sensor model to SensorViewModel | ||
impl From<crate::datamodel::Sensor> for SensorViewModel { | ||
fn from(sensor: crate::datamodel::Sensor) -> Self { | ||
Self { | ||
uuid: sensor.uuid, | ||
name: sensor.name, | ||
created_at: None, // non view Sensors do not have a created_at field | ||
sensor_type: sensor.sensor_type.to_string(), | ||
unit: sensor.unit.map(|unit| unit.name), | ||
labels: sensor | ||
.labels | ||
.iter() | ||
.map(|(key, value)| (key.clone(), value.clone())) | ||
.collect(), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,66 @@ | ||
use crate::crud::list_cursor::ListCursor; | ||
use crate::crud::viewmodel::sensor_viewmodel::SensorViewModel; | ||
use crate::ingestors::http::app_error::AppError; | ||
use crate::ingestors::http::state::HttpServerState; | ||
use axum::extract::State; | ||
use anyhow::Result; | ||
use axum::extract::{Query, State}; | ||
use axum::Json; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
#[derive(Debug, Deserialize)] | ||
pub struct ListSensorsQuery { | ||
pub cursor: Option<String>, | ||
pub limit: Option<usize>, | ||
} | ||
|
||
#[derive(Debug, Serialize)] | ||
pub struct ListSensorsResponse { | ||
pub sensors: Vec<SensorViewModel>, | ||
pub cursor: Option<String>, | ||
} | ||
|
||
/// List all the sensors. | ||
#[utoipa::path( | ||
get, | ||
path = "/sensors", | ||
path = "/api/v1/sensors", | ||
tag = "SensApp", | ||
responses( | ||
(status = 200, description = "List of sensors", body = Vec<String>) | ||
) | ||
(status = 200, description = "List of sensors", body = Vec<SensorViewModel>) | ||
), | ||
params( | ||
("cursor" = Option<String>, Query, description = "Cursor to start listing from"), | ||
("limit" = Option<u64>, Query, description = "Limit the number of sensors to return, 1000 by default", maximum = 100_000, minimum = 1), | ||
), | ||
)] | ||
pub async fn list_sensors( | ||
State(state): State<HttpServerState>, | ||
) -> Result<Json<Vec<String>>, AppError> { | ||
let sensors = state.storage.list_sensors().await?; | ||
Ok(Json(sensors)) | ||
Query(query): Query<ListSensorsQuery>, | ||
) -> Result<Json<ListSensorsResponse>, AppError> { | ||
let cursor = query | ||
.cursor | ||
.map(|cursor| ListCursor::parse(&cursor)) | ||
.unwrap_or_else(|| Ok(ListCursor::default())) | ||
.map_err(AppError::BadRequest)?; | ||
|
||
let limit = query.limit.unwrap_or(1000); | ||
if limit == 0 || limit > 100_000 { | ||
return Err(AppError::BadRequest(anyhow::anyhow!( | ||
"Limit must be between 1 and 100,000" | ||
))); | ||
} | ||
|
||
let (sensors, next_cursor) = | ||
state | ||
.storage | ||
.list_sensors(cursor, limit) | ||
.await | ||
.map_err(|error| { | ||
eprintln!("Failed to list sensors: {:?}", error); | ||
AppError::InternalServerError(error) | ||
})?; | ||
|
||
Ok(Json(ListSensorsResponse { | ||
sensors, | ||
cursor: next_cursor.map(|cursor| cursor.to_string()), | ||
})) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.