CornyPhoenix/fipa-sl

View on GitHub
src/CornyPhoenix/Fipa/Sl/Serializer/DefaultTupleSerializer.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

namespace CornyPhoenix\Fipa\Sl\Serializer;

use CornyPhoenix\Fipa\Sl\Context\TupleContext;
use CornyPhoenix\Fipa\Sl\Exception\ParsingException;
use CornyPhoenix\Fipa\Sl\FloatTerm;
use CornyPhoenix\Fipa\Sl\GenericTuple;
use CornyPhoenix\Fipa\Sl\IntegerTerm;
use CornyPhoenix\Fipa\Sl\LiteralTerm;
use CornyPhoenix\Fipa\Sl\Registry\TupleRegistry;
use CornyPhoenix\Fipa\Sl\StringTerm;
use CornyPhoenix\Fipa\Sl\Term;
use CornyPhoenix\Fipa\Sl\Tuple;

/**
 * @package CornyPhoenix\Fipa\Sl\Context
 * @author moellers
 */
class DefaultTupleSerializer implements TupleSerializer
{

    /**
     * @var TupleContext
     */
    private $context;

    /**
     * @var TupleRegistry
     */
    private $registry;

    /**
     * DefaultTupleSerializer constructor.
     *
     * @param TupleContext $context
     * @param TupleRegistry $registry
     */
    public function __construct(TupleContext $context, TupleRegistry $registry)
    {
        $this->context = $context;
        $this->registry = $registry;
    }

    /**
     * Serializes a tuple.
     *
     * @param Term $term
     * @return string
     */
    public function serialize(Term $term)
    {
        return $term->toString($this->context);
    }

    /**
     * Unserializes data and returns a tuple.
     *
     * @param string $string
     * @param Term|null $target
     * @return Term
     * @throws ParsingException
     */
    public function unserialize($string, Term $target = null)
    {
        $string = trim($string);

        // Parse Literal terms.
        if ($target instanceof LiteralTerm) {
            return $target->fromString($string, $this->context);
        }

        // Match frame and content of tuple.
        list(, $frame, $content) = $this->matchTupleString($string);

        // Create tuple object if not exists.
        $target = $this->createTuple($target, $frame);

        if ($target instanceof Tuple) {
            // Check frame equality.
            if ($target->getFrame() !== $frame) {
                throw new ParsingException('Invalid frame found: ' . $frame);
            }

            // Parse content if not empty.
            if (!empty($content)) {
                $this->unserializeContent($target, $content);
            }
        }

        return $target;
    }

    /**
     * @param Tuple $target
     * @param string $content
     */
    private function unserializeContent(Tuple $target, $content)
    {
        $offset = 0;
        $key = null;
        while ($offset < strlen($content)) {
            $char = $content[$offset];
            switch ($char) {
                // Skip whitespaces.
                case " ":
                case "\t":
                case "\n":
                case "\r":
                case "\0":
                case "\x0B":
                    $offset++;
                    break;

                // Found a nested tuple.
                case $this->context->getOpeningDelimiter():
                    $level = 0;
                    for ($pos = $offset; $pos < strlen($content); $pos++) {
                        if ($content[$pos] === $this->context->getOpeningDelimiter()) {
                            $level++;
                        } elseif ($content[$pos] === $this->context->getClosingDelimiter()) {
                            $level--;
                        }

                        if ($level == 0) {
                            break;
                        }
                    }
                    $this->foundNestedTuple($target, $content, $offset, $pos, $key);
                    $offset = $pos + 1;
                    $key = null;
                    break;

                // Found a key.
                case $this->context->getKeyDelimiter():
                    $offset++;
                    preg_match('/(' . $this->context->getFrameRegEx() . '+)\s+/', $content, $matches, 0, $offset);
                    list($all, $key) = $matches;
                    $offset += strlen($all);
                    break;

                // Found a number.
                case "0":
                case "1":
                case "2":
                case "3":
                case "4":
                case "5":
                case "6":
                case "7":
                case "8":
                case "9":
                case "-":
                case ".":
                    if (preg_match('/(-?(\d+(\.\d*)?|\.\d+))/', $content, $matches, 0, $offset)) {
                        list(, $number) = $matches;
                        if (strpos($number, '.') !== false) {
                            $type = new FloatTerm();
                        } else {
                            $type = new IntegerTerm();
                        }
                        $type->fromString($number, $this->context);
                        $this->addTermToTuple($target, $type, $key);
                        $offset += strlen($number);
                        break;
                    }
                    $offset++;
                    break;

                // Found a string.
                case $this->context->getStringDelimiter():
                default:
                    $pos = $this->context->findEndOfString($content, $offset);
                    $this->foundNestedTuple($target, $content, $offset, $pos - 1, $key, new StringTerm());
                    $offset = $pos;
                    $key = null;
            }
        }
    }

    /**
     * @param Term $target
     * @param string $content
     * @param int $startsAt
     * @param int $endsAt
     * @param string|null $key
     * @param Term $nestedTarget
     * @throws ParsingException
     */
    private function foundNestedTuple(Term $target, $content, $startsAt, $endsAt, $key, Term $nestedTarget = null)
    {
        $nestedTupleString = substr($content, $startsAt, $endsAt - $startsAt + 1);
        $nestedTuple = $this->unserialize($nestedTupleString, $nestedTarget);
        if ($target instanceof Tuple) {
            $this->addTermToTuple($target, $nestedTuple, $key);
        }
    }

    /**
     * @param Tuple $target
     * @param Term $nestedTerm
     * @param string|null $key
     */
    private function addTermToTuple(Tuple $target, Term $nestedTerm, $key)
    {
        if (null !== $key) {
            $target->setParameter($key, $nestedTerm);
        } else {
            $target->addTerm($nestedTerm);
        }
    }

    /**
     * @param Term|null $target
     * @param string $frame
     * @return Term|Tuple
     */
    private function createTuple(Term $target = null, $frame)
    {
        if (null === $target) {
            $target = $this->registry->createTuple($frame);
            if (null === $target) {
                $target = new GenericTuple($frame);
                return $target;
            }
            return $target;
        }

        return $target;
    }

    /**
     * @param string $string
     * @return array
     * @throws ParsingException
     */
    private function matchTupleString($string)
    {
        // Get delimiter regex.
        $opening = $this->context->getOpeningDelimiterRegEx();
        $closing = $this->context->getClosingDelimiterRegEx();
        $frameRegEx = $this->context->getFrameRegEx();

        if (!preg_match(sprintf('/^%s\s*(%s+)\s+(.*)%s$/', $opening, $frameRegEx, $closing), $string, $matches)) {
            throw new ParsingException('Could not parse: ' . $string);
        }

        return $matches;
    }
}