diff --git a/README.md b/README.md index ea5f48c..f988739 100755 --- a/README.md +++ b/README.md @@ -56,6 +56,23 @@ $detectAndFeturesResponse = $searchByImageClient->getSimilar('url'); Your agrument is: `url` - url to the image on which you want to detect products and features. In result you're getting `DetectAndFeaturesResponse` that contains all detection returned by api. +#### Search by feature + +To search products with previously found feature use `searchByFeature` + +```php +use Answear\WideEyesBundle\Service\SearchByImageClient; + +$detectAndFeturesResponse = $searchByImageClient->searchByFeature('featureId', 'label', 'gender', 'filters'); +``` + +Your agruments are: + * `featureId` - featureId you got form DetectAndFeatures + * `label` - label you got form DetectAndFeatures + * `gender` - gender you got from DetectAndFeatures (optional) + * `filters` - result filters (optional) + +In result you're getting `SearchByFeatureResponse` that contains all found products uids meeting your criteria. Final notes ------------ diff --git a/src/DependencyInjection/AnswearWideEyesExtension.php b/src/DependencyInjection/AnswearWideEyesExtension.php index 7314a48..1d30b5c 100755 --- a/src/DependencyInjection/AnswearWideEyesExtension.php +++ b/src/DependencyInjection/AnswearWideEyesExtension.php @@ -26,11 +26,11 @@ public function load(array $configs, ContainerBuilder $container): void $definition = $container->getDefinition(ConfigProvider::class); $definition->setArguments( [ - $config['similarApiUrl'], - $config['searchByImageApiUrl'], + $config['similar']['apiUrl'], + $config['searchByImage']['apiUrl'], $config['publicKey'], - $config['similarRequestTimeout'], - $config['searchByImageRequestTimeout'], + $config['similar']['requestTimeout'], + $config['searchByImage']['requestTimeout'], $config['connectionTimeout'], ] ); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 1cc9de9..1bb6711 100755 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -21,12 +21,20 @@ public function getConfigTreeBuilder(): TreeBuilder $treeBuilder->getRootNode() ->children() - ->scalarNode('similarApiUrl')->defaultValue(self::SIMILAR_API_URL)->end() - ->scalarNode('searchByImageApiUrl')->defaultValue(self::SEARCH_BY_IMAGE_API_URL)->end() - ->scalarNode('publicKey')->cannotBeEmpty()->end() - ->floatNode('connectionTimeout')->defaultValue(self::CONNECTION_TIMEOUT)->end() - ->floatNode('similarRequestTimeout')->defaultValue(self::SIMILAR_REQUEST_TIMEOUT)->end() - ->floatNode('searchByImageRequestTimeout')->defaultValue(self::SEARCH_BY_IMAGE_REQUEST_TIMEOUT)->end() + ->scalarNode('publicKey')->cannotBeEmpty()->end() + ->floatNode('connectionTimeout')->defaultValue(self::CONNECTION_TIMEOUT)->end() + ->arrayNode('similar')->addDefaultsIfNotSet() + ->children() + ->scalarNode('apiUrl')->defaultValue(self::SIMILAR_API_URL)->end() + ->floatNode('requestTimeout')->defaultValue(self::SIMILAR_REQUEST_TIMEOUT)->end() + ->end() + ->end() + ->arrayNode('searchByImage')->addDefaultsIfNotSet() + ->children() + ->scalarNode('apiUrl')->defaultValue(self::SEARCH_BY_IMAGE_API_URL)->end() + ->floatNode('requestTimeout')->defaultValue(self::SEARCH_BY_IMAGE_REQUEST_TIMEOUT)->end() + ->end() + ->end() ->end(); return $treeBuilder; diff --git a/src/Request/SearchByFeatureRequest.php b/src/Request/SearchByFeatureRequest.php new file mode 100644 index 0000000..912d0a7 --- /dev/null +++ b/src/Request/SearchByFeatureRequest.php @@ -0,0 +1,35 @@ +featureId = $featureId; + $this->label = $label; + $this->gender = $gender; + $this->filters = $filters; + } + + public function toJson(): string + { + return json_encode( + array_filter( + [ + 'featureId' => $this->featureId, + 'label' => $this->label, + 'gender' => $this->gender, + 'filters' => $this->filters, + ] + ) + ); + } +} diff --git a/src/Response/SearchByFeatureResponse.php b/src/Response/SearchByFeatureResponse.php new file mode 100644 index 0000000..969a67c --- /dev/null +++ b/src/Response/SearchByFeatureResponse.php @@ -0,0 +1,43 @@ +uids = $uids; + } + + public static function fromArray(array $response): SearchByFeatureResponse + { + try { + $products = $response['products']; + $responseUids = array_map( + static function ($item) { + Assert::keyExists($item, 'uid'); + + return $item['uid']; + }, + $products, + ); + + return new self($responseUids); + } catch (\Throwable $e) { + throw new MalformedResponse($e->getMessage(), $response, $e); + } + } + + public function getUids(): array + { + return $this->uids; + } +} diff --git a/src/Service/AbstractClient.php b/src/Service/AbstractClient.php index 4e14c74..7419fca 100644 --- a/src/Service/AbstractClient.php +++ b/src/Service/AbstractClient.php @@ -13,9 +13,6 @@ abstract class AbstractClient { - protected const SEARCH_BY_ID_ENDPOINT = 'v4/SearchById'; - protected const DETECT_AND_FEATURES_ENDPOINT = 'v4/DetectAndFeatures'; - protected ConfigProvider $configProvider; protected ClientInterface $guzzle; diff --git a/src/Service/SearchByImageClient.php b/src/Service/SearchByImageClient.php index be3d38b..627fb87 100644 --- a/src/Service/SearchByImageClient.php +++ b/src/Service/SearchByImageClient.php @@ -5,12 +5,17 @@ namespace Answear\WideEyesBundle\Service; use Answear\WideEyesBundle\Request\DetectAndFeaturesRequest; +use Answear\WideEyesBundle\Request\SearchByFeatureRequest; use Answear\WideEyesBundle\Response\DetectAndFeaturesResponse; +use Answear\WideEyesBundle\Response\SearchByFeatureResponse; use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; class SearchByImageClient extends AbstractClient { + private const DETECT_AND_FEATURES_ENDPOINT = 'v4/DetectAndFeatures'; + private const SEARCH_BY_FEATURE = 'v4/SearchByFeature'; + public function __construct(ConfigProvider $configProvider, ?ClientInterface $client = null) { parent::__construct( @@ -30,4 +35,15 @@ public function detectAndFeatures(string $image): DetectAndFeaturesResponse $this->request(self::DETECT_AND_FEATURES_ENDPOINT, new DetectAndFeaturesRequest($image)) ); } + + public function searchByFeature( + string $featureId, + string $label, + ?string $gender = null, + ?string $filters = null + ): SearchByFeatureResponse { + return SearchByFeatureResponse::fromArray( + $this->request(self::SEARCH_BY_FEATURE, new SearchByFeatureRequest($featureId, $label, $gender, $filters)) + ); + } } diff --git a/src/Service/SimilarClient.php b/src/Service/SimilarClient.php index 1151972..41efeff 100755 --- a/src/Service/SimilarClient.php +++ b/src/Service/SimilarClient.php @@ -11,6 +11,8 @@ class SimilarClient extends AbstractClient { + private const SEARCH_BY_ID_ENDPOINT = 'v4/SearchById'; + public function __construct(ConfigProvider $configProvider, ?ClientInterface $client = null) { parent::__construct( diff --git a/tests/Unit/Request/SearchByFeatureRequestTest.php b/tests/Unit/Request/SearchByFeatureRequestTest.php new file mode 100644 index 0000000..3b9ead3 --- /dev/null +++ b/tests/Unit/Request/SearchByFeatureRequestTest.php @@ -0,0 +1,37 @@ +toJson() + ); + } + + /** + * @test + */ + public function requestWithGenderAndCountryIsCorrect(): void + { + $request = new SearchByFeatureRequest('featureId', 'label', 'female', 'pl.inStock == true'); + + self::assertSame( + '{"featureId":"featureId","label":"label","gender":"female","filters":"pl.inStock == true"}', + $request->toJson() + ); + } +} diff --git a/tests/Unit/Response/SearchByFeatureResponseTest.php b/tests/Unit/Response/SearchByFeatureResponseTest.php new file mode 100644 index 0000000..f7a341e --- /dev/null +++ b/tests/Unit/Response/SearchByFeatureResponseTest.php @@ -0,0 +1,107 @@ + [ + [ + 'uid' => 'uid1', + ], + [ + 'uid' => 'uid2', + ], + [ + 'uid' => 'uid3', + ], + ], + ]; + + $response = SearchByFeatureResponse::fromArray($responseData); + + self::assertEquals( + [ + 'uid1', + 'uid2', + 'uid3', + ], + $response->getUids() + ); + } + + /** + * @test + */ + public function malformedResponseWithoutUidInOneItem(): void + { + $responseData = [ + 'products' => [ + [ + 'uid' => 'uid1', + ], + [ + 'id' => 'uid2', + ], + [ + 'uid' => 'uid3', + ], + ], + ]; + + $this->expectException(MalformedResponse::class); + SearchByFeatureResponse::fromArray($responseData); + } + + /** + * @test + */ + public function malformedResponseWithoutUid(): void + { + $responseData = [ + 'products' => [ + [ + 'result' => 'uid1', + ], + [ + 'result' => 'uid2', + ], + ], + ]; + + $this->expectException(MalformedResponse::class); + SearchByFeatureResponse::fromArray($responseData); + } + + /** + * @test + */ + public function malformedResponseWithoutProducts(): void + { + $responseData = [ + [ + 'uid' => 'uid1', + ], + [ + 'uid' => 'uid2', + ], + [ + 'uid' => 'uid3', + ], + ]; + + $this->expectException(MalformedResponse::class); + SearchByFeatureResponse::fromArray($responseData); + } +} diff --git a/tests/Unit/Service/SearchByImageClientTest.php b/tests/Unit/Service/SearchByImageClientDetectAndFeatureTest.php similarity index 96% rename from tests/Unit/Service/SearchByImageClientTest.php rename to tests/Unit/Service/SearchByImageClientDetectAndFeatureTest.php index 7116113..d5c54f5 100644 --- a/tests/Unit/Service/SearchByImageClientTest.php +++ b/tests/Unit/Service/SearchByImageClientDetectAndFeatureTest.php @@ -9,7 +9,7 @@ use Answear\WideEyesBundle\Service\SearchByImageClient; use GuzzleHttp\Psr7\Response; -class SearchByImageClientTest extends AbstractClientTest +class SearchByImageClientDetectAndFeatureTest extends AbstractClientTest { private const IMAGE_PATH = 'path'; @@ -58,7 +58,7 @@ public function successfulDetectAndFeatures(): void /** * @test */ - public function responeWithWrongPropertiesInDetecions(): void + public function responseWithWrongPropertiesInDetections(): void { $this->guzzleHandler->append(new Response(200, [], $this->prepareNotProperResponse())); diff --git a/tests/Unit/Service/SearchByImageClientSearchByFeatureTest.php b/tests/Unit/Service/SearchByImageClientSearchByFeatureTest.php new file mode 100644 index 0000000..c184112 --- /dev/null +++ b/tests/Unit/Service/SearchByImageClientSearchByFeatureTest.php @@ -0,0 +1,166 @@ +client = new SearchByImageClient($this->configProvider, $this->setupGuzzle()); + } + + /** + * @dataProvider dataProvider + * @test + */ + public function successfulSearchByFeature( + string $featureId, + string $label, + ?string $gender = null, + ?string $country = null + ): void { + $uid1 = 'uid1'; + $uid2 = 'uid2'; + $uid3 = 'uid3'; + + $this->guzzleHandler->append(new Response(200, [], $this->prepareProperResponse($uid1, $uid2, $uid3))); + + $result = $this->client->searchByFeature($featureId, $label, $gender, $country); + + self::assertSame([$uid1, $uid2, $uid3], $result->getUids()); + self::assertCount(1, $this->guzzleHistory); + } + + public function dataProvider(): iterable + { + yield [ + self::FEATURE_ID, + self::LABEL, + self::GENDER, + self::COUNTRY, + ]; + + yield [ + self::FEATURE_ID, + self::LABEL, + ]; + + yield [ + self::FEATURE_ID, + self::LABEL, + self::GENDER, + ]; + + yield [ + self::FEATURE_ID, + self::LABEL, + null, + self::COUNTRY, + ]; + } + + /** + * @test + */ + public function responseWithoutProducts(): void + { + $this->guzzleHandler->append(new Response(200, [], $this->prepareNotProperResponse())); + + $this->expectException(MalformedResponse::class); + $this->expectExceptionMessage('Undefined index: products'); + + $this->client->searchByFeature(self::FEATURE_ID, self::LABEL); + } + + /** + * @test + */ + public function responseWithoutResult(): void + { + $this->guzzleHandler->append(new Response(200, [], '{"success":true}')); + + $this->expectException(MalformedResponse::class); + $this->expectExceptionMessage('Expected the key "results" to exist.'); + + $this->client->searchByFeature(self::FEATURE_ID, self::LABEL); + } + + /** + * @test + */ + public function responseWithoutArray(): void + { + $this->guzzleHandler->append(new Response(200, [], '"result":[]')); + + $this->expectException(MalformedResponse::class); + $this->expectExceptionMessage('Expected an array. Got: NULL'); + + $this->client->searchByFeature(self::FEATURE_ID, self::LABEL); + } + + /** + * @test + */ + public function serviceUnavailable(): void + { + $this->guzzleHandler->append(new Response(500, [], '{}')); + + $this->expectException(ServiceUnavailable::class); + + $this->client->searchByFeature(self::FEATURE_ID, self::LABEL); + } + + private function prepareProperResponse(string $uid1, string $uid2, string $uid3): string + { + $result = [ + 'results' => [ + 'products' => [ + [ + 'uid' => $uid1, + 'otherData' => 'some data', + ], + [ + 'uid' => $uid2, + ], + [ + 'uid' => $uid3, + 'category' => 'category', + ], + ], + ], + ]; + + return \json_encode($result); + } + + private function prepareNotProperResponse(): string + { + return \json_encode( + [ + 'results' => [ + 'items' => [ + [ + 'uid' => 'uid', + ], + ], + ], + ] + ); + } +}