Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Create API to update flight plans #12

Merged
merged 13 commits into from
Mar 14, 2022
27 changes: 27 additions & 0 deletions protobufs/atc/v1/airplane.proto
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@ message GetAirplaneResponse {
Airplane airplane = 1;
}

message UpdateFlightPlanRequest {
string id = 1;
repeated Node flight_plan = 2;
}

message UpdateFlightPlanResponse {
oneof payload {
UpdateFlightPlanSuccess success = 1;
UpdateFlightPlanError error = 2;
}
}

message UpdateFlightPlanSuccess {}
message UpdateFlightPlanError {
// buf:lint:ignore ENUM_VALUE_PREFIX
// buf:lint:ignore ENUM_ZERO_VALUE_SUFFIX
enum ValidationError {
UNSPECIFIED = 0;
NODE_OUT_OF_BOUNDS = 1;
NOT_IN_LOGICAL_ORDER = 2;
HAS_SHARP_TURNS = 3;
INVALID_FIRST_NODE = 4;
}
repeated ValidationError errors = 1;
}

service AirplaneService {
rpc GetAirplane(GetAirplaneRequest) returns (GetAirplaneResponse);
rpc UpdateFlightPlan(UpdateFlightPlanRequest) returns (UpdateFlightPlanResponse);
}
1 change: 1 addition & 0 deletions protobufs/buf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ version: v1
lint:
use:
- DEFAULT
allow_comment_ignores: true

breaking:
use:
Expand Down
229 changes: 227 additions & 2 deletions src/atc-game/src/api/airplane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ use std::sync::Arc;

use tonic::{Request, Response, Status};

use atc::v1::{GetAirplaneRequest, GetAirplaneResponse};
use atc::v1::update_flight_plan_response::Payload;
use atc::v1::{
GetAirplaneRequest, GetAirplaneResponse, UpdateFlightPlanError, UpdateFlightPlanRequest,
UpdateFlightPlanResponse, UpdateFlightPlanSuccess,
};

use crate::command::CommandSender;
use crate::components::{AirplaneId, FlightPlan};
use crate::store::Store;
use crate::Command;

#[derive(Clone, Debug)]
pub struct AirplaneService {
#[allow(dead_code)] // TODO: Remove when updating a flight plan
command_bus: CommandSender,
store: Arc<Store>,
}
Expand Down Expand Up @@ -38,4 +43,224 @@ impl atc::v1::airplane_service_server::AirplaneService for AirplaneService {
)))
}
}

async fn update_flight_plan(
&self,
request: Request<UpdateFlightPlanRequest>,
) -> Result<Response<UpdateFlightPlanResponse>, Status> {
let request = request.into_inner();
let id = request.id;

let airplane = match self.store.get(&id) {
Some(airplane) => airplane,
None => {
return Err(Status::not_found(&format!(
"No airplane with id {id} was found"
)));
}
};

let previous_flight_plan = (&airplane.flight_plan).into();
let new_flight_plan: FlightPlan = (&request.flight_plan).into();

if let Err(errors) = new_flight_plan.validate(&previous_flight_plan) {
let errors = errors.iter().map(|error| (*error).into()).collect();

return Ok(Response::new(UpdateFlightPlanResponse {
payload: Some(Payload::Error(UpdateFlightPlanError { errors })),
}));
};

if self
.command_bus
.send(Command::UpdateFlightPlan(
AirplaneId::new(id),
new_flight_plan,
))
.is_err()
{
return Err(Status::internal("failed to queue command"));
}

Ok(Response::new(UpdateFlightPlanResponse {
payload: Some(Payload::Success(UpdateFlightPlanSuccess {})),
}))
}
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use tokio::sync::broadcast::channel;
use tonic::{Code, Request};

use atc::v1::airplane_service_server::AirplaneService as ServiceTrait;
use atc::v1::update_flight_plan_error::ValidationError;
use atc::v1::update_flight_plan_response::Payload;
use atc::v1::{Airplane, GetAirplaneRequest, UpdateFlightPlanRequest};

use crate::api::airplane::AirplaneService;
use crate::api::IntoApi;
use crate::command::CommandReceiver;
use crate::components::{AirplaneId, FlightPlan, Location};
use crate::map::{Tile, MAP_HEIGHT_RANGE, MAP_WIDTH_RANGE};
use crate::{Command, Store};

fn setup() -> (CommandReceiver, Arc<Store>, AirplaneService) {
let (command_sender, command_receiver) = channel::<Command>(1024);
let store = Arc::new(Store::new());
let service = AirplaneService::new(command_sender, store.clone());

(command_receiver, store, service)
}

fn init_airplane(id: &str, store: &Arc<Store>) -> (AirplaneId, Location, FlightPlan) {
let id = AirplaneId::new(id.into());
let location = Location::new(0, 0);
let flight_plan = FlightPlan::new(vec![Tile::new(0, 0)]);

let airplane = Airplane {
id: id.clone().into_api(),
location: Some(location.into_api()),
flight_plan: flight_plan.clone().into_api(),
};

store.insert("AT-4321".into(), airplane);

(id, location, flight_plan)
}

#[tokio::test]
async fn get_airplane_with_wrong_id() {
let (_command_bus, _store, service) = setup();

let request = Request::new(GetAirplaneRequest {
id: "AT-4321".into(),
});
let status = service.get_airplane(request).await.unwrap_err();

assert_eq!(status.code(), Code::NotFound);
}

#[tokio::test]
async fn get_airplane_for_existing_plane() {
let (_command_bus, store, service) = setup();
let (_id, _location, _flight_plan) = init_airplane("AT-4321", &store);

let request = Request::new(GetAirplaneRequest {
id: "AT-4321".into(),
});
let response = service.get_airplane(request).await.unwrap();

let payload = response.into_inner();
let airplane = payload.airplane.unwrap();

assert_eq!("AT-4321", &airplane.id);
}

#[tokio::test]
async fn update_flight_plan_with_wrong_id() {
let (_command_bus, _store, service) = setup();

let request = Request::new(UpdateFlightPlanRequest {
id: "AT-4321".into(),
flight_plan: vec![Tile::new(0, 0).into_api()],
});
let status = service.update_flight_plan(request).await.unwrap_err();

assert_eq!(status.code(), Code::NotFound);
}

#[tokio::test]
async fn update_flight_plan_with_invalid_plan() {
let (mut command_bus, store, service) = setup();
let (_id, _location, _flight_plan) = init_airplane("AT-4321", &store);

let request = Request::new(UpdateFlightPlanRequest {
id: "AT-4321".into(),
flight_plan: vec![
Tile::new(1, 0).into_api(),
Tile::new(3, 0).into_api(),
Tile::new(1, 0).into_api(),
Tile::new(MAP_WIDTH_RANGE.start() - 1, MAP_HEIGHT_RANGE.start() - 1).into_api(),
],
});
let response = service.update_flight_plan(request).await.unwrap();

let actual_errors = match response.into_inner().payload.unwrap() {
Payload::Error(error) => error.errors,
_ => panic!("unexpected payload"),
};
let expected_errors: Vec<i32> = vec![
ValidationError::NodeOutOfBounds.into(),
ValidationError::NotInLogicalOrder.into(),
ValidationError::InvalidFirstNode.into(),
ValidationError::HasSharpTurns.into(),
];

assert_eq!(expected_errors, actual_errors);
assert!(command_bus.try_recv().is_err());
}

#[tokio::test]
async fn update_flight_plan_fails_to_queue_command() {
let (command_bus, store, service) = setup();
std::mem::drop(command_bus);

let id = AirplaneId::new("AT-4321".into());
let location = Location::new(0, 0);
let flight_plan = FlightPlan::new(vec![Tile::new(0, 0)]);

let airplane = Airplane {
id: id.into_api(),
location: Some(location.into_api()),
flight_plan: flight_plan.into_api(),
};

store.insert("AT-4321".into(), airplane);

let request = Request::new(UpdateFlightPlanRequest {
id: "AT-4321".into(),
flight_plan: vec![Tile::new(0, 0).into_api()],
});
let status = service.update_flight_plan(request).await.unwrap_err();

assert_eq!(status.code(), Code::Internal);
}

#[tokio::test]
async fn update_flight_plan_with_valid_plan() {
let (mut command_bus, store, service) = setup();

let id = AirplaneId::new("AT-4321".into());
let location = Location::new(0, 0);
let flight_plan = FlightPlan::new(vec![Tile::new(0, 0)]);

let airplane = Airplane {
id: id.into_api(),
location: Some(location.into_api()),
flight_plan: flight_plan.into_api(),
};

store.insert("AT-4321".into(), airplane);

let new_flight_plan = FlightPlan::new(vec![Tile::new(0, 0), Tile::new(1, 0)]);

let request = Request::new(UpdateFlightPlanRequest {
id: "AT-4321".into(),
flight_plan: new_flight_plan.clone().into_api(),
});
let response = service.update_flight_plan(request).await.unwrap();

if let Payload::Error(_) = response.into_inner().payload.unwrap() {
panic!("unexpected payload");
}

let command = command_bus.try_recv().unwrap();
let Command::UpdateFlightPlan(airplane_id, flight_plan) = command;

assert_eq!("AT-4321", airplane_id.get());
assert_eq!(new_flight_plan, flight_plan);
}
}
4 changes: 4 additions & 0 deletions src/atc-game/src/components/airplane_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use crate::api::IntoApi;
pub struct AirplaneId(String);

impl AirplaneId {
pub fn new(id: String) -> Self {
Self(id)
}

#[allow(dead_code)] // TODO: Remove when the id is read
pub fn get(&self) -> &str {
&self.0
Expand Down
26 changes: 15 additions & 11 deletions src/atc-game/src/components/flight_plan.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
use bevy::prelude::*;

use atc::v1::update_flight_plan_error::ValidationError;
use atc::v1::Node as ApiNode;

use crate::api::IntoApi;
use crate::map::{Tile, MAP_HEIGHT_RANGE, MAP_WIDTH_RANGE};

#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[allow(dead_code)] // Remove when flight plans get validated
pub enum ValidationError {
InvalidFirstNode, // TODO: Find a more descriptive name
HasSharpTurns,
NodeOutOfBounds,
NotInLogicalOrder,
}

#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Component)]
pub struct FlightPlan(Vec<Tile>);

Expand All @@ -30,7 +22,6 @@ impl FlightPlan {
&mut self.0
}

#[allow(dead_code)] // Remove when flight plans get validated
pub fn validate(&self, previous_flight_plan: &FlightPlan) -> Result<(), Vec<ValidationError>> {
let errors: Vec<ValidationError> = vec![
self.is_within_map_bounds(),
Expand Down Expand Up @@ -100,6 +91,18 @@ impl FlightPlan {
}
}

impl From<&Vec<atc::v1::Node>> for FlightPlan {
fn from(api_flight_plan: &Vec<atc::v1::Node>) -> Self {
let tiles = api_flight_plan
.iter()
.rev()
.map(|node| Tile::new(node.x, node.y))
.collect();

FlightPlan(tiles)
}
}

impl IntoApi for FlightPlan {
type ApiType = Vec<ApiNode>;

Expand All @@ -110,7 +113,8 @@ impl IntoApi for FlightPlan {

#[cfg(test)]
mod tests {
use crate::components::ValidationError;
use atc::v1::update_flight_plan_error::ValidationError;

use crate::map::{Tile, MAP_HEIGHT_RANGE, MAP_WIDTH_RANGE};

use super::FlightPlan;
Expand Down
5 changes: 5 additions & 0 deletions src/atc-game/src/components/location.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ pub struct Location {
}

impl Location {
#[cfg(test)]
pub fn new(x: i32, y: i32) -> Self {
Location { x, y }
}

#[allow(dead_code)] // TODO: Remove when the value is read
pub fn x(&self) -> i32 {
self.x
Expand Down
Loading