diff --git a/CHANGELOG.md b/CHANGELOG.md index 26a7b7ff..bb9d03e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] + +#### Added + +* [#411](https://github.com/shlinkio/shlink/issues/411) Added new `meta` property on the `ShortUrl` REST API model. + + These endpoints are affected and include the new property when suitable: + + * `GET /short-urls` - List short URLs. + * `GET /short-urls/shorten` - Create a short URL (for integrations). + * `GET /short-urls/{shortCode}` - Get one short URL. + * `POST /short-urls` - Create short URL. + + The property includes the values `validSince`, `validUntil` and `maxVisits` in a single object. All of them are nullable. + + ```json + { + "validSince": "2016-01-01T00:00:00+02:00", + "validUntil": null, + "maxVisits": 100 + } + ``` + +#### Changed + +* *Nothing* + +### Deprecated + +* *Nothing* + +#### Removed + +* *Nothing* + +#### Fixed + +* *Nothing* + + ## 1.17.0 - 2019-05-13 #### Added diff --git a/docs/swagger/definitions/ShortUrl.json b/docs/swagger/definitions/ShortUrl.json index 14cd0fe5..9b259249 100644 --- a/docs/swagger/definitions/ShortUrl.json +++ b/docs/swagger/definitions/ShortUrl.json @@ -29,6 +29,9 @@ }, "description": "A list of tags applied to this short URL" }, + "meta": { + "$ref": "./ShortUrlMeta.json" + }, "originalUrl": { "deprecated": true, "type": "string", diff --git a/docs/swagger/definitions/ShortUrlMeta.json b/docs/swagger/definitions/ShortUrlMeta.json new file mode 100644 index 00000000..370a548b --- /dev/null +++ b/docs/swagger/definitions/ShortUrlMeta.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "required": ["validSince", "validUntil", "maxVisits"], + "properties": { + "validSince": { + "description": "The date (in ISO-8601 format) from which this short code will be valid", + "type": "string", + "nullable": true + }, + "validUntil": { + "description": "The date (in ISO-8601 format) until which this short code will be valid", + "type": "string", + "nullable": true + }, + "maxVisits": { + "description": "The maximum number of allowed visits for this short code", + "type": "number", + "nullable": true + } + } +} diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index e55b1162..2254a732 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -100,7 +100,12 @@ "tags": [ "games", "tech" - ] + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 100 + } }, { "shortCode": "12Kb3", @@ -110,7 +115,12 @@ "visitsCount": 1029, "tags": [ "shlink" - ] + ], + "meta": { + "validSince": null, + "validUntil": null, + "maxVisits": null + } }, { "shortCode": "123bA", @@ -118,7 +128,12 @@ "longUrl": "https://www.google.com", "dateCreated": "2015-10-01T20:34:16+02:00", "visitsCount": 25, - "tags": [] + "tags": [], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": null + } } ], "pagination": { @@ -227,7 +242,12 @@ "tags": [ "games", "tech" - ] + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 500 + } } } }, diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json index 00af215d..803d77d5 100644 --- a/docs/swagger/paths/v1_short-urls_shorten.json +++ b/docs/swagger/paths/v1_short-urls_shorten.json @@ -64,7 +64,12 @@ "tags": [ "games", "tech" - ] + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 100 + } }, "text/plain": "https://doma.in/abc123" } diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json index c312db8a..41d1499c 100644 --- a/docs/swagger/paths/v1_short-urls_{shortCode}.json +++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json @@ -44,7 +44,12 @@ "visitsCount": 1029, "tags": [ "shlink" - ] + ], + "meta": { + "validSince": "2017-01-21T00:00:00+02:00", + "validUntil": null, + "maxVisits": 100 + } } } }, diff --git a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php index 17be8628..d44ce109 100644 --- a/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php +++ b/module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php @@ -17,6 +17,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Zend\Paginator\Paginator; +use function array_flip; +use function array_intersect_key; use function array_values; use function count; use function explode; @@ -29,6 +31,14 @@ class ListShortUrlsCommand extends Command public const NAME = 'short-url:list'; private const ALIASES = ['shortcode:list', 'short-code:list']; + private const COLUMNS_WHITELIST = [ + 'shortCode', + 'shortUrl', + 'longUrl', + 'dateCreated', + 'visitsCount', + 'tags', + ]; /** @var ShortUrlServiceInterface */ private $shortUrlService; @@ -125,8 +135,7 @@ class ListShortUrlsCommand extends Command unset($shortUrl['tags']); } - unset($shortUrl['originalUrl']); - $rows[] = array_values($shortUrl); + $rows[] = array_values(array_intersect_key($shortUrl, array_flip(self::COLUMNS_WHITELIST))); } ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage( diff --git a/module/Core/src/Transformer/ShortUrlDataTransformer.php b/module/Core/src/Transformer/ShortUrlDataTransformer.php index dc9f40d5..6bd2bfa4 100644 --- a/module/Core/src/Transformer/ShortUrlDataTransformer.php +++ b/module/Core/src/Transformer/ShortUrlDataTransformer.php @@ -8,6 +8,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait; use function Functional\invoke; +use function Functional\invoke_if; class ShortUrlDataTransformer implements DataTransformerInterface { @@ -36,9 +37,23 @@ class ShortUrlDataTransformer implements DataTransformerInterface 'dateCreated' => $value->getDateCreated()->toAtomString(), 'visitsCount' => $value->getVisitsCount(), 'tags' => invoke($value->getTags(), '__toString'), + 'meta' => $this->buildMeta($value), // Deprecated 'originalUrl' => $longUrl, ]; } + + private function buildMeta(ShortUrl $shortUrl): array + { + $validSince = $shortUrl->getValidSince(); + $validUntil = $shortUrl->getValidUntil(); + $maxVisits = $shortUrl->getMaxVisits(); + + return [ + 'validSince' => invoke_if($validSince, 'toAtomString'), + 'validUntil' => invoke_if($validUntil, 'toAtomString'), + 'maxVisits' => $maxVisits, + ]; + } } diff --git a/module/Core/test/Transformer/ShortUrlDataTransformerTest.php b/module/Core/test/Transformer/ShortUrlDataTransformerTest.php new file mode 100644 index 00000000..5f3a093b --- /dev/null +++ b/module/Core/test/Transformer/ShortUrlDataTransformerTest.php @@ -0,0 +1,75 @@ +transformer = new ShortUrlDataTransformer([]); + } + + /** + * @test + * @dataProvider provideShortUrls + */ + public function properMetadataIsReturned(ShortUrl $shortUrl, array $expectedMeta): void + { + ['meta' => $meta] = $this->transformer->transform($shortUrl); + + $this->assertEquals($expectedMeta, $meta); + } + + public function provideShortUrls(): iterable + { + $maxVisits = random_int(1, 1000); + $now = Chronos::now(); + + yield 'no metadata' => [new ShortUrl('', ShortUrlMeta::createEmpty()), [ + 'validSince' => null, + 'validUntil' => null, + 'maxVisits' => null, + ]]; + yield 'max visits only' => [new ShortUrl('', ShortUrlMeta::createFromParams(null, null, null, $maxVisits)), [ + 'validSince' => null, + 'validUntil' => null, + 'maxVisits' => $maxVisits, + ]]; + yield 'max visits and valid since' => [ + new ShortUrl('', ShortUrlMeta::createFromParams($now, null, null, $maxVisits)), + [ + 'validSince' => $now->toAtomString(), + 'validUntil' => null, + 'maxVisits' => $maxVisits, + ], + ]; + yield 'both dates' => [ + new ShortUrl('', ShortUrlMeta::createFromParams($now, $now->subDays(10))), + [ + 'validSince' => $now->toAtomString(), + 'validUntil' => $now->subDays(10)->toAtomString(), + 'maxVisits' => null, + ], + ]; + yield 'everything' => [ + new ShortUrl('', ShortUrlMeta::createFromParams($now, $now->subDays(5), null, $maxVisits)), + [ + 'validSince' => $now->toAtomString(), + 'validUntil' => $now->subDays(5)->toAtomString(), + 'maxVisits' => $maxVisits, + ], + ]; + } +} diff --git a/module/Rest/test-api/Action/ListShortUrlsTest.php b/module/Rest/test-api/Action/ListShortUrlsTest.php index 5d6acc8a..77966214 100644 --- a/module/Rest/test-api/Action/ListShortUrlsTest.php +++ b/module/Rest/test-api/Action/ListShortUrlsTest.php @@ -24,6 +24,11 @@ class ListShortUrlsTest extends ApiTestCase 'dateCreated' => '2019-01-01T00:00:00+00:00', 'visitsCount' => 3, 'tags' => ['foo'], + 'meta' => [ + 'validSince' => null, + 'validUntil' => null, + 'maxVisits' => null, + ], 'originalUrl' => 'https://shlink.io', ], [ @@ -35,6 +40,11 @@ class ListShortUrlsTest extends ApiTestCase 'dateCreated' => '2019-01-01T00:00:00+00:00', 'visitsCount' => 2, 'tags' => ['foo', 'bar'], + 'meta' => [ + 'validSince' => '2020-05-01T00:00:00+00:00', + 'validUntil' => null, + 'maxVisits' => null, + ], 'originalUrl' => 'https://blog.alejandrocelaya.com/2017/12/09' . '/acmailer-7-0-the-most-important-release-in-a-long-time/', @@ -46,6 +56,11 @@ class ListShortUrlsTest extends ApiTestCase 'dateCreated' => '2019-01-01T00:00:00+00:00', 'visitsCount' => 0, 'tags' => [], + 'meta' => [ + 'validSince' => null, + 'validUntil' => null, + 'maxVisits' => 2, + ], 'originalUrl' => 'https://shlink.io', ], ], diff --git a/module/Rest/test-api/Fixtures/ShortUrlsFixture.php b/module/Rest/test-api/Fixtures/ShortUrlsFixture.php index 62c16c74..51bdaaad 100644 --- a/module/Rest/test-api/Fixtures/ShortUrlsFixture.php +++ b/module/Rest/test-api/Fixtures/ShortUrlsFixture.php @@ -24,7 +24,7 @@ class ShortUrlsFixture extends AbstractFixture $defShortUrl = $this->setShortUrlDate(new ShortUrl( 'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/', - ShortUrlMeta::createFromParams(Chronos::now()->addDays(3)) + ShortUrlMeta::createFromParams(Chronos::parse('2020-05-01')) ))->setShortCode('def456'); $manager->persist($defShortUrl);