Skip to content

N°7914 ✨ REST: Allow class based output fields #668

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 59 additions & 25 deletions application/applicationextension.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -1914,38 +1914,72 @@ public static function GetClass($oData, $sParamName)
public static function GetFieldList($sClass, $oData, $sParamName)
{
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
$aShowFields = array();
if ($sFields == '*')
{
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
{
$aShowFields[$sClass][] = $sAttCode;
}
return match($sFields) {
'*' => self::GetFieldListForClass($sClass),
'*+' => self::GetFieldListForParentClass($sClass),
default => self::GetLimitedFieldListForClass($sClass, $sFields, $sParamName),
};
}

public static function HasRequestedExtendedOutput(string $sFields): bool
{
return match($sFields) {
'*' => false,
'*+' => true,
default => substr_count($sFields, ':') > 1,
};
}

public static function HasRequestedAllOutputFields(string $sFields): bool
{
return match($sFields) {
'*', '*+' => true,
default => false,
};
}

protected static function GetFieldListForClass(string $sClass): array
{
return [$sClass => array_keys(MetaModel::ListAttributeDefs($sClass))];
}

protected static function GetFieldListForParentClass(string $sClass): array
{
$aFieldList = array();
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
$aFieldList = array_merge($aFieldList, self::GetFieldListForClass($sRefClass));
}
elseif ($sFields == '*+')
return $aFieldList;
}

protected static function GetLimitedFieldListForSingleClass(string $sClass, string $sFields, string $sParamName): array
{
$aFieldList = [$sClass => []];
foreach (explode(',', $sFields) as $sAttCode)
{
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass)
$sAttCode = trim($sAttCode);
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
{
foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef)
{
$aShowFields[$sRefClass][] = $sAttCode;
}
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
}
$aFieldList[$sClass][] = $sAttCode;
}
else
{
foreach (explode(',', $sFields) as $sAttCode)
{
$sAttCode = trim($sAttCode);
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
{
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
}
$aShowFields[$sClass][] = $sAttCode;
}
return $aFieldList;
}

protected static function GetLimitedFieldListForClass(string $sClass, string $sFields, string $sParamName): array
{
if (!str_contains($sFields, ':')) {
return self::GetLimitedFieldListForSingleClass($sClass, $sFields, $sParamName);
}

return $aShowFields;
$aFieldList = [];
$aFieldListParts = explode(';', $sFields);
foreach ($aFieldListParts as $sClassFields) {
list($sSubClass, $sSubClassFields) = explode(':', $sClassFields);
$aFieldList = array_merge($aFieldList, self::GetLimitedFieldListForSingleClass(trim($sSubClass), trim($sSubClassFields), $sParamName));
}
return $aFieldList;
}

/**
Expand Down
7 changes: 4 additions & 3 deletions core/restservices.class.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,8 @@ public function ExecOperation($sVersion, $sVerb, $aParams)
case 'core/get':
$sClass = RestUtils::GetClass($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
$sShowFields = RestUtils::GetOptionalParam($aParams, 'output_fields', '*');
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
$iLimit = (int)RestUtils::GetOptionalParam($aParams, 'limit', 0);
$iPage = (int)RestUtils::GetOptionalParam($aParams, 'page', 1);

Expand All @@ -528,7 +528,8 @@ public function ExecOperation($sVersion, $sVerb, $aParams)
}
else
{
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*')

if (!RestUtils::HasRequestedAllOutputFields($sShowFields))
{
$aFields = $aShowFields[$sClass];
//Id is not a valid attribute to optimize
Expand All @@ -542,7 +543,7 @@ public function ExecOperation($sVersion, $sVerb, $aParams)

while ($oObject = $oObjectSet->Fetch())
{
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
$oResult->AddObject(0, '', $oObject, $aShowFields, RestUtils::HasRequestedExtendedOutput($sShowFields));
}
$oResult->message = "Found: ".$oObjectSet->Count();
}
Expand Down
56 changes: 54 additions & 2 deletions tests/php-unit-tests/unitary-tests/webservices/RestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,58 @@ public function testCoreApiGet(){
$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
}

public function testCoreApiGetWithUnionAndDifferentOutputFields(){
// Create ticket
$description = date('dmY H:i:s');
$oUserRequest = $this->CreateSampleTicket($description);
$oChange = $this->CreateSampleTicket($description, 'Change');
$iUserRequestId = $oUserRequest->GetKey();
$sUserRequestRef = $oUserRequest->Get('ref');
$iChangeId = $oChange->GetKey();
$sChangeRef = $oChange->Get('ref');

$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
{
"operation": "core/get",
"class": "Ticket",
"key": "SELECT UserRequest WHERE id=$iUserRequestId UNION SELECT Change WHERE id=$iChangeId",
"output_fields": "Ticket:ref;UserRequest:ref,status,origin;Change:ref,status,outage"
}
JSON);

$sExpectedJsonOuput = <<<JSON
{
"code": 0,
"message": "Found: 2",
"objects": {
"Change::$iChangeId": {
"class": "Change",
"code": 0,
"fields": {
"outage": "no",
"ref": "$sChangeRef",
"status": "new"
},
"key": "$iChangeId",
"message": ""
},
"UserRequest::$iUserRequestId": {
"class": "UserRequest",
"code": 0,
"fields": {
"origin": "phone",
"ref": "$sUserRequestRef",
"status": "new"
},
"key": "$iUserRequestId",
"message": ""
}
}
}
JSON;
$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
}

public function testCoreApiCreate()
{
// Create ticket
Expand Down Expand Up @@ -253,9 +305,9 @@ public function testCoreApiDelete()
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

private function CreateSampleTicket($description)
private function CreateSampleTicket($description, $sType = 'UserRequest')
{
$oTicket = $this->createObject('UserRequest', [
$oTicket = $this->createObject($sType, [
'org_id' => $this->getTestOrgId(),
"title" => "Houston, got a problem",
"description" => $description
Expand Down
101 changes: 101 additions & 0 deletions tests/php-unit-tests/unitary-tests/webservices/RestUtilsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace Combodo\iTop\Test\UnitTest\Webservices;

use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
use MetaModel;
use RestUtils;
use Ticket;
use UserRequest;


class RestUtilsTest extends ItopDataTestCase
{
public function testGetFieldListForSingleClass(): void
{
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'ref,start_date,end_date'], 'output_fields');
$this->assertSame([Ticket::class => ['ref', 'start_date', 'end_date']], $aList);
}

public function testGetFieldListForSingleClassWithInvalidFieldNameFails(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('output_fields: invalid attribute code \'something\'');
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'ref,something'], 'output_fields');
$this->assertSame([Ticket::class => ['ref', 'start_date', 'end_date']], $aList);
}

public function testGetFieldListWithAsteriskOnParentClass(): void
{
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => '*'], 'output_fields');
$this->assertArrayHasKey(Ticket::class, $aList);
$this->assertContains('operational_status', $aList[Ticket::class]);
$this->assertNotContains('status', $aList[Ticket::class], 'Representation of Class Ticket should not contain status, since it is defined by children');
}

public function testGetFieldListWithAsteriskPlusOnParentClass(): void
{
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => '*+'], 'output_fields');
$this->assertArrayHasKey(Ticket::class, $aList);
$this->assertArrayHasKey(UserRequest::class, $aList);
$this->assertContains('operational_status', $aList[Ticket::class]);
$this->assertContains('status', $aList[UserRequest::class]);
}

public function testGetFieldListForMultipleClasses(): void
{
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'Ticket:ref,start_date,end_date;UserRequest:ref,status'], 'output_fields');
$this->assertArrayHasKey(Ticket::class, $aList);
$this->assertArrayHasKey(UserRequest::class, $aList);
$this->assertContains('ref', $aList[Ticket::class]);
$this->assertContains('end_date', $aList[Ticket::class]);
$this->assertNotContains('status', $aList[Ticket::class]);
$this->assertContains('status', $aList[UserRequest::class]);
$this->assertNotContains('end_date', $aList[UserRequest::class]);
}

public function testGetFieldListForMultipleClassesWithInvalidFieldNameFails(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('output_fields: invalid attribute code \'something\'');
RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'Ticket:ref;UserRequest:ref,something'], 'output_fields');
}

/**
* @dataProvider extendedOutputDataProvider
*/
public function testIsExtendedOutputRequest(bool $bExpected, string $sFields): void
{
$this->assertSame($bExpected, RestUtils::HasRequestedExtendedOutput($sFields));
}

/**
* @dataProvider allFieldsOutputDataProvider
*/
public function testIsAllFieldsOutputRequest(bool $bExpected, string $sFields): void
{
$this->assertSame($bExpected, RestUtils::HasRequestedAllOutputFields($sFields));
}

public function extendedOutputDataProvider(): array
{
return [
[false, 'ref,start_date,end_date'],
[false, '*'],
[true, '*+'],
[false, 'Ticket:ref'],
[true, 'Ticket:ref;UserRequest:ref'],
];
}

public function allFieldsOutputDataProvider(): array
{
return [
[false, 'ref,start_date,end_date'],
[true, '*'],
[true, '*+'],
[false, 'Ticket:ref'],
[false, 'Ticket:ref;UserRequest:ref'],
];
}
}