Skip to content

Commit

Permalink
[feat] Add custom endpoint and metadata (#4)
Browse files Browse the repository at this point in the history
* [chore] Add php testing script per version

* [chore] Clean up empty newlines

* Add *.lock files to .gitignore

* [feat] Add custom endpoint option

* [fix] Edit offset, limit, item_ids count limit to 10000

* [feat] Add metadata attribute to recommender response

* [chore] Change string limits to 1~500

* [fix] Make bad custom endpoint raise exception

* [feat] Add warning message to json parser

* [chore] Add version to composer.json

* [fix] Fix typo

* [chore] Add more validation to event params

* [fix] Fix typo

* [fix] Fix typo and remove unnecessary tests

* [fix] Prepend hyphen to customEndpoint

* [fix] Add string length validation to custom endpoint

* [fix] Omitted commit

* [fix] Omitted commit
  • Loading branch information
eomiso authored Oct 23, 2022
1 parent e4def9e commit 4349dcd
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 120 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "zaikorea/zaiclient",
"version": "2.2.0",
"description": "Z.Ai official client SDK",
"hompage": "https://www.zaikorea.org",
"autoload": {
Expand Down
4 changes: 2 additions & 2 deletions src/ZaiClient/Configs/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@

class Config
{
const EVENTS_API_ENDPOINT = 'https://collector-api.zaikorea.org';
const EVENTS_API_ENDPOINT = "https://collector-api%s.zaikorea.org";
const EVENTS_API_PATH = '/events';

const ML_API_ENDPOINT = 'https://ml-api.zaikorea.org';
const ML_API_ENDPOINT = "https://ml-api%s.zaikorea.org";
const ML_API_PATH_PREFIX = '/clients/%s/recommenders';

const HMAC_ALGORITHM = 'sha256';
Expand Down
21 changes: 13 additions & 8 deletions src/ZaiClient/Requests/EventInBatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class EventInBatch implements \JsonSerializable
{
protected $user_id;
protected $item_id; // string or array
protected $item_id; // string
protected $timestamp;
protected $event_type;
protected $event_value;
Expand All @@ -29,16 +29,21 @@ public function __construct($user_id, $item_id, $timestamp, $event_type, $event_
$this->item_id = is_string($item_id) ? $item_id : strval($item_id);
$this->timestamp = strval($timestamp);
$this->event_type = $event_type;
$this->event_value = is_null($event_value) ? "null" : strval($event_value);
$this->event_value = is_null($event_value) ?
"null" : substr(strval($event_value), 0, 500); // clip by 500 letters

if (!(strlen($this->user_id) > 0 && strlen($this->user_id) <= 100))
throw new \InvalidArgumentException('Length of user id must be between 1 and 100.');
if (!(strlen($this->user_id) > 0 && strlen($this->user_id) <= 500))
throw new \InvalidArgumentException('Length of user id must be between 1 and 500.');

if (!(strlen($this->item_id) > 0 && strlen($this->item_id) <= 100))
throw new \InvalidArgumentException('Length of item id must be between 1 and 100.');
if (!(strlen($this->item_id) > 0 && strlen($this->item_id) <= 500))
throw new \InvalidArgumentException('Length of item id must be between 1 and 500.');

if (!(strlen($this->event_type) > 0 && strlen($this->event_type) <= 100))
throw new \InvalidArgumentException('Length of event type must be between 1 and 100.');
if (!($this->timestamp >= 1648871097 || $this->timestamp <= 2147483647))
throw new \InvalidArgumentException('Invalid timestamp.');
if (!(strlen($this->event_type) > 0 && strlen($this->event_type) <= 500))
throw new \InvalidArgumentException('Length of event type must be between 1 and 500.');
if (strlen($this->event_value) == 0)
throw new \InvalidArgumentException('Length of event value must be at least 1.');
}

function jsonSerialize()
Expand Down
6 changes: 0 additions & 6 deletions src/ZaiClient/Requests/RecommendationRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ abstract class RecommendationRequest implements \JsonSerializable
*/
abstract public function getPath($client_id);

/**
* Get full URI with path
* @return string PATH to use for request
*/
abstract public function getURIPath($client_id);

/**
*
* @return int
Expand Down
25 changes: 8 additions & 17 deletions src/ZaiClient/Requests/RelatedItemsRecommendationRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,19 @@ class RelatedItemsRecommendationRequest extends RecommendationRequest

public function __construct($item_id, $limit, $options = array())
{
if (is_null($item_id) || !(strlen($item_id) > 0 && strlen($item_id) <= 100))
throw new \InvalidArgumentException('Length of item id must be between 1 and 100.');
if (!(0 < $limit && $limit <= 1000000))
throw new \InvalidArgumentException('Limit must be between 1 and 1000,000.');
if (is_null($item_id) || !(strlen($item_id) > 0 && strlen($item_id) <= 500))
throw new \InvalidArgumentException('Length of item id must be between 1 and 500.');
if (is_null($limit) || !(0 <= $limit && $limit <= 10000))
throw new \InvalidArgumentException('Limit must be between 0 and 10,000.');
if (!is_array($options))
throw new \InvalidArgumentException("Options must be given as an array.");
if (isset($options['offset'])) {

if (!(0 <= $options['offset'] && $options['offset'] <= 1000000))
throw new \InvalidArgumentException('Offset must be between 0 and 1000,000.');
if (!(0 <= $options['offset'] && $options['offset'] <= 10000))
throw new \InvalidArgumentException('Offset must be between 0 and 10,000.');
}
if (isset($options['recommendation_type'])) { // php tip! isset() returns false if the value of $options['recommendation_type'] is null
if (!(0 < strlen($options['recommendation_type'] && strlen($options['recommendation_type']) <= 100)))
throw new \InvalidArgumentException('Length of recommendation type must be between 1 and 100.');
if (!(0 < strlen($options['recommendation_type'] && strlen($options['recommendation_type']) <= 500)))
throw new \InvalidArgumentException('Length of recommendation type must be between 1 and 500.');
}
if (isset($options['recommendation_options'])) {
if (!is_array($options['recommendation_options']) || !$this->isAssoc($options['recommendation_options'])) {
Expand Down Expand Up @@ -60,12 +59,4 @@ public function getPath($client_id)
return sprintf(Config::ML_API_PATH_PREFIX . self::RECOMMENDER_PATH, $client_id);
}

/**
* Get full URI with path
* @return string PATH to use for request
*/
public function getURIPath($client_id)
{
return Config::ML_API_ENDPOINT . $this->getPath($client_id);
}
}
32 changes: 12 additions & 20 deletions src/ZaiClient/Requests/RerankingRecommendationRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,31 @@ class RerankingRecommendationRequest extends RecommendationRequest
*/
public function __construct($user_id, $item_ids, $options = array())
{
if (!(is_null($user_id) || strlen($user_id) > 0 && strlen($user_id) <= 100))
throw new \InvalidArgumentException('Length of user id must be between 1 and 100 or null.');
if (!(is_null($user_id) || strlen($user_id) > 0 && strlen($user_id) <= 500))
throw new \InvalidArgumentException('Length of user id must be between 1 and 500.');
if (!is_array($item_ids))
throw new \InvalidArgumentException("item_ids must be an array");
if (!(0 < count($item_ids) && count($item_ids) <= 1000000))
throw new \InvalidArgumentException("Length of item_ids must be between 1 and 1000,000.");
if (!(0 <= count($item_ids) && count($item_ids) <= 10000))
throw new \InvalidArgumentException("Length of item_ids must be between 0 and 10,000.");

foreach ($item_ids as $item_id) {
if (!(strlen($item_id) > 0 && strlen($item_id) <= 100))
throw new \InvalidArgumentException('Length of item id must be between 1 and 100.');
if (!(strlen($item_id) > 0 && strlen($item_id) <= 500))
throw new \InvalidArgumentException('Length of item id must be between 1 and 500.');
}
if (!is_array($options))
throw new \InvalidArgumentException("Options must be given as an array.");

if (isset($options['offset'])) {
if (!(0 <= $options['offset'] && $options['offset'] <= 1000000))
throw new \InvalidArgumentException('Offset must be between 0 and 1000,000.');
if (!(0 <= $options['offset'] && $options['offset'] <= 10000))
throw new \InvalidArgumentException('Offset must be between 0 and 10,000.');
}
if (isset($options['limit'])) {
if (!(0 < $options['limit'] && $options['limit'] <= 1000000))
throw new \InvalidArgumentException('Limit must be between 1 and 1000,000.');
if (!(0 <= $options['limit'] && $options['limit'] <= 10000))
throw new \InvalidArgumentException('Limit must be between 0 and 10,000.');
}
if (isset($options['recommendation_type'])) { // php tip! isset() returns false if the value of $options['recommendation_type'] is null
if (!(0 < strlen($options['recommendation_type'] && strlen($options['recommendation_type']) <= 100)))
throw new \InvalidArgumentException('Length of recommendation type must be between 1 and 100.');
if (!(0 < strlen($options['recommendation_type'] && strlen($options['recommendation_type']) <= 500)))
throw new \InvalidArgumentException('Length of recommendation type must be between 1 and 500.');
}
if (isset($options['recommendation_options'])) {
if (!is_array($options['recommendation_options']) || !$this->isAssoc($options['recommendation_options'])) {
Expand Down Expand Up @@ -77,12 +77,4 @@ public function getPath($client_id)
return sprintf(Config::ML_API_PATH_PREFIX . self::RECOMMENDER_PATH, $client_id);
}

/**
* Get full URI with path
* @return string PATH to use for request
*/
public function getURIPath($client_id)
{
return Config::ML_API_ENDPOINT . $this->getPath($client_id);
}
}
24 changes: 8 additions & 16 deletions src/ZaiClient/Requests/UserRecommendationRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ class UserRecommendationRequest extends RecommendationRequest

public function __construct($user_id, $limit, $options = array())
{
if (!(is_null($user_id) || strlen($user_id) > 0 && strlen($user_id) <= 100))
throw new \InvalidArgumentException('Length of user id must be between 1 and 100 or null.');
if (!(0 < $limit && $limit <= 1000000))
throw new \InvalidArgumentException('Limit must be between 1 and 1000,000.');
if (!(is_null($user_id) || strlen($user_id) > 0 && strlen($user_id) <= 500))
throw new \InvalidArgumentException('Length of user id must be between 1 and 500.');
if (is_null($limit) || !(0 <= $limit && $limit <= 10000))
throw new \InvalidArgumentException('Limit must be between 0 and 10,000.');
if (!is_array($options))
throw new \InvalidArgumentException('Options must be given as an array.');
if (isset($options['offset'])) {
if (!(0 <= $options['offset'] && $options['offset'] <= 1000000))
throw new \InvalidArgumentException('Offset must be between 0 and 1000,000.');
if (!(0 <= $options['offset'] && $options['offset'] <= 10000))
throw new \InvalidArgumentException('Offset must be between 0 and 10,000.');
}
if (isset($options['recommendation_type'])) { // php tip! isset() returns false if the value of $options['recommendation_type'] is null
if (!(0 < strlen($options['recommendation_type'] && strlen($options['recommendation_type']) <= 100)))
throw new \InvalidArgumentException('Length of recommendation type must be between 1 and 100.');
if (!(0 < strlen($options['recommendation_type'] && strlen($options['recommendation_type']) <= 500)))
throw new \InvalidArgumentException('Length of recommendation type must be between 1 and 500.');
}
if (isset($options['recommendation_options'])) {
if (!is_array($options['recommendation_options']) || !$this->isAssoc($options['recommendation_options'])) {
Expand Down Expand Up @@ -59,12 +59,4 @@ public function getPath($client_id)
return sprintf(Config::ML_API_PATH_PREFIX . self::RECOMMENDER_PATH, $client_id);
}

/**
* Get full URI with path
* @return string PATH to use for request
*/
public function getURIPath($client_id)
{
return Config::ML_API_ENDPOINT . $this->getPath($client_id);
}
}
29 changes: 29 additions & 0 deletions src/ZaiClient/Responses/RecommendationResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class RecommendationResponse
* @var float $timestamp timestamp from server
*/
private $timestamp;

/**
* @var array $metadata associative array
*/
private $metadata;

/**
* Set Items with array of item_ids
Expand Down Expand Up @@ -54,6 +59,20 @@ public function setTimestamp($timestamp)
$this->timestamp = $timestamp;
}

/**
* Set Metadata with associative array
* @param string $metadata
*/
public function setMetadata($metadata)
{
$this->metadata = json_decode($metadata, $assoc=True);
if (!is_array($this->metadata)) {
$warningMessage = "Failed to parse the metadata to object, returning an empty object. metadata: " . $metadata;
trigger_error($warningMessage, E_USER_WARNING);
$this->metadata = array();
}
}

/**
* Get Array of items
* @return int
Expand Down Expand Up @@ -81,12 +100,22 @@ public function getTimestamp()
return $this->timestamp;
}

/**
* Get Metadata
* @return array Metadata from server
*/
public function getMetadata()
{
return $this->metadata;
}

public function __toString()
{
return "RecommendationResponse{\n" .
"\titems=" . implode(" | ", $this->items) . "\n" .
"\tcount={$this->count}\n" .
"\ttimestamp={$this->timestamp}\n" .
"\tmetadata=" . print_r($this->metadata, true) . "\n" .
"}\n";
}
}
52 changes: 45 additions & 7 deletions src/ZaiClient/ZaiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class ZaiClient
private $guzzle_client;
private $json_mapper;
private $options;

private $ml_api_endpoint;
private $collector_api_endpoint;

public function __construct($client_id, $secret, $options=array())
{
Expand All @@ -43,8 +44,12 @@ public function __construct($client_id, $secret, $options=array())
$this->json_mapper = new JsonMapper();
$this->options = [
'connect_timeout' => $this->resolveTimeoutOptions('connect_timeout', $options),
'read_timeout' => $this->resolveTimeoutOptions('read_timeout', $options)
'read_timeout' => $this->resolveTimeoutOptions('read_timeout', $options),
'custom_endpoint' => $this->resolveEndpointOptions('custom_endpoint', $options)
];

$this->ml_api_endpoint = sprintf(Config::ML_API_ENDPOINT, $this->options['custom_endpoint']);
$this->collector_api_endpoint = sprintf(Config::EVENTS_API_ENDPOINT, $this->options['custom_endpoint']);
}

/**
Expand All @@ -63,7 +68,7 @@ public function addEventLog($event)
try {
$response = $this->guzzle_client->request(
'POST',
Config::EVENTS_API_ENDPOINT . Config::EVENTS_API_PATH,
$this->collector_api_endpoint . Config::EVENTS_API_PATH,
[
'headers' => $headers,
'body' => Utils::streamFor($body),
Expand Down Expand Up @@ -101,7 +106,7 @@ public function updateEventLog($event)
try {
$response = $this->guzzle_client->request(
'PUT',
Config::EVENTS_API_ENDPOINT . Config::EVENTS_API_PATH,
$this->collector_api_endpoint . Config::EVENTS_API_PATH,
[
'headers' => $headers,
'body' => Utils::streamFor($body),
Expand Down Expand Up @@ -136,7 +141,7 @@ public function deleteEventLog($event)
try {
$response = $this->guzzle_client->request(
'DELETE',
Config::EVENTS_API_ENDPOINT . Config::EVENTS_API_PATH,
$this->collector_api_endpoint . Config::EVENTS_API_PATH,
[
'headers' => $headers,
'body' => Utils::streamFor($body),
Expand All @@ -163,17 +168,18 @@ public function deleteEventLog($event)
*/
public function getRecommendations($request)
{
$path = $request->getPath($this->zai_client_id);
$headers = ZaiHeaders::generateZaiHeaders(
$this->zai_client_id,
$this->zai_secret,
$request->getPath($this->zai_client_id)
$path
);
$body = json_encode($request);

try {
$response = $this->guzzle_client->request(
'POST',
$request->getURIPath($this->zai_client_id),
$this->ml_api_endpoint . $path,
[
'headers' => $headers,
'body' => Utils::streamFor($body),
Expand Down Expand Up @@ -209,8 +215,40 @@ public function resolveTimeoutOptions($key, $options)
return 30;
}

public function resolveEndpointOptions($key, $options)
{
if (isset($options[$key])) {
if (!is_string($options[$key]))
throw new \InvalidArgumentException('Custom endpoint option must be a string');

if (strlen($options[$key]) > 10)
throw new \InvalidArgumentException('Custom endpoint should be less than or equal to 10.');

$pattern = "/^[a-zA-Z0-9-]*$/";

$is_match = preg_match($pattern, $options[$key]);

if ($is_match)
return "-" . $options[$key];
else
throw new \InvalidArgumentException('Only alphanumeric characters are allowed for custom endpoint.');
}

return "";
}

public function getOptions()
{
return $this->options;
}

public function getCollectorApiEndpoint()
{
return $this->collector_api_endpoint;
}

public function getMlApiEndpoint()
{
return $this->ml_api_endpoint;
}
}
Loading

0 comments on commit 4349dcd

Please sign in to comment.