diff --git a/src/CrowdinApiClient/Api/ApplicationApi.php b/src/CrowdinApiClient/Api/ApplicationApi.php
index 0afdca8c..470ee7bd 100644
--- a/src/CrowdinApiClient/Api/ApplicationApi.php
+++ b/src/CrowdinApiClient/Api/ApplicationApi.php
@@ -5,6 +5,8 @@
namespace CrowdinApiClient\Api;
use CrowdinApiClient\Model\ApplicationData;
+use CrowdinApiClient\Model\ApplicationInstallation;
+use CrowdinApiClient\ModelCollection;
/**
* Use API to manage custom application data.
@@ -76,6 +78,81 @@ public function deleteApplicationData(string $applicationIdentifier, string $pat
return $this->_delete($url);
}
+ /**
+ * List Application Installations
+ * @link https://developer.crowdin.com/api/v2/#operation/api.applications.installations.getMany API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.installations.getMany API Documentation Enterprise
+ *
+ * @param array $params
+ * integer $params[limit]
+ * integer $params[offset]
+ * integer $params[installedBy]
+ * string $params[orderBy]
+ * @return ModelCollection
+ */
+ public function listInstallations(array $params = []): ModelCollection
+ {
+ return $this->_list('applications/installations', ApplicationInstallation::class, $params);
+ }
+
+ /**
+ * Install Application
+ * @link https://developer.crowdin.com/api/v2/#operation/api.applications.installations.post API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.installations.post API Documentation Enterprise
+ *
+ * @param array $data
+ * string $data[url] required
+ * array $data[permissions]
+ * array $data[modules]
+ * bool $data[assignAgent]
+ * @return ApplicationInstallation|null
+ */
+ public function installApplication(array $data): ?ApplicationInstallation
+ {
+ return $this->_create('applications/installations', ApplicationInstallation::class, $data);
+ }
+
+ /**
+ * Get Application Installation
+ * @link https://developer.crowdin.com/api/v2/#operation/api.applications.installations.get API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.installations.get API Documentation Enterprise
+ *
+ * @param string $identifier
+ * @return ApplicationInstallation|null
+ */
+ public function getInstallation(string $identifier): ?ApplicationInstallation
+ {
+ return $this->_get('applications/installations/' . $identifier, ApplicationInstallation::class);
+ }
+
+ /**
+ * Delete Application Installation
+ * @link https://developer.crowdin.com/api/v2/#operation/api.applications.installations.delete API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.installations.delete API Documentation Enterprise
+ *
+ * @param string $identifier
+ * @param bool $force
+ * @return mixed
+ */
+ public function deleteInstallation(string $identifier, bool $force = false)
+ {
+ $params = $force ? ['force' => true] : [];
+ return $this->_delete('applications/installations/' . $identifier, $params);
+ }
+
+ /**
+ * Edit Application Installation
+ * @link https://developer.crowdin.com/api/v2/#operation/api.applications.installations.patch API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.installations.patch API Documentation Enterprise
+ *
+ * @param ApplicationInstallation $installation
+ * @return ApplicationInstallation|null
+ */
+ public function updateInstallation(ApplicationInstallation $installation): ?ApplicationInstallation
+ {
+ return $this->_update('applications/installations/' . $installation->getIdentifier(), $installation);
+ }
+
/**
* Edit Application Data
* @link https://developer.crowdin.com/api/v2/#operation/api.applications.api.patch API Documentation
@@ -83,7 +160,7 @@ public function deleteApplicationData(string $applicationIdentifier, string $pat
*
* @param string $applicationIdentifier
* @param string $path
- * @param array $data
+ * @param array $data Application-specific key-value payload (free-form, not RFC 6902 patch operations)
* @return ApplicationData|null
*/
public function editApplicationData(string $applicationIdentifier, string $path, array $data): ?ApplicationData
diff --git a/src/CrowdinApiClient/Model/ApplicationInstallation.php b/src/CrowdinApiClient/Model/ApplicationInstallation.php
new file mode 100644
index 00000000..d091dc9a
--- /dev/null
+++ b/src/CrowdinApiClient/Model/ApplicationInstallation.php
@@ -0,0 +1,165 @@
+identifier = (string)$this->getDataProperty('identifier');
+ $this->name = (string)$this->getDataProperty('name');
+ $this->installedBy = $this->getDataProperty('installedBy');
+ $this->description = $this->getDataProperty('description');
+ $this->logo = (string)$this->getDataProperty('logo');
+ $this->agent = $this->getDataProperty('agent');
+ $this->baseUrl = (string)$this->getDataProperty('baseUrl');
+ $this->manifestUrl = (string)$this->getDataProperty('manifestUrl');
+ $this->createdAt = $this->getDataProperty('createdAt');
+ $this->modules = (array)$this->getDataProperty('modules');
+ $this->scopes = (array)$this->getDataProperty('scopes');
+ $this->permissions = (array)$this->getDataProperty('permissions');
+ $this->defaultPermissions = (array)$this->getDataProperty('defaultPermissions');
+ $this->limitReached = (bool)$this->getDataProperty('limitReached');
+ $this->stringBasedAvailable = (bool)$this->getDataProperty('stringBasedAvailable');
+ }
+
+ public function getIdentifier(): string
+ {
+ return $this->identifier;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getInstalledBy(): ?array
+ {
+ return $this->installedBy;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function getLogo(): string
+ {
+ return $this->logo;
+ }
+
+ /**
+ * @return array|null
+ */
+ public function getAgent(): ?array
+ {
+ return $this->agent;
+ }
+
+ public function getBaseUrl(): string
+ {
+ return $this->baseUrl;
+ }
+
+ public function getManifestUrl(): string
+ {
+ return $this->manifestUrl;
+ }
+
+ public function getCreatedAt(): ?string
+ {
+ return $this->createdAt;
+ }
+
+ public function getModules(): array
+ {
+ return $this->modules;
+ }
+
+ public function getScopes(): array
+ {
+ return $this->scopes;
+ }
+
+ public function getPermissions(): array
+ {
+ return $this->permissions;
+ }
+
+ public function getDefaultPermissions(): array
+ {
+ return $this->defaultPermissions;
+ }
+
+ public function isLimitReached(): bool
+ {
+ return $this->limitReached;
+ }
+
+ public function isStringBasedAvailable(): bool
+ {
+ return $this->stringBasedAvailable;
+ }
+
+ public function setPermissions(array $permissions): void
+ {
+ $this->permissions = $permissions;
+ }
+
+ public function setModules(array $modules): void
+ {
+ $this->modules = $modules;
+ }
+}
diff --git a/tests/CrowdinApiClient/Api/ApplicationApiTest.php b/tests/CrowdinApiClient/Api/ApplicationApiTest.php
index 68ad2c16..a2cceef0 100644
--- a/tests/CrowdinApiClient/Api/ApplicationApiTest.php
+++ b/tests/CrowdinApiClient/Api/ApplicationApiTest.php
@@ -5,6 +5,8 @@
namespace CrowdinApiClient\Tests\Api;
use CrowdinApiClient\Model\ApplicationData;
+use CrowdinApiClient\Model\ApplicationInstallation;
+use CrowdinApiClient\ModelCollection;
class ApplicationApiTest extends AbstractTestApi
{
@@ -26,6 +28,109 @@ protected function getResponseJson(): string
]);
}
+ protected function getInstallationData(): array
+ {
+ return [
+ 'identifier' => 'my-app',
+ 'name' => 'My Application',
+ 'installedBy' => ['id' => 1, 'username' => 'admin'],
+ 'description' => 'App description',
+ 'logo' => '/resources/images/logo.png',
+ 'agent' => null,
+ 'baseUrl' => 'https://localhost.dev',
+ 'manifestUrl' => 'https://localhost.dev/manifest.json',
+ 'createdAt' => '2023-11-29T10:00:00+00:00',
+ 'modules' => [],
+ 'scopes' => ['application'],
+ 'permissions' => ['user' => ['value' => 'all']],
+ 'defaultPermissions' => [],
+ 'limitReached' => false,
+ 'stringBasedAvailable' => false,
+ ];
+ }
+
+ protected function getInstallationResponseJson(): string
+ {
+ return json_encode(['data' => $this->getInstallationData()]);
+ }
+
+ public function testListInstallations(): void
+ {
+ $this->mockRequest([
+ 'path' => '/applications/installations',
+ 'method' => 'get',
+ 'response' => json_encode([
+ 'data' => [
+ ['data' => $this->getInstallationData()],
+ ],
+ 'pagination' => [['offset' => 0, 'limit' => 25]],
+ ]),
+ ]);
+
+ $list = $this->crowdin->application->listInstallations();
+
+ $this->assertInstanceOf(ModelCollection::class, $list);
+ $this->assertCount(1, $list);
+ $this->assertInstanceOf(ApplicationInstallation::class, $list[0]);
+ $this->assertEquals('my-app', $list[0]->getIdentifier());
+ $this->assertEquals('My Application', $list[0]->getName());
+ }
+
+ public function testInstallApplication(): void
+ {
+ $this->mockRequest([
+ 'path' => '/applications/installations',
+ 'method' => 'post',
+ 'body' => json_encode(['url' => 'https://localhost.dev/manifest.json']),
+ 'response' => $this->getInstallationResponseJson(),
+ ]);
+
+ $installation = $this->crowdin->application->installApplication([
+ 'url' => 'https://localhost.dev/manifest.json',
+ ]);
+
+ $this->assertInstanceOf(ApplicationInstallation::class, $installation);
+ $this->assertEquals('my-app', $installation->getIdentifier());
+ $this->assertEquals('My Application', $installation->getName());
+ }
+
+ public function testGetInstallation(): void
+ {
+ $this->mockRequestGet(
+ '/applications/installations/my-app',
+ $this->getInstallationResponseJson()
+ );
+
+ $installation = $this->crowdin->application->getInstallation('my-app');
+
+ $this->assertInstanceOf(ApplicationInstallation::class, $installation);
+ $this->assertEquals('my-app', $installation->getIdentifier());
+ $this->assertEquals('My Application', $installation->getName());
+ }
+
+ public function testDeleteInstallation(): void
+ {
+ $this->mockRequestDelete('/applications/installations/my-app');
+
+ $this->crowdin->application->deleteInstallation('my-app');
+ }
+
+ public function testUpdateInstallation(): void
+ {
+ $this->mockRequestPatch(
+ '/applications/installations/my-app',
+ $this->getInstallationResponseJson()
+ );
+
+ $installation = new ApplicationInstallation($this->getInstallationData());
+ $installation->setPermissions(['user' => ['value' => 'managers']]);
+
+ $result = $this->crowdin->application->updateInstallation($installation);
+
+ $this->assertInstanceOf(ApplicationInstallation::class, $result);
+ $this->assertEquals('my-app', $result->getIdentifier());
+ }
+
public function testGetApplicationData(): void
{
$this->mockRequestGet(
diff --git a/tests/CrowdinApiClient/Model/ApplicationInstallationTest.php b/tests/CrowdinApiClient/Model/ApplicationInstallationTest.php
new file mode 100644
index 00000000..30651d07
--- /dev/null
+++ b/tests/CrowdinApiClient/Model/ApplicationInstallationTest.php
@@ -0,0 +1,126 @@
+model = new ApplicationInstallation([
+ 'identifier' => 'my-app',
+ 'name' => 'My Application',
+ 'installedBy' => ['id' => 1, 'username' => 'admin'],
+ 'description' => 'App description',
+ 'logo' => '/resources/images/logo.png',
+ 'agent' => null,
+ 'baseUrl' => 'https://localhost.dev',
+ 'manifestUrl' => 'https://localhost.dev/manifest.json',
+ 'createdAt' => '2023-11-29T10:00:00+00:00',
+ 'modules' => [['key' => 'module-1', 'type' => 'custom']],
+ 'scopes' => ['application'],
+ 'permissions' => ['user' => ['value' => 'all'], 'project' => ['value' => 'own']],
+ 'defaultPermissions' => ['user' => ['value' => 'all']],
+ 'limitReached' => false,
+ 'stringBasedAvailable' => true,
+ ]);
+ }
+
+ public function testGetIdentifier(): void
+ {
+ $this->assertEquals('my-app', $this->model->getIdentifier());
+ }
+
+ public function testGetName(): void
+ {
+ $this->assertEquals('My Application', $this->model->getName());
+ }
+
+ public function testGetInstalledBy(): void
+ {
+ $this->assertEquals(['id' => 1, 'username' => 'admin'], $this->model->getInstalledBy());
+ }
+
+ public function testGetDescription(): void
+ {
+ $this->assertEquals('App description', $this->model->getDescription());
+ }
+
+ public function testGetLogo(): void
+ {
+ $this->assertEquals('/resources/images/logo.png', $this->model->getLogo());
+ }
+
+ public function testGetAgent(): void
+ {
+ $this->assertNull($this->model->getAgent());
+ }
+
+ public function testGetBaseUrl(): void
+ {
+ $this->assertEquals('https://localhost.dev', $this->model->getBaseUrl());
+ }
+
+ public function testGetManifestUrl(): void
+ {
+ $this->assertEquals('https://localhost.dev/manifest.json', $this->model->getManifestUrl());
+ }
+
+ public function testGetCreatedAt(): void
+ {
+ $this->assertEquals('2023-11-29T10:00:00+00:00', $this->model->getCreatedAt());
+ }
+
+ public function testGetModules(): void
+ {
+ $this->assertEquals([['key' => 'module-1', 'type' => 'custom']], $this->model->getModules());
+ }
+
+ public function testGetScopes(): void
+ {
+ $this->assertEquals(['application'], $this->model->getScopes());
+ }
+
+ public function testGetPermissions(): void
+ {
+ $this->assertEquals(
+ ['user' => ['value' => 'all'], 'project' => ['value' => 'own']],
+ $this->model->getPermissions()
+ );
+ }
+
+ public function testGetDefaultPermissions(): void
+ {
+ $this->assertEquals(['user' => ['value' => 'all']], $this->model->getDefaultPermissions());
+ }
+
+ public function testIsLimitReached(): void
+ {
+ $this->assertFalse($this->model->isLimitReached());
+ }
+
+ public function testIsStringBasedAvailable(): void
+ {
+ $this->assertTrue($this->model->isStringBasedAvailable());
+ }
+
+ public function testSetPermissions(): void
+ {
+ $newPermissions = ['user' => ['value' => 'managers']];
+ $this->model->setPermissions($newPermissions);
+ $this->assertEquals($newPermissions, $this->model->getPermissions());
+ }
+
+ public function testSetModules(): void
+ {
+ $newModules = [['key' => 'module-2', 'type' => 'custom']];
+ $this->model->setModules($newModules);
+ $this->assertEquals($newModules, $this->model->getModules());
+ }
+}