Skip to content

Commit b9b2dc3

Browse files
committed
✨ REST: Allow class based output fields
When performing core/get on REST Service, you may now define the output_fields parameter based on the class of the object being returned. Previously you could either only return fields from the parent object or enforce return all the fields for each object (with `*+`). With this commit you can also pass `<Class>:<output_fields>;<Class>:<output_fields>;...`.
1 parent 143a59a commit b9b2dc3

File tree

4 files changed

+218
-30
lines changed

4 files changed

+218
-30
lines changed

application/applicationextension.inc.php

+59-25
Original file line numberDiff line numberDiff line change
@@ -1914,38 +1914,72 @@ public static function GetClass($oData, $sParamName)
19141914
public static function GetFieldList($sClass, $oData, $sParamName)
19151915
{
19161916
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
1917-
$aShowFields = array();
1918-
if ($sFields == '*')
1919-
{
1920-
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
1921-
{
1922-
$aShowFields[$sClass][] = $sAttCode;
1923-
}
1917+
return match($sFields) {
1918+
'*' => self::GetFieldListForClass($sClass),
1919+
'*+' => self::GetFieldListForParentClass($sClass),
1920+
default => self::GetLimitedFieldListForClass($sClass, $sFields, $sParamName),
1921+
};
1922+
}
1923+
1924+
public static function HasRequestedExtendedOutput(string $sFields): bool
1925+
{
1926+
return match($sFields) {
1927+
'*' => false,
1928+
'*+' => true,
1929+
default => substr_count($sFields, ':') > 1,
1930+
};
1931+
}
1932+
1933+
public static function HasRequestedAllOutputFields(string $sFields): bool
1934+
{
1935+
return match($sFields) {
1936+
'*', '*+' => true,
1937+
default => false,
1938+
};
1939+
}
1940+
1941+
protected static function GetFieldListForClass(string $sClass): array
1942+
{
1943+
return [$sClass => array_keys(MetaModel::ListAttributeDefs($sClass))];
1944+
}
1945+
1946+
protected static function GetFieldListForParentClass(string $sClass): array
1947+
{
1948+
$aFieldList = array();
1949+
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
1950+
$aFieldList = array_merge($aFieldList, self::GetFieldListForClass($sRefClass));
19241951
}
1925-
elseif ($sFields == '*+')
1952+
return $aFieldList;
1953+
}
1954+
1955+
protected static function GetLimitedFieldListForSingleClass(string $sClass, string $sFields, string $sParamName): array
1956+
{
1957+
$aFieldList = [$sClass => []];
1958+
foreach (explode(',', $sFields) as $sAttCode)
19261959
{
1927-
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass)
1960+
$sAttCode = trim($sAttCode);
1961+
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
19281962
{
1929-
foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef)
1930-
{
1931-
$aShowFields[$sRefClass][] = $sAttCode;
1932-
}
1963+
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
19331964
}
1965+
$aFieldList[$sClass][] = $sAttCode;
19341966
}
1935-
else
1936-
{
1937-
foreach (explode(',', $sFields) as $sAttCode)
1938-
{
1939-
$sAttCode = trim($sAttCode);
1940-
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode)))
1941-
{
1942-
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
1943-
}
1944-
$aShowFields[$sClass][] = $sAttCode;
1945-
}
1967+
return $aFieldList;
1968+
}
1969+
1970+
protected static function GetLimitedFieldListForClass(string $sClass, string $sFields, string $sParamName): array
1971+
{
1972+
if (!str_contains($sFields, ':')) {
1973+
return self::GetLimitedFieldListForSingleClass($sClass, $sFields, $sParamName);
19461974
}
19471975

1948-
return $aShowFields;
1976+
$aFieldList = [];
1977+
$aFieldListParts = explode(';', $sFields);
1978+
foreach ($aFieldListParts as $sClassFields) {
1979+
list($sSubClass, $sSubClassFields) = explode(':', $sClassFields);
1980+
$aFieldList = array_merge($aFieldList, self::GetLimitedFieldListForSingleClass(trim($sSubClass), trim($sSubClassFields), $sParamName));
1981+
}
1982+
return $aFieldList;
19491983
}
19501984

19511985
/**

core/restservices.class.inc.php

+4-3
Original file line numberDiff line numberDiff line change
@@ -503,8 +503,8 @@ public function ExecOperation($sVersion, $sVerb, $aParams)
503503
case 'core/get':
504504
$sClass = RestUtils::GetClass($aParams, 'class');
505505
$key = RestUtils::GetMandatoryParam($aParams, 'key');
506+
$sShowFields = RestUtils::GetOptionalParam($aParams, 'output_fields', '*');
506507
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
507-
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
508508
$iLimit = (int)RestUtils::GetOptionalParam($aParams, 'limit', 0);
509509
$iPage = (int)RestUtils::GetOptionalParam($aParams, 'page', 1);
510510

@@ -528,7 +528,8 @@ public function ExecOperation($sVersion, $sVerb, $aParams)
528528
}
529529
else
530530
{
531-
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*')
531+
532+
if (!RestUtils::HasRequestedAllOutputFields($sShowFields))
532533
{
533534
$aFields = $aShowFields[$sClass];
534535
//Id is not a valid attribute to optimize
@@ -542,7 +543,7 @@ public function ExecOperation($sVersion, $sVerb, $aParams)
542543

543544
while ($oObject = $oObjectSet->Fetch())
544545
{
545-
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
546+
$oResult->AddObject(0, '', $oObject, $aShowFields, RestUtils::HasRequestedExtendedOutput($sShowFields));
546547
}
547548
$oResult->message = "Found: ".$oObjectSet->Count();
548549
}

tests/php-unit-tests/unitary-tests/webservices/RestTest.php

+54-2
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,58 @@ public function testCoreApiGet(){
142142
$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
143143
}
144144

145+
public function testCoreApiGetWithUnionAndDifferentOutputFields(){
146+
// Create ticket
147+
$description = date('dmY H:i:s');
148+
$oUserRequest = $this->CreateSampleTicket($description);
149+
$oChange = $this->CreateSampleTicket($description, 'Change');
150+
$iUserRequestId = $oUserRequest->GetKey();
151+
$sUserRequestRef = $oUserRequest->Get('ref');
152+
$iChangeId = $oChange->GetKey();
153+
$sChangeRef = $oChange->Get('ref');
154+
155+
$sJSONOutput = $this->CallCoreRestApi_Internally(<<<JSON
156+
{
157+
"operation": "core/get",
158+
"class": "Ticket",
159+
"key": "SELECT UserRequest WHERE id=$iUserRequestId UNION SELECT Change WHERE id=$iChangeId",
160+
"output_fields": "Ticket:ref;UserRequest:ref,status,origin;Change:ref,status,outage"
161+
}
162+
JSON);
163+
164+
$sExpectedJsonOuput = <<<JSON
165+
{
166+
"code": 0,
167+
"message": "Found: 2",
168+
"objects": {
169+
"Change::$iChangeId": {
170+
"class": "Change",
171+
"code": 0,
172+
"fields": {
173+
"outage": "no",
174+
"ref": "$sChangeRef",
175+
"status": "new"
176+
},
177+
"key": "$iChangeId",
178+
"message": ""
179+
},
180+
"UserRequest::$iUserRequestId": {
181+
"class": "UserRequest",
182+
"code": 0,
183+
"fields": {
184+
"origin": "phone",
185+
"ref": "$sUserRequestRef",
186+
"status": "new"
187+
},
188+
"key": "$iUserRequestId",
189+
"message": ""
190+
}
191+
}
192+
}
193+
JSON;
194+
$this->assertJsonStringEqualsJsonString($sExpectedJsonOuput, $sJSONOutput);
195+
}
196+
145197
public function testCoreApiCreate()
146198
{
147199
// Create ticket
@@ -253,9 +305,9 @@ public function testCoreApiDelete()
253305
//
254306
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
255307

256-
private function CreateSampleTicket($description)
308+
private function CreateSampleTicket($description, $sType = 'UserRequest')
257309
{
258-
$oTicket = $this->createObject('UserRequest', [
310+
$oTicket = $this->createObject($sType, [
259311
'org_id' => $this->getTestOrgId(),
260312
"title" => "Houston, got a problem",
261313
"description" => $description
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
namespace Combodo\iTop\Test\UnitTest\Webservices;
4+
5+
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
6+
use MetaModel;
7+
use RestUtils;
8+
use Ticket;
9+
use UserRequest;
10+
11+
12+
class RestUtilsTest extends ItopDataTestCase
13+
{
14+
public function testGetFieldListForSingleClass(): void
15+
{
16+
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'ref,start_date,end_date'], 'output_fields');
17+
$this->assertSame([Ticket::class => ['ref', 'start_date', 'end_date']], $aList);
18+
}
19+
20+
public function testGetFieldListForSingleClassWithInvalidFieldNameFails(): void
21+
{
22+
$this->expectException(\Exception::class);
23+
$this->expectExceptionMessage('output_fields: invalid attribute code \'something\'');
24+
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'ref,something'], 'output_fields');
25+
$this->assertSame([Ticket::class => ['ref', 'start_date', 'end_date']], $aList);
26+
}
27+
28+
public function testGetFieldListWithAsteriskOnParentClass(): void
29+
{
30+
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => '*'], 'output_fields');
31+
$this->assertArrayHasKey(Ticket::class, $aList);
32+
$this->assertContains('operational_status', $aList[Ticket::class]);
33+
$this->assertNotContains('status', $aList[Ticket::class], 'Representation of Class Ticket should not contain status, since it is defined by children');
34+
}
35+
36+
public function testGetFieldListWithAsteriskPlusOnParentClass(): void
37+
{
38+
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => '*+'], 'output_fields');
39+
$this->assertArrayHasKey(Ticket::class, $aList);
40+
$this->assertArrayHasKey(UserRequest::class, $aList);
41+
$this->assertContains('operational_status', $aList[Ticket::class]);
42+
$this->assertContains('status', $aList[UserRequest::class]);
43+
}
44+
45+
public function testGetFieldListForMultipleClasses(): void
46+
{
47+
$aList = RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'Ticket:ref,start_date,end_date;UserRequest:ref,status'], 'output_fields');
48+
$this->assertArrayHasKey(Ticket::class, $aList);
49+
$this->assertArrayHasKey(UserRequest::class, $aList);
50+
$this->assertContains('ref', $aList[Ticket::class]);
51+
$this->assertContains('end_date', $aList[Ticket::class]);
52+
$this->assertNotContains('status', $aList[Ticket::class]);
53+
$this->assertContains('status', $aList[UserRequest::class]);
54+
$this->assertNotContains('end_date', $aList[UserRequest::class]);
55+
}
56+
57+
public function testGetFieldListForMultipleClassesWithInvalidFieldNameFails(): void
58+
{
59+
$this->expectException(\Exception::class);
60+
$this->expectExceptionMessage('output_fields: invalid attribute code \'something\'');
61+
RestUtils::GetFieldList(Ticket::class, (object) ['output_fields' => 'Ticket:ref;UserRequest:ref,something'], 'output_fields');
62+
}
63+
64+
/**
65+
* @dataProvider extendedOutputDataProvider
66+
*/
67+
public function testIsExtendedOutputRequest(bool $bExpected, string $sFields): void
68+
{
69+
$this->assertSame($bExpected, RestUtils::HasRequestedExtendedOutput($sFields));
70+
}
71+
72+
/**
73+
* @dataProvider allFieldsOutputDataProvider
74+
*/
75+
public function testIsAllFieldsOutputRequest(bool $bExpected, string $sFields): void
76+
{
77+
$this->assertSame($bExpected, RestUtils::HasRequestedAllOutputFields($sFields));
78+
}
79+
80+
public function extendedOutputDataProvider(): array
81+
{
82+
return [
83+
[false, 'ref,start_date,end_date'],
84+
[false, '*'],
85+
[true, '*+'],
86+
[false, 'Ticket:ref'],
87+
[true, 'Ticket:ref;UserRequest:ref'],
88+
];
89+
}
90+
91+
public function allFieldsOutputDataProvider(): array
92+
{
93+
return [
94+
[false, 'ref,start_date,end_date'],
95+
[true, '*'],
96+
[true, '*+'],
97+
[false, 'Ticket:ref'],
98+
[false, 'Ticket:ref;UserRequest:ref'],
99+
];
100+
}
101+
}

0 commit comments

Comments
 (0)