Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - Versionable serialization #24

Merged
merged 36 commits into from
Jun 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ab5f913
Move Unserializer into Serialization namespace + add interface
e1himself Jun 13, 2019
d445810
Implement external value serialization
e1himself Jun 13, 2019
1d968ba
Drop in-model serialization in favour of external class
e1himself Jun 13, 2019
c1d0d26
Add a test for Serializer class
e1himself Jun 13, 2019
9b19097
Fix mismatched sprintf() arguments number
e1himself Jun 13, 2019
d176423
Rename methods to match Serializer naming
e1himself Jun 13, 2019
d5a5ca0
Refactor Unserialize to keep version shape check next to shape usage
e1himself Jun 13, 2019
c6737fd
Update tests to also check Inline node serialization
e1himself Jun 13, 2019
f10cbb4
Fix bug
e1himself Jun 13, 2019
ba03b06
Extract shape validation code into a reusable ShapeValidator class
e1himself Jun 13, 2019
f2a46f9
Combine Serializer and Unserializer into a single interface/class
e1himself Jun 13, 2019
8f53ebe
Move ShapeValidator into a sub-namespace
e1himself Jun 13, 2019
05d13f1
Extract Serializer code into a specific version serializer
e1himself Jun 19, 2019
8b4305b
Rename test class method for clarity
e1himself Jun 19, 2019
9c04407
Rename test files for consistency
e1himself Jun 19, 2019
877e380
Move test classes into sub-namespaces
e1himself Jun 19, 2019
99eccc6
Disconnect Document & Value model tests from serializer
e1himself Jun 19, 2019
1eec4c5
Implement a rich test for v0.40 serializer
e1himself Jun 19, 2019
8ba73c6
Add test for unserialization error
e1himself Jun 19, 2019
490bb0c
Update Serializer interface
e1himself Jun 19, 2019
9f328d2
Re-start SerializerTest
e1himself Jun 19, 2019
0ab75a9
Add a test for patch version serializer matching
e1himself Jun 19, 2019
3a31708
Test unsupported version serialization
e1himself Jun 19, 2019
08ca23c
Also check different versions deserialization
e1himself Jun 19, 2019
7d5e594
Check for latest version serialization
e1himself Jun 19, 2019
784de7a
Allow to provide default serialization version per instance
e1himself Jun 19, 2019
f58666c
Add a complete value integration test for v0.40 serializer
e1himself Jun 19, 2019
bb3ef56
Shrink EntitySerializer interface to two public methods
e1himself Jun 19, 2019
9e78ffb
Extract large hard-coded values into fixtures
e1himself Jun 19, 2019
bca2c01
Implement v0.46 serializer
e1himself Jun 20, 2019
82f284c
Test serializing to different document structures
e1himself Jun 20, 2019
3ff766e
Rename interface to better distinguish it from ValueSerializer
e1himself Jun 20, 2019
cdcc5fa
Allow to set own serialization versions map
e1himself Jun 20, 2019
66cefbd
Fix formatting
e1himself Jun 20, 2019
9f05446
Provide rich PHP doc comments to the serializer methods
e1himself Jun 20, 2019
bd30e87
Extract version serializer factory to allow customizing the behavior
e1himself Jun 20, 2019
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
12 changes: 0 additions & 12 deletions src/Model/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,4 @@ public function getText(): string
}
return $text;
}

public function jsonSerialize()
{
return (object) [
'object' => Entity::BLOCK,
'type' => $this->type,
'data' => (object) $this->data,
'nodes' => array_map(function (Entity $node) {
return $node->jsonSerialize();
}, $this->nodes)
];
}
}
11 changes: 0 additions & 11 deletions src/Model/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,4 @@ public function getText(): string
}
return $text;
}

public function jsonSerialize()
{
return (object) [
'object' => Entity::DOCUMENT,
'data' => (object) $this->data,
'nodes' => array_map(function (Entity $node) {
return $node->jsonSerialize();
}, $this->nodes),
];
}
}
2 changes: 1 addition & 1 deletion src/Model/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Prezly\Slate\Model;

interface Entity extends \JsonSerializable
interface Entity
{
const BLOCK = "block";
const DOCUMENT = "document";
Expand Down
12 changes: 0 additions & 12 deletions src/Model/Inline.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,4 @@ public function getText(): string
}
return $text;
}

public function jsonSerialize()
{
return (object) [
'object' => Entity::INLINE,
'type' => $this->type,
'data' => (object) $this->data,
'nodes' => array_map(function (Entity $node) {
return $node->jsonSerialize();
}, $this->nodes)
];
}
}
11 changes: 0 additions & 11 deletions src/Model/Leaf.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,4 @@ public function withMarks(array $marks): Leaf
{
return new self($this->text, $marks);
}

public function jsonSerialize()
{
return (object) [
'object' => Entity::LEAF,
'text' => $this->text,
'marks' => array_map(function (Mark $mark) {
return $mark->jsonSerialize();
}, $this->marks)
];
}
}
9 changes: 0 additions & 9 deletions src/Model/Mark.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,4 @@ public function withData(array $data): Mark
{
return new self($this->type, $data);
}

public function jsonSerialize()
{
return (object) [
'object' => Entity::MARK,
'type' => $this->type,
'data' => (object) $this->data,
];
}
}
10 changes: 0 additions & 10 deletions src/Model/Text.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,4 @@ public function getText(): string
}
return $text;
}

public function jsonSerialize()
{
return (object) [
'object' => Entity::TEXT,
'leaves' => array_map(function (Leaf $leaf) {
return $leaf->jsonSerialize();
}, $this->leaves)
];
}
}
13 changes: 4 additions & 9 deletions src/Model/Value.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Prezly\Slate\Model;

use Prezly\Slate\Serialization\Serializer;

class Value implements Entity
{
/** @var Document */
Expand Down Expand Up @@ -29,16 +31,9 @@ public function withDocument(Document $document): Value
return new self($document);
}

public function jsonSerialize()
{
return (object) [
'object' => Entity::VALUE,
'document' => $this->document->jsonSerialize(),
];
}

public function toJson(int $options = 0): string
{
return json_encode($this->jsonSerialize(), $options);
$serializer = new Serializer(Serializer::LATEST_SERIALIZATION_VERSION, $options);
return $serializer->toJson($this);
}
}
45 changes: 45 additions & 0 deletions src/Serialization/DefaultVersionSerializerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
namespace Prezly\Slate\Serialization;

use Prezly\Slate\Serialization\Exceptions\UnsupprotedVersionException;
use Prezly\Slate\Serialization\Versions\v0_40_VersionSerializer;
use Prezly\Slate\Serialization\Versions\v0_46_VersionSerializer;
use Prezly\Slate\Serialization\Versions\VersionSerializer;

class DefaultVersionSerializerFactory implements VersionSerializerFactory
{
private const SERIALIZATION_VERSIONS = [
'0.40' => v0_40_VersionSerializer::class,
'0.41' => v0_40_VersionSerializer::class,
'0.42' => v0_40_VersionSerializer::class,
'0.43' => v0_40_VersionSerializer::class,
'0.44' => v0_40_VersionSerializer::class,
'0.45' => v0_40_VersionSerializer::class,
// 0.46 - leaves data combined into text nodes
'0.46' => v0_46_VersionSerializer::class,
'0.47' => v0_46_VersionSerializer::class,
];

/** @var array */
private $serialization_versions;

public function __construct(array $serialization_versions = null)
{
$this->serialization_versions = $serialization_versions ?? self::SERIALIZATION_VERSIONS;
}

public function getSerializer(string $version): VersionSerializer
{
$generic_version = implode('.', array_slice(explode('.', $version), 0, 2));

if (! isset($this->serialization_versions[$generic_version])) {
throw new UnsupprotedVersionException($version);
}

$serializer_class = $this->serialization_versions[$generic_version];
/** @var \Prezly\Slate\Serialization\Versions\VersionSerializer $serializer */
$serializer = new $serializer_class();

return $serializer;
}
}
14 changes: 14 additions & 0 deletions src/Serialization/Exceptions/UnsupprotedVersionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
namespace Prezly\Slate\Serialization\Exceptions;

use InvalidArgumentException;

class UnsupprotedVersionException extends InvalidArgumentException
{
public function __construct(string $version)
{
$message = "Unsupported serialization version requested: {$version}";

parent::__construct($message, 0, null);
}
}
92 changes: 92 additions & 0 deletions src/Serialization/Serializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php
namespace Prezly\Slate\Serialization;

use Prezly\Slate\Model\Entity;
use Prezly\Slate\Model\Value;
use Prezly\Slate\Serialization\Support\ShapeValidator;
use stdClass;

class Serializer implements ValueSerializer
{
public const LATEST_SERIALIZATION_VERSION = '0.47';

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

/** @var int */
private $json_encode_options;

/** @var \Prezly\Slate\Serialization\VersionSerializerFactory */
private $factory;

/**
* @param string|null $default_version Default serialization version to use
* when serializing/unserializing with no version set.
* @param int|null $json_encode_options JSON options to use for json_encode().
* @param VersionSerializerFactory|null Factory used to get VersionSerializer for a given factory.
*/
public function __construct(
?string $default_version = self::LATEST_SERIALIZATION_VERSION,
int $json_encode_options = null,
?VersionSerializerFactory $factory = null
) {
$this->default_version = $default_version ?? self::LATEST_SERIALIZATION_VERSION;
$this->json_encode_options = $json_encode_options;
$this->factory = $factory ?? new DefaultVersionSerializerFactory();
}

/**
* Serialize value to JSON
*
* Optionally you can provide desired serialization version.
*
* If no version argument provided, default serialization version
* will be used (which is set to LATEST by default).
*
* @param \Prezly\Slate\Model\Value $value
* @param string|null $version
* @return string
*/
public function toJson(Value $value, ?string $version = null): string
{
return json_encode(
$this->serializeValue($value, $version),
$this->json_encode_options
);
}

/**
* Unserialize value from JSON
*
* Optional you can provide serialization version to use
* in case if value JSON does not have "version" property.
*
* If no version argument is given, default serialization
* version will be implied (which is set to LATEST by default).
*
* @param string $value
* @param string|null $default_version
* @return \Prezly\Slate\Model\Value
*/
public function fromJson(string $value, ?string $default_version = null): Value
{
return $this->unserializeValue(json_decode($value, false));
}

private function serializeValue(Value $value, ?string $version): stdClass
{
$version = $version ?? $this->default_version;
$object = $this->factory->getSerializer($version)->serializeValue($value);
$object->version = $version;

return $object;
}

private function unserializeValue($value, ?string $default_version = null): Value
{
$object = ShapeValidator::validateSlateObject($value, Entity::VALUE);
$version = $object->version ?? $default_version ?? $this->default_version;

return $this->factory->getSerializer($version)->unserializeValue($object);
}
}
71 changes: 71 additions & 0 deletions src/Serialization/Support/ShapeValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php
namespace Prezly\Slate\Serialization\Support;

use InvalidArgumentException;
use stdClass;

/**
* @internal Please do not use this class outside of this package.
* It's considered internal API and thus is not a subject for semantic versioning.
* The interface may change in future without major version bump.
*/
class ShapeValidator
{
/**
* @param \stdClass|mixed $object
* @param string|null $object_type
* @param callable[] $shape [ string $property_name => string $check_function, ... ]
* @return \stdClass
* @throws \InvalidArgumentException
*/
public static function validateSlateObject($object, string $object_type = null, array $shape = []): stdClass
{
// Validate it's an stdClass
if (! $object instanceof stdClass) {
throw new InvalidArgumentException(sprintf(
'Unexpected JSON value given: %s. An object is expected to construct %s.',
gettype($object),
ucfirst($object_type) ?: 'a Slate structure object'
));
}

// Validate "object" property presence
if (! property_exists($object, 'object')) {
throw new InvalidArgumentException(sprintf(
'Invalid JSON structure given to construct %s. It should have "object" property.',
ucfirst($object_type)
));
}

// Validate "object" property value
if ($object_type !== null && $object_type !== $object->object) {
throw new InvalidArgumentException(sprintf(
'Invalid JSON structure given to construct %s. It should have "object" property set to "%s".',
ucfirst($object_type),
$object_type
));
}

// Validate Shape
foreach ($shape as $property => $checker) {
if (! property_exists($object, $property)) {
throw new InvalidArgumentException(sprintf(
'Unexpected JSON structure given for %s. A %s should have "%s" property.',
ucfirst($object_type),
ucfirst($object_type),
$property
));
}
if (! $checker($object->$property)) {
throw new InvalidArgumentException(sprintf(
'Unexpected JSON structure given for %s. The "%s" property should be %s.',
ucfirst($object_type),
$property,
substr($checker, 0, 3) === 'is_' ? substr($checker, 3) : $checker
));
}
}

return $object;
}
}
11 changes: 11 additions & 0 deletions src/Serialization/ValueSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
namespace Prezly\Slate\Serialization;

use Prezly\Slate\Model\Value;

interface ValueSerializer
{
public function toJson(Value $value, ?string $version = null): string;

public function fromJson(string $value, ?string $default_version = null): Value;
}
9 changes: 9 additions & 0 deletions src/Serialization/VersionSerializerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php
namespace Prezly\Slate\Serialization;

use Prezly\Slate\Serialization\Versions\VersionSerializer;

interface VersionSerializerFactory
{
public function getSerializer(string $version): VersionSerializer;
}
Loading