Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
35 changes: 32 additions & 3 deletions src/AiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use WordPress\AiClient\ProviderImplementations\Google\GoogleProvider;
use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\Http\HttpTransporterFactory;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
Expand Down Expand Up @@ -114,14 +115,42 @@ public static function defaultRegistry(): ProviderRegistry
/**
* Checks if a provider is configured and available for use.
*
* Supports multiple input formats for developer convenience:
* - ProviderAvailabilityInterface: Direct availability check
* - string (provider ID): e.g., AiClient::isConfigured('openai')
* - string (class name): e.g., AiClient::isConfigured(OpenAiProvider::class)
*
* When using string input, this method leverages the ProviderRegistry's centralized
* dependency management, ensuring HttpTransporter and authentication are properly
* injected into availability instances.
*
* @since 0.1.0
*
* @param ProviderAvailabilityInterface $availability The provider availability instance to check.
* @param ProviderAvailabilityInterface|string|class-string<ProviderInterface> $availabilityOrIdOrClassName
* The provider availability instance, provider ID, or provider class name.
* @return bool True if the provider is configured and available, false otherwise.
*/
public static function isConfigured(ProviderAvailabilityInterface $availability): bool
public static function isConfigured($availabilityOrIdOrClassName): bool
{
return $availability->isConfigured();
// Handle direct ProviderAvailabilityInterface (backward compatibility)
if ($availabilityOrIdOrClassName instanceof ProviderAvailabilityInterface) {
return $availabilityOrIdOrClassName->isConfigured();
}

// Handle string input (provider ID or class name) via registry
if (is_string($availabilityOrIdOrClassName)) {
return self::defaultRegistry()->isProviderConfigured($availabilityOrIdOrClassName);
}

throw new \InvalidArgumentException(
'Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name. ' .
sprintf(
'Received: %s',
is_object($availabilityOrIdOrClassName)
? get_class($availabilityOrIdOrClassName)
: gettype($availabilityOrIdOrClassName)
)
);
}

/**
Expand Down
138 changes: 138 additions & 0 deletions tests/unit/AiClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use WordPress\AiClient\AiClient;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\DTO\UserMessage;
use WordPress\AiClient\ProviderImplementations\OpenAi\OpenAiProvider;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\ProviderRegistry;
Expand Down Expand Up @@ -246,6 +247,143 @@ public function testIsConfiguredReturnsFalseWhenProviderIsNotConfigured(): void
$this->assertFalse($result);
}

/**
* Tests isConfigured method with provider ID string leverages default registry.
*/
public function testIsConfiguredWithProviderIdString(): void
{
// This test will use the actual default registry since we can't easily mock static methods
// The default registry should have providers registered, so we test the delegation path
$result = AiClient::isConfigured('openai');

// The result will be false because no actual API keys are configured in tests,
// but the important thing is that no exception is thrown and the registry delegation works
$this->assertIsBool($result);
}

/**
* Tests isConfigured method with provider class name leverages default registry.
*/
public function testIsConfiguredWithProviderClassName(): void
{
// This test will use the actual default registry since we can't easily mock static methods
// The default registry should have providers registered, so we test the delegation path
$result = AiClient::isConfigured(OpenAiProvider::class);

// The result will be false because no actual API keys are configured in tests,
// but the important thing is that no exception is thrown and the registry delegation works
$this->assertIsBool($result);
}

/**
* Tests isConfigured method with provider ID uses default registry when none provided.
*/
public function testIsConfiguredWithProviderIdUsesDefaultRegistry(): void
{
// This test will use the actual default registry since we can't easily mock static methods
// The default registry should have providers registered, so we test the delegation path
$result = AiClient::isConfigured('openai');

// The result will be false because no actual API keys are configured in tests,
// but the important thing is that no exception is thrown and the registry delegation works
$this->assertIsBool($result);
}

/**
* Tests isConfigured method with provider class name uses default registry when none provided.
*/
public function testIsConfiguredWithProviderClassNameUsesDefaultRegistry(): void
{
// This test will use the actual default registry since we can't easily mock static methods
// The default registry should have providers registered, so we test the delegation path
$result = AiClient::isConfigured(OpenAiProvider::class);

// The result will be false because no actual API keys are configured in tests,
// but the important thing is that no exception is thrown and the registry delegation works
$this->assertIsBool($result);
}

/**
* Tests isConfigured method throws exception for invalid parameter types.
*/
public function testIsConfiguredThrowsExceptionForInvalidParameterTypes(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage(
'Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name. ' .
'Received: integer'
);

AiClient::isConfigured(123);
}

/**
* Data provider for invalid isConfigured parameter types.
*
* @return array<string, array{mixed, string}>
*/
public function invalidIsConfiguredParameterTypesProvider(): array
{
return [
'integer parameter' => [123, 'integer'],
'array parameter' => [['invalid_array'], 'array'],
'object parameter' => [new \stdClass(), 'stdClass'],
'boolean parameter' => [true, 'boolean'],
'null parameter' => [null, 'NULL'],
];
}

/**
* Tests that isConfigured rejects all invalid parameter types consistently.
*
* @dataProvider invalidIsConfiguredParameterTypesProvider
* @param mixed $invalidParam
*/
public function testIsConfiguredRejectsInvalidParameterTypes($invalidParam, string $expectedType): void
{
try {
AiClient::isConfigured($invalidParam);
$this->fail("Expected InvalidArgumentException for isConfigured with $expectedType");
} catch (\InvalidArgumentException $e) {
$this->assertStringContainsString(
'Parameter must be a ProviderAvailabilityInterface instance, provider ID string, ' .
'or provider class name.',
$e->getMessage(),
"isConfigured should reject invalid parameter type: $expectedType"
);
$this->assertStringContainsString(
"Received: $expectedType",
$e->getMessage(),
"isConfigured should include received type in error message"
);
}
}

/**
* Tests backward compatibility - isConfigured still works with ProviderAvailabilityInterface.
*/
public function testIsConfiguredBackwardCompatibility(): void
{
// Test that the original interface-based approach still works exactly as before
$mockAvailability = $this->createMock(ProviderAvailabilityInterface::class);
$mockAvailability->expects($this->once())
->method('isConfigured')
->willReturn(true);

// Should work without registry parameter
$result = AiClient::isConfigured($mockAvailability);
$this->assertTrue($result);

// Should work in all cases with interface input
$mockAvailability2 = $this->createMock(ProviderAvailabilityInterface::class);
$mockAvailability2->expects($this->once())
->method('isConfigured')
->willReturn(false);

$result2 = AiClient::isConfigured($mockAvailability2);
$this->assertFalse($result2);
}

/**
* Tests generateResult delegates to generateTextResult when model supports text generation.
*/
Expand Down