Skip to content

Commit

Permalink
Add bnas-analysis endpoints (#79)
Browse files Browse the repository at this point in the history
Adds the GET, POST and PATCH endpoints for the `/bnas/analysis` route.

Drive-by:
- Creates the BnaRequestExt to extend the Request object capabilities
  for the BNA lambdas need.

Signed-off-by: Rémy Greinhofer <[email protected]>
  • Loading branch information
rgreinho authored Feb 2, 2024
1 parent bf2843f commit c9837a0
Show file tree
Hide file tree
Showing 17 changed files with 593 additions and 55 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/deployment-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@ jobs:
- name: Deploy lambdas
run: |
LAMBDAS="get-bnas
get-bnas-analysis
get-bnas-cities
get-bnas-results
get-cities
get-cities-bnas
get-cities-submissions
patch-bnas-analysis
patch-cities-submissions
post-bnas-analysis
post-bnas-enqueue
post-cities-submissions"
echo $LAMBDAS \
Expand Down
28 changes: 28 additions & 0 deletions docs/database.dbml
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,31 @@ Ref: bna.summary.bna_uuid - bna.opportunity.bna_uuid
Ref: bna.summary.bna_uuid - bna.recreation.bna_uuid
Ref: bna.summary.bna_uuid - bna.infrastructure.bna_uuid
Ref: bna.summary.bna_uuid - bna.features.bna_uuid

enum brokenspoke_status {
pending
started
complete
}

enum brokenspoke_state {
pipeline
sqs_message
setup
analysis
export
}

Table bna.brokenspoke_pipeline {
brokenspoke_pipeline_id int [pk, increment]
state brokenspoke_state
state_machine_id uuid
sqs_message json
neon_branch_id varchar(50)
fargate_task_id uuid
s3_bucket varchar(50)

indexes {
broken_spoke_pipeline_id
}
}
42 changes: 21 additions & 21 deletions effortless/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use crate::{
error::{APIError, APIErrorSource, APIErrors},
fragment::{self, get_apigw_request_id},
};
use lambda_http::{http::StatusCode, Request};
use serde::Deserialize;
use lambda_http::{http::StatusCode, Request, RequestPayloadExt};
use serde::de::DeserializeOwned;
use std::{fmt::Display, str::FromStr};

/// Parse the first path parameter found in the API Gateway request, into the provided type.
Expand Down Expand Up @@ -87,7 +87,7 @@ where
///
/// ```rust
/// use effortless::api::parse_request_body;
/// use lambda_http::{Request, RequestExt};
/// use lambda_http::{http::{self, StatusCode}, Body, Request, RequestExt};
///
/// use serde::Deserialize;
///
Expand All @@ -97,31 +97,22 @@ where
/// pub last_name: String
/// }
///
/// let event = Request::new("{\n \"first_name\": \"Rosa\",\n \"last_name\": \"Maria\"\n}".into())
/// .with_request_context(lambda_http::request::RequestContext::ApiGatewayV2(
/// lambda_http::aws_lambda_events::apigw::ApiGatewayV2httpRequestContext::default(),
/// ));
/// let event = http::Request::builder()
/// .header(http::header::CONTENT_TYPE, "application/json")
/// .body(Body::from(r#"{"first_name": "Rosa","last_name": "Maria"}"#))
/// .expect("failed to build request");
/// let person = parse_request_body::<Person>(&event).unwrap();
/// assert_eq!(person.first_name, "Rosa");
/// assert_eq!(person.last_name, "Maria");
/// ```
pub fn parse_request_body<T>(event: &Request) -> Result<T, APIErrors>
where
T: for<'a> Deserialize<'a>,
T: DeserializeOwned,
{
match fragment::parse_request_body::<T>(event) {
Ok(o) => Ok(o),
Err(e) => {
let api_error = APIError::new(
get_apigw_request_id(event),
StatusCode::BAD_REQUEST,
"Invalid Body",
e.to_string().as_str(),
None,
);
Err(APIErrors::new(&[api_error]))
}
}
let payload = event
.payload::<T>()
.map_err(|e| invalid_body(event, e.to_string().as_str()))?;
Ok(payload.ok_or_else(|| invalid_body(event, "No request body was provided."))?)
}

/// Create an APIError representing an item not found error.
Expand Down Expand Up @@ -176,3 +167,12 @@ pub fn invalid_body(event: &Request, details: &str) -> APIError {
None,
)
}

/// Create and APIError from and API Gateway event, representing a parameter issue.
pub fn invalid_path_parameter(event: &Request, parameter: &str, details: &str) -> APIError {
APIError::with_pointer(
get_apigw_request_id(event),
parameter,
format!("invalid path parameter `{parameter}`: {details}").as_str(),
)
}
87 changes: 76 additions & 11 deletions effortless/src/fragment.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
use lambda_http::{aws_lambda_events::query_map::QueryMap, Request, RequestExt};
use lambda_http::{aws_lambda_events::query_map::QueryMap, http, Request, RequestExt};
use serde::Deserialize;
use std::str::FromStr;

fn parse_parameter<T>(qm: &QueryMap, parameter: &str) -> Option<Result<T, <T as FromStr>::Err>>
where
T: FromStr,
{
match qm.first(parameter) {
Some(parameter_str) => {
let parsed_parameter = parameter_str.parse::<T>();
match parsed_parameter {
Ok(param) => Some(Ok(param)),
Err(e) => Some(Err(e)),
}
}
None => None,
}
qm.first(parameter)
.map(|parameter_str| match parameter_str.parse::<T>() {
Ok(param) => Some(Ok(param)),
Err(e) => Some(Err(e)),
})?
}

/// Parse the first matching path parameter into the provided type.
Expand Down Expand Up @@ -104,3 +99,73 @@ pub fn get_apigw_request_id(event: &Request) -> Option<String> {
_ => None,
}
}

/// Attempt to create an extension trait for [`lambda_http::Request`].
pub trait BnaRequestExt {
/// Return the first matching parameter from the QueryMap, deserialized into its type T, if it exists.
fn parse_parameter<T>(qm: &QueryMap, parameter: &str) -> Option<Result<T, <T as FromStr>::Err>>
where
T: FromStr,
{
qm.first(parameter)
.map(|parameter_str| match parameter_str.parse::<T>() {
Ok(param) => Some(Ok(param)),
Err(e) => Some(Err(e)),
})?
}

/// Return the first matching path parameter if it exists.
fn first_path_parameter(&self, parameter: &str) -> Option<String>;

/// Return the first matching query string parameter if it exists.
fn first_query_string_parameter(&self, parameter: &str) -> Option<String>;

/// Return the first matching path parameter deserialized into its type T, if it exists.
fn path_parameter<T>(&self, parameter: &str) -> Option<Result<T, T::Err>>
where
T: FromStr;

/// Return the first matching path parameter deserialized into its type T, if it exists.
fn query_string_parameter<T>(&self, parameter: &str) -> Option<Result<T, T::Err>>
where
T: FromStr;

/// Returns the Api Gateway Request ID from an ApiGatewayV2 event.
///
/// If there is no request ID or the event is not coming from an ApiGatewayV2, the
/// function returns None.
fn apigw_request_id(&self) -> Option<String>;
}

impl<B> BnaRequestExt for http::Request<B> {
fn first_path_parameter(&self, parameter: &str) -> Option<String> {
self.path_parameters().first(parameter).map(String::from)
}

fn first_query_string_parameter(&self, parameter: &str) -> Option<String> {
self.query_string_parameters()
.first(parameter)
.map(String::from)
}

fn path_parameter<T>(&self, parameter: &str) -> Option<Result<T, <T as FromStr>::Err>>
where
T: FromStr,
{
Self::parse_parameter::<T>(&self.path_parameters(), parameter)
}

fn query_string_parameter<T>(&self, parameter: &str) -> Option<Result<T, <T as FromStr>::Err>>
where
T: FromStr,
{
Self::parse_parameter::<T>(&self.query_string_parameters(), parameter)
}

fn apigw_request_id(&self) -> Option<String> {
match self.request_context() {
lambda_http::request::RequestContext::ApiGatewayV2(payload) => payload.request_id,
_ => None,
}
}
}
23 changes: 23 additions & 0 deletions entity/src/entities/brokenspoke_pipeline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
use super::sea_orm_active_enums::BrokenspokeState;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "brokenspoke_pipeline")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub state: Option<BrokenspokeState>,
pub state_machine_id: Option<String>,
pub sqs_message: Option<Json>,
pub neon_branch_id: Option<String>,
pub fargate_task_id: Option<Uuid>,
pub s3_bucket: Option<String>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
1 change: 1 addition & 0 deletions entity/src/entities/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pub mod prelude;

pub mod brokenspoke_pipeline;
pub mod census;
pub mod city;
pub mod core_services;
Expand Down
1 change: 1 addition & 0 deletions entity/src/entities/prelude.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
pub use super::brokenspoke_pipeline::Entity as BrokenspokePipeline;
pub use super::census::Entity as Census;
pub use super::city::Entity as City;
pub use super::core_services::Entity as CoreServices;
Expand Down
14 changes: 14 additions & 0 deletions entity/src/entities/sea_orm_active_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ pub enum ApprovalStatus {
#[sea_orm(string_value = "Rejected")]
Rejected,
}
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "brokenspoke_state")]
pub enum BrokenspokeState {
#[sea_orm(string_value = "analysis")]
Analysis,
#[sea_orm(string_value = "export")]
Export,
#[sea_orm(string_value = "pipeline")]
Pipeline,
#[sea_orm(string_value = "setup")]
Setup,
#[sea_orm(string_value = "sqs_message")]
SqsMessage,
}
Loading

0 comments on commit c9837a0

Please sign in to comment.