Skip to content

Commit

Permalink
Merge pull request #19 from Zylius/update_tests
Browse files Browse the repository at this point in the history
Type update command now understands elasticsearch date format
  • Loading branch information
saimaz committed Nov 12, 2014
2 parents b45c270 + f1c9ef4 commit 4fdf0ce
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 1 deletion.
73 changes: 73 additions & 0 deletions Mapping/DateHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/*
* This file is part of the ONGR package.
*
* (c) NFQ Technologies UAB <info@nfq.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ONGR\ElasticsearchBundle\Mapping;

/**
* Helps to format elasticsearch time string to interval in seconds.
*/
class DateHelper
{
/**
* Parses elasticsearch type of string into milliseconds.
*
* @param string $timeString
*
* @return int
*
* @throws \InvalidArgumentException
*/
public static function parseString($timeString)
{
$results = [];
preg_match_all('/(\d+)([a-zA-Z]+)/', $timeString, $results);
$values = $results[1];
$units = $results[2];

if (count($values) != count($units) || count($values) == 0) {
throw new \InvalidArgumentException("Invalid time string '{$timeString}'.");
}

$result = 0;
foreach ($values as $key => $value) {
$result += $value * self::charToInterval($units[$key]);
}

return $result;
}

/**
* Converts a string to time interval.
*
* @param string $value
*
* @return int
*
* @throws \InvalidArgumentException
*/
private static function charToInterval($value)
{
switch($value) {
case 'w':
return 604800000;
case 'd':
return 86400000;
case 'h':
return 3600000;
case 'm':
return 60000;
case 'ms':
return 1;
default:
throw new \InvalidArgumentException("Unknown time unit '{$value}'.");
}
}
}
31 changes: 31 additions & 0 deletions Mapping/MappingTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ class MappingTool
protected $ignoredFields = [
'type' => 'object',
'_routing' => ['required' => true],
'format' => 'dateOptionalTime',
];

/**
* @var array
*/
protected $formatFields = [
'_ttl' => 'handleTime',
];

/**
Expand Down Expand Up @@ -108,11 +116,34 @@ private function arrayFilterRecursive(array $array)
unset($array[$key]);
continue;
}

if (array_key_exists($key, $this->formatFields)) {
$array[$key] = call_user_func([$this, $this->formatFields[$key]], $array[$key]);
}

if (is_array($array[$key])) {
$array[$key] = $this->arrayFilterRecursive($array[$key]);
}
}

return $array;
}

/**
* Change time formats to fit elasticsearch.
*
* @param array $value
*
* @return array
*/
private function handleTime($value)
{
if (!isset($value['default']) || !is_string($value['default'])) {
return $value;
}

$value['default'] = DateHelper::parseString($value['default']);

return $value;
}
}
7 changes: 7 additions & 0 deletions Resources/doc/mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ class content implements DocumentInterface
`type` parameter is for type name. This parame is optional, if there will be no param set Elasticsearch bundle will create type with lovercase class name. Additional params:
* **TTL (time to live)** - `_ttl={"enabled": true, "default": "1d"}` param with which you can enable documents to have time to live and set default time interval. After time runs out document deletes itself automatically.

> You can use time units specified in [elasticsearch documentation][es-time-units].
ESB parses it if needed using [DateHelper][date-helper], e.g. for type mapping update.

`DocumentTrait` includes support with all special fields in elasticsearch document such as `_id`, `_source`, `_ttl`, `_parent` handling.
`DocumentTrait` has all parameters and setters already defined for you. Once there will be _ttl set Elasticsearch bundle will handle it automatically.

Expand Down Expand Up @@ -173,3 +176,7 @@ class content implements DocumentInterface
To define object fields the same `@ES\Property` annotations could be used. In the objects there is possibility to define other objects.

> Nested types can be defined the same way as objects, except @ES\Nested annotation must be used.

[es-time-units]:http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping-ttl-field.html#_default
[date-helper]:/Mapping/DateHelper.php
200 changes: 200 additions & 0 deletions Tests/Functional/Command/TypeUpdateCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?php

/*
* This file is part of the ONGR package.
*
* (c) NFQ Technologies UAB <info@nfq.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace ONGR\ElasticsearchBundle\Tests\Functional\Command;

use ONGR\ElasticsearchBundle\Command\IndexCreateCommand;
use ONGR\ElasticsearchBundle\Command\TypeUpdateCommand;
use ONGR\ElasticsearchBundle\ORM\Manager;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Functional tests for type update command.
*/
class TypeUpdateCommandTest extends WebTestCase
{
/**
* @var string
*/
private $documentDir;

/**
* @var Manager
*/
private $manager;

/**
* @var Application
*/
private $app;

/**
* @var string
*/
private $file;

/**
* @var ContainerInterface
*/
private $container;

/**
* {@inheritdoc}
*/
public function setUp()
{
// Only a single instance of container should be used on all commands and throughout the test.
$this->container = self::createClient()->getContainer();
$this->manager = $this->container->get('es.manager');

// Set up custom document to test mapping with.
$this->documentDir = $this->container->get('kernel')->locateResource('@ONGRTestingBundle/Document/');
$this->file = $this->documentDir . 'Article.php';

// Create index for testing.
$this->app = new Application();
$this->createIndexCommand();
}

/**
* Check if update works as expected.
*/
public function testExecute()
{
$this->assertMappingNotSet("Article mapping shouldn't be defined yet.");

copy($this->documentDir . 'documentSample.txt', $this->file);

$this->runUpdateCommand();
$this->assertMappingSet('Article mapping should be defined after update.');
}

/**
* Check if updating works with type selected.
*/
public function testExecuteType()
{
$this->assertMappingNotSet("Article mapping shouldn't be defined yet.");

copy($this->documentDir . 'documentSample.txt', $this->file);

$this->runUpdateCommand('product');
$this->assertMappingNotSet("Article mapping shouldn't be defined, type selected was `product`.");

$this->runUpdateCommand('article');
$this->assertMappingSet('Article mapping should be defined after update, type selected was `article`.');
}

/**
* Check if up to date mapping check works.
*/
public function testExecuteUpdated()
{
$this->assertStringStartsWith('Types are already up to date.', $this->runUpdateCommand());
$this->assertMappingNotSet("Article was never added, type shouldn't be added.");
}

/**
* Asserts mapping is set and correct.
*
* @param string $message
*/
protected function assertMappingSet($message)
{
$mapping = $this->manager->getConnection()->getMapping('article');
$this->assertNotNull($mapping, $message);
$expectedMapping = [
'properties' => [
'title' => [
'type' => 'string',
]
]
];
$this->assertEquals($expectedMapping, $mapping);
}

/**
* Asserts mapping isn't set.
*
* @param string $message
*/
protected function assertMappingNotSet($message)
{
$this->assertNull($this->manager->getConnection()->getMapping('article'), $message);
}

/**
* Runs update command.
*
* @param string $type
*
* @return string
*/
protected function runUpdateCommand($type = '')
{
$command = new TypeUpdateCommand();
$command->setContainer($this->container);

$this->app->add($command);
$commandToTest = $this->app->find('es:type:update');
$commandTester = new CommandTester($commandToTest);

$result = $commandTester->execute(
[
'command' => $commandToTest->getName(),
'--force' => true,
'--type' => $type,
]
);

$this->assertEquals(0, $result, "Mapping update wasn't executed successfully.");

return $commandTester->getDisplay();
}

/**
* Creates index for testing.
*
* @param string $manager
*/
protected function createIndexCommand($manager = 'default')
{
$command = new IndexCreateCommand();
$command->setContainer($this->container);

$this->app->add($command);
$command = $this->app->find('es:index:create');
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'command' => $command->getName(),
'--manager' => $manager,
]
);
}

/**
* {@inheritdoc}
*/
public function tearDown()
{
try {
$this->manager->getConnection()->dropIndex();
} catch (Exception $ex) {
// Index wasn't actually created.
}
@unlink($this->file);
}
}
Loading

0 comments on commit 4fdf0ce

Please sign in to comment.