From b7439cbd17896c52ca625c7311712f00260ad8c5 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 6 Mar 2022 17:12:00 -0800 Subject: [PATCH] Add metadata to FastAPI endpoints for OpenAPI. operation_ids give us better function names when generating the typescript API from the openapi.json. BaseModel.Config.title does the same for type names. Response models (or 204 status codes) need to be explicit or the API will be declared as returning any. --- game/server/combat/models.py | 3 +++ game/server/controlpoints/models.py | 3 +++ game/server/controlpoints/routes.py | 32 +++++++++++++++++++++------ game/server/debuggeometries/models.py | 9 ++++++++ game/server/debuggeometries/routes.py | 12 +++++++--- game/server/flights/models.py | 3 +++ game/server/flights/routes.py | 10 ++++++--- game/server/frontlines/models.py | 3 +++ game/server/frontlines/routes.py | 6 +++-- game/server/game/models.py | 3 +++ game/server/game/routes.py | 2 +- game/server/leaflet.py | 2 ++ game/server/mapzones/models.py | 12 ++++++++++ game/server/mapzones/routes.py | 10 ++++++--- game/server/navmesh/models.py | 3 +++ game/server/navmesh/routes.py | 2 +- game/server/qt/routes.py | 30 ++++++++++++++++++++----- game/server/supplyroutes/models.py | 3 +++ game/server/supplyroutes/routes.py | 2 +- game/server/tgos/models.py | 3 +++ game/server/tgos/routes.py | 4 ++-- game/server/waypoints/models.py | 3 +++ game/server/waypoints/routes.py | 12 ++++++++-- 23 files changed, 142 insertions(+), 30 deletions(-) diff --git a/game/server/combat/models.py b/game/server/combat/models.py index 092a4e21..f579743c 100644 --- a/game/server/combat/models.py +++ b/game/server/combat/models.py @@ -18,6 +18,9 @@ class FrozenCombatJs(BaseModel): target_positions: list[LeafletPoint] | None footprint: list[LeafletPoly] | None + class Config: + title = "FrozenCombat" + @staticmethod def for_combat(combat: FrozenCombat, theater: ConflictTheater) -> FrozenCombatJs: if isinstance(combat, AirCombat): diff --git a/game/server/controlpoints/models.py b/game/server/controlpoints/models.py index 3bd5875c..b21314b6 100644 --- a/game/server/controlpoints/models.py +++ b/game/server/controlpoints/models.py @@ -20,6 +20,9 @@ class ControlPointJs(BaseModel): destination: LeafletPoint | None sidc: str + class Config: + title = "ControlPoint" + @staticmethod def for_control_point(control_point: ControlPoint) -> ControlPointJs: destination = None diff --git a/game/server/controlpoints/routes.py b/game/server/controlpoints/routes.py index 3bd9850b..0e644b3f 100644 --- a/game/server/controlpoints/routes.py +++ b/game/server/controlpoints/routes.py @@ -1,6 +1,6 @@ from dcs import Point from dcs.mapping import LatLng -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Body, Depends, HTTPException, status from game import Game from .models import ControlPointJs @@ -12,14 +12,18 @@ from ...sim import GameUpdateEvents router: APIRouter = APIRouter(prefix="/control-points") -@router.get("/") +@router.get( + "/", operation_id="list_control_points", response_model=list[ControlPointJs] +) def list_control_points( game: Game = Depends(GameContext.require), ) -> list[ControlPointJs]: return ControlPointJs.all_in_game(game) -@router.get("/{cp_id}") +@router.get( + "/{cp_id}", operation_id="get_control_point_by_id", response_model=ControlPointJs +) def get_control_point( cp_id: int, game: Game = Depends(GameContext.require) ) -> ControlPointJs: @@ -32,7 +36,11 @@ def get_control_point( return ControlPointJs.for_control_point(cp) -@router.get("/{cp_id}/destination-in-range") +@router.get( + "/{cp_id}/destination-in-range", + operation_id="control_point_destination_in_range", + response_model=bool, +) def destination_in_range( cp_id: int, lat: float, lng: float, game: Game = Depends(GameContext.require) ) -> bool: @@ -47,9 +55,15 @@ def destination_in_range( return cp.destination_in_range(point) -@router.put("/{cp_id}/destination") +@router.put( + "/{cp_id}/destination", + operation_id="set_control_point_destination", + status_code=status.HTTP_204_NO_CONTENT, +) def set_destination( - cp_id: int, destination: LeafletPoint, game: Game = Depends(GameContext.require) + cp_id: int, + destination: LeafletPoint = Body(..., title="destination"), + game: Game = Depends(GameContext.require), ) -> None: cp = game.theater.find_control_point_by_id(cp_id) if cp is None: @@ -77,7 +91,11 @@ def set_destination( EventStream.put_nowait(GameUpdateEvents().update_control_point(cp)) -@router.put("/{cp_id}/cancel-travel") +@router.put( + "/{cp_id}/cancel-travel", + operation_id="clear_control_point_destination", + status_code=status.HTTP_204_NO_CONTENT, +) def cancel_travel(cp_id: int, game: Game = Depends(GameContext.require)) -> None: cp = game.theater.find_control_point_by_id(cp_id) if cp is None: diff --git a/game/server/debuggeometries/models.py b/game/server/debuggeometries/models.py index 7ee33322..3fd3348e 100644 --- a/game/server/debuggeometries/models.py +++ b/game/server/debuggeometries/models.py @@ -16,6 +16,9 @@ class HoldZonesJs(BaseModel): permissible_zones: list[LeafletPoly] = Field(alias="permissibleZones") preferred_lines: list[LeafletPoly] = Field(alias="preferredLines") + class Config: + title = "HoldZones" + @classmethod def empty(cls) -> HoldZonesJs: return HoldZonesJs( @@ -62,6 +65,9 @@ class IpZonesJs(BaseModel): permissibleZone: LeafletPoly = Field(alias="permissibleZone") safeZones: list[LeafletPoly] = Field(alias="safeZones") + class Config: + title = "IpZones" + @classmethod def empty(cls) -> IpZonesJs: return IpZonesJs(homeBubble=[], ipBubble=[], permissibleZone=[], safeZones=[]) @@ -89,6 +95,9 @@ class JoinZonesJs(BaseModel): permissible_zones: list[LeafletPoly] = Field(alias="permissibleZones") preferred_lines: list[LeafletPoly] = Field(alias="preferredLines") + class Config: + title = "JoinZones" + @classmethod def empty(cls) -> JoinZonesJs: return JoinZonesJs( diff --git a/game/server/debuggeometries/routes.py b/game/server/debuggeometries/routes.py index 3b6f50e4..b85b4250 100644 --- a/game/server/debuggeometries/routes.py +++ b/game/server/debuggeometries/routes.py @@ -9,19 +9,25 @@ from .models import HoldZonesJs, IpZonesJs, JoinZonesJs router: APIRouter = APIRouter(prefix="/debug/waypoint-geometries") -@router.get("/hold/{flight_id}") +@router.get( + "/hold/{flight_id}", operation_id="get_debug_hold_zones", response_model=HoldZonesJs +) def hold_zones( flight_id: UUID, game: Game = Depends(GameContext.require) ) -> HoldZonesJs: return HoldZonesJs.for_flight(game.db.flights.get(flight_id), game) -@router.get("/ip/{flight_id}") +@router.get( + "/ip/{flight_id}", operation_id="get_debug_ip_zones", response_model=IpZonesJs +) def ip_zones(flight_id: UUID, game: Game = Depends(GameContext.require)) -> IpZonesJs: return IpZonesJs.for_flight(game.db.flights.get(flight_id), game) -@router.get("/join/{flight_id}") +@router.get( + "/join/{flight_id}", operation_id="get_debug_join_zones", response_model=JoinZonesJs +) def join_zones( flight_id: UUID, game: Game = Depends(GameContext.require) ) -> JoinZonesJs: diff --git a/game/server/flights/models.py b/game/server/flights/models.py index f306a2c0..cb337603 100644 --- a/game/server/flights/models.py +++ b/game/server/flights/models.py @@ -22,6 +22,9 @@ class FlightJs(BaseModel): sidc: str waypoints: list[FlightWaypointJs] | None + class Config: + title = "Flight" + @staticmethod def for_flight(flight: Flight, with_waypoints: bool) -> FlightJs: # Don't provide a location for aircraft that aren't in the air. Later we can diff --git a/game/server/flights/routes.py b/game/server/flights/routes.py index d66d60a1..7ffe1680 100644 --- a/game/server/flights/routes.py +++ b/game/server/flights/routes.py @@ -12,14 +12,14 @@ from game.server.leaflet import LeafletPoly, ShapelyUtil router: APIRouter = APIRouter(prefix="/flights") -@router.get("/") +@router.get("/", operation_id="list_flights", response_model=list[FlightJs]) def list_flights( with_waypoints: bool = False, game: Game = Depends(GameContext.require) ) -> list[FlightJs]: return FlightJs.all_in_game(game, with_waypoints) -@router.get("/{flight_id}") +@router.get("/{flight_id}", operation_id="get_flight_by_id", response_model=FlightJs) def get_flight( flight_id: UUID, with_waypoints: bool = False, @@ -29,7 +29,11 @@ def get_flight( return FlightJs.for_flight(flight, with_waypoints) -@router.get("/{flight_id}/commit-boundary") +@router.get( + "/{flight_id}/commit-boundary", + operation_id="get_commit_boundary_for_flight", + response_model=LeafletPoly, +) def commit_boundary( flight_id: UUID, game: Game = Depends(GameContext.require) ) -> LeafletPoly: diff --git a/game/server/frontlines/models.py b/game/server/frontlines/models.py index 1eab8d13..7d6c3292 100644 --- a/game/server/frontlines/models.py +++ b/game/server/frontlines/models.py @@ -17,6 +17,9 @@ class FrontLineJs(BaseModel): id: UUID extents: list[LeafletPoint] + class Config: + title = "FrontLine" + @staticmethod def for_front_line(front_line: FrontLine) -> FrontLineJs: a = front_line.position.point_from_heading( diff --git a/game/server/frontlines/routes.py b/game/server/frontlines/routes.py index ec217c5f..c62433d0 100644 --- a/game/server/frontlines/routes.py +++ b/game/server/frontlines/routes.py @@ -9,12 +9,14 @@ from ..dependencies import GameContext router: APIRouter = APIRouter(prefix="/front-lines") -@router.get("/") +@router.get("/", operation_id="list_front_lines", response_model=list[FrontLineJs]) def list_front_lines(game: Game = Depends(GameContext.require)) -> list[FrontLineJs]: return FrontLineJs.all_in_game(game) -@router.get("/{front_line_id}") +@router.get( + "/{front_line_id}", operation_id="get_front_line_by_id", response_model=FrontLineJs +) def get_front_line( front_line_id: UUID, game: Game = Depends(GameContext.require) ) -> FrontLineJs: diff --git a/game/server/game/models.py b/game/server/game/models.py index 80ce07de..e6029d5d 100644 --- a/game/server/game/models.py +++ b/game/server/game/models.py @@ -23,6 +23,9 @@ class GameJs(BaseModel): flights: list[FlightJs] map_center: LeafletPoint + class Config: + title = "Game" + @staticmethod def from_game(game: Game) -> GameJs: return GameJs( diff --git a/game/server/game/routes.py b/game/server/game/routes.py index dad7d7ba..f960d485 100644 --- a/game/server/game/routes.py +++ b/game/server/game/routes.py @@ -7,7 +7,7 @@ from .models import GameJs router: APIRouter = APIRouter(prefix="/game") -@router.get("/") +@router.get("/", operation_id="get_game_state", response_model=GameJs) def game_state(game: Game | None = Depends(GameContext.get)) -> GameJs | None: if game is None: return None diff --git a/game/server/leaflet.py b/game/server/leaflet.py index 9ec58543..dcc25ffd 100644 --- a/game/server/leaflet.py +++ b/game/server/leaflet.py @@ -19,6 +19,8 @@ class LeafletPoint(BaseModel): class Config: orm_mode = True + title = "LatLng" + class ShapelyUtil: @staticmethod diff --git a/game/server/mapzones/models.py b/game/server/mapzones/models.py index 25789305..329ab6db 100644 --- a/game/server/mapzones/models.py +++ b/game/server/mapzones/models.py @@ -12,11 +12,17 @@ class MapZonesJs(BaseModel): exclusion: list[LeafletPoly] sea: list[LeafletPoly] + class Config: + title = "MapZones" + class UnculledZoneJs(BaseModel): position: LeafletPoint radius: float + class Config: + title = "UnculledZone" + class ThreatZonesJs(BaseModel): full: list[LeafletPoly] @@ -24,6 +30,9 @@ class ThreatZonesJs(BaseModel): air_defenses: list[LeafletPoly] radar_sams: list[LeafletPoly] + class Config: + title = "ThreatZones" + @classmethod def from_zones(cls, zones: ThreatZones, theater: ConflictTheater) -> ThreatZonesJs: return ThreatZonesJs( @@ -37,3 +46,6 @@ class ThreatZonesJs(BaseModel): class ThreatZoneContainerJs(BaseModel): blue: ThreatZonesJs red: ThreatZonesJs + + class Config: + title = "ThreatZoneContainer" diff --git a/game/server/mapzones/routes.py b/game/server/mapzones/routes.py index 54ca8d99..8b64bc97 100644 --- a/game/server/mapzones/routes.py +++ b/game/server/mapzones/routes.py @@ -8,7 +8,7 @@ from ..leaflet import ShapelyUtil router: APIRouter = APIRouter(prefix="/map-zones") -@router.get("/terrain") +@router.get("/terrain", operation_id="get_terrain_zones", response_model=MapZonesJs) def get_terrain(game: Game = Depends(GameContext.require)) -> MapZonesJs: zones = game.theater.landmap if zones is None: @@ -21,7 +21,9 @@ def get_terrain(game: Game = Depends(GameContext.require)) -> MapZonesJs: ) -@router.get("/unculled") +@router.get( + "/unculled", operation_id="list_unculled_zones", response_model=list[UnculledZoneJs] +) def get_unculled_zones( game: Game = Depends(GameContext.require), ) -> list[UnculledZoneJs]: @@ -33,7 +35,9 @@ def get_unculled_zones( ] -@router.get("/threats") +@router.get( + "/threats", operation_id="get_threat_zones", response_model=ThreatZoneContainerJs +) def get_threat_zones( game: Game = Depends(GameContext.require), ) -> ThreatZoneContainerJs: diff --git a/game/server/navmesh/models.py b/game/server/navmesh/models.py index 7bfe7199..672f153e 100644 --- a/game/server/navmesh/models.py +++ b/game/server/navmesh/models.py @@ -8,3 +8,6 @@ from game.server.leaflet import LeafletPoly class NavMeshPolyJs(BaseModel): poly: LeafletPoly threatened: bool + + class Config: + title = "NavMeshPoly" diff --git a/game/server/navmesh/routes.py b/game/server/navmesh/routes.py index ffd76c7d..b7858612 100644 --- a/game/server/navmesh/routes.py +++ b/game/server/navmesh/routes.py @@ -8,7 +8,7 @@ from ..leaflet import ShapelyUtil router: APIRouter = APIRouter(prefix="/navmesh") -@router.get("/", response_model=list[NavMeshPolyJs]) +@router.get("/", operation_id="get_navmesh", response_model=list[NavMeshPolyJs]) def get( for_player: bool, game: Game = Depends(GameContext.require) ) -> list[NavMeshPolyJs]: diff --git a/game/server/qt/routes.py b/game/server/qt/routes.py index ae6e1ff9..ac08b261 100644 --- a/game/server/qt/routes.py +++ b/game/server/qt/routes.py @@ -8,7 +8,11 @@ from ..dependencies import GameContext, QtCallbacks, QtContext router: APIRouter = APIRouter(prefix="/qt") -@router.post("/create-package/front-line/{front_line_id}") +@router.post( + "/create-package/front-line/{front_line_id}", + operation_id="open_new_front_line_package_dialog", + status_code=status.HTTP_204_NO_CONTENT, +) def new_front_line_package( front_line_id: UUID, game: Game = Depends(GameContext.require), @@ -17,7 +21,11 @@ def new_front_line_package( qt.create_new_package(game.db.front_lines.get(front_line_id)) -@router.post("/create-package/tgo/{tgo_id}") +@router.post( + "/create-package/tgo/{tgo_id}", + operation_id="open_new_tgo_package_dialog", + status_code=status.HTTP_204_NO_CONTENT, +) def new_tgo_package( tgo_id: UUID, game: Game = Depends(GameContext.require), @@ -26,7 +34,11 @@ def new_tgo_package( qt.create_new_package(game.db.tgos.get(tgo_id)) -@router.post("/info/tgo/{tgo_id}") +@router.post( + "/info/tgo/{tgo_id}", + operation_id="open_tgo_info_dialog", + status_code=status.HTTP_204_NO_CONTENT, +) def show_tgo_info( tgo_id: UUID, game: Game = Depends(GameContext.require), @@ -35,7 +47,11 @@ def show_tgo_info( qt.show_tgo_info(game.db.tgos.get(tgo_id)) -@router.post("/create-package/control-point/{cp_id}") +@router.post( + "/create-package/control-point/{cp_id}", + operation_id="open_new_control_point_package_dialog", + status_code=status.HTTP_204_NO_CONTENT, +) def new_cp_package( cp_id: int, game: Game = Depends(GameContext.require), @@ -50,7 +66,11 @@ def new_cp_package( qt.create_new_package(cp) -@router.post("/info/control-point/{cp_id}") +@router.post( + "/info/control-point/{cp_id}", + operation_id="open_control_point_info_dialog", + status_code=status.HTTP_204_NO_CONTENT, +) def show_control_point_info( cp_id: int, game: Game = Depends(GameContext.require), diff --git a/game/server/supplyroutes/models.py b/game/server/supplyroutes/models.py index 66bf349a..98bcc675 100644 --- a/game/server/supplyroutes/models.py +++ b/game/server/supplyroutes/models.py @@ -68,6 +68,9 @@ class SupplyRouteJs(BaseModel): blue: bool active_transports: list[str] + class Config: + title = "SupplyRoute" + @staticmethod def for_link( game: Game, a: ControlPoint, b: ControlPoint, points: list[Point], sea: bool diff --git a/game/server/supplyroutes/routes.py b/game/server/supplyroutes/routes.py index 3475fbd5..675b5ba5 100644 --- a/game/server/supplyroutes/routes.py +++ b/game/server/supplyroutes/routes.py @@ -7,7 +7,7 @@ from ..dependencies import GameContext router: APIRouter = APIRouter(prefix="/supply-routes") -@router.get("/") +@router.get("/", operation_id="list_supply_routes", response_model=list[SupplyRouteJs]) def list_supply_routes( game: Game = Depends(GameContext.require), ) -> list[SupplyRouteJs]: diff --git a/game/server/tgos/models.py b/game/server/tgos/models.py index a6e797c4..1133fd39 100644 --- a/game/server/tgos/models.py +++ b/game/server/tgos/models.py @@ -25,6 +25,9 @@ class TgoJs(BaseModel): dead: bool # TODO: Event stream sidc: str # TODO: Event stream + class Config: + title = "Tgo" + @staticmethod def for_tgo(tgo: TheaterGroundObject) -> TgoJs: if not tgo.might_have_aa: diff --git a/game/server/tgos/routes.py b/game/server/tgos/routes.py index 4653bf58..cc84dc22 100644 --- a/game/server/tgos/routes.py +++ b/game/server/tgos/routes.py @@ -9,11 +9,11 @@ from ..dependencies import GameContext router: APIRouter = APIRouter(prefix="/tgos") -@router.get("/") +@router.get("/", operation_id="list_tgos", response_model=list[TgoJs]) def list_tgos(game: Game = Depends(GameContext.require)) -> list[TgoJs]: return TgoJs.all_in_game(game) -@router.get("/{tgo_id}") +@router.get("/{tgo_id}", operation_id="get_tgo_by_id", response_model=TgoJs) def get_tgo(tgo_id: UUID, game: Game = Depends(GameContext.require)) -> TgoJs: return TgoJs.for_tgo(game.db.tgos.get(tgo_id)) diff --git a/game/server/waypoints/models.py b/game/server/waypoints/models.py index 00b140d3..f01e6016 100644 --- a/game/server/waypoints/models.py +++ b/game/server/waypoints/models.py @@ -34,6 +34,9 @@ class FlightWaypointJs(BaseModel): include_in_path: bool timing: str + class Config: + title = "Waypoint" + @staticmethod def for_waypoint( waypoint: FlightWaypoint, flight: Flight, waypoint_idx: int diff --git a/game/server/waypoints/routes.py b/game/server/waypoints/routes.py index 9f51f468..506cbd48 100644 --- a/game/server/waypoints/routes.py +++ b/game/server/waypoints/routes.py @@ -34,14 +34,22 @@ def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]: ] -@router.get("/{flight_id}", response_model=list[FlightWaypointJs]) +@router.get( + "/{flight_id}", + operation_id="list_all_waypoints_for_flight", + response_model=list[FlightWaypointJs], +) def all_waypoints_for_flight( flight_id: UUID, game: Game = Depends(GameContext.require) ) -> list[FlightWaypointJs]: return waypoints_for_flight(game.db.flights.get(flight_id)) -@router.post("/{flight_id}/{waypoint_idx}/position") +@router.post( + "/{flight_id}/{waypoint_idx}/position", + operation_id="set_waypoint_position", + status_code=status.HTTP_204_NO_CONTENT, +) def set_position( flight_id: UUID, waypoint_idx: int,