Overhaul of ACARS/PIREP APIs

This commit is contained in:
Nabeel Shahzad 2018-01-04 19:33:23 -06:00
parent f4e3964f9d
commit b62fa7757c
10 changed files with 491 additions and 86 deletions

View File

@ -68,7 +68,8 @@ class Handler extends ExceptionHandler
'error' => [
'code' => $exception->getCode() ,
'http_code' => $http_code,
'message' => $exception->getMessage()
'message' => $exception->getMessage(),
'trace' => $exception->getTrace()[0],
]
], $status);
}

View File

@ -21,18 +21,29 @@ use App\Repositories\PirepRepository;
use App\Http\Resources\Acars as AcarsResource;
use App\Http\Resources\Pirep as PirepResource;
use App\Http\Resources\AcarsLog as AcarsLogResource;
use App\Http\Resources\AcarsRoute as AcarsRouteResource;
use App\Http\Controllers\AppBaseController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class PirepController extends AppBaseController
class PirepController extends RestController
{
protected $acarsRepo,
$geoSvc,
$pirepRepo,
$pirepSvc;
public static $acars_rules = [
'altitude',
'level',
'heading',
'vs',
'gs',
'transponder',
'autopilot',
'fuel_flow',
'log',
'lat',
'lon',
'created_at',
];
protected $check_attrs = [
public static $pirep_rules = [
'airline_id',
'aircraft_id',
'dpt_airport_id',
@ -48,6 +59,18 @@ class PirepController extends AppBaseController
'notes',
];
protected $acarsRepo,
$geoSvc,
$pirepRepo,
$pirepSvc;
/**
* PirepController constructor.
* @param AcarsRepository $acarsRepo
* @param GeoService $geoSvc
* @param PirepRepository $pirepRepo
* @param PIREPService $pirepSvc
*/
public function __construct(
AcarsRepository $acarsRepo,
GeoService $geoSvc,
@ -72,22 +95,17 @@ class PirepController extends AppBaseController
* status, and whatever other statuses may be defined
*
* TODO: Allow extra fields, etc to be set. Aircraft, etc
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
*/
public function prefile(Request $request)
{
Log::info('PIREP Prefile, user '.Auth::user()->id, $request->toArray());
$attrs = [
'user_id' => Auth::user()->id,
'state' => PirepState::IN_PROGRESS,
'status' => PirepStatus::PREFILE,
];
foreach ($this->check_attrs as $attr) {
if ($request->filled($attr)) {
$attrs[$attr] = $request->get($attr);
}
}
$attrs = $this->getFromReq($request, self::$pirep_rules, [
'user_id' => Auth::user()->id,
'state' => PirepState::IN_PROGRESS,
'status' => PirepStatus::PREFILE,
]);
$pirep = new Pirep($attrs);
@ -130,16 +148,10 @@ class PirepController extends AppBaseController
throw new BadRequestHttpException('PIREP has been cancelled, updates can\'t be posted');
}
$attrs = [
'state' => PirepState::PENDING,
'status' => PirepStatus::ARRIVED,
];
foreach($this->check_attrs as $attr) {
if($request->filled($attr)) {
$attrs[$attr] = $request->get($attr);
}
}
$attrs = $this->getFromReq($request, self::$pirep_rules, [
'state' => PirepState::PENDING,
'status' => PirepStatus::ARRIVED,
]);
$pirep_fields = [];
if($request->filled('fields')) {
@ -187,7 +199,7 @@ class PirepController extends AppBaseController
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory
*/
public function acars_get($id, Request $request)
public function acars_geojson($id, Request $request)
{
$pirep = $this->pirepRepo->find($id);
$geodata = $this->geoSvc->getFeatureFromAcars($pirep);
@ -197,11 +209,28 @@ class PirepController extends AppBaseController
]);
}
/**
* Return the GeoJSON for the ACARS line
* @param $id
* @param Request $request
* @return AcarsRouteResource
*/
public function acars_get($id, Request $request)
{
$pirep = $this->pirepRepo->find($id);
AcarsRouteResource::withoutWrapping();
return new AcarsRouteResource(Acars::where([
'pirep_id' => $id,
'type' => AcarsType::FLIGHT_PATH
])->orderBy('created_at', 'asc')->get());
}
/**
* Post ACARS updates for a PIREP
* @param $id
* @param Request $request
* @return AcarsResource
* @return AcarsRouteResource
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
*/
public function acars_store($id, Request $request)
@ -214,20 +243,31 @@ class PirepController extends AppBaseController
}
Log::info('Posting ACARS update', $request->toArray());
$attrs = $request->toArray();
$attrs['pirep_id'] = $id;
$attrs['type'] = AcarsType::FLIGHT_PATH;
$this->validate($request, ['positions' => 'required']);
$positions = $request->post()['positions'];
$update = Acars::create($attrs);
$update->save();
foreach($positions as $position)
{
try {
$attrs = $this->getFromReq(
$position,
self::$acars_rules,
['pirep_id' => $id, 'type' => AcarsType::FLIGHT_PATH]
);
$update = Acars::create($attrs);
$update->save();
} catch (\Exception $e) {
Log::error($e);
}
}
# Change the PIREP status
$pirep->status = PirepStatus::ENROUTE;
$pirep->save();
AcarsResource::withoutWrapping();
return new AcarsResource($update);
return $this->acars_get($id, $request);
}
/**
@ -235,7 +275,7 @@ class PirepController extends AppBaseController
* But rather in a log file.
* @param $id
* @param Request $request
* @return AcarsResource
* @return AcarsLogResource
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
*/
public function acars_log($id, Request $request)
@ -247,20 +287,103 @@ class PirepController extends AppBaseController
throw new BadRequestHttpException('PIREP has been cancelled, updates can\'t be posted');
}
Log::info('Posting ACARS update', $request->toArray());
$attrs = $request->toArray();
Log::info('Posting ACARS log', $request->toArray());
$attrs['pirep_id'] = $id;
$attrs['type'] = AcarsType::LOG;
$attrs = $this->getFromReq($request, [
'log' => 'required',
'lat' => 'nullable',
'lon' => 'nullable',
], ['pirep_id' => $id, 'type' => AcarsType::LOG]);
$update = Acars::create($attrs);
$update->save();
$acars = Acars::create($attrs);
$acars->save();
# Change the PIREP status
$pirep->status = PirepStatus::ENROUTE;
$pirep->save();
AcarsLogResource::withoutWrapping();
return new AcarsLogResource($acars);
}
AcarsResource::withoutWrapping();
return new AcarsResource($update);
/**
* @param $id
* @param Request $request
* @return AcarsRouteResource
*/
public function route_get($id, Request $request)
{
$this->pirepRepo->find($id);
AcarsRouteResource::withoutWrapping();
return new AcarsRouteResource(Acars::where([
'pirep_id' => $id,
'type' => AcarsType::ROUTE
])->orderBy('order', 'asc')->get());
}
/**
* Post the ROUTE for a PIREP, can be done from the ACARS log
* @param $id
* @param Request $request
* @return AcarsRouteResource
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
*/
public function route_post($id, Request $request)
{
$pirep = $this->pirepRepo->find($id);
# Check if the status is cancelled...
if ($pirep->state === PirepState::CANCELLED) {
throw new BadRequestHttpException('PIREP has been cancelled, updates can\'t be posted');
}
Log::info('Posting ACARS ROUTE', $request->toArray());
$this->validate($request, [
'route.*.name' => 'required',
'route.*.order' => 'required|int',
'route.*.nav_type' => 'nullable|int',
'route.*.lat' => 'required|numeric',
'route.*.lon' => 'required|numeric',
]);
$route = $request->all()['route'];
foreach($route as $position) {
$attrs = [
'pirep_id' => $id,
'type' => AcarsType::ROUTE,
'name' => $position['name'],
'order' => $position['order'],
'lat' => $position['lat'],
'lon' => $position['lon'],
];
if(array_key_exists('nav_type', $position)) {
$attrs['nav_type'] = $position['nav_type'];
}
try {
$acars = Acars::create($attrs);
$acars->save();
} catch (\Exception $e) {
Log::error($e);
}
}
return $this->route_get($id, $request);
}
/**
* @param $id
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function route_delete($id, Request $request)
{
$this->pirepRepo->find($id);
Acars::where([
'pirep_id' => $id,
'type' => AcarsType::ROUTE
])->delete();
return $this->message('Route deleted');
}
}

View File

@ -0,0 +1,89 @@
<?php
/**
* Created by IntelliJ IDEA.
* User: nabeelshahzad
* Date: 1/4/18
* Time: 4:20 PM
*/
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class RestController
{
/**
* Shortcut function to get the attributes from a request while running the validations
* @param Request $request
* @param array $attrs_or_validations
* @param array $addtl_fields
* @return array
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
*/
public function getFromReq($request, $attrs_or_validations, $addtl_fields=null)
{
# See if a list of values is passed in, or if a validation list is passed in
$is_validation = false;
if(\count(array_filter(array_keys($attrs_or_validations), '\is_string')) > 0) {
$is_validation = true;
}
if($is_validation) {
$this->validate($request, $attrs_or_validations);
}
$fields = [];
foreach($attrs_or_validations as $idx => $field) {
if($is_validation) {
$field = $idx;
}
if($request instanceof Request) {
if ($request->filled($field)) {
$fields[$field] = $request->get($field);
}
} else {
if(array_key_exists($field, $request)) {
$fields[$field] = $request[$field];
}
}
}
if(!empty($addtl_fields) && \is_array($addtl_fields)) {
$fields = array_merge($fields, $addtl_fields);
}
return $fields;
}
/**
* Run a validation
* @param $request
* @param $rules
* @return bool
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
*/
public function validate(Request $request, $rules)
{
$validator = Validator::make($request->all(), $rules);
if (!$validator->passes()) {
throw new BadRequestHttpException($validator->errors(), null, 400);
}
return true;
}
/**
* Simple normalized method for forming the JSON responses
* @param $message
* @return \Illuminate\Http\JsonResponse
*/
public function message($message)
{
return response()->json([
'message' => $message
]);
}
}

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers;
use InfyOm\Generator\Utils\ResponseUtil;
use Response;
/**
@ -18,13 +17,5 @@ use Response;
*/
class AppBaseController extends Controller
{
public function sendResponse($result, $message)
{
return Response::json(ResponseUtil::makeResponse($message, $result));
}
public function sendError($error, $code = 404)
{
return Response::json(ResponseUtil::makeError($error), $code);
}
}

View File

@ -104,7 +104,7 @@ class Pirep extends BaseModel
{
return $this->hasMany('App\Models\Acars', 'pirep_id')
->where('type', AcarsType::ROUTE)
->orderBy('created_at', 'asc');
->orderBy('order', 'asc');
}
public function aircraft()

View File

@ -29,10 +29,16 @@ Route::group(['middleware' => ['api.auth']], function ()
Route::post('pireps/prefile', 'PirepController@prefile');
Route::post('pireps/{id}/file', 'PirepController@file');
Route::post('pireps/{id}/cancel', 'PirepController@cancel');
Route::post('pireps/{id}/acars', 'PirepController@acars_store');
Route::get('pireps/{id}/acars/geojson', 'PirepController@acars_geojson');
Route::get('pireps/{id}/acars/position', 'PirepController@acars_get');
Route::post('pireps/{id}/acars/position', 'PirepController@acars_store');
Route::post('pireps/{id}/acars/log', 'PirepController@acars_log');
Route::get('pireps/{id}/route', 'PirepController@route_get');
Route::post('pireps/{id}/route', 'PirepController@route_post');
Route::delete('pireps/{id}/route', 'PirepController@route_delete');
# This is the info of the user whose token is in use
Route::get('user', 'UserController@index');
Route::get('users/{id}', 'UserController@get');

View File

@ -13,7 +13,7 @@
<script type="text/javascript">
phpvms.render_live_map({
'update_uri': '{!! url('/api/acars') !!}',
'pirep_uri': '{!! url('/api/pireps/{id}/acars') !!}',
'pirep_uri': '{!! url('/api/pireps/{id}/acars/geojson') !!}',
'aircraft_icon': '{!! public_asset('/assets/img/acars/aircraft.png') !!}',
});
</script>

View File

@ -14,6 +14,33 @@ class AcarsTest extends TestCase
$this->addData('base');
}
/**
* @param $route
* @param $points
* @param array $addtl_fields
*/
protected function allPointsInRoute($route, $points, $addtl_fields=[])
{
if(empty($addtl_fields)) {
$addtl_fields = [];
}
$fields = array_merge([
'name',
'order',
'lat',
'lon'
], $addtl_fields);
$this->assertEquals(\count($route), \count($points));
foreach($route as $idx => $point) {
//$this->assertHasKeys($points[$idx], $fields);
foreach($fields as $f) {
$this->assertEquals($point[$f], $points[$idx][$f]);
}
}
}
protected function getPirep($pirep_id)
{
$resp = $this->withHeaders($this->apiHeaders())
@ -55,30 +82,73 @@ class AcarsTest extends TestCase
$this->assertEquals(PirepState::IN_PROGRESS, $pirep['state']);
$this->assertEquals(PirepStatus::PREFILE, $pirep['status']);
$uri = '/api/pireps/' . $pirep_id . '/acars/position';
# Test missing positions field
# Post an ACARS update
$update = [];
$response = $this->withHeaders($this->apiHeaders())->post($uri, $update);
$response->assertStatus(400);
# Post an ACARS update
$uri = '/api/pireps/' . $pirep_id . '/acars';
$acars = factory(App\Models\Acars::class)->make()->toArray();
$response = $this->withHeaders($this->apiHeaders())->post($uri, $acars);
$response->assertStatus(201);
unset($acars['id']);
$update = ['positions' => [$acars]];
$response = $this->withHeaders($this->apiHeaders())->post($uri, $update);
$response->assertStatus(200);
$body = $response->json();
$this->assertNotNull($body['id']);
$this->assertEquals($pirep_id, $body['pirep_id']);
$this->assertNotNull($body[0]['id']);
$this->assertEquals($pirep_id, $body[0]['pirep_id']);
//$this->assertHasKeys($body, $this->fillableFields(new \App\Models\Acars));
# Make sure PIREP state moved into ENROUTE
$pirep = $this->getPirep($pirep_id);
$this->assertEquals(PirepState::IN_PROGRESS, $pirep['state']);
$this->assertEquals(PirepStatus::ENROUTE, $pirep['status']);
$uri = '/api/pireps/' . $pirep_id . '/acars';
$response = $this->withHeaders($this->apiHeaders())->get($uri);
$response->assertStatus(200);
$body = $response->json();
/*$body = $response->json();
$this->assertEquals(1, $this->count($body));
$this->assertEquals($pirep_id, $body[0]['pirep_id']);*/
$this->assertNotNull($body);
$this->assertCount(1, $body);
$this->assertEquals($acars['lat'], $body[0]['lat']);
$this->assertEquals($acars['lon'], $body[0]['lon']);
//$this->allPointsInRoute([$acars], $body);
}
/**
* Test publishing multiple, batched updates
*/
public function testMultipleAcarsPositionUpdates()
{
$pirep = factory(App\Models\Pirep::class)->make()->toArray();
$uri = '/api/pireps/prefile';
$response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep);
$response->assertStatus(201);
$pirep_id = $response->json()['id'];
$uri = '/api/pireps/' . $pirep_id . '/acars/position';
# Post an ACARS update
$acars_count = \random_int(5, 50);
$acars = factory(App\Models\Acars::class, $acars_count)->make(['id'=>''])->toArray();
$update = ['positions' => $acars];
$response = $this->withHeaders($this->apiHeaders())->post($uri, $update);
$response->assertStatus(200)->assertJsonCount($acars_count);
$response = $this->withHeaders($this->apiHeaders())->get($uri);
$response->assertStatus(200)->assertJsonCount($acars_count);
}
/**
*
*/
public function testNonExistentPirepGet()
{
$uri = '/api/pireps/DOESNTEXIST/acars';
@ -86,35 +156,136 @@ class AcarsTest extends TestCase
$response->assertStatus(404);
}
/**
*
*/
public function testNonExistentPirepStore()
{
$uri = '/api/pireps/DOESNTEXIST/acars';
$uri = '/api/pireps/DOESNTEXIST/acars/position';
$acars = factory(App\Models\Acars::class)->make()->toArray();
$response = $this->withHeaders($this->apiHeaders())->post($uri, $acars);
$response->assertStatus(404);
}
/**
*
*/
public function testAcarsIsoDate()
{
$pirep = factory(App\Models\Pirep::class)->make()->toArray();
$uri = '/api/pireps/prefile';
$response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep);
$pirep_id = $response->json()['id'];
$dt = date('c');
$uri = '/api/pireps/' . $pirep_id . '/acars';
$acars = factory(App\Models\Acars::class)->make()->toArray();
$acars['sim_time'] = $dt;
$uri = '/api/pireps/' . $pirep_id . '/acars/position';
$acars = factory(App\Models\Acars::class)->make([
'sim_time' => $dt
])->toArray();
$response = $this->withHeaders($this->apiHeaders())->post($uri, $acars);
$response->assertStatus(201);
/*$uri = '/api/pireps/' . $pirep_id . '/acars';
$response = $this->withHeaders($this->apiHeaders())->get($uri);
$update = ['positions' => [$acars]];
$response = $this->withHeaders($this->apiHeaders())->post($uri, $update);
$response->assertStatus(200);
}
/**
* Test the validation
*/
public function testAcarsInvalidRoutePost()
{
$pirep = factory(App\Models\Pirep::class)->make()->toArray();
$uri = '/api/pireps/prefile';
$response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep);
$pirep_id = $response->json()['id'];
$post_route = ['order' => 1, 'name' => 'NAVPOINT'];
$uri = '/api/pireps/' . $pirep_id . '/route';
$response = $this->withHeaders($this->apiHeaders())->post($uri, $post_route);
$response->assertStatus(400);
$post_route = [
['order' => 1, 'name' => 'NAVPOINT', 'lat' => 'notanumber', 'lon' => 34.11]
];
$uri = '/api/pireps/' . $pirep_id . '/route';
$response = $this->withHeaders($this->apiHeaders())->post($uri, $post_route);
$response->assertStatus(400);
}
public function testAcarsLogPost()
{
$pirep = factory(App\Models\Pirep::class)->make()->toArray();
$uri = '/api/pireps/prefile';
$response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep);
$pirep_id = $response->json()['id'];
$acars = factory(App\Models\Acars::class)->make();
$post_log = [
'log' => $acars->log
];
$uri = '/api/pireps/' . $pirep_id . '/acars/log';
$response = $this->withHeaders($this->apiHeaders())->post($uri, $post_log);
$response->assertStatus(201);
}
/**
*
*/
public function testAcarsRoutePost()
{
$pirep = factory(App\Models\Pirep::class)->make()->toArray();
$uri = '/api/pireps/prefile';
$response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep);
$pirep_id = $response->json()['id'];
$order = 1;
$post_route = [];
$route_count = \random_int(5, 50);
$route = factory(App\Models\Navdata::class, $route_count)->create();
foreach($route as $position) {
$post_route[] = [
'order' => $order,
'name' => $position->name,
'lat' => $position->lat,
'lon' => $position->lon,
];
++$order;
}
$uri = '/api/pireps/'.$pirep_id.'/route';
$response = $this->withHeaders($this->apiHeaders())->post($uri, ['route' => $post_route]);
$response->assertStatus(200)->assertJsonCount($route_count);
$body = $response->json();
$this->assertEquals($dt, $body[0]['sim_time']);*/
$this->allPointsInRoute($post_route, $body);
/**
* Get
*/
$uri = '/api/pireps/' . $pirep_id . '/route';
$response = $this->withHeaders($this->apiHeaders())->get($uri);
$response->assertStatus(200)->assertJsonCount($route_count);
$body = $response->json();
$this->allPointsInRoute($post_route, $body);
/**
* Delete and then recheck
*/
$uri = '/api/pireps/' . $pirep_id . '/route';
$response = $this->withHeaders($this->apiHeaders())->delete($uri);
$response->assertStatus(200);
$uri = '/api/pireps/' . $pirep_id . '/route';
$response = $this->withHeaders($this->apiHeaders())->get($uri);
$response->assertStatus(200)->assertJsonCount(0);
}
/**

View File

@ -193,10 +193,13 @@ class PIREPTest extends TestCase
$response = $this->withHeaders($this->apiHeaders())->post($uri, $pirep);
$pirep_id = $response->json()['id'];
$uri = '/api/pireps/' . $pirep_id . '/acars';
$uri = '/api/pireps/' . $pirep_id . '/acars/position';
$acars = factory(App\Models\Acars::class)->make()->toArray();
$response = $this->withHeaders($this->apiHeaders())->post($uri, $acars);
$response->assertStatus(201);
$response = $this->withHeaders($this->apiHeaders())->post($uri, [
'positions' => [$acars]
]);
$response->assertStatus(200);
# Cancel it
$uri = '/api/pireps/' . $pirep_id . '/cancel';
@ -204,7 +207,7 @@ class PIREPTest extends TestCase
$response->assertStatus(200);
# Should get a 400 when posting an ACARS update
$uri = '/api/pireps/' . $pirep_id . '/acars';
$uri = '/api/pireps/' . $pirep_id . '/acars/position';
$acars = factory(App\Models\Acars::class)->make()->toArray();
$response = $this->withHeaders($this->apiHeaders())->post($uri, $acars);
$response->assertStatus(400);

View File

@ -71,6 +71,27 @@ abstract class TestCase extends Illuminate\Foundation\Testing\TestCase
{
$svc = app('\App\Services\DatabaseService');
$file_path = base_path('tests/data/' . $file . '.yml');
$svc->seed_from_yaml_file($file_path);
try {
$svc->seed_from_yaml_file($file_path);
} catch (Exception $e) {
}
}
public function fillableFields(\Illuminate\Database\Eloquent\Model $model)
{
//$klass = new $model();
return $model->fillable;
}
/**
* Make sure an object has the list of keys
* @param $obj
* @param array $keys
*/
public function assertHasKeys($obj, $keys=[])
{
foreach($keys as $key) {
$this->assertArrayHasKey($key, $obj);
}
}
}