diff --git a/src/CrowdinApiClient/Api/ApplicationApi.php b/src/CrowdinApiClient/Api/ApplicationApi.php
new file mode 100644
index 00000000..b547037e
--- /dev/null
+++ b/src/CrowdinApiClient/Api/ApplicationApi.php
@@ -0,0 +1,174 @@
+_get($url, ApplicationData::class, $params);
+ }
+
+ /**
+ * Add Application Data
+ * @link https://developer.crowdin.com/api/v2/#operation/api.applications.api.post API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.post API Documentation Enterprise
+ *
+ * @param string $applicationIdentifier
+ * @param string $path
+ * @param array $data
+ * @return ApplicationData|null
+ */
+ public function addApplicationData(string $applicationIdentifier, string $path, array $data): ?ApplicationData
+ {
+ $url = sprintf('applications/%s/api/%s', $applicationIdentifier, $path);
+ return $this->_post($url, ApplicationData::class, $data);
+ }
+
+ /**
+ * Update or Restore Application Data
+ * @link https://developer.crowdin.com/api/v2/#operation/api.applications.api.put API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.put API Documentation Enterprise
+ *
+ * @param string $applicationIdentifier
+ * @param string $path
+ * @param array $data
+ * @return ApplicationData|null
+ */
+ public function updateOrRestoreApplicationData(string $applicationIdentifier, string $path, array $data): ?ApplicationData
+ {
+ $url = sprintf('applications/%s/api/%s', $applicationIdentifier, $path);
+ return $this->_put($url, ApplicationData::class, $data);
+ }
+
+ /**
+ * Delete Application Data
+ * @link https://developer.crowdin.com/api/v2/#operation/api.applications.api.delete API Documentation
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.delete API Documentation Enterprise
+ *
+ * @param string $applicationIdentifier
+ * @param string $path
+ * @return mixed
+ */
+ public function deleteApplicationData(string $applicationIdentifier, string $path)
+ {
+ $url = sprintf('applications/%s/api/%s', $applicationIdentifier, $path);
+ 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
+ * @link https://developer.crowdin.com/enterprise/api/v2/#operation/api.applications.api.patch API Documentation Enterprise
+ *
+ * @param string $applicationIdentifier
+ * @param string $path
+ * @param array $data Application-specific key-value payload (free-form, not RFC 6902 patch operations)
+ * string $data[op] required Patch operation to perform (replace, add, remove, test)
+ * string $data[path] required
+ * mixed $data[value] The value to be used within the operation (string, int, bool, or object)
+ * @return ApplicationData|null
+ */
+ public function editApplicationData(string $applicationIdentifier, string $path, array $data): ?ApplicationData
+ {
+ $url = sprintf('applications/%s/api/%s', $applicationIdentifier, $path);
+ return $this->_patch($url, ApplicationData::class, $data);
+ }
+}
diff --git a/src/CrowdinApiClient/Crowdin.php b/src/CrowdinApiClient/Crowdin.php
index 7ea7616e..61887b3e 100644
--- a/src/CrowdinApiClient/Crowdin.php
+++ b/src/CrowdinApiClient/Crowdin.php
@@ -52,6 +52,7 @@
* @property \CrowdinApiClient\Api\AiApi|\CrowdinApiClient\Api\Enterprise\AiApi $ai
* @property \CrowdinApiClient\Api\Enterprise\FieldApi $field
* @property \CrowdinApiClient\Api\StyleGuideApi $styleGuide
+ * @property \CrowdinApiClient\Api\ApplicationApi $application
*/
class Crowdin
{
@@ -126,6 +127,7 @@ class Crowdin
'securityLog',
'ai',
'styleGuide',
+ 'application',
];
protected $servicesEnterprise = [
@@ -170,6 +172,7 @@ class Crowdin
'ai',
'field',
'styleGuide',
+ 'application',
];
public function __construct(array $config)
diff --git a/src/CrowdinApiClient/Model/ApplicationData.php b/src/CrowdinApiClient/Model/ApplicationData.php
new file mode 100644
index 00000000..9f7f6980
--- /dev/null
+++ b/src/CrowdinApiClient/Model/ApplicationData.php
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 00000000..e0ade29f
--- /dev/null
+++ b/tests/CrowdinApiClient/Api/ApplicationApiTest.php
@@ -0,0 +1,224 @@
+ 'value',
+ 'count' => 1,
+ ];
+ }
+
+ protected function getResponseJson(): string
+ {
+ return json_encode([
+ 'data' => $this->getResponseData(),
+ ]);
+ }
+
+ 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(
+ '/applications/my-application/api/settings',
+ $this->getResponseJson()
+ );
+
+ $data = $this->crowdin->application->getApplicationData($this->applicationIdentifier, $this->path);
+
+ $this->assertInstanceOf(ApplicationData::class, $data);
+ $this->assertEquals($this->getResponseData(), $data->getData());
+ $this->assertEquals('value', $data->getDataProperty('key'));
+ $this->assertEquals(1, $data->getDataProperty('count'));
+ }
+
+ public function testAddApplicationData(): void
+ {
+ $requestBody = ['key' => 'new-value'];
+
+ $this->mockRequest([
+ 'path' => '/applications/my-application/api/settings',
+ 'method' => 'post',
+ 'body' => json_encode($requestBody),
+ 'response' => $this->getResponseJson(),
+ ]);
+
+ $data = $this->crowdin->application->addApplicationData(
+ $this->applicationIdentifier,
+ $this->path,
+ $requestBody
+ );
+
+ $this->assertInstanceOf(ApplicationData::class, $data);
+ $this->assertEquals($this->getResponseData(), $data->getData());
+ }
+
+ public function testUpdateOrRestoreApplicationData(): void
+ {
+ $requestBody = ['key' => 'updated-value'];
+
+ $this->mockRequest([
+ 'path' => '/applications/my-application/api/settings',
+ 'method' => 'put',
+ 'body' => json_encode($requestBody),
+ 'response' => $this->getResponseJson(),
+ ]);
+
+ $data = $this->crowdin->application->updateOrRestoreApplicationData(
+ $this->applicationIdentifier,
+ $this->path,
+ $requestBody
+ );
+
+ $this->assertInstanceOf(ApplicationData::class, $data);
+ $this->assertEquals($this->getResponseData(), $data->getData());
+ }
+
+ public function testDeleteApplicationData(): void
+ {
+ $this->mockRequestDelete('/applications/my-application/api/settings');
+
+ $this->crowdin->application->deleteApplicationData($this->applicationIdentifier, $this->path);
+ }
+
+ public function testEditApplicationData(): void
+ {
+ $operations = [
+ [
+ 'op' => 'replace',
+ 'path' => '/key',
+ 'value' => 'edited-value'
+ ],
+ ];
+
+ $this->mockRequest([
+ 'path' => '/applications/my-application/api/settings',
+ 'method' => 'patch',
+ 'body' => json_encode($operations),
+ 'response' => $this->getResponseJson(),
+ ]);
+
+ $data = $this->crowdin->application->editApplicationData(
+ $this->applicationIdentifier,
+ $this->path,
+ $operations
+ );
+
+ $this->assertInstanceOf(ApplicationData::class, $data);
+ $this->assertEquals($this->getResponseData(), $data->getData());
+ }
+}
diff --git a/tests/CrowdinApiClient/Model/ApplicationDataTest.php b/tests/CrowdinApiClient/Model/ApplicationDataTest.php
new file mode 100644
index 00000000..9ac60da4
--- /dev/null
+++ b/tests/CrowdinApiClient/Model/ApplicationDataTest.php
@@ -0,0 +1,39 @@
+model = new ApplicationData([
+ 'key' => 'value',
+ 'count' => 42,
+ 'nested' => ['a' => 1],
+ ]);
+ }
+
+ public function testGetData(): void
+ {
+ $this->assertEquals([
+ 'key' => 'value',
+ 'count' => 42,
+ 'nested' => ['a' => 1],
+ ], $this->model->getData());
+ }
+
+ public function testGetDataProperty(): void
+ {
+ $this->assertEquals('value', $this->model->getDataProperty('key'));
+ $this->assertEquals(42, $this->model->getDataProperty('count'));
+ $this->assertEquals(['a' => 1], $this->model->getDataProperty('nested'));
+ $this->assertNull($this->model->getDataProperty('missing'));
+ }
+}
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());
+ }
+}