<?php declare(strict_types=1);
/**
 * Copyright © OXID eSales AG. All rights reserved.
 * See LICENSE file for license details.
 */

namespace OxidEsales\ComposerPlugin\Installer\Package;

use OxidEsales\ComposerPlugin\Utilities\CopyFileManager\CopyGlobFilteredFileManager;
use Webmozart\Glob\Iterator\GlobIterator;
use Webmozart\PathUtil\Path;

/**
 * @inheritdoc
 */
class ShopPackageInstaller extends AbstractPackageInstaller
{
    const FILE_TO_CHECK_IF_PACKAGE_INSTALLED = 'index.php';
    const SHOP_SOURCE_CONFIGURATION_FILE = 'config.inc.php';
    const DISTRIBUTION_FILE_EXTENSION_MARK = '.dist';
    const SHOP_SOURCE_DIRECTORY = 'source';
    const SHOP_SOURCE_SETUP_DIRECTORY = 'Setup';
    const HTACCESS_FILTER = '**/.htaccess';
    const ROBOTS_EXCLUSION_FILTER = '**/robots.txt';
    const SETUP_FILES_FILTER = self::SHOP_SOURCE_SETUP_DIRECTORY . DIRECTORY_SEPARATOR . AbstractPackageInstaller::BLACKLIST_ALL_FILES;

    /**
     * @return bool
     */
    public function isInstalled()
    {
        return file_exists(
            Path::join($this->getTargetDirectoryOfShopSource(), self::FILE_TO_CHECK_IF_PACKAGE_INSTALLED)
        );
    }

    /**
     * Copies all shop files from vendors to source directory.
     *
     * @param string $packagePath
     */
    public function install($packagePath)
    {
        $this->writeInstallingMessage($this->getPackageTypeDescription());
        $this->writeCopyingMessage();
        $this->copyPackage($packagePath);
        $this->writeDoneMessage();
    }

    /**
     * Overwrites files in core directories.
     *
     * @param string $packagePath
     */
    public function update($packagePath)
    {
        $this->writeUpdatingMessage($this->getPackageTypeDescription());
        $question = 'All files in the following directories will be overwritten:' . PHP_EOL .
                    '- ' . $this->getTargetDirectoryOfShopSource() . PHP_EOL .
                    'Do you want to overwrite them? (y/N) ';

        if ($this->askQuestionIfNotInstalled($question)) {
            $this->writeCopyingMessage();
            $this->copyPackage($packagePath);
            $this->writeDoneMessage();
        } else {
            $this->writeSkippedMessage();
        }
    }

    /**
     * @param string $packagePath
     */
    private function copyPackage($packagePath)
    {
        $this->copyShopSourceFromPackageToTarget($packagePath);
        $this->copySetupFiles($packagePath);
        $this->copyConfigurationDistFileWithinTarget();
        $this->copyHtaccessFiles($packagePath);
        $this->copyRobotsExclusionFiles($packagePath);
    }

    /**
     * Copy shop source files from package source to defined target path.
     *
     * @param string $packagePath
     */
    private function copyShopSourceFromPackageToTarget($packagePath)
    {
        $filtersToApply = [
            $this->getBlacklistFilterValue(),
            [self::HTACCESS_FILTER],
            [self::ROBOTS_EXCLUSION_FILTER],
            [self::SETUP_FILES_FILTER],
            $this->getVCSFilter(),
        ];

        CopyGlobFilteredFileManager::copy(
            $this->getPackageDirectoryOfShopSource($packagePath),
            $this->getTargetDirectoryOfShopSource(),
            $this->getCombinedFilters($filtersToApply)
        );
    }

    /**
     * Copy shop's configuration file from distribution file.
     */
    private function copyConfigurationDistFileWithinTarget()
    {
        $pathToConfig       = Path::join($this->getTargetDirectoryOfShopSource(), self::SHOP_SOURCE_CONFIGURATION_FILE);
        $pathToConfigDist   = $pathToConfig . self::DISTRIBUTION_FILE_EXTENSION_MARK;

        $this->copyFileIfIsMissing($pathToConfigDist, $pathToConfig);
    }

    /**
     * Copy shop's htaccess files from package.
     *
     * @param string $packagePath Absolute path which points to shop's package directory.
     */
    private function copyHtaccessFiles($packagePath)
    {
        $this->copyFilesFromSourceToInstallationByFilter(
            $packagePath,
            self::HTACCESS_FILTER
        );
    }

    /**
     * Copy shop's robots exclusion files from package.
     *
     * @param string $packagePath Absolute path which points to shop's package directory.
     */
    private function copyRobotsExclusionFiles($packagePath)
    {
        $this->copyFilesFromSourceToInstallationByFilter(
            $packagePath,
            self::ROBOTS_EXCLUSION_FILTER
        );
    }

    /**
     * Copy shop's setup files from package.
     *
     * @param string $packagePath Absolute path which points to shop's package directory.
     */
    private function copySetupFiles($packagePath)
    {
        $packageDirectoryOfShopSource = $this->getPackageDirectoryOfShopSource($packagePath);
        $installationDirectoryOfShopSource = $this->getTargetDirectoryOfShopSource();

        $shopConfigFileName = Path::join($installationDirectoryOfShopSource, self::SHOP_SOURCE_CONFIGURATION_FILE);

        if ($this->isConfigFileNotConfiguredOrMissing($shopConfigFileName)) {
            CopyGlobFilteredFileManager::copy(
                Path::join($packageDirectoryOfShopSource, self::SHOP_SOURCE_SETUP_DIRECTORY),
                Path::join($installationDirectoryOfShopSource, self::SHOP_SOURCE_SETUP_DIRECTORY)
            );
        }
    }

    /**
     * Return true if config file is not configured or missing.
     *
     * @param string $shopConfigFileName Absolute path to shop configuration file to check.
     *
     * @return bool
     */
    private function isConfigFileNotConfiguredOrMissing($shopConfigFileName)
    {
        if (!file_exists($shopConfigFileName)) {
            return true;
        }

        $shopConfigFileContents = file_get_contents($shopConfigFileName);
        $wordsIndicatingNotConfigured = [
            '<dbHost>',
            '<dbName>',
            '<dbUser>',
            '<dbPwd>',
            '<sShopURL>',
            '<sShopDir>',
            '<sCompileDir>',
        ];

        foreach ($wordsIndicatingNotConfigured as $word) {
            if (strpos($shopConfigFileContents, $word) !== false) {
                return true;
            }
        }

        return false;
    }

    /**
     * Return package directory which points to shop's source directory.
     *
     * @param string $packagePath Absolute path which points to shop's package directory.
     *
     * @return string
     */
    private function getPackageDirectoryOfShopSource($packagePath)
    {
        return Path::join($packagePath, self::SHOP_SOURCE_DIRECTORY);
    }

    /**
     * Return target directory where shop's source files needs to be copied.
     *
     * @return string
     */
    private function getTargetDirectoryOfShopSource()
    {
        return $this->getRootDirectory();
    }

    /**
     * Copy files from source to installation by filter.
     *
     * @param string $packagePath
     * @param string $filter
     */
    private function copyFilesFromSourceToInstallationByFilter($packagePath, $filter)
    {
        $sourceDirectory    = $this->getPackageDirectoryOfShopSource($packagePath);
        $filteredFiles      = $this->getFilteredFiles($sourceDirectory, $filter);

        foreach ($filteredFiles as $packageFilePath) {
            $installationFilePath = $this->getAbsoluteFilePathFromInstallation(
                $sourceDirectory,
                $packageFilePath
            );

            $this->copyFileIfIsMissing($packageFilePath, $installationFilePath);
        }
    }

    /**
     * Copy file if is missing.
     *
     * @param string $sourcePath
     * @param string $destinationPath
     */
    private function copyFileIfIsMissing($sourcePath, $destinationPath)
    {
        if (!file_exists($destinationPath)) {
            CopyGlobFilteredFileManager::copy(
                $sourcePath,
                $destinationPath
            );
        }
    }

    /**
     * Return filtered files.
     *
     * @param   string $directory
     * @param   string $filter
     *
     * @return  GlobIterator
     */
    private function getFilteredFiles($directory, $filter)
    {
        $glob = Path::join($directory, $filter);

        return new GlobIterator($glob);
    }

    /**
     * Return absolute path to file from installation.
     *
     * @param   string  $sourcePackageDirectory
     * @param   string  $absolutePathToFileFromPackage
     *
     * @return  string
     */
    private function getAbsoluteFilePathFromInstallation(
        $sourcePackageDirectory,
        $absolutePathToFileFromPackage
    ) {
        $installationDirectoryOfShopSource = $this->getTargetDirectoryOfShopSource();

        $relativePathOfSourceFromPackage = Path::makeRelative(
            $absolutePathToFileFromPackage,
            $sourcePackageDirectory
        );

        $absolutePathToFileFromInstallation = Path::join(
            $installationDirectoryOfShopSource,
            $relativePathOfSourceFromPackage
        );

        return $absolutePathToFileFromInstallation;
    }

    /**
     * @return string
     */
    private function getPackageTypeDescription(): string
    {
        return 'OXID eShop package';
    }
}


<?php declare(strict_types=1);
/**
 * Copyright © OXID eSales AG. All rights reserved.
 * See LICENSE file for license details.
 */

namespace OxidEsales\ComposerPlugin\Installer\Package;

use Composer\Package\PackageInterface;
use OxidEsales\ComposerPlugin\Utilities\CopyFileManager\CopyGlobFilteredFileManager;
use Webmozart\PathUtil\Path;

/**
 * @inheritdoc
 */
class ThemePackageInstaller extends AbstractPackageInstaller
{
    const METADATA_FILE_NAME = 'theme.php';
    const PATH_TO_THEMES = "Application/views";

    /**
     * @return bool
     */
    public function isInstalled()
    {
        return file_exists($this->formThemeTargetPath().'/'.static::METADATA_FILE_NAME);
    }

    /**
     * Copies theme files to shop directory.
     *
     * @param string $packagePath
     */
    public function install($packagePath)
    {
        $this->writeInstallingMessage($this->getPackageTypeDescription());
        $this->writeCopyingMessage();
        $this->copyPackage($packagePath);
        $this->writeDoneMessage();
    }

    /**
     * Overwrites theme files.
     *
     * @param string $packagePath
     */
    public function update($packagePath)
    {
        $this->writeUpdatingMessage($this->getPackageTypeDescription());
        $question = 'All files in the following directories will be overwritten:' . PHP_EOL .
                    '- ' . $this->formThemeTargetPath() . PHP_EOL .
                    '- ' . Path::join($this->getRootDirectory(), $this->formAssetsDirectoryName()) . PHP_EOL .
                    'Do you want to overwrite them? (y/N) ';

        if ($this->askQuestionIfNotInstalled($question)) {
            $this->writeCopyingMessage();
            $this->copyPackage($packagePath);
            $this->writeDoneMessage();
        } else {
            $this->writeSkippedMessage();
        }
    }

    /**
     * @param string $packagePath
     */
    protected function copyPackage($packagePath)
    {
        $filtersToApply = [
            [Path::join($this->formAssetsDirectoryName(), AbstractPackageInstaller::BLACKLIST_ALL_FILES)],
            $this->getBlacklistFilterValue(),
            $this->getVCSFilter(),
        ];

        CopyGlobFilteredFileManager::copy(
            $packagePath,
            $this->formThemeTargetPath(),
            $this->getCombinedFilters($filtersToApply)
        );

        $this->installAssets($packagePath);
    }

    /**
     * @return string
     */
    protected function formThemeTargetPath()
    {
        $package = $this->getPackage();
        $themeDirectoryName = $this->formThemeDirectoryName($package);
        return "{$this->getRootDirectory()}/" . static::PATH_TO_THEMES . "/$themeDirectoryName";
    }

    /**
     * @param string $packagePath
     */
    protected function installAssets($packagePath)
    {
        $package = $this->getPackage();
        $target = $this->getRootDirectory() . '/out/' . $this->formThemeDirectoryName($package);

        $assetsDirectory = $this->formAssetsDirectoryName();
        $source = $packagePath . '/' . $assetsDirectory;

        if (file_exists($source)) {
            CopyGlobFilteredFileManager::copy(
                $source,
                $target,
                $this->getBlacklistFilterValue()
            );
        }
    }

    /**
     * @param PackageInterface $package
     * @return string
     */
    protected function formThemeDirectoryName($package)
    {
        $themePath = $this->getExtraParameterValueByKey(static::EXTRA_PARAMETER_KEY_TARGET);
        if (is_null($themePath)) {
            $themePath = explode('/', $package->getName())[1];
        }
        return $themePath;
    }

    /**
     * @return null|string
     */
    protected function formAssetsDirectoryName()
    {
        $assetsDirectory = $this->getExtraParameterValueByKey(static::EXTRA_PARAMETER_KEY_ASSETS);
        if (is_null($assetsDirectory)) {
            $assetsDirectory = 'out';
        }
        return $assetsDirectory;
    }

    /**
     * @return string
     */
    protected function getPackageTypeDescription(): string
    {
        return 'theme package';
    }
}


<?php declare(strict_types=1);
/**
 * Copyright © OXID eSales AG. All rights reserved.
 * See LICENSE file for license details.
 */

namespace OxidEsales\ComposerPlugin\Installer\Package;

use OxidEsales\ComposerPlugin\Utilities\CopyFileManager\CopyGlobFilteredFileManager;
use Webmozart\PathUtil\Path;

/**
 * @inheritdoc
 */
class ModulePackageInstaller extends AbstractPackageInstaller
{
    const MODULES_DIRECTORY = 'modules';

    /**
     * @return bool
     */
    public function isInstalled()
    {
        return file_exists($this->formTargetPath());
    }

    /**
     * Copies module files to shop directory.
     *
     * @param string $packagePath
     */
    public function install($packagePath)
    {
        $this->writeInstallingMessage($this->getPackageTypeDescription());
        $this->writeCopyingMessage();
        $this->copyPackage($packagePath);
        $this->writeDoneMessage();
    }

    /**
     * Update module files.
     *
     * @param string $packagePath
     */
    public function update($packagePath)
    {
        $this->writeUpdatingMessage($this->getPackageTypeDescription());
        $question = 'All files in the following directories will be overwritten:' . PHP_EOL .
                    '- ' . $this->formTargetPath() . PHP_EOL .
                    'Do you want to overwrite them? (y/N) ';

        if ($this->askQuestionIfNotInstalled($question)) {
            $this->writeCopyingMessage();
            $this->copyPackage($packagePath);
            $this->writeDoneMessage();
        } else {
            $this->writeSkippedMessage();
        }
    }

    /**
     * Copy files from package source to defined target path.
     *
     * @param string $packagePath Absolute path to the package.
     */
    protected function copyPackage($packagePath)
    {
        $filtersToApply = [
            $this->getBlacklistFilterValue(),
            $this->getVCSFilter(),
        ];

        CopyGlobFilteredFileManager::copy(
            $this->formSourcePath($packagePath),
            $this->formTargetPath(),
            $this->getCombinedFilters($filtersToApply)
        );
    }

    /**
     * If module source directory option provided add it's relative path.
     * Otherwise return plain package path.
     *
     * @param string $packagePath
     *
     * @return string
     */
    protected function formSourcePath($packagePath)
    {
        $sourceDirectory = $this->getExtraParameterValueByKey(static::EXTRA_PARAMETER_KEY_SOURCE);

        return !empty($sourceDirectory)?
            Path::join($packagePath, $sourceDirectory):
            $packagePath;
    }

    /**
     * @return string
     */
    protected function formTargetPath()
    {
        $targetDirectory = $this->getExtraParameterValueByKey(
            static::EXTRA_PARAMETER_KEY_TARGET,
            $this->getPackage()->getName()
        );

        return Path::join($this->getRootDirectory(), static::MODULES_DIRECTORY, $targetDirectory);
    }

    /**
     * @return string
     */
    private function getPackageTypeDescription(): string
    {
        return 'module package';
    }
}


<?php declare(strict_types=1);
/**
 * Copyright © OXID eSales AG. All rights reserved.
 * See LICENSE file for license details.
 */

namespace OxidEsales\ComposerPlugin\Utilities;

/**
 * Class VfsFileStructureOperator.
 */
class VfsFileStructureOperator
{
    /**
     * Convert given flat file system structure into nested one.
     *
     * @param array|null $flatFileSystemStructure
     *
     * @return array
     */
    public static function nest($flatFileSystemStructure = null)
    {
        if (null !== $flatFileSystemStructure && false === is_array($flatFileSystemStructure)) {
            throw new \InvalidArgumentException("Given input argument must be an array.");
        }

        if (null === $flatFileSystemStructure) {
            return [];
        }

        $nestedFileSystemStructure = [];

        foreach ($flatFileSystemStructure as $pathEntry => $contents) {
            $pathEntries = explode(DIRECTORY_SEPARATOR, $pathEntry);

            $pointerToBranch = &$nestedFileSystemStructure;
            foreach ($pathEntries as $singlePathEntry) {
                $singlePathEntry = trim($singlePathEntry);

                if ($singlePathEntry !== '') {
                    if (!is_array($pointerToBranch)) {
                        $pointerToBranch = [];
                    }

                    if (!key_exists($singlePathEntry, $pointerToBranch)) {
                        $pointerToBranch[$singlePathEntry] = [];
                    }

                    $pointerToBranch = &$pointerToBranch[$singlePathEntry];
                }
            }

            if (substr($pathEntry, -1) !== DIRECTORY_SEPARATOR) {
                $pointerToBranch = $contents;
            }
        }

        return $nestedFileSystemStructure;
    }
}


<?php declare(strict_types=1);
/**
 * Copyright © OXID eSales AG. All rights reserved.
 * See LICENSE file for license details.
 */

namespace OxidEsales\ComposerPlugin\Utilities\CopyFileManager;

use OxidEsales\ComposerPlugin\Utilities\CopyFileManager\GlobMatcher\GlobMatcher;
use OxidEsales\ComposerPlugin\Utilities\CopyFileManager\GlobMatcher\Iteration\BlacklistFilterIterator;
use Symfony\Component\Filesystem\Filesystem;
use Webmozart\PathUtil\Path;

/**
 * Class CopyGlobFilteredFileManager.
 *
 * Copies files/directories from source to destination which matches the criteria described in a glob filter.
 */
class CopyGlobFilteredFileManager
{
    /**
     * Copy files/directories from source to destination.
     *
     * @param string $sourcePath         Absolute path to file or directory.
     * @param string $destinationPath    Absolute path to file or directory.
     * @param array  $globExpressionList List of glob expressions, e.g. ["*.txt", "*.pdf"].
     *
     * @throws \InvalidArgumentException If given $sourcePath is not a string.
     * @throws \InvalidArgumentException If given $destinationPath is not a string.
     *
     * @return null
     */
    public static function copy($sourcePath, $destinationPath, $globExpressionList = [])
    {
        if (!is_string($sourcePath)) {
            $message = "Given value \"$sourcePath\" is not a valid source path entry. ".
                "Valid entry must be an absolute path to an existing file or directory.";

            throw new \InvalidArgumentException($message);
        }

        if (!is_string($destinationPath)) {
            $message = "Given value \"$destinationPath\" is not a valid destination path entry. ".
                "Valid entry must be an absolute path to an existing directory.";

            throw new \InvalidArgumentException($message);
        }

        if (!file_exists($sourcePath)) {
            return;
        }

        if (is_dir($sourcePath)) {
            self::copyDirectory($sourcePath, $destinationPath, $globExpressionList);
        } else {
            self::copyFile($sourcePath, $destinationPath, $globExpressionList);
        }
    }

    /**
     * Returns relative path from an absolute path to a file.
     *
     * @param string $sourcePath Absolute path to a file.
     *
     * @return string
     */
    private static function getRelativePathForSingleFile($sourcePath)
    {
        return Path::makeRelative($sourcePath, Path::getDirectory($sourcePath));
    }

    /**
     * Return an iterator which iterates through a given directory tree in a one-dimensional fashion.
     *
     * Consider the following file/directory structure as an example:
     *
     *   * directory_a
     *     * file_a_a
     *   * directory_b
     *     * file_b_a
     *     * file_b_b
     *   * file_c
     *
     * RecursiveDirectoryIterator would iterate through:
     *   * directory_a [iterator]
     *   * directory_b [iterator]
     *   * file_c [SplFileInfo]
     *
     * In contrast current method would iterate through:
     *   * directory_a [SplFileInfo]
     *   * directory_a/file_a_a [SplFileInfo]
     *   * directory_b [SplFileInfo]
     *   * directory_b/file_b_a [SplFileInfo]
     *   * directory_b/file_b_b [SplFileInfo]
     *   * file_c [SplFileInfo]
     *
     * @param string $sourcePath Absolute path to directory.
     *
     * @return \Iterator
     */
    private static function getFlatFileListIterator($sourcePath)
    {
        $recursiveFileIterator = new \RecursiveDirectoryIterator($sourcePath, \FilesystemIterator::SKIP_DOTS);
        $flatFileListIterator = new \RecursiveIteratorIterator($recursiveFileIterator);

        return $flatFileListIterator;
    }

    /**
     * Copy whole directory using given glob filters.
     *
     * @param string $sourcePath         Absolute path to directory.
     * @param string $destinationPath    Absolute path to directory.
     * @param array  $globExpressionList List of glob expressions, e.g. ["*.txt", "*.pdf"].
     */
    private static function copyDirectory($sourcePath, $destinationPath, $globExpressionList)
    {
        $filesystem = new Filesystem();

        $flatFileListIterator = self::getFlatFileListIterator($sourcePath);
        $filteredFileListIterator = new BlacklistFilterIterator(
            $flatFileListIterator,
            $sourcePath,
            $globExpressionList
        );

        $filesystem->mirror($sourcePath, $destinationPath, $filteredFileListIterator, ["override" => true]);
    }

    /**
     * Copy file using given glob filters.
     *
     * @param string $sourcePathOfFile   Absolute path to file.
     * @param string $destinationPath    Absolute path to directory.
     * @param array  $globExpressionList List of glob expressions, e.g. ["*.txt", "*.pdf"].
     */
    private static function copyFile($sourcePathOfFile, $destinationPath, $globExpressionList)
    {
        $filesystem = new Filesystem();

        $relativeSourcePath = self::getRelativePathForSingleFile($sourcePathOfFile);

        if (!GlobMatcher::matchAny($relativeSourcePath, $globExpressionList)) {
            $filesystem->copy($sourcePathOfFile, $destinationPath, ["override" => true]);
        }
    }
}


<?php
/**
 * @package yii2-schema-org
 * @author Simon Karlen <simi.albi@gmail.com>
 */

namespace simialbi\yii2\schemaorg\composer;

use Composer\Script\Event;
use Yii;
use yii\helpers\ArrayHelper;

class Installer
{
    /**
     * Special method to run tasks defined in `[extra][yii\composer\Installer::postInstallUpdate]` key in `composer.json`
     *
     * @param Event $event
     * @since 2.0.5
     */
    public static function postInstallUpdate($event)
    {
        static::runCommands($event, __METHOD__);
    }

    /**
     * Special method to run tasks defined in `[extra][$extraKey]` key in `composer.json`
     *
     * @param Event $event
     * @param string $extraKey
     */
    protected static function runCommands($event, $extraKey)
    {
        $params = $event->getComposer()->getPackage()->getExtra();
        if (isset($params[$extraKey]) && is_array($params[$extraKey])) {
            foreach ($params[$extraKey] as $method => $args) {
                $args = array_merge([
                    'vendorPath' => $event->getComposer()->getConfig()->get('vendor-dir')
                ], (array)$args);
                call_user_func([__CLASS__, $method], $args);
            }
        }
    }

    /**
     * Run model generation action
     *
     * @param array $config Configuration for model generation
     *
     * @throws \yii\base\InvalidConfigException
     * @throws \yii\console\Exception
     * @throws \yii\base\InvalidRouteException
     */
    public static function generateModels(array $config)
    {
        $schemas = ArrayHelper::getValue($config, 'schemas', []);
        $namespace = ArrayHelper::getValue($config, 'namespace', 'simialbi\yii2\schemaorg\models');
        $folder = ArrayHelper::getValue($config, 'folder', sprintf(
            '%1$s%2$ssimialbi%2$syii2-schema-org%2$ssrc%2$smodels',
            $config['vendorPath'],
            DIRECTORY_SEPARATOR
        ));
        $removeOld = ArrayHelper::getValue($config, 'removeOld', true);

        require_once $config['vendorPath'] . DIRECTORY_SEPARATOR . 'autoload.php';
        require_once $config['vendorPath'] . DIRECTORY_SEPARATOR . 'yiisoft' . DIRECTORY_SEPARATOR . 'yii2' . DIRECTORY_SEPARATOR . 'Yii.php';

        Yii::setAlias('@runtime', rtrim(str_replace(['\\', '/'], '/', sys_get_temp_dir()), '/'));

        new \yii\console\Application([
            'id' => 'Commmand runner',
            'basePath' => __DIR__,
            'vendorPath' => $config['vendorPath'],
            'bootstrap' => ['schema'],
            'aliases' => [],
            'modules' => [
                'schema' => [
                    'class' => 'simialbi\yii2\schemaorg\Module'
                ]
            ]
        ]);
        $options = [
            'namespace' => $namespace,
            'folder' => $folder,
            'removeOld' => $removeOld
        ];
        if (!empty($schemas)) {
            $options['schemas'] = implode(',', $schemas);
        }
        Yii::$app->runAction('schema/models/generate', $options);

        Yii::$app = null;
        unset($app);
    }
}


<?php
/**
 * @link https://github.com/simialbi/yii2-schema-org
 * @copyright Copyright (c) 2017 Simon Karlen
 * @license MIT
 */

namespace simialbi\yii2\schemaorg\commands;

use Yii;
use yii\console\Controller;
use yii\console\ExitCode;
use yii\helpers\ArrayHelper;
use yii\helpers\Console;
use yii\helpers\FileHelper;
use yii\helpers\Json;

/**
 * Class SchemaOrgController
 *
 * @package simialbi\yii2\schemaorg\commands
 * @author Simon Karlen <simi.albi@outlook.com>
 * @author Mehdi Achour <machour@gmail.com>
 *
 * @property \simialbi\yii2\schemaorg\Module $module
 */
class ModelsController extends Controller
{
    /**
     * The schemas definition file url pattern
     */
    const DEFINITION_FILE = 'http://schema.org/version/%s/all-layers.jsonld';

    /**
     * {@inheritdoc}
     */
    public $defaultAction = 'generate';

    /**
     * @var array The schemas requested by the user
     */
    public $schemas = [];

    /**
     * @var string The namespace for generated classes and traits
     */
    public $namespace = 'simialbi\yii2\schemaorg\models';

    /**
     * @var string The target folder for generated classes and traits
     */
    public $folder = '@simialbi/yii2/schemaorg/models';

    /**
     * @var boolean Wether to remove old files before re-generating
     */
    public $removeOld = false;

    /**
     * @var array Classes not to generate
     */
    public $blackList = ['Class', 'Boolean', 'Date', 'DateTime', 'Float', 'Number', 'Text', 'Time'];

    /**
     * Generates the user-requested schemas along with all required traits
     *
     * @param string $version The Schema.org version to use when generating files
     * @return int The exit code
     * @throws \yii\base\Exception
     */
    public function actionGenerate($version = 'latest')
    {
        if (empty($this->namespace)) {
            $this->stderr('You must specify a namespace for the generated files using --namespace', Console::FG_RED);
            $this->stderr("\n");
            return ExitCode::CONFIG;
        }

        if (empty($this->folder)) {
            $this->stderr('You must specify a folder for the generated files using --folder', Console::FG_RED);
            $this->stderr("\n");
            return ExitCode::CONFIG;
        }

        if ($this->schemas) {
            $this->stdout('Generating schema.org models "');
            $this->stdout(implode(',', $this->schemas), Console::FG_YELLOW, Console::BOLD);
            $this->stdout('" from version: ');
        } else {
            $this->stdout('Generating ');
            $this->stdout('all', Console::FG_YELLOW, Console::BOLD);
            $this->stdout(' schema.org models from version: ');
        }
        $this->stdout($version, Console::FG_GREEN, Console::ITALIC);
        $this->stdout("\n");

        $this->namespace = Yii::getAlias($this->namespace);
        $this->folder = Yii::getAlias($this->folder);

        $this->stdout("Creating directories \n");
        FileHelper::createDirectory($this->folder, 0777, true);
        FileHelper::createDirectory(Yii::getAlias('@runtime/cache'), 0777);

        if ($this->removeOld) {
            $files = FileHelper::findFiles($this->folder, ['except' => ['Model.php']]);
            $this->stdout('Clearing old files: found ' . count($files), Console::FG_YELLOW);
            $this->stdout("\n");
            foreach ($files as $file) {
                FileHelper::unlink($file);
            }
        }

        $file = Yii::getAlias("@runtime/cache/schemas-$version.json");
        if (!file_exists($file) || $version === 'latest') {
            $url = sprintf(self::DEFINITION_FILE, $version);
            copy($url, $file);
        }

        $json = Json::decode(file_get_contents($file));
        /* Old version
        $schemas = ArrayHelper::remove($json, '@graph', []);

        $data = [];
        foreach ($schemas as $schema) {
//            $id = ArrayHelper::remove($schema, '@id');
            $graphs = ArrayHelper::remove($schema, '@graph');
            foreach ($graphs as $k => $graph) {
                if (!isset($graph['@type'])) {
                    unset($graphs[$k]);
                    continue;
                }
                if (is_array($graph['@type'])) {
                    $graphs[$k]['@type'] = implode(',', $graph['@type']);
                }
            }
            $data = ArrayHelper::merge($data, ArrayHelper::index($graphs, null, ['@type']));
        }
        /*/ // @since 3.5
        $graphs = ArrayHelper::remove($json, '@graph', []);

        $data = [];
        foreach ($graphs as $k => $graph) {
            if (!isset($graph['@type'])) {
                unset($graphs[$k]);
                continue;
            }
            if (is_array($graph['@type'])) {
                $graphs[$k]['@type'] = implode(',', $graph['@type']);
            }
        }
        $data = ArrayHelper::merge($data, ArrayHelper::index($graphs, null, ['@type']));
        //*/

        $properties = ArrayHelper::remove($data, 'rdf:Property');
        $classes = ArrayHelper::index(ArrayHelper::remove($data, 'rdfs:Class'), '@id');

        foreach ($properties as $property) {
            $label = ArrayHelper::remove($property, 'rdfs:label', '');
            if (is_array($label) && isset($label['@value'])) {
                $label = $label['@value'];
            }
            if (empty($label)) {
                continue;
            }
            $classUsages = ArrayHelper::remove($property, 'http://schema.org/domainIncludes', []);
            $rangeIncludes = ArrayHelper::remove($property, 'http://schema.org/rangeIncludes');
            $comment = ArrayHelper::remove($property, 'rdfs:comment', '');
            $see = ArrayHelper::remove($property, '@id');
            if ($rangeIncludes === null) {
                continue;
            }
            if (ArrayHelper::isAssociative($rangeIncludes)) {
                $rangeIncludes = [$rangeIncludes];
            }
            foreach ($classUsages as $className) {
                if (is_array($className)) {
                    $className = ArrayHelper::remove($className, '@id', $className);
                }
                ArrayHelper::setValue($classes, [$className, 'properties'], array_merge(
                    ArrayHelper::getValue($classes, [$className, 'properties'], []),
                    [
                        [
                            'name' => $label,
                            'types' => array_flip(array_flip(array_map(
                                [$this, 'mapType'],
                                ArrayHelper::getColumn(
                                    $rangeIncludes,
                                    '@id'
                                )
                            ))),
                            'description' => preg_replace('#<br ?/?>#i', '', $comment),
                            'see' => $see
                        ]
                    ]
                ));
            }
        }

        $this->traverseClasses($classes);

        if (!empty($this->schemas)) {
            foreach ($classes as $key => $class) {
                unset($classes[$key]);
                $key = str_replace('http://schema.org/', '', $key);
                $classes[$key] = $class;
            }
            $classes = ArrayHelper::filter($classes, $this->schemas);
        }

        $this->stdout("\n");
        foreach ($classes as $class) {
            $label = ArrayHelper::remove($class, 'rdfs:label', '');
            if (is_array($label) && isset($label['@value'])) {
                $label = $label['@value'];
            }
            if (empty($label) || false !== in_array($label, $this->blackList)) {
                continue;
            }
            // e.g. https://schema.org/3DModel generates as className "3DModel" what's not allowed in PHP
            $className = (preg_match('#^[0-9]#', $label)) ? 'Model' . $label : $label;

            $this->stdout('*** creating class ');
            $this->stdout($this->namespace . '\\' . $className, Console::FG_YELLOW, Console::BOLD);
            $this->stdout("\n");

            $contents = $this->renderPartial('class', [
                'namespace' => $this->namespace,
                'description' => preg_replace(
                    '#<br ?/?>#i',
                    '',
                    ArrayHelper::getValue($class, 'rdfs:comment', '')
                ),
                'className' => $className,
                'label' => $label,
                'properties' => ArrayHelper::index(ArrayHelper::getValue($class, 'properties', []), 'name')
            ]);
            if (false !== file_put_contents($this->folder . '/' . $className . '.php', $contents)) {
                $this->stdout("*** class {$this->namespace}\\$className created", Console::FG_GREEN);
                $this->stdout("\n\n");
            } else {
                $this->stderr("*** failed to create {$this->namespace}\\$className", Console::FG_RED);
                $this->stderr("\n\n");
            }
        }

        return ExitCode::OK;
    }

    /**
     * {@inheritdoc}
     */
    public function options($actionID)
    {
        $options = parent::options($actionID);

        if ($actionID === 'generate') {
            $options[] = 'schemas';
            $options[] = 'namespace';
            $options[] = 'folder';
            $options[] = 'removeOld';
            $options[] = 'blackList';
        }

        return $options;
    }

    /**
     * Get data type
     * @param string $type
     * @return string
     */
    private function mapType($type)
    {
        $type = str_replace('http://schema.org/', '', $type);

        switch ($type) {
            case 'URL':
            case 'Text':
            case 'Date':
            case 'DateTime':
            case 'Time':
                return 'string';
            case 'Number':
                return 'float';
            case 'Integer':
                return 'int';
            case 'Boolean':
            case 'True':
            case 'False':
                return 'bool';
        }

        return $type;
    }

    /**
     * Traverse all classes and inherit properties from parents
     *
     * @param array $classes
     * @param array $allClasses
     */
    private function traverseClasses(array &$classes, $allClasses = null)
    {
        if ($allClasses === null) {
            $allClasses = $classes;
        }

        foreach ($classes as &$class) {
            $parentClasses = ArrayHelper::remove($class, 'rdfs:subClassOf', []);
            if (ArrayHelper::isAssociative($parentClasses)) {
                $parentClasses = [$parentClasses];
            }

            foreach ($parentClasses as $k => $parentClass) {
                $parentClasses[$k] = ArrayHelper::getValue($allClasses, $parentClass['@id']);
                if ($parentClass === null) {
                    continue;
                }
            }

            $this->traverseClasses($parentClasses, $allClasses);

            foreach ($parentClasses as $parentClass) {
                ArrayHelper::setValue($class, 'properties', ArrayHelper::merge(
                    ArrayHelper::getValue($class, 'properties', []),
                    ArrayHelper::getValue($parentClass, 'properties', [])
                ));
            }
        }
    }
}


<?php
/**
 * @link https://github.com/simialbi/yii2-schema-org
 * @copyright Copyright (c) 2017 Simon Karlen
 * @license MIT
 */

namespace simialbi\yii2\schemaorg\models;

use yii\helpers\StringHelper;

/**
 * Class Model
 * @package simialbi\yii2\schemaorg\models
 * @author Simon Karlen <simi.albi@outlook.com>
 */
class Model extends \yii\base\Model
{
    /**
     * @var string identifier of this model (rendered as @id)
     */
    public $id;

    /**
     * Converts the model into an array for usage in ld+json.
     *
     * This method will first identify which fields to be included in the resulting array by calling [[resolveFields()]].
     * It will then turn the model into an array with these fields. If `$recursive` is true,
     * any embedded objects will also be converted into arrays.
     *
     * If the model implements the [[Linkable]] interface, the resulting array will also have a `_link` element
     * which refers to a list of links as specified by the interface.
     *
     * @param array $fields the fields being requested. If empty, all fields as specified by [[fields()]] will be returned.
     * @param array $expand the additional fields being requested for exporting. Only fields declared in [[extraFields()]]
     * will be considered.
     * @param bool $recursive whether to recursively return array representation of embedded objects.
     *
     * @return array the array representation of the object
     */
    public function toJsonLDArray($fields = [], $expand = [], $recursive = true)
    {
        return array_merge([
            '@context' => 'http://schema.org'
        ], $this->toArray($fields, $expand, $recursive));
    }

    /**
     * @inheritdoc
     */
    public function toArray(array $fields = [], array $expand = [], $recursive = true)
    {
        $array = array_filter(parent::toArray($fields, $expand, $recursive), function ($item) {
            return !is_null($item);
        });
        $keys = array_keys($array);
        $values = array_values($array);

        foreach ($keys as &$key) {
            $key = str_replace('_', '-', $key);
        }

        return array_combine($keys, $values);
    }

    /**
     * @inheritdoc
     */
    public function fields()
    {
        $fields = array_merge(
            parent::fields(),
            [
                '@id' => 'id',
                '@type' => function () {
                    return StringHelper::basename(static::class);
                }
            ]
        );

        unset($fields['id']);

        return $fields;
    }
}


<?php
/**
 * @link https://github.com/nirvana-msu/yii2-jsonld-helper
 * @link https://github.com/simialbi/yii2-schema-org
 * @copyright Copyright (c) 2016 Alexander Stepanov
 * @copyright Copyright (c) 2017 Simon Karlen
 * @license MIT
 */

namespace simialbi\yii2\schemaorg\helpers;

use Yii;
use yii\base\InvalidArgumentException;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\helpers\Json;
use yii\helpers\Url;

/**
 * Helper class for registering structured data markup in JSON-LD format
 *
 * @author Alexander Stepanov <student_vmk@mail.ru>
 * @author Simon Karlen <simi.albi@outlook.com>
 * @author Mehdi Achour <machour@gmail.com>
 */
class JsonLDHelper
{
    /**
     * @var \simialbi\yii2\schemaorg\models\Model[] Keeps schema org models wich will be displayed on page
     */
    private static $_models = [];

    /**
     * Adds BreadcrumbList schema.org markup based on the application view `breadcrumbs` parameter
     * @throws \yii\base\InvalidConfigException
     */
    public static function addBreadCrumbList()
    {
        if (!class_exists('\simialbi\yii2\schemaorg\models\BreadcrumbList')) {
            return;
        }

        $view = Yii::$app->view;

        /* @var $breadcrumbList \simialbi\yii2\schemaorg\models\Model */
        $breadcrumbs = ArrayHelper::getValue($view->params, 'breadcrumbs', []);
        $breadcrumbList = Yii::createObject([
            'class' => 'simialbi\yii2\schemaorg\models\BreadcrumbList'
        ]);
        if (!empty($breadcrumbs)) {
            $position = 1;
            foreach ($breadcrumbs as $breadcrumb) {
                $listItem = Yii::createObject([
                    'class' => 'simialbi\yii2\schemaorg\models\ListItem',
                    'position' => $position++
                ]);
                if (is_array($breadcrumb)) {
                    $listItem->item = [
                        '@id' => Url::to(ArrayHelper::getValue($breadcrumb, 'url', ''), true),
                        'name' => ArrayHelper::getValue($breadcrumb, 'label', '')
                    ];
                } else {
                    $listItem->item = [
                        '@id' => Yii::$app->request->absoluteUrl,
                        'name' => $breadcrumb
                    ];
                }
                $breadcrumbList->itemListElement[] = $listItem;
            }
        }

        self::add($breadcrumbList);
    }

    /**
     * Add model to json+ld rendering queue
     *
     * @param \simialbi\yii2\schemaorg\models\Model $model
     */
    public static function add($model)
    {
        self::$_models[] = $model;
    }

    /**
     * Renders ld+json data
     */
    public static function render()
    {
        if (!empty(self::$_models)) {
            foreach (self::$_models as $model) {
                try {
                    echo Html::script(Json::encode($model->toJsonLDArray()), ['type' => 'application/ld+json']) . "\n";
                } catch (InvalidArgumentException $e) {
                    $logger = Yii::$app->log->logger;
                    $logger->log($e->getMessage(), $logger::LEVEL_ERROR);
                }
            }
        }
    }
}


<?php

/*
 * This file is part of ToInlineStyleEmailBundle.
 *
 * (c) Roberto Trunfio <roberto@trunfio.it>
 *
 * Part of the functions and fields description is taken from CSSToInlineStyle
 * project by Tijs Verkoyen <php-css-to-inline-styles@verkoyen.eu>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace RobertoTru\ToInlineStyleEmailBundle\Converter;

use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Templating\EngineInterface;
use TijsVerkoyen\CssToInlineStyles\CssToInlineStyles;

/**
 * ToInlineStyleEmailConverter is a shortcut for using in Symfony2 the utility
 * CssToInlineStyles developed by Tijs Verkoyen.
 *
 * The suggested use of this utility is as a service.
 *
 * @author Roberto Trunfio <roberto@trunfio.it>
 */
class ToInlineStyleEmailConverter
{
    /**
     * Container is used to get template engine instead of direct injection.
     * Direct injection is not used to avoid circular reference exception when rendering using twig tag
     *
     * @var ContainerInterface
     */
    private $container;

    /**
     * The class used for CSS-to-inline-style conversion.
     *
     * @var \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles
     */
    protected $cssToInlineStyles;
    /**
     * A string that represents the HTML file to be sent. Twig templates must be
     * rendered and then passed to this class.
     *
     * @var string
     */
    protected $html;
    /**
     * A string that contains the CSS for the HTML file.
     *
     * @var string
     */
    protected $css;

    /**
     * Construct the service.
     *
     * @param ContainerInterface $container container is used to get templating engine
     * for twig templates. This is optional. Set this param when configuring this
     * class as a service.
     */
    public function __construct(ContainerInterface $container = null)
    {
        $this->container = $container;
        $this->cssToInlineStyles = new CssToInlineStyles();
    }

    /**
     * Set the string which represents the CSS for the HTML file to be sent as email.
     *
     * @param string $css
     */
    public function setCSS($css)
    {
        $this->css = $css;
    }

    /**
     * Set the string which represents the HTML file to be sent as email.
     *
     * @param string $html
     */
    public function setHTML($html)
    {
        $this->html = $html;
    }

    /**
     * Set the string which represents the HTML file to be sent as email by rendering a template.
     *
     * @param string $view is the view to be rendered, e.g. Acme:MyBundle:MyController:index.html.twig
     * @param array[optional] $parameters the array of options to be used for template rendering. This field is optional
     * @throws MissingTemplatingEngineException The TwigEngine must be passed to the constructor, otherwise an exception is thrown
     */
    public function setHTMLByView($view, array $parameters = array())
    {
        if (!$this->container) {
            throw new MissingTemplatingEngineException("To use this function, a Container object must be passed to the constructor (@service_container service)");
        }
        /** @var EngineInterface $engine */
        $engine = $this->container->get('templating');
        $this->setHTML($engine->render($view, $parameters));
    }

    /**
     * Generate the HTML ready to be sent as email.
     *
     * @return string the HTML ready to be sent with an inline-style
     * @throws MissingParamException the HTML and CSS are mandatory.
     */
    public function generateStyledHTML()
    {
        if (is_null($this->html)) {
            throw new MissingParamException("The HTML must be set");
        }

        if (!is_string($this->html)) {
            throw new MissingParamException("The HTML must be a valid string");
        }

        if (is_null($this->css)) {
            throw new MissingParamException("The CSS must be set");
        }

        if (!is_string($this->css)) {
            throw new MissingParamException("The CSS must be a valid string");
        }

        return  $this->cssToInlineStyles->convert($this->html, $this->css);
    }

    /**
     * Inline CSS inside of HTML and return resulting HTML
     * @param string $html
     * @param string $css
     * @return string
     */
    public function inlineCSS($html, $css)
    {
        return $this->cssToInlineStyles->convert($html, $css);
    }
}


<?php

/*
 * This file is part of ToInlineStyleEmailBundle.
 *
 * (c) Roberto Trunfio <roberto@trunfio.it>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace RobertoTru\ToInlineStyleEmailBundle\Twig;

use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Templating\TemplateNameParserInterface;
use Twig_Node;
use Twig_Token;

class InlineCssParser extends \Twig_TokenParser
{
    /**
     * @var FileLocatorInterface
     */
    private $locator;

    /**
     * @var string
     */
    protected $webRoot;

    /**
     * @var bool
     */
    private $debug;

    /**
     * @param FileLocatorInterface $locator used to get css asset real path
     * @param string $webRoot web root of the project
     * @param bool $debug in debug mode css is not inlined but read on each render
     */
    public function __construct(FileLocatorInterface $locator, $webRoot, $debug = false)
    {
        $this->locator = $locator;
        $this->webRoot = $webRoot;
        $this->debug = $debug;
    }

    /**
     * Parses a token and returns a node.
     *
     * @param Twig_Token $token A Twig_Token instance
     *
     * @return Twig_Node A Twig_Node instance
     */
    public function parse(Twig_Token $token)
    {
        $lineNo = $token->getLine();
        $stream = $this->parser->getStream();
        if ($stream->test(Twig_Token::STRING_TYPE)) {
            $css = $this->resolvePath($stream->expect(Twig_Token::STRING_TYPE)->getValue());
        } else {
            $css = $this->parser->getExpressionParser()->parseExpression();
        }
        $stream->expect(Twig_Token::BLOCK_END_TYPE);
        $body = $this->parser->subparse(array($this, 'decideEnd'), true);
        $stream->expect(Twig_Token::BLOCK_END_TYPE);

        return new InlineCssNode($body, $css, $lineNo, $this->debug);
    }

    /**
     * Gets the tag name associated with this token parser.
     *
     * @return string The tag name
     */
    public function getTag()
    {
        return 'inlinecss';
    }

    public function decideEnd(Twig_Token $token)
    {
        return $token->test('endinlinecss');
    }

    /**
     * Resolve path to absolute if any bundle is mentioned
     * @param string $path
     * @return string
     */
    private function resolvePath($path)
    {
        try {
            return $this->locator->locate($path, $this->webRoot);
        } catch (\InvalidArgumentException $e) {
            // happens when path is not bundle relative
            return $this->webRoot . '/' . $path;
        }
    }
}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace CloudCreativity\JsonApi\Testing\Constraints;

use CloudCreativity\JsonApi\Testing\Compare;
use CloudCreativity\JsonApi\Testing\Document;
use PHPUnit\Framework\Constraint\Constraint;
use SebastianBergmann\Comparator\ComparisonFailure;

/**
 * Class OnlySubsetsInList
 *
 * @package CloudCreativity\JsonApi\Testing
 */
class OnlySubsetsInList extends Constraint
{

    /**
     * @var array
     */
    protected $expected;

    /**
     * @var string
     */
    protected $pointer;

    /**
     * @var bool
     */
    protected $strict;

    /**
     * OnlySubsetsInList constructor.
     *
     * @param array $expected
     *      the expected object
     * @param string $pointer
     *      the JSON pointer to the array in the JSON API document.
     * @param bool $strict
     */
    public function __construct(array $expected, string $pointer, bool $strict = true)
    {
        parent::__construct();
        $this->expected = $expected;
        $this->pointer = $pointer;
        $this->strict = $strict;
    }

    /**
     * @inheritdoc
     */
    public function matches($other): bool
    {
        $other = Document::cast($other)->get($this->pointer);

        if (!is_array($other)) {
            return false;
        }

        $allValid = collect($other)->every(function ($item) {
            return $this->expected((array) $item);
        });

        if (!$allValid) {
            return $allValid;
        }

        return collect($this->expected)->every(function ($expected) use ($other) {
            return $this->exists($expected, $other);
        });
    }

    /**
     * @return string
     */
    public function toString(): string
    {
        return Compare::stringify($this->expected);
    }

    /**
     * @inheritdoc
     */
    protected function fail($other, $description, ComparisonFailure $comparisonFailure = null): void
    {
        if (!$comparisonFailure) {
            $comparisonFailure = Compare::failure(
                $this->expected,
                Document::cast($other)->get($this->pointer),
                true
            );
        }

        parent::fail($other, $description, $comparisonFailure);
    }

    /**
     * @inheritdoc
     */
    protected function failureDescription($document): string
    {
        return "the array at [{$this->pointer}] only contains the subsets:" . PHP_EOL
            . $this->toString() . PHP_EOL . PHP_EOL
            . "within JSON API document:" . PHP_EOL
            . Document::cast($document);
    }

    /**
     * Is the actual subset expected?
     *
     * @param array $actual
     * @return bool
     */
    protected function expected(array $actual): bool
    {
        return collect($this->expected)->contains(function ($expected) use ($actual) {
            return $this->compare($expected, $actual);
        });
    }

    /**
     * Does the expected subset exist in the actual array?
     *
     * @param array $expected
     * @param mixed $actual
     * @return bool
     */
    protected function exists(array $expected, $actual): bool
    {
        if (!is_array($actual)) {
            return false;
        }

        return collect($actual)->contains(function ($item) use ($expected) {
            return $this->compare($expected, $item);
        });
    }

    /**
     * @param array $expected
     * @param mixed $actual
     * @return bool
     */
    protected function compare(array $expected, $actual): bool
    {
        return Compare::subset($expected, $actual, $this->strict);
    }

}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace CloudCreativity\JsonApi\Testing;

use CloudCreativity\JsonApi\Testing\Constraints\HttpStatusIs;
use PHPUnit\Framework\Assert as PHPUnitAssert;

/**
 * Class HttpAssert
 *
 * @package CloudCreativity\JsonApi\Testing
 */
class HttpAssert
{

    private const JSON_MEDIA_TYPE = 'application/json';
    private const JSON_API_MEDIA_TYPE = 'application/vnd.api+json';
    private const STATUS_OK = 200;
    private const STATUS_CREATED = 201;
    private const STATUS_ACCEPTED = 202;
    private const STATUS_NO_CONTENT = 204;

    /**
     * Assert that the HTTP status matches the expected status.
     *
     * This assertion will print any JSON API errors in the supplied content if
     * the HTTP status does not match. This is useful for understanding why the status
     * is not the expected status when an error has occurred.
     *
     * @param $status
     * @param int $expected
     * @param mixed|null $content
     * @param string $message
     * @return void
     */
    public static function assertStatusCode(
        $status,
        int $expected,
        $content = null,
        string $message = ''
    ): void
    {
        PHPUnitAssert::assertThat(
            $status,
            new HttpStatusIs($expected, $content),
            $message
        );
    }

    /**
     * Assert that there is content with the expected media type.
     *
     * @param $type
     * @param $content
     * @param string $expected
     * @param string $message
     * @return Document
     */
    public static function assertContent(
        $type,
        $content,
        $expected = self::JSON_API_MEDIA_TYPE,
        string $message = ''
    ): Document
    {
        PHPUnitAssert::assertSame($type, $expected, $message ?: "Expecting content with media type {$expected}.");
        PHPUnitAssert::assertNotEmpty($content, $message ?: 'Expecting HTTP body to have content.');

        return Document::cast($content);
    }

    /**
     * Assert a JSON HTTP message with an expected status.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param int $expected
     *      the expected HTTP status.
     * @param string $message
     * @return Document
     */
    public static function assertJson(
        $status,
        $contentType,
        $content,
        int $expected = self::STATUS_OK,
        string $message = ''
    ): Document
    {
        self::assertStatusCode($status, $expected, $content, $message);

        return self::assertContent($contentType, $content, self::JSON_MEDIA_TYPE, $message);
    }

    /**
     * Assert a JSON API HTTP message with an expected status.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param int $expected
     *      the expected HTTP status.
     * @param string $message
     * @return Document
     */
    public static function assertJsonApi(
        $status,
        $contentType,
        $content,
        int $expected = self::STATUS_OK,
        string $message = ''
    ): Document
    {
        self::assertStatusCode($status, $expected, $content, $message);

        return self::assertContent($contentType, $content, self::JSON_API_MEDIA_TYPE, $message);
    }

    /**
     * Assert that a resource was fetched.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $expected
     *      the expected resource, or a subset of the expected resource.
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertFetchedOne(
        $status,
        $contentType,
        $content,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertHash($expected, '/data', $strict, $message);
    }

    /**
     * Assert that an exact resource or resource collection was fetched.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $expected
     *      the expected content of the document's data member.
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertFetchedExact(
        $status,
        $contentType,
        $content,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertExact($expected, '/data', $strict, $message);
    }

    /**
     * Assert that the fetched data is null.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param string $message
     * @return Document
     */
    public static function assertFetchedNull($status, $contentType, $content, string $message = ''): Document
    {
        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertNull('/data', $message);
    }

    /**
     * Assert that a resource collection was fetched.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertFetchedMany(
        $status,
        $contentType,
        $content,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertList($expected, '/data', $strict, $message);
    }

    /**
     * Assert that a resource collection was fetched in the expected order.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertFetchedManyInOrder(
        $status,
        $contentType,
        $content,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        if (empty($expected)) {
            return self::assertFetchedNone($strict, $contentType, $content, $message);
        }

        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertListInOrder($expected, '/data', $strict, $message);
    }

    /**
     * Assert that an empty resource collection was fetched.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param string $message
     * @return Document
     */
    public static function assertFetchedNone($status, $contentType, $content, string $message = ''): Document
    {
        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertListEmpty('/data', $message);
    }

    /**
     * Assert that a to-one relationship was fetched.
     *
     * If either type or id are null, then it will be asserted that the data member of the content
     * is null.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param string|null $type
     * @param string|null $id
     * @param string $message
     * @return Document
     */
    public static function assertFetchedToOne(
        $status,
        $contentType,
        $content,
        ?string $type,
        string $id = null,
        string $message = ''
    ): Document
    {
        if (is_null($type) || is_null($id)) {
            return self::assertFetchedNull($status, $contentType, $content, $message);
        }

        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertIdentifier($type, $id, '/data', $message);
    }

    /**
     * Assert that a to-many relationship was fetched.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertFetchedToMany(
        $status,
        $contentType,
        $content,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        if (empty($expected)) {
            return self::assertFetchedNone($strict, $contentType, $content, $message);
        }

        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertIdentifiersList($expected, '/data', $strict, $message);
    }

    /**
     * Assert that a to-many relationship was fetched in the expected order.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertFetchedToManyInOrder(
        $status,
        $contentType,
        $content,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        if (empty($expected)) {
            return self::assertFetchedNone($strict, $contentType, $content, $message);
        }

        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertIdentifiersListInOrder($expected, '/data', $strict, $message);
    }

    /**
     * Assert that a resource was created with a server generated id.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param $location
     * @param string $expectedLocation
     *      the expected location without the id.
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertCreatedWithServerId(
        $status,
        $contentType,
        $content,
        $location,
        string $expectedLocation,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        self::assertStatusCode($status, self::STATUS_CREATED, $content, $message);
        $document = self::assertServerGeneratedId($contentType, $content, $expected, $strict, $message);
        $id = $document->get('/data/id');

        PHPUnitAssert::assertNotEmpty($id, $message ?: 'Expecting content to contain a resource id.');
        $expectedLocation = rtrim($expectedLocation, '/') . '/' . $id;
        PHPUnitAssert::assertSame($expectedLocation, $location, $message ?: 'Unexpected Location header.');

        return $document;
    }

    /**
     * Assert that a resource was created with a client generated id.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param $location
     * @param string $expectedLocation
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertCreatedWithClientId(
        $status,
        $contentType,
        $content,
        $location,
        string $expectedLocation,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        $expectedId = $expected['id'] ?? null;

        if (!$expectedId) {
            throw new \InvalidArgumentException('Expected resource hash must have an id.');
        }

        self::assertStatusCode($status, self::STATUS_CREATED, $content, $message);
        PHPUnitAssert::assertSame(
            "$expectedLocation/{$expectedId}",
            $location,
            $message ?: 'Unexpected Location header.'
        );

        return self::assertContent($contentType, $content, self::JSON_API_MEDIA_TYPE, $message)
            ->assertHash($expected, '/data', $strict, $message);
    }

    /**
     * Assert that a resource was created with a no content response.
     *
     * @param $status
     * @param $location
     * @param $expectedLocation
     * @param string $message = ''
     * @return void
     */
    public static function assertCreatedNoContent(
        $status,
        $location,
        $expectedLocation,
        string $message = ''
    ): void
    {
        self::assertStatusCode($status, self::STATUS_NO_CONTENT, null, $message);
        PHPUnitAssert::assertSame($expectedLocation, $location, $message ?: 'Unexpected Location header.');
    }

    /**
     * Assert that an asynchronous process was accepted with a server id.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param $contentLocation
     * @param string $expectedLocation
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertAcceptedWithId(
        $status,
        $contentType,
        $content,
        $contentLocation,
        string $expectedLocation,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        self::assertStatusCode($status, self::STATUS_ACCEPTED, $content, $message);
        $document = self::assertServerGeneratedId($contentType, $content, $expected, $strict, $message);
        $id = $document->get('/data/id');

        PHPUnitAssert::assertNotEmpty($id, $message ?: 'Expecting content to contain a resource id.');
        $expectedLocation = rtrim($expectedLocation, '/') . '/' . $id;
        PHPUnitAssert::assertSame($expectedLocation, $contentLocation, $message ?: 'Unexpected Location header.');

        return $document;
    }

    /**
     * Assert a no content response.
     *
     * @param $status
     * @param string|null
     * @param string $message
     * @return void
     */
    public static function assertNoContent($status, string $content = null, string $message = ''): void
    {
        self::assertStatusCode($status, self::STATUS_NO_CONTENT, $content, $message);
        PHPUnitAssert::assertEmpty($content, $message ?: 'Expecting HTTP body content to be empty.');
    }

    /**
     * Assert a top-level meta response without data.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertMetaWithoutData(
        $status,
        $contentType,
        $content,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertNotExists('/data', $message)
            ->assertMeta($expected, $strict, $message);
    }

    /**
     * Assert an exact top-level meta response without data.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertExactMetaWithoutData(
        $status,
        $contentType,
        $content,
        array $expected,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        return self::assertJsonApi($status, $contentType, $content, self::STATUS_OK, $message)
            ->assertNotExists('/data', $message)
            ->assertExactMeta($expected, $strict, $message);
    }

    /**
     * Assert the document contains a single error that matches the supplied error.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param int $expectedStatus
     * @param array $error
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertError(
        $status,
        $contentType,
        $content,
        int $expectedStatus,
        array $error = [],
        bool $strict = true,
        string $message = ''
    ): Document
    {
        $document = self::assertJsonApi($status, $contentType, $content, $expectedStatus, $message)
            ->assertNotExists('/data', $message);

        if ($error) {
            $document->assertError($error, $strict, $message);
        } else {
            $document->assertExists('/error', $message);
        }

        return $document;
    }

    /**
     * Assert the document contains an exact single error that matches the supplied error.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param int $expectedStatus
     * @param array $error
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertExactError(
        $status,
        $contentType,
        $content,
        int $expectedStatus,
        array $error,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        $document = self::assertJsonApi($status, $contentType, $content, $expectedStatus, $message)
            ->assertNotExists('/data', $message)
            ->assertExactError($error, $strict, $message);

        return $document;
    }

    /**
     * Assert the document contains a single error that matches the supplied error and has a status member.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $error
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertErrorStatus(
        $status,
        $contentType,
        $content,
        array $error = [],
        bool $strict = true,
        string $message = ''
    ): Document
    {
        $expectedStatus = $error['status'] ?? null;

        if (!$expectedStatus) {
            throw new \InvalidArgumentException('Expecting error to have a status member.');
        }

        return self::assertError(
            $status,
            $contentType,
            $content,
            (int) $expectedStatus,
            $error,
            $strict,
            $message
        );
    }

    /**
     * Assert the document contains an exact single error that matches the supplied error and has a status member.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param array $error
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertExactErrorStatus(
        $status,
        $contentType,
        $content,
        array $error,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        $expectedStatus = $error['status'] ?? null;

        if (!$expectedStatus) {
            throw new \InvalidArgumentException('Expecting error to have a status member.');
        }

        return self::assertExactError(
            $status,
            $contentType,
            $content,
            (int) $expectedStatus,
            $error,
            $strict,
            $message
        );
    }

    /**
     * Assert the HTTP message contains the supplied error within its errors member.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param int $expectedStatus
     * @param array $error
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertHasError(
        $status,
        $contentType,
        $content,
        int $expectedStatus,
        array $error = [],
        bool $strict = true,
        string $message = ''
    ): Document
    {
        if (empty($error)) {
            $error = ['status' => (string) $status];
        }

        return self::assertJsonApi($status, $contentType, $content, $expectedStatus, $message)
            ->assertHasError($error, $strict, $message);
    }

    /**
     * Assert the HTTP message contains the supplied error within its errors member.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param int $expectedStatus
     * @param array $error
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertHasExactError(
        $status,
        $contentType,
        $content,
        int $expectedStatus,
        array $error,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        if (empty($error)) {
            $error = ['status' => (string) $status];
        }

        return self::assertJsonApi($status, $contentType, $content, $expectedStatus, $message)
            ->assertHasExactError($error, $strict, $message);
    }

    /**
     * Assert the HTTP status contains the supplied errors.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param int $expectedStatus
     * @param array $errors
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertErrors(
        $status,
        $contentType,
        $content,
        int $expectedStatus,
        array $errors,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        return self::assertJsonApi($status, $contentType, $content, $expectedStatus, $message)
            ->assertErrors($errors, $strict, $message);
    }

    /**
     * Assert the HTTP status contains the exact supplied errors.
     *
     * @param $status
     * @param $contentType
     * @param $content
     * @param int $expectedStatus
     * @param array $errors
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    public static function assertExactErrors(
        $status,
        $contentType,
        $content,
        int $expectedStatus,
        array $errors,
        bool $strict = true,
        string $message = ''
    ): Document
    {
        return self::assertJsonApi($status, $contentType, $content, $expectedStatus, $message)
            ->assertExactErrors($errors, $strict, $message);
    }

    /**
     * @param $contentType
     * @param $content
     * @param array $expected
     * @param bool $strict
     * @param string $message
     * @return Document
     */
    private static function assertServerGeneratedId(
        $contentType,
        $content,
        array $expected,
        bool $strict,
        string $message = ''
    ): Document
    {
        $id = $expected['id'] ?? null;

        $document = self::assertContent($contentType, $content, self::JSON_API_MEDIA_TYPE, $message)
            ->assertHash($expected, '/data', $strict, $message);

        if ($id) {
            PHPUnitAssert::assertSame($id, $document->get('/data/id'), $message);
        } else {
            $document->assertExists('/data/id', $message);
        }

        return $document;
    }
}


<?php
/**
 * Copyright 2019 Cloud Creativity Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace CloudCreativity\JsonApi\Testing\Concerns;

use CloudCreativity\JsonApi\Testing\Compare;
use CloudCreativity\JsonApi\Testing\Document;
use CloudCreativity\JsonApi\Testing\HttpAssert;
use Illuminate\Contracts\Routing\UrlRoutable;

/**
 * Trait HasHttpAssertions
 *
 * @package CloudCreativity\JsonApi\Testing
 */
trait HasHttpAssertions
{

    /**
     * @var Document|null
     */
    protected $document;

    /**
     * @var string|null
     */
    private $expectedType;

    /**
     * Get the JSON API document.
     *
     * @return Document
     */
    public function getDocument(): Document
    {
        if (!$this->document) {
            $this->document = HttpAssert::assertContent(
                $this->getContentType(),
                $this->getContent()
            );
        }

        return $this->document;
    }

    /**
     * @return string
     */
    public function getExpectedType(): string
    {
        if (!$this->expectedType) {
            throw new \LogicException('An expected resource type must be set.');
        }

        return $this->expectedType;
    }

    /**
     * @param string $type
     * @return $this
     */
    public function willSeeResourceType(string $type): self
    {
        return $this->willSeeType($type);
    }

    /**
     * Set the expected resource type for the data member of the JSON document.
     *
     * @param string $type
     * @return $this
     */
    public function willSeeType(string $type): self
    {
        if (empty($type)) {
            throw new \InvalidArgumentException('Expected type must be a non-empty string.');
        }

        $this->expectedType = $type;

        return $this;
    }

    /**
     * @param $status
     * @return $this
     */
    public function assertStatusCode($status): self
    {
        HttpAssert::assertStatusCode($this->getStatusCode(), $status, $this->getContent());

        return $this;
    }

    /**
     * Assert that a resource was fetched.
     *
     * @param UrlRoutable|string|int|array $expected
     *      the expected resource, or a subset of the expected resource.
     * @param bool $strict
     * @return $this
     */
    public function assertFetchedOne($expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertFetchedOne(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->identifier($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that an exact resource was fetched.
     *
     * @param UrlRoutable|string|int|array $expected
     *      the expected resource.
     * @param bool $strict
     * @return $this
     */
    public function assertFetchedOneExact($expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertFetchedExact(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->identifier($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that the fetched data is null.
     *
     * @return $this
     */
    public function assertFetchedNull(): self
    {
        $this->document = HttpAssert::assertFetchedNull(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent()
        );

        return $this;
    }

    /**
     * Assert that a resource collection was fetched.
     *
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertFetchedMany($expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertFetchedMany(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->identifiers($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that an exact resource collection was fetched.
     *
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertFetchedManyExact($expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertFetchedExact(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->identifiers($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that a resource collection was fetched in the expected order.
     *
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertFetchedManyInOrder($expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertFetchedManyInOrder(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->identifiers($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that an empty resource collection was fetched.
     *
     * @return $this
     */
    public function assertFetchedNone(): self
    {
        $this->document = HttpAssert::assertFetchedNone(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent()
        );

        return $this;
    }

    /**
     * Assert that a to-one relationship was fetched.
     *
     * If either type or id are null, then it will be asserted that the data member of the content
     * is null.
     *
     * Prov
     *
     * @param UrlRoutable|string|int $id
     * @return $this
     */
    public function assertFetchedToOne($id): self
    {
        $identifier = $this->identifier($id);

        $this->document = HttpAssert::assertFetchedToOne(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $identifier['type'],
            $identifier['id']
        );

        return $this;
    }

    /**
     * Assert that a to-many relationship was fetched.
     *
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertFetchedToMany($expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertFetchedToMany(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->identifiers($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that a to-many relationship was fetched in the expected order.
     *
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertFetchedToManyInOrder($expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertFetchedToManyInOrder(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->identifiers($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that a resource was created with a server generated id.
     *
     * @param string $expectedLocation
     *      the expected location without the id.
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertCreatedWithServerId(string $expectedLocation, $expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertCreatedWithServerId(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->getLocation(),
            $expectedLocation,
            $this->identifier($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that a resource was created with a client generated id.
     *
     * @param string $expectedLocation
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertCreatedWithClientId(string $expectedLocation, $expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertCreatedWithClientId(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->getLocation(),
            $expectedLocation,
            $this->identifier($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that a resource was created with a no content response.
     *
     * @param $expectedLocation
     * @return $this
     */
    public function assertCreatedNoContent(string $expectedLocation): self
    {
        HttpAssert::assertCreatedNoContent(
            $this->getStatusCode(),
            $this->getLocation(),
            $expectedLocation
        );

        return $this;
    }

    /**
     * Assert response is a JSON API resource updated response.
     *
     * For a resource update, we typically expect either:
     *
     * - 200 OK with resource content; or
     * - 204 No Content
     *
     * Alternatively a top-level meta only response is acceptable. If this is expected,
     * it can be asserted using `assertMetaWithoutData`.
     *
     * @param array $expected
     *      array representation of the expected resource, or null for a no-content response
     * @param bool $strict
     * @return $this
     */
    public function assertUpdated(array $expected = null, bool $strict = true): self
    {
        if (is_null($expected)) {
            return $this->assertNoContent();
        }

        return $this->assertFetchedOne($expected, $strict);
    }

    /**
     * Assert response is a JSON API resource deleted response.
     *
     * The JSON API spec says that:
     *
     * - A server MUST return a 204 No Content status code if a deletion request is successful
     * and no content is returned.
     * - A server MUST return a 200 OK status code if a deletion request is successful and the server responds
     * with only top-level meta data.
     *
     * @param array|null $expected
     *      the expected top-level meta, or null for no content response.
     * @param bool $strict
     * @return $this
     */
    public function assertDeleted(array $expected = null, bool $strict = true): self
    {
        if (is_null($expected)) {
            return $this->assertNoContent();
        }

        return $this->assertMetaWithoutData($expected, $strict);
    }

    /**
     * Assert that an asynchronous process was accepted with a server id.
     *
     * @param string $expectedLocation
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertAcceptedWithId(string $expectedLocation, $expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertAcceptedWithId(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $this->getContentLocation(),
            $expectedLocation,
            $this->identifier($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert a no content response.
     *
     * @return $this
     */
    public function assertNoContent(): self
    {
        HttpAssert::assertNoContent($this->getStatusCode(), $this->getContent());

        return $this;
    }

    /**
     * Assert that the expected resource is included in the document.
     *
     * @param string $type
     * @param UrlRoutable|string|int|null $id
     * @return $this
     */
    public function assertIsIncluded(string $type, $id): self
    {
        $identifier = $this->identifier(compact('type', 'id'));

        $this->getDocument()->assertIncludedContainsResource(
            $identifier['type'],
            $identifier['id']
        );

        return $this;
    }

    /**
     * Assert that the included member contains the supplied resource.
     *
     * @param UrlRoutable|string|int|array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertIncludes($expected, bool $strict = true): self
    {
        $this->getDocument()->assertIncludedContainsHash(
            $this->identifier($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert that the document's included member matches the expected array.
     *
     * This does not assert the order of the included member because there is no significance to
     * the order of resources in the included member.
     *
     * @param array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertIncluded(array $expected, bool $strict = true): self
    {
        $this->getDocument()->assertIncluded(
            $this->identifiers($expected),
            $strict
        );

        return $this;
    }

    /**
     * Assert a top-level meta response without data.
     *
     * @param array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertMetaWithoutData(array $expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertMetaWithoutData(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $expected,
            $strict
        );

        return $this;
    }

    /**
     * Assert an exact top-level meta response without data.
     *
     * @param array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertExactMetaWithoutData(array $expected, bool $strict = true): self
    {
        $this->document = HttpAssert::assertExactMetaWithoutData(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $expected,
            $strict
        );

        return $this;
    }

    /**
     * Assert that the top-level meta matches the expected values.
     *
     * @param array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertMeta(array $expected, bool $strict = true): self
    {
        $this->getDocument()->assertMeta($expected, $strict);

        return $this;
    }

    /**
     * Assert that the top-level meta is exactly the expected meta.
     *
     * @param array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertExactMeta(array $expected, bool $strict = true): self
    {
        $this->getDocument()->assertExactMeta($expected, $strict);

        return $this;
    }

    /**
     * Assert that the top-level links match the expected values.
     *
     * @param array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertLinks(array $expected, bool $strict = true): self
    {
        $this->getDocument()->assertLinks($expected, $strict);

        return $this;
    }

    /**
     * Assert that the top-level links are exactly the expected links.
     *
     * @param array $expected
     * @param bool $strict
     * @return $this
     */
    public function assertExactLinks(array $expected, bool $strict = true): self
    {
        $this->getDocument()->assertExactLinks($expected, $strict);

        return $this;
    }

    /**
     * Assert the document contains a single error that matches the supplied error.
     *
     * @param int $status
     * @param array $error
     * @param bool $strict
     * @return $this
     */
    public function assertError(int $status, array $error = [], bool $strict = true): self
    {
        $this->document = HttpAssert::assertError(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $status,
            $error,
            $strict
        );

        return $this;
    }

    /**
     * Assert the document contains a single  error that exactly matches the supplied error.
     *
     * @param int $status
     * @param array $error
     * @param bool $strict
     * @return $this
     */
    public function assertExactError(int $status, array $error, bool $strict = true): self
    {
        $this->document = HttpAssert::assertExactError(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $status,
            $error,
            $strict
        );

        return $this;
    }

    /**
     * Assert the document contains a single error that matches the supplied error and has a status member.
     *
     * @param array $error
     * @param bool $strict
     * @return $this
     */
    public function assertErrorStatus(array $error, bool $strict = true): self
    {
        $this->document = HttpAssert::assertErrorStatus(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $error,
            $strict
        );

        return $this;
    }

    /**
     * Assert the document contains a single error that exactly matches the supplied error and has a status member.
     *
     * @param array $error
     * @param bool $strict
     * @return $this
     */
    public function assertExactErrorStatus(array $error, bool $strict = true): self
    {
        $this->document = HttpAssert::assertExactErrorStatus(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $error,
            $strict
        );

        return $this;
    }

    /**
     * Assert the HTTP message contains the supplied error within its errors member.
     *
     * @param int $status
     * @param array $error
     * @param bool $strict
     * @return $this
     */
    public function assertHasError(int $status, array $error = [], bool $strict = true): self
    {
        $this->document = HttpAssert::assertHasError(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $status,
            $error,
            $strict
        );

        return $this;
    }

    /**
     * Assert the HTTP message contains the exact supplied error within its errors member.
     *
     * @param int $status
     * @param array $error
     * @param bool $strict
     * @return $this
     */
    public function assertHasExactError(int $status, array $error, bool $strict = true): self
    {
        $this->document = HttpAssert::assertHasExactError(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $status,
            $error,
            $strict
        );

        return $this;
    }

    /**
     * Assert the HTTP status contains the supplied errors.
     *
     * @param int $status
     * @param array $errors
     * @param bool $strict
     * @return $this
     */
    public function assertErrors(int $status, array $errors, bool $strict = true): self
    {
        $this->document = HttpAssert::assertErrors(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $status,
            $errors,
            $strict
        );

        return $this;
    }

    /**
     * Assert the HTTP status contains the supplied exact errors.
     *
     * @param int $status
     * @param array $errors
     * @param bool $strict
     * @return $this
     */
    public function assertExactErrors(int $status, array $errors, bool $strict = true): self
    {
        $this->document = HttpAssert::assertExactErrors(
            $this->getStatusCode(),
            $this->getContentType(),
            $this->getContent(),
            $status,
            $errors,
            $strict
        );

        return $this;
    }

    /**
     * Ensure the value is an array of identifiers.
     *
     * @param UrlRoutable|string|int|iterable $ids
     * @param string|null $type
     * @return array
     */
    protected function identifiers($ids, string $type = null): array
    {
        return Compare::identifiers($ids, $type ?: $this->getExpectedType());
    }

    /**
     * Ensure the value is a resource identifier.
     *
     * @param UrlRoutable|string|int|array $id
     * @param string|null $type
     * @return array
     */
    protected function identifier($id, string $type = null): array
    {
        return Compare::identifier($id, $type ?: $this->getExpectedType());
    }

    /**
     * Does the value identify a resource?
     *
     * @param $id
     * @return bool
     */
    protected function identifiable($id): bool
    {
        return Compare::identifiable($id);
    }
}