From d2f50808ef9b96afc36d864adec74f10c9cea9af Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Tue, 19 Jun 2018 18:31:55 -0700 Subject: implement (most) of stats endpoint --- rust/fatcat-api/README.md | 3 +- rust/fatcat-api/api.yaml | 24 ++++++++++- rust/fatcat-api/api/swagger.yaml | 42 +++++++++++++++++++ rust/fatcat-api/examples/client.rs | 8 +++- rust/fatcat-api/examples/server_lib/server.rs | 8 +++- rust/fatcat-api/src/client.rs | 51 ++++++++++++++++++++++- rust/fatcat-api/src/lib.rs | 16 +++++++ rust/fatcat-api/src/mimetypes.rs | 8 ++++ rust/fatcat-api/src/models.rs | 13 ++++++ rust/fatcat-api/src/server.rs | 60 ++++++++++++++++++++++++++- rust/src/api_server.rs | 59 ++++++++++++++++++++++++-- rust/src/lib.rs | 2 + rust/tests/test_api_server.rs | 11 +++++ 13 files changed, 296 insertions(+), 9 deletions(-) diff --git a/rust/fatcat-api/README.md b/rust/fatcat-api/README.md index 2a969005..4b40f7ee 100644 --- a/rust/fatcat-api/README.md +++ b/rust/fatcat-api/README.md @@ -13,7 +13,7 @@ To see how to make this your own, look here: [README](https://github.com/swagger-api/swagger-codegen/blob/master/README.md) - API version: 0.1.0 -- Build date: 2018-06-17T23:06:55.598Z +- Build date: 2018-06-20T01:30:08.472Z This autogenerated project defines an API crate `fatcat` which contains: * An `Api` trait defining the API in Rust. @@ -77,6 +77,7 @@ cargo run --example client GetEditorChangelog cargo run --example client GetFile cargo run --example client GetRelease cargo run --example client GetReleaseFiles +cargo run --example client GetStats cargo run --example client GetWork cargo run --example client GetWorkReleases cargo run --example client LookupContainer diff --git a/rust/fatcat-api/api.yaml b/rust/fatcat-api/api.yaml index 44fde1f6..253cec04 100644 --- a/rust/fatcat-api/api.yaml +++ b/rust/fatcat-api/api.yaml @@ -39,7 +39,6 @@ x-entity-props: &ENTITYPROPS type: object additionalProperties: {} - definitions: error_response: type: object @@ -312,6 +311,12 @@ definitions: type: string role: type: string + stats_response: + type: object + properties: + extra: + type: object + additionalProperties: {} x-entity-responses: &ENTITYRESPONSES 400: @@ -788,3 +793,20 @@ paths: description: Generic Error schema: $ref: "#/definitions/error_response" + /stats: + get: + operationId: "get_stats" + parameters: + - name: more + in: query + type: string + required: false + responses: + 200: + description: Success + schema: + $ref: "#/definitions/stats_response" + default: + description: Generic Error + schema: + $ref: "#/definitions/error_response" diff --git a/rust/fatcat-api/api/swagger.yaml b/rust/fatcat-api/api/swagger.yaml index d34061fe..3740cbb2 100644 --- a/rust/fatcat-api/api/swagger.yaml +++ b/rust/fatcat-api/api/swagger.yaml @@ -1480,6 +1480,40 @@ paths: path: "/editgroup/:id/accept" HttpMethod: "Post" httpmethod: "post" + /stats: + get: + operationId: "get_stats" + parameters: + - name: "more" + in: "query" + required: false + type: "string" + formatString: "{:?}" + example: "Some(\"more_example\".to_string())" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/stats_response" + x-responseId: "Success" + x-uppercaseResponseId: "SUCCESS" + uppercase_operation_id: "GET_STATS" + uppercase_data_type: "STATSRESPONSE" + producesJson: true + default: + description: "Generic Error" + schema: + $ref: "#/definitions/error_response" + x-responseId: "GenericError" + x-uppercaseResponseId: "GENERIC_ERROR" + uppercase_operation_id: "GET_STATS" + uppercase_data_type: "ERRORRESPONSE" + producesJson: true + operation_id: "get_stats" + uppercase_operation_id: "GET_STATS" + path: "/stats" + HttpMethod: "Get" + httpmethod: "get" definitions: error_response: type: "object" @@ -2016,6 +2050,14 @@ definitions: index: 1 raw: "raw" upperCaseName: "RELEASE_CONTRIB" + stats_response: + type: "object" + properties: + extra: + type: "object" + example: + extra: "{}" + upperCaseName: "STATS_RESPONSE" editgroup_edits: properties: containers: diff --git a/rust/fatcat-api/examples/client.rs b/rust/fatcat-api/examples/client.rs index cf53eb75..28fac094 100644 --- a/rust/fatcat-api/examples/client.rs +++ b/rust/fatcat-api/examples/client.rs @@ -14,7 +14,7 @@ use clap::{App, Arg}; use fatcat::{AcceptEditgroupResponse, ApiError, ApiNoContext, ContextWrapperExt, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, GetContainerResponse, GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileResponse, GetReleaseFilesResponse, - GetReleaseResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse}; + GetReleaseResponse, GetStatsResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse}; #[allow(unused_imports)] use futures::{future, stream, Future, Stream}; @@ -39,6 +39,7 @@ fn main() { "GetFile", "GetRelease", "GetReleaseFiles", + "GetStats", "GetWork", "GetWorkReleases", "LookupContainer", @@ -179,6 +180,11 @@ fn main() { println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from(""))); } + Some("GetStats") => { + let result = client.get_stats(Some("more_example".to_string())).wait(); + println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from(""))); + } + Some("GetWork") => { let result = client.get_work("id_example".to_string()).wait(); println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from(""))); diff --git a/rust/fatcat-api/examples/server_lib/server.rs b/rust/fatcat-api/examples/server_lib/server.rs index 9ce8b5ff..5f4d7acb 100644 --- a/rust/fatcat-api/examples/server_lib/server.rs +++ b/rust/fatcat-api/examples/server_lib/server.rs @@ -13,7 +13,7 @@ use fatcat::models; use fatcat::{AcceptEditgroupResponse, Api, ApiError, Context, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, GetContainerResponse, GetCreatorReleasesResponse, GetCreatorResponse, GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileResponse, GetReleaseFilesResponse, GetReleaseResponse, - GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse}; + GetStatsResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse}; #[derive(Copy, Clone)] pub struct Server; @@ -149,6 +149,12 @@ impl Api for Server { Box::new(futures::failed("Generic failure".into())) } + fn get_stats(&self, more: Option, context: &Context) -> Box + Send> { + let context = context.clone(); + println!("get_stats({:?}) - X-Span-ID: {:?}", more, context.x_span_id.unwrap_or(String::from("")).clone()); + Box::new(futures::failed("Generic failure".into())) + } + fn get_work(&self, id: String, context: &Context) -> Box + Send> { let context = context.clone(); println!("get_work(\"{}\") - X-Span-ID: {:?}", id, context.x_span_id.unwrap_or(String::from("")).clone()); diff --git a/rust/fatcat-api/src/client.rs b/rust/fatcat-api/src/client.rs index c5c4a320..809fd1d6 100644 --- a/rust/fatcat-api/src/client.rs +++ b/rust/fatcat-api/src/client.rs @@ -36,7 +36,7 @@ use swagger::{ApiError, Context, XSpanId}; use models; use {AcceptEditgroupResponse, Api, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, GetContainerResponse, GetCreatorReleasesResponse, GetCreatorResponse, - GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileResponse, GetReleaseFilesResponse, GetReleaseResponse, GetWorkReleasesResponse, GetWorkResponse, + GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileResponse, GetReleaseFilesResponse, GetReleaseResponse, GetStatsResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse}; /// Convert input into a base path, e.g. "http://example:123". Also checks the scheme as it goes. @@ -1464,6 +1464,55 @@ impl Api for Client { Box::new(futures::done(result)) } + fn get_stats(&self, param_more: Option, context: &Context) -> Box + Send> { + // Query parameters + let query_more = param_more.map_or_else(String::new, |query| format!("more={more}&", more = query.to_string())); + + let url = format!("{}/v0/stats?{more}", self.base_path, more = utf8_percent_encode(&query_more, QUERY_ENCODE_SET)); + + let hyper_client = (self.hyper_client)(); + let request = hyper_client.request(hyper::method::Method::Get, &url); + let mut custom_headers = hyper::header::Headers::new(); + + context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); + + let request = request.headers(custom_headers); + + // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). + fn parse_response(mut response: hyper::client::response::Response) -> Result { + match response.status.to_u16() { + 200 => { + let mut buf = String::new(); + response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; + let body = serde_json::from_str::(&buf)?; + + Ok(GetStatsResponse::Success(body)) + } + 0 => { + let mut buf = String::new(); + response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?; + let body = serde_json::from_str::(&buf)?; + + Ok(GetStatsResponse::GenericError(body)) + } + code => { + let mut buf = [0; 100]; + let debug_body = match response.read(&mut buf) { + Ok(len) => match str::from_utf8(&buf[..len]) { + Ok(body) => Cow::from(body), + Err(_) => Cow::from(format!("", &buf[..len].to_vec())), + }, + Err(e) => Cow::from(format!("", e)), + }; + Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", code, response.headers, debug_body))) + } + } + } + + let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); + Box::new(futures::done(result)) + } + fn get_work(&self, param_id: String, context: &Context) -> Box + Send> { let url = format!("{}/v0/work/{id}", self.base_path, id = utf8_percent_encode(¶m_id.to_string(), PATH_SEGMENT_ENCODE_SET)); diff --git a/rust/fatcat-api/src/lib.rs b/rust/fatcat-api/src/lib.rs index c926966e..fd0cfe54 100644 --- a/rust/fatcat-api/src/lib.rs +++ b/rust/fatcat-api/src/lib.rs @@ -278,6 +278,14 @@ pub enum GetReleaseFilesResponse { GenericError(models::ErrorResponse), } +#[derive(Debug, PartialEq)] +pub enum GetStatsResponse { + /// Success + Success(models::StatsResponse), + /// Generic Error + GenericError(models::ErrorResponse), +} + #[derive(Debug, PartialEq)] pub enum GetWorkResponse { /// Found Entity @@ -394,6 +402,8 @@ pub trait Api { fn get_release_files(&self, id: String, context: &Context) -> Box + Send>; + fn get_stats(&self, more: Option, context: &Context) -> Box + Send>; + fn get_work(&self, id: String, context: &Context) -> Box + Send>; fn get_work_releases(&self, id: String, context: &Context) -> Box + Send>; @@ -451,6 +461,8 @@ pub trait ApiNoContext { fn get_release_files(&self, id: String) -> Box + Send>; + fn get_stats(&self, more: Option) -> Box + Send>; + fn get_work(&self, id: String) -> Box + Send>; fn get_work_releases(&self, id: String) -> Box + Send>; @@ -564,6 +576,10 @@ impl<'a, T: Api> ApiNoContext for ContextWrapper<'a, T> { self.api().get_release_files(id, &self.context()) } + fn get_stats(&self, more: Option) -> Box + Send> { + self.api().get_stats(more, &self.context()) + } + fn get_work(&self, id: String) -> Box + Send> { self.api().get_work(id, &self.context()) } diff --git a/rust/fatcat-api/src/mimetypes.rs b/rust/fatcat-api/src/mimetypes.rs index 68e5ec04..be7e792f 100644 --- a/rust/fatcat-api/src/mimetypes.rs +++ b/rust/fatcat-api/src/mimetypes.rs @@ -328,6 +328,14 @@ pub mod responses { lazy_static! { pub static ref GET_RELEASE_FILES_GENERIC_ERROR: Mime = mime!(Application / Json); } + /// Create Mime objects for the response content types for GetStats + lazy_static! { + pub static ref GET_STATS_SUCCESS: Mime = mime!(Application / Json); + } + /// Create Mime objects for the response content types for GetStats + lazy_static! { + pub static ref GET_STATS_GENERIC_ERROR: Mime = mime!(Application / Json); + } /// Create Mime objects for the response content types for GetWork lazy_static! { pub static ref GET_WORK_FOUND_ENTITY: Mime = mime!(Application / Json); diff --git a/rust/fatcat-api/src/models.rs b/rust/fatcat-api/src/models.rs index 140e0c2e..15247ce6 100644 --- a/rust/fatcat-api/src/models.rs +++ b/rust/fatcat-api/src/models.rs @@ -617,6 +617,19 @@ impl ReleaseRef { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct StatsResponse { + #[serde(rename = "extra")] + #[serde(skip_serializing_if = "Option::is_none")] + pub extra: Option, +} + +impl StatsResponse { + pub fn new() -> StatsResponse { + StatsResponse { extra: None } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Success { #[serde(rename = "message")] diff --git a/rust/fatcat-api/src/server.rs b/rust/fatcat-api/src/server.rs index 2716dfc5..7eb73122 100644 --- a/rust/fatcat-api/src/server.rs +++ b/rust/fatcat-api/src/server.rs @@ -38,7 +38,7 @@ use swagger::{ApiError, Context, XSpanId}; use models; use {AcceptEditgroupResponse, Api, CreateContainerBatchResponse, CreateContainerResponse, CreateCreatorBatchResponse, CreateCreatorResponse, CreateEditgroupResponse, CreateFileBatchResponse, CreateFileResponse, CreateReleaseBatchResponse, CreateReleaseResponse, CreateWorkBatchResponse, CreateWorkResponse, GetContainerResponse, GetCreatorReleasesResponse, GetCreatorResponse, - GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileResponse, GetReleaseFilesResponse, GetReleaseResponse, GetWorkReleasesResponse, GetWorkResponse, + GetEditgroupResponse, GetEditorChangelogResponse, GetEditorResponse, GetFileResponse, GetReleaseFilesResponse, GetReleaseResponse, GetStatsResponse, GetWorkReleasesResponse, GetWorkResponse, LookupContainerResponse, LookupCreatorResponse, LookupFileResponse, LookupReleaseResponse}; header! { (Warning, "Warning") => [String] } @@ -2099,6 +2099,64 @@ where "GetReleaseFiles", ); + let api_clone = api.clone(); + router.get( + "/v0/stats", + move |req: &mut Request| { + let mut context = Context::default(); + + // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). + fn handle_request(req: &mut Request, api: &T, context: &mut Context) -> Result + where + T: Api, + { + context.x_span_id = Some(req.headers.get::().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); + context.auth_data = req.extensions.remove::(); + context.authorization = req.extensions.remove::(); + + // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) + let query_params = req.get::().unwrap_or_default(); + let param_more = query_params.get("more").and_then(|list| list.first()).and_then(|x| x.parse::().ok()); + + match api.get_stats(param_more, context).wait() { + Ok(rsp) => match rsp { + GetStatsResponse::Success(body) => { + let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + + let mut response = Response::with((status::Status::from_u16(200), body_string)); + response.headers.set(ContentType(mimetypes::responses::GET_STATS_SUCCESS.clone())); + + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + + Ok(response) + } + GetStatsResponse::GenericError(body) => { + let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize"); + + let mut response = Response::with((status::Status::from_u16(0), body_string)); + response.headers.set(ContentType(mimetypes::responses::GET_STATS_GENERIC_ERROR.clone())); + + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + + Ok(response) + } + }, + Err(_) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) + } + } + } + + handle_request(req, &api_clone, &mut context).or_else(|mut response| { + context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); + Ok(response) + }) + }, + "GetStats", + ); + let api_clone = api.clone(); router.get( "/v0/work/:id", diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index 44f266a4..7c84ad81 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -114,6 +114,17 @@ macro_rules! wrap_lookup_handler { } } +macro_rules! count_entity { + ($table:ident, $conn:expr) => {{ + let count: i64 = $table::table + .filter($table::is_live.eq(true)) + .filter($table::redirect_id.is_null()) + .select(diesel::dsl::count($table::id)) + .first($conn)?; + count + }}; +} + #[derive(Clone)] pub struct Server { pub db_pool: ConnectionPool, @@ -363,7 +374,8 @@ impl Server { .load(&conn)?; rows.into_iter() - .map(|(rev, ident, _)| release_row2entity(Some(ident), rev, &conn)).collect() + .map(|(rev, ident, _)| release_row2entity(Some(ident), rev, &conn)) + .collect() } fn get_file_handler(&self, id: String) -> Result { @@ -429,7 +441,8 @@ impl Server { .load(&conn)?; rows.into_iter() - .map(|(rev, ident, _)| file_row2entity(Some(ident), rev, &conn)).collect() + .map(|(rev, ident, _)| file_row2entity(Some(ident), rev, &conn)) + .collect() } fn get_work_handler(&self, id: String) -> Result { @@ -456,7 +469,8 @@ impl Server { .load(&conn)?; rows.into_iter() - .map(|(rev, ident)| release_row2entity(Some(ident), rev, &conn)).collect() + .map(|(rev, ident)| release_row2entity(Some(ident), rev, &conn)) + .collect() } fn create_container_handler( @@ -875,6 +889,31 @@ impl Server { .collect(); Ok(entries) } + + fn get_stats_handler(&self, _more: Option) -> Result { + let conn = self.db_pool.get().expect("db_pool error"); + + let merged_editgroups: i64 = changelog::table + .select(diesel::dsl::count(changelog::id)) + .first(&conn)?; + + let val = json!({ + "entity_counts": { + "container": count_entity!(container_ident, &conn), + "creator": count_entity!(creator_ident, &conn), + "file": count_entity!(file_ident, &conn), + "release": count_entity!(release_ident, &conn), + "work": count_entity!(work_ident, &conn), + }, + "merged_editgroups": merged_editgroups, + // TODO: "release_with_file": , + // TODO: "release_with_doi": , + // TODO: "creator_with_orcid": , + // TODO: "files_with_release": , + // TODO: "container_with_issnl": , + }); + Ok(StatsResponse { extra: Some(val) }) + } } impl Api for Server { @@ -1089,4 +1128,18 @@ impl Api for Server { }; Box::new(futures::done(Ok(ret))) } + + fn get_stats( + &self, + more: Option, + _context: &Context, + ) -> Box + Send> { + let ret = match self.get_stats_handler(more.clone()) { + Ok(stats) => GetStatsResponse::Success(stats), + Err(e) => GetStatsResponse::GenericError(ErrorResponse { + message: e.to_string(), + }), + }; + Box::new(futures::done(Ok(ret))) + } } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 8ec850c5..8334b3a5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -12,6 +12,7 @@ extern crate hyper; #[macro_use] extern crate error_chain; extern crate iron; +#[macro_use] extern crate serde_json; pub mod api_helpers; @@ -27,6 +28,7 @@ mod errors { R2d2(::diesel::r2d2::Error); Uuid(::uuid::ParseError); Io(::std::io::Error) #[cfg(unix)]; + Serde(::serde_json::Error); } } } diff --git a/rust/tests/test_api_server.rs b/rust/tests/test_api_server.rs index 05dd493f..b8f8b3c4 100644 --- a/rust/tests/test_api_server.rs +++ b/rust/tests/test_api_server.rs @@ -429,3 +429,14 @@ fn test_accept_editgroup() { .unwrap(); assert_eq!(c, 1); } + +#[test] +fn test_stats() { + let (headers, router, _conn) = setup(); + + check_response( + request::get("http://localhost:9411/v0/stats", headers, &router), + status::Ok, + Some("merged_editgroups"), + ); +} -- cgit v1.2.3