use dropshot::endpoint;
use dropshot::ApiDescription;
use dropshot::ConfigDropshot;
use dropshot::ConfigLogging;
use dropshot::ConfigLoggingLevel;
use dropshot::HttpError;
use dropshot::HttpResponseOk;
use dropshot::PaginationOrder;
use dropshot::PaginationOrder::Ascending;
use dropshot::PaginationOrder::Descending;
use dropshot::PaginationParams;
use dropshot::Query;
use dropshot::RequestContext;
use dropshot::ResultsPage;
use dropshot::ServerBuilder;
use dropshot::WhichPage;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::ops::Bound;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Clone, JsonSchema, Serialize)]
struct Project {
id: Uuid,
name: String,
}
#[derive(Clone, JsonSchema, Serialize)]
struct Disk {
id: Uuid,
name: String,
}
#[derive(Clone, JsonSchema, Serialize)]
struct Instance {
id: Uuid,
name: String,
}
trait HasIdentity {
fn id(&self) -> &Uuid;
fn name(&self) -> &str;
}
macro_rules! impl_HasIdentity {
($T:ident) => {
impl HasIdentity for $T {
fn id(&self) -> &Uuid {
&self.id
}
fn name(&self) -> &str {
&self.name
}
}
};
}
impl_HasIdentity!(Project);
impl_HasIdentity!(Disk);
impl_HasIdentity!(Instance);
#[derive(Deserialize, Clone, JsonSchema, Serialize)]
struct ExScanParams {
#[serde(default = "default_sort_mode")]
sort: ExSortMode,
}
fn default_sort_mode() -> ExSortMode {
ExSortMode::ByNameAscending
}
#[derive(Deserialize, Clone, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
enum ExSortMode {
ByIdAscending,
ByIdDescending,
ByNameAscending,
ByNameDescending,
}
#[derive(Debug, Deserialize, JsonSchema, Serialize)]
#[serde(rename_all = "kebab-case")]
enum ExPageSelector {
Id(PaginationOrder, Uuid),
Name(PaginationOrder, String),
}
fn page_selector<T: HasIdentity>(
item: &T,
scan_params: &ExScanParams,
) -> ExPageSelector {
match scan_params {
ExScanParams { sort: ExSortMode::ByIdAscending } => {
ExPageSelector::Id(Ascending, *item.id())
}
ExScanParams { sort: ExSortMode::ByIdDescending } => {
ExPageSelector::Id(Descending, *item.id())
}
ExScanParams { sort: ExSortMode::ByNameAscending } => {
ExPageSelector::Name(Ascending, item.name().to_string())
}
ExScanParams { sort: ExSortMode::ByNameDescending } => {
ExPageSelector::Name(Descending, item.name().to_string())
}
}
}
fn scan_params(p: &WhichPage<ExScanParams, ExPageSelector>) -> ExScanParams {
ExScanParams {
sort: match p {
WhichPage::First(ExScanParams { sort }) => sort.clone(),
WhichPage::Next(ExPageSelector::Id(Ascending, ..)) => {
ExSortMode::ByIdAscending
}
WhichPage::Next(ExPageSelector::Id(Descending, ..)) => {
ExSortMode::ByIdDescending
}
WhichPage::Next(ExPageSelector::Name(Ascending, ..)) => {
ExSortMode::ByNameAscending
}
WhichPage::Next(ExPageSelector::Name(Descending, ..)) => {
ExSortMode::ByNameDescending
}
},
}
}
#[endpoint {
method = GET,
path = "/projects"
}]
async fn example_list_projects(
rqctx: RequestContext<DataCollection>,
query: Query<PaginationParams<ExScanParams, ExPageSelector>>,
) -> Result<HttpResponseOk<ResultsPage<Project>>, HttpError> {
let pag_params = query.into_inner();
let limit = rqctx.page_limit(&pag_params)?.get() as usize;
let data = rqctx.context();
let scan_params = scan_params(&pag_params.page);
let iter = do_list(
&data,
&scan_params,
&pag_params.page,
&data.projects_by_name,
&data.projects_by_id,
);
let items = iter.take(limit).map(|p| (*p).clone()).collect();
Ok(HttpResponseOk(ResultsPage::new(items, &scan_params, page_selector)?))
}
#[endpoint {
method = GET,
path = "/disks"
}]
async fn example_list_disks(
rqctx: RequestContext<DataCollection>,
query: Query<PaginationParams<ExScanParams, ExPageSelector>>,
) -> Result<HttpResponseOk<ResultsPage<Disk>>, HttpError> {
let pag_params = query.into_inner();
let limit = rqctx.page_limit(&pag_params)?.get() as usize;
let data = rqctx.context();
let scan_params = scan_params(&pag_params.page);
let iter = do_list(
&data,
&scan_params,
&pag_params.page,
&data.disks_by_name,
&data.disks_by_id,
);
let items = iter.take(limit).map(|p| (*p).clone()).collect();
Ok(HttpResponseOk(ResultsPage::new(items, &scan_params, page_selector)?))
}
#[endpoint {
method = GET,
path = "/instances"
}]
async fn example_list_instances(
rqctx: RequestContext<DataCollection>,
query: Query<PaginationParams<ExScanParams, ExPageSelector>>,
) -> Result<HttpResponseOk<ResultsPage<Instance>>, HttpError> {
let pag_params = query.into_inner();
let limit = rqctx.page_limit(&pag_params)?.get() as usize;
let data = rqctx.context();
let scan_params = scan_params(&pag_params.page);
let iter = do_list(
&data,
&scan_params,
&pag_params.page,
&data.instances_by_name,
&data.instances_by_id,
);
let items = iter.take(limit).map(|p| (*p).clone()).collect();
Ok(HttpResponseOk(ResultsPage::new(items, &scan_params, page_selector)?))
}
fn do_list<'a, T>(
data: &'a DataCollection,
scan_params: &ExScanParams,
p: &'a WhichPage<ExScanParams, ExPageSelector>,
by_name: &'a BTreeMap<String, Arc<T>>,
by_id: &'a BTreeMap<Uuid, Arc<T>>,
) -> ItemIter<'a, T>
where
T: Clone + JsonSchema + Serialize + Send + Sync + 'static,
{
match p {
WhichPage::First(_) => match scan_params.sort {
ExSortMode::ByIdAscending => data.iter_asc(by_id),
ExSortMode::ByIdDescending => data.iter_desc(by_id),
ExSortMode::ByNameAscending => data.iter_asc(by_name),
ExSortMode::ByNameDescending => data.iter_desc(by_name),
},
WhichPage::Next(ExPageSelector::Id(Ascending, id)) => {
data.iter_asc_from(by_id, id)
}
WhichPage::Next(ExPageSelector::Id(Descending, id)) => {
data.iter_desc_from(by_id, id)
}
WhichPage::Next(ExPageSelector::Name(Ascending, name)) => {
data.iter_asc_from(by_name, name)
}
WhichPage::Next(ExPageSelector::Name(Descending, name)) => {
data.iter_desc_from(by_name, name)
}
}
}
#[tokio::main]
async fn main() -> Result<(), String> {
let port = std::env::args()
.nth(1)
.map(|p| p.parse::<u16>())
.transpose()
.map_err(|e| format!("failed to parse \"port\" argument: {}", e))?
.unwrap_or(0);
let ctx = DataCollection::new();
let config_dropshot = ConfigDropshot {
bind_address: SocketAddr::from((Ipv4Addr::LOCALHOST, port)),
..Default::default()
};
let config_logging =
ConfigLogging::StderrTerminal { level: ConfigLoggingLevel::Debug };
let log = config_logging
.to_logger("example-pagination-basic")
.map_err(|error| format!("failed to create logger: {}", error))?;
let mut api = ApiDescription::new();
api.register(example_list_projects).unwrap();
api.register(example_list_disks).unwrap();
api.register(example_list_instances).unwrap();
let server = ServerBuilder::new(api, ctx, log)
.config(config_dropshot)
.start()
.map_err(|error| format!("failed to create server: {}", error))?;
server.await
}
struct DataCollection {
projects_by_name: BTreeMap<String, Arc<Project>>,
projects_by_id: BTreeMap<Uuid, Arc<Project>>,
disks_by_name: BTreeMap<String, Arc<Disk>>,
disks_by_id: BTreeMap<Uuid, Arc<Disk>>,
instances_by_name: BTreeMap<String, Arc<Instance>>,
instances_by_id: BTreeMap<Uuid, Arc<Instance>>,
}
type ItemIter<'a, T> = Box<dyn Iterator<Item = Arc<T>> + 'a>;
impl DataCollection {
pub fn new() -> DataCollection {
let mut data = DataCollection {
projects_by_id: BTreeMap::new(),
projects_by_name: BTreeMap::new(),
disks_by_id: BTreeMap::new(),
disks_by_name: BTreeMap::new(),
instances_by_id: BTreeMap::new(),
instances_by_name: BTreeMap::new(),
};
for n in 1..1000 {
let pname = format!("project{:03}", n);
let project =
Arc::new(Project { id: Uuid::new_v4(), name: pname.clone() });
data.projects_by_name.insert(pname.clone(), Arc::clone(&project));
data.projects_by_id.insert(project.id, project);
let dname = format!("disk{:03}", n);
let disk =
Arc::new(Disk { id: Uuid::new_v4(), name: dname.clone() });
data.disks_by_name.insert(dname.clone(), Arc::clone(&disk));
data.disks_by_id.insert(disk.id, disk);
let iname = format!("disk{:03}", n);
let instance =
Arc::new(Instance { id: Uuid::new_v4(), name: iname.clone() });
data.instances_by_name.insert(iname.clone(), Arc::clone(&instance));
data.instances_by_id.insert(instance.id, instance);
}
data
}
pub fn iter_asc<'a, T: Clone + 'static, K>(
&'a self,
tree: &'a BTreeMap<K, Arc<T>>,
) -> ItemIter<'a, T> {
self.make_iter(tree.iter())
}
pub fn iter_desc<'a, T: Clone + 'static, K>(
&'a self,
tree: &'a BTreeMap<K, Arc<T>>,
) -> ItemIter<'a, T> {
self.make_iter(tree.iter().rev())
}
pub fn iter_asc_from<'a, T: Clone + 'static, K: Clone + Ord>(
&'a self,
tree: &'a BTreeMap<K, Arc<T>>,
last_seen: &K,
) -> ItemIter<'a, T> {
let iter =
tree.range((Bound::Excluded(last_seen.clone()), Bound::Unbounded));
self.make_iter(iter)
}
pub fn iter_desc_from<'a, T: Clone + 'static, K: Clone + Ord>(
&'a self,
tree: &'a BTreeMap<K, Arc<T>>,
last_seen: &K,
) -> ItemIter<'a, T> {
let iter = tree
.range((Bound::Unbounded, Bound::Excluded(last_seen.clone())))
.rev();
self.make_iter(iter)
}
fn make_iter<'a, K, I, T>(&'a self, iter: I) -> ItemIter<'a, T>
where
I: Iterator<Item = (K, &'a Arc<T>)> + 'a,
T: Clone + 'static,
{
Box::new(iter.map(|(_, item)| Arc::clone(item)))
}
}