<?php
require __DIR__ . '/../vendor/autoload.php';

$stageDirectory = __DIR__ . '/artifacts/staging';
$projectRoot = __DIR__ . '/../';
$burgomaster = new \Burgomaster($stageDirectory, $projectRoot);
$autoloaderFilename = 'aws-autoloader.php';

$metaFiles = ['README.md', 'LICENSE', 'NOTICE', 'CHANGELOG.md'];
foreach ($metaFiles as $file) {
    $burgomaster->deepCopy($file, $file);
}

$sdkFiles = new \RecursiveIteratorIterator(
    new \RecursiveDirectoryIterator(realpath('src'))
);
$sdkFiles = new CallbackFilterIterator($sdkFiles, function (SplFileInfo $file) {
    return !in_array($file->getBasename('.php'), [
        'docs-2.json',
        'examples-1.json',
    ]);
});

$burgomaster->recursiveCopy('src', 'Aws', ['php', 'php.gz'], $sdkFiles);
$burgomaster->recursiveCopy('vendor/aws/aws-php-sns-message-validator/src', 'Aws/Sns');
$burgomaster->recursiveCopy('vendor/mtdowling/jmespath.php/src', 'JmesPath');
$burgomaster->recursiveCopy('vendor/guzzlehttp/guzzle/src', 'GuzzleHttp');
$burgomaster->recursiveCopy('vendor/guzzlehttp/psr7/src', 'GuzzleHttp/Psr7');
$burgomaster->recursiveCopy('vendor/guzzlehttp/promises/src', 'GuzzleHttp/Promise');
$burgomaster->recursiveCopy('vendor/psr/http-message/src', 'Psr/Http/Message');

$autoloaderContents = [
    'Aws/functions.php',
    'GuzzleHttp/functions_include.php',
    'GuzzleHttp/Psr7/functions_include.php',
    'GuzzleHttp/Promise/functions_include.php',
    'JmesPath/JmesPath.php',
];

if (file_exists($projectRoot . 'vendor/symfony/polyfill-intl-idn')) {
    $burgomaster->recursiveCopy($projectRoot . 'vendor/symfony/polyfill-intl-idn', 'Symfony/Polyfill/Intl/Idn');
    array_push($autoloaderContents, 'Symfony/Polyfill/Intl/Idn/bootstrap.php');
}

if (file_exists($projectRoot . 'vendor/psr/http-client/src')) {
    $burgomaster->recursiveCopy($projectRoot . 'vendor/psr/http-client/src', 'Psr/Http/Client');
}

$burgomaster->createAutoloader($autoloaderContents, $autoloaderFilename);

$burgomaster->createZip(__DIR__ . "/artifacts/aws.zip");
$burgomaster->createPhar(
    __DIR__ . "/artifacts/aws.phar",
    null,
    $autoloaderFilename,
    'aws-' . \Aws\Sdk::VERSION . '.phar'
);

$burgomaster->startSection('test-phar');
$burgomaster->exec('php ' . __DIR__ . '/test-phar.php');
$burgomaster->endSection();


#!/usr/bin/env php
<?php

require __DIR__ . '/artifacts/aws.phar';
require __DIR__ . '/artifacts/phpunit.phar';
require __DIR__ . '/artifacts/behat.phar';


<?php

/**
 * A trait that provides a method for linting a PHP file. It will use
 * `opcache_compile` if available and fall back to shelling out to `php -l`
 * otherwise.
 *
 * @internal
 */
trait PhpFileLinterTrait
{
    /**
     * @param string $path
     *
     * @return bool
     */
    private function lintFile($path)
    {
        static $linter;

        if (empty($linter)) {
            $linter = function_exists('opcache_get_status')
                && !empty(opcache_get_status(false)['opcache_enabled'])
                    ? [$this, 'opcacheLint']
                    : [$this, 'commandLineLint'];
        }

        return call_user_func($linter, $path);
    }

    /**
     * @param string $path
     *
     * @return bool
     */
    private function commandLineLint($path)
    {
        list($output, $exitCode) = [[], 1];
        exec("php -l $path", $output, $exitCode);

        return 0 === $exitCode;
    }

    /**
     * Uses the Zend OPCache control functions to perform an in-process
     * validation of a file. This function will fail on code that declares new
     * symbols (e.g., classes or functions) if they have already been loaded
     * into the current process.
     *
     * @param string $path
     *
     * @return bool
     */
    private function opcacheLint($path)
    {
        opcache_invalidate($path, true);

        return @opcache_compile_file($path);
    }
}


<?php

class ServiceBuilder
{
    private $namespace;
    private $model;
    private $clientPath;
    private $exceptionPath;

    public function __construct(
        $namespace,
        array $model,
        $clientPath,
        $exceptionPath
    ) {
        $this->namespace = $namespace;
        $this->model = $model;
        $this->clientPath = $clientPath;
        $this->exceptionPath = $exceptionPath;
    }

    public function buildClient()
    {
        return $this->buildFileIfMissing('Client');
    }

    public function buildException()
    {
        return $this->buildFileIfMissing('Exception');
    }

    private function buildFileIfMissing($fileType)
    {
        $property = lcfirst($fileType) . 'Path';
        $method = 'generateDefault' . $fileType;
        if (!file_exists($this->{$property})) {
            if (!is_dir(dirname($this->{$property}))) {
                mkdir(dirname($this->{$property}), 0755, true);
            }

            file_put_contents($this->{$property}, $this->{$method}(), LOCK_EX);
        }

        return $this;
    }

    private function getFullName()
    {
        return $this->model['metadata']['serviceFullName'];
    }

    private function generateDefaultClient()
    {
        return <<<EOPHP
<?php
namespace Aws\\{$this->namespace};

use Aws\\AwsClient;

/**
 * This client is used to interact with the **{$this->getFullName()}** service.
 */
class {$this->namespace}Client extends AwsClient {}

EOPHP;
    }

    private function generateDefaultException()
    {
        return <<<EOPHP
<?php
namespace Aws\\{$this->namespace}\\Exception;

use Aws\\Exception\\AwsException;

/**
 * Represents an error interacting with the **{$this->getFullName()}** service.
 */
class {$this->namespace}Exception extends AwsException {}

EOPHP;
    }
}


<?php
require __DIR__ . '/artifacts/aws.phar';

$conf = [
    'credentials' => ['key' => 'foo', 'secret' => 'bar'],
    'region'      => 'us-west-2',
    'version'     => 'latest'
];

// Ensure that a client can be created.
$s3 = new Aws\S3\S3Client($conf);
// Ensure that waiters can be found.
$s3->getPaginator('ListObjects');

// Legacy factory instantiation.
Aws\DynamoDb\DynamoDbClient::factory($conf);

// JMESPath autoloader
\JmesPath\search('foo', ['foo' => 'bar']);

// React autoloader
$checks = [
    'GuzzleHttp\\Psr7\\stream_for',
    'GuzzleHttp\\Promise\\inspect',
    'JmesPath\\search',
    'Aws\\dir_iterator',
];

foreach ($checks as $check) {
    if (!function_exists($check)) {
        echo $check . ' not found';
        exit(1);
    }
}

echo 'Version=' . Aws\Sdk::VERSION;


<?php

require __DIR__ . '/ChangelogBuilder.php';

use Aws\Build\Changelog\ChangelogBuilder;

$params = [];

$options = getopt('v');

$params['verbose'] = isset($option['v']) ? $option['v'] : true;

$changelogBuilder = new ChangelogBuilder($params);

$changelogBuilder->buildChangelog();

$changelogBuilder->fixEndpointFile();

$changelogBuilder->cleanNextReleaseFolder();


<?php
namespace Aws\Build\Changelog;

/**
 * @internal
 */
class ChangelogBuilder
{
    /** @var boolean */
    private $verbose;

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

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

    /** @var boolean */
    private $newServiceFlag = false;

    const CHANGELOG_FEATURE = 'feature';
    const CHANGELOG_API_CHANGE = 'api-change';
    const CHANGELOG_ENHANCEMENT = 'enhancement';
    const CHANGELOG_BUGFIX = 'bugfix';
    const CHANGELOG_DOCUMENTATION = 'documentation';

    /**
     *  The constructor requires following configure parameters:
     * - base_dir: (String) Path to the base directory where the `.changes` folder is located. Default is empty string.
     * - release_notes_output_dir: (String) Path to `.changes` folder where new release json will be put and content of
     *                              `nextrelease` folder will be deleted
     * - verbose: (Boolean) Flag to enable(true)/disable(false) verbose mode
     */
    public function __construct(array $params)
    {
        $this->baseDir = isset($params['base_dir']) ? $params['base_dir'] : '';
        $this->releaseNotesOutputDir = isset($params['release_notes_output_dir'])
            ? $params['release_notes_output_dir']
            : '';
        $this->verbose = isset($params['verbose']) ? $params['verbose'] : false;
    }

    public function isNewService()
    {
        return $this->newServiceFlag;
    }

    private function readChangelog()
    {
        $releaseDir = $this->baseDir . '.changes/nextrelease/';
        $changelogEntries = [];
        if (!is_dir($releaseDir) || !$dh = opendir($releaseDir)) {
            throw new \InvalidArgumentException(
                "nextrelease directory doesn't exists or is not readable at location $releaseDir"
            );
        }
        //Ignore any files starting with a (.) dot
        $files = preg_grep('/^([^.])/', scandir($releaseDir));
        if (empty($files)) {
            throw new \InvalidArgumentException("No release notes files found in $releaseDir folder");
        }
        foreach ($files as $file) {
            $str = file_get_contents($releaseDir . $file);
            $changelogEntries = array_merge($changelogEntries, $this->cleanJSON(json_decode($str)));
        }
        closedir($dh);

        $this->newServiceFlag = count(array_filter($changelogEntries, function ($change) {
                return $change->type === self::CHANGELOG_FEATURE;
            })) > 0;

        return $changelogEntries;
    }

    private function cleanJSON($arr)
    {
        if (empty($arr) || !is_array($arr)) {
            throw new \RuntimeException('Invalid Input', 2);
        }

        return $arr;
    }

    private function createTag($changelogFile)
    {
        if (!file_exists($changelogFile)) {
            throw new \RuntimeException('Changelog File Not Found', 2);
        }
        $lines = file($changelogFile);
        $tag = explode(".", explode(" ", $lines[2])[1]);
        if ($tag[0] == 'next') {
            throw new \InvalidArgumentException('Untagged changes exits in CHANGELOG.md', 1);
        }
        if ($this->newServiceFlag) {
            //Minor Version Bump if a newservice is being released
            ++$tag[1];
            $tag[2] = 0;
        } else {
            ++$tag[2];
        }

        return implode(".", $tag);
    }

    private function createChangelogJson($changelog, $tag)
    {
        $fp = fopen($this->releaseNotesOutputDir . ".changes/" . $tag, 'w');
        fwrite($fp, json_encode($changelog, JSON_PRETTY_PRINT) . PHP_EOL);
        fclose($fp);
    }

    private function writeToChangelog($changelog, $changelogFile)
    {
        if (!file_exists($changelogFile)) {
            throw new \InvalidArgumentException('Changelog File Not Found', 2);
        }
        $newChangeLog = "## next release\n\n" . $changelog . "\n";
        $lines = file($changelogFile);
        $lines[2] = $newChangeLog . $lines[2];
        file_put_contents($changelogFile, $lines);
    }

    public function cleanNextReleaseFolder()
    {
        $nextReleaseDir = $this->baseDir . '.changes/nextrelease/';
        $files = preg_grep('/^([^.])/', scandir($nextReleaseDir));
        foreach ($files as $file) {
            if (is_file($nextReleaseDir . $file)) {
                unlink($nextReleaseDir . $file);
            }
        }
    }

    private function generateChangelogString($changelog)
    {
        usort($changelog, function ($a, $b) {
            return strcmp($a->category, $b->category);
        });
        $str = "";
        foreach ($changelog as $log) {
            $str .= "* `Aws\\" . $log->category . "` - " . $log->description . "\n";
        }
        return $str;
    }

    public function buildChangelog()
    {
        $changelogFile = $this->baseDir . 'CHANGELOG.md';
        $newChangelog = $this->readChangelog();
        $tag = $this->createTag($changelogFile);
        putenv("TAG=$tag");
        if ($this->verbose) {
            echo 'Tag for next release ' . $tag . "\n";
        }
        $this->createChangelogJson($newChangelog, $tag);
        $ChangelogUpdate = $this->generateChangelogString($newChangelog);
        if ($this->verbose) {
            echo "$ChangelogUpdate";
        }
        $this->writeToChangelog($ChangelogUpdate, $this->releaseNotesOutputDir . 'CHANGELOG.md');
    }
}


<?php
namespace Aws\Build\Docs;

use Aws\Api\ListShape;
use Aws\Api\MapShape;
use Aws\Api\Service as SdkService;
use Aws\Api\Shape;
use Aws\Api\StructureShape;

/**
 * @internal
 */
class CodeSnippetGenerator
{
    /** @var SdkService */
    private $service;

    /** @var bool[][] */
    private $issues = [];

    public function __construct(SdkService $service)
    {
        $this->service = $service;
    }

    private function logIssue($shapeName, $message, $level=E_WARNING)
    {
        if (!isset($this->issues[$shapeName])) {
            $this->issues[$shapeName] = [];
        }

        if (!isset($this->issues[$shapeName][$message])) {
            $this->issues[$shapeName][$level][$message] = true;
        }
    }

    public function getIssues()
    {
        return $this->issues;
    }

    public function generateInput($operation, $params, $comments)
    {
        $messageShape = $this->service->getOperation($operation)->getInput();
        $code = $this->visit($messageShape, $params, '', [], $comments);

        return "\$result = \$client->" . lcfirst($operation) . "($code);";
    }

    public function generateOutput($operation, $params, $comments)
    {
        $messageShape = $this->service->getOperation($operation)->getOutput();
        return $this->visit($messageShape, $params, '', [], $comments);
    }

    public function __invoke($operation, $params, $comments, $isInput = true)
    {
        return $isInput
            ? $this->generateInput($operation, $params, $comments)
            : $this->generateOutput($operation, $params, $comments);
    }

    private static function isTraversableShapeValue($value) {
        return is_array($value) || $value instanceof \Traversable;
    }

    private function visit(Shape $shape, $value, $indent, $path, $comments)
    {
        switch ($shape['type']) {
            case 'structure':
                return $this->structure($shape, $value, $indent, $path, $comments);
            case 'list':
                return $this->arr($shape, $value, $indent, $path, $comments);
            case 'map':
                return $this->map($shape, $value, $indent, $path, $comments);
            case 'string':
                if (self::isTraversableShapeValue($value)) {
                    $this->logIssue($shape->getName(), 'Invalid Conversion To String');
                }
                return "'{$value}'";
            case 'timestamp':
                return "<DateTimeInterface>";
            case 'blob':
                return "<BLOB>";
            default:
                return $value;
        }
    }

    private function structure(StructureShape $shape, $value, $indent, $path, $comments)
    {
        if (!self::isTraversableShapeValue($value)) {
            $this->logIssue($shape->getName(), 'Invalid Conversion to Structure');
        }

        $lines = ['['];
        foreach ($value as $key => $val) {
            $path[] = ".{$key}";
            $comment = $this->getCommentFor($path, $comments);
            try {
                $shapeVal = $this->visit($shape->getMember($key), $val, "{$indent}    ", $path, $comments);
            } catch (\InvalidArgumentException $e) {
                $this->logIssue($shape->getName(), 'Example Shape Discrepancy');
                break;
            }
            $lines[] = rtrim("{$indent}    '{$key}' => {$shapeVal}, {$comment}");
            array_pop($path);
        }
        $lines[] = "{$indent}]";
        return implode("\n", $lines);
    }

    private function arr(ListShape $shape, $value, $indent, $path, $comments)
    {
        if (!self::isTraversableShapeValue($value)) {
            $this->logIssue($shape->getName(), 'Invalid Conversion to Array');
        }

        $lines = ['['];
        foreach ($value as $ind => $val) {
            $path[] = "[{ind}]";
            $comment = $this->getCommentFor($path, $comments);
            $shapeVal = $this->visit($shape->getMember(), $val, "{$indent}    ", $path, $comments);
            $lines[] = rtrim("{$indent}    {$shapeVal}, {$comment}");
            array_pop($path);
        }
        $lines[] = "{$indent}]";

        return implode("\n", $lines);
    }

    private function map(MapShape $shape, $value, $indent, $path, $comments)
    {
        if (!self::isTraversableShapeValue($value)) {
            $this->logIssue($shape->getName(), 'Invalid Conversion to Map');
        }

        $lines = ['['];
        foreach ($value as $key => $val) {
            $path[] = ".{$key}";
            $comment = $this->getCommentFor($path, $comments);
            $shapeVal = $this->visit($shape->getValue(), $val, "{$indent}    ", $path, $comments);
            $lines[] = rtrim("{$indent}    '{$key}' => {$shapeVal}, {$comment}");
            array_pop($path);
        }
        $lines[] = "{$indent}]";

        return implode("\n", $lines);
    }

    private function getCommentFor($path, $comments)
    {
        $key = preg_replace('/^\./', '', implode('', $path));
        if (isset($comments[$key])) {
            return '// ' . $comments[$key];
        }

        return '';
    }
}


<?php
namespace Aws\Build\Docs;

use Aws\Api\AbstractModel;
use Aws\Api\ApiProvider;
use Aws\Api\ListShape;
use Aws\Api\MapShape;
use Aws\Api\Operation;
use Aws\Api\Service as Api;
use Aws\Api\StructureShape;
use Aws\Api\DocModel;
use TokenReflection\Broker;
use TokenReflection\ReflectionBase;
use TokenReflection\ReflectionFunction;
use TokenReflection\ReflectionMethod;

/**
 * Builds documentation for a given service.
 *
 * @internal
 */
class DocsBuilder
{
    /** @var string HTML template to replace {{ contents }} */
    private $template;

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

    /** @var ApiProvider */
    private $apiProvider;

    /** @var \SplObjectStorage Hash of members to skip when generating shape docs. */
    private $skipMembers;

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

    /** @var string[] */
    private $quickLinks;

    /** @var string[] */
    private $sources;

    /** @var bool[][][] */
    private $issues = [];

    /** @var bool Enables writing of build-issues.log file when set. */
    private $issueLoggingEnabled;

    /** @var array Printable error names for build-issues.log file */
    private static $ERROR_PRINT_NAMES =[
        E_ERROR              => 'Error',
        E_WARNING            => 'Warning',
        E_PARSE              => 'Parse Error',
        E_NOTICE             => 'Notice',
        E_CORE_ERROR         => 'Core Error',
        E_CORE_WARNING       => 'Core Warning',
        E_COMPILE_ERROR      => 'Compile Error',
        E_COMPILE_WARNING    => 'Compile Warning',
        E_USER_ERROR         => 'User Error',
        E_USER_WARNING       => 'User Warning',
        E_USER_NOTICE        => 'User Notice',
        E_STRICT             => 'Strict Notice',
        E_RECOVERABLE_ERROR  => 'Recoverable Error'
    ];

    public function __construct(
        ApiProvider $provider,
        $outputDir,
        $template,
        $baseUrl,
        array $quickLinks,
        array $sources,
        $issueLoggingEnabled = false
    ) {
        $this->apiProvider = $provider;
        $this->outputDir = $outputDir;
        $this->template = $template;
        $this->baseUrl = $baseUrl;
        $this->quickLinks = $quickLinks;
        $this->sources = $sources;
        $this->issueLoggingEnabled = $issueLoggingEnabled;
    }

    public function build()
    {
        $this->skipMembers = new \SplObjectStorage();
        fwrite(STDOUT, "Parsing available service API versions...\n");
        // Collect versions
        $services = [];
        $aliases = [];

        foreach ($this->gatherServiceVersions() as $name => $data) {
            // Skip "latest"
            unset($data['versions']['latest']);
            $ns = $data['namespace'];
            foreach ($data['versions'] as $alias => $version) {
                list($api, $docModel) = call_user_func(
                    "Aws\\{$ns}\\{$ns}Client::applyDocFilters",
                    ApiProvider::resolve($this->apiProvider, 'api', $name, $version),
                    ApiProvider::resolve($this->apiProvider, 'docs', $name, $version)
                );
                $service = new Service($api, $docModel);
                $title = isset($service->shortTitle) && $service->shortTitle !== ''
                    ? $service->shortTitle
                    : $service->title;

                if (isset($services[$title][$version])) {
                    if (empty($aliases[$title][$version])) {
                        $aliases[$title][$version] = [];
                    }
                    $aliases[$title][$version] []= $alias;
                    continue;
                }
                $examples = $this->loadExamples($name, $version);
                if (method_exists("Aws\\{$ns}\\{$ns}Client", 'addDocExamples')) {
                    $examples = call_user_func(
                        "Aws\\{$ns}\\{$ns}Client::addDocExamples",
                        $examples
                    );
                }
                $this->renderService($service, $examples);
                $services[$title][$version] = $service;
            }
        }

        uasort($services, function($a, $b) {
            $serviceA = current($a);
            $serviceB = current($b);
            return strcasecmp($serviceA->namespace, $serviceB->namespace);
        });
        $this->updateHomepage($services);
        $this->updateClients($services);
        $this->updateExceptions($services);
        $this->updateAliases($services, $aliases);
        $this->updateSitemap();
        $this->updateSearch($services);
        if ($this->issueLoggingEnabled) {
            $this->updateIssues();
        }
    }

    private function updateHomepage(array $services)
    {
        $this->updateServiceTable($services);
        $this->updateQuickLinks($services);
    }

    private function updateServiceTable(array $services)
    {
        fwrite(STDOUT, "Building homepage service table\n");
        // Build up the list of services for the homepage.
        $servicesTable = '';

        foreach ($services as $versions) {
            krsort($versions);
            $service = reset($versions);
            $servicesTable .= "
serviceLink}\">{$service->title}	";
            $servicesTable .= "clientLink}\">{$service->client}	";
            $servicesTable .= "
";
            $latest = count($versions) > 1 ? ' (latest)' : '';
            foreach ($versions as $sv) {
                $servicesTable .= "
serviceLink}\">{$sv->version} {$latest}
";
                $latest = '';
            }
            $servicesTable .= "

";
        }

        $this->replaceInner('index', $servicesTable, ':services:');
    }

    private function updateQuickLinks(array $services)
    {
        fwrite(STDOUT, "Updating homepage quick links\n");

        // Determine which services in the provided array should have a quick link
        $services = array_filter($services, function (array $versions) {
            return 0 < count(array_filter($versions, function (Service $service) {
                return in_array($service->name, $this->quickLinks);
            }));
        });

        // Drop all but the latest version of each service from the array
        $services = array_map(function (array $versions) {
            return array_shift($versions);
        }, $services);

        $serviceMap = [];
        foreach ($services as $service) {
            $isOlderVersion = isset($serviceMap[$service->name]) &&
                $service->version < $serviceMap[$service->name]->version;
            if ($isOlderVersion) {
                continue;
            }
            $serviceMap[$service->name] = $service;
        }
        $services = array_values($serviceMap);

        // Sort the services in the order provided in the config
        usort($services, function (Service $a, Service $b) {
            return array_search($a->name, $this->quickLinks)
                - array_search($b->name, $this->quickLinks);
        });

        // Build up the quick links for the home page
        $quickLinks = '';
        foreach ($services as $service) {
            $title = $service->shortTitle ?: $service->title;
            $quickLinks .= <<<EOT

    serviceLink}" role="button">
        name}"> {$title}

EOT;
        }

        $this->replaceInner('index', $quickLinks, ':quickLinks:');
    }

    private function updateIssues()
    {
        $text = '';
        foreach ($this->issues as $level=>$levelIssues) {
            foreach ($levelIssues as $serviceName => $versions) {
                foreach ($versions as $serviceVersion => $messages) {
                    foreach (array_keys($messages) as $message) {
                        if (!empty($text)) {
                            $text .= PHP_EOL;
                        }
                        $levelName = isset(DocsBuilder::$ERROR_PRINT_NAMES[$level])
                            ? DocsBuilder::$ERROR_PRINT_NAMES[$level]
                            : 'Unknown';

                        $text .= '[' . date("Y-m-d H:i:s (T)") . '] '
                            . '[' . $levelName . '] '
                            . $serviceName . '-' . $serviceVersion
                            . ': ' . $message;
                    }
                }
            }
        }

        return (bool) file_put_contents("{$this->outputDir}/build-issues.log", $text);
    }

    private function renderService(Service $service, $examples)
    {
        $html = new HtmlDocument;
        $html->open('div', 'page-header');
        $html->elem('h1', null, "$service->title {$service->version}");
        $html->close();
        $metadata = <<<EOT

Client: clientLink}">{$service->client}

Service ID: {$service->name}

Version: {$service->version}

EOT;
        $html->elem('dl', 'tree well', $metadata);
        $desc = <<<EOT
This page describes the parameters and results for the operations of the
{$service->fullTitle}, and shows how to use the clientLink}">{$service->client}
object to call the described operations. This documentation is specific to the
{$service->version} API version of the service.
EOT;
        $html->elem('p', null, $desc);
        $html->elem('h2', null, 'Operation Summary');
        $desc = <<<EOT
Each of the following operations can be created from a client using
\$client->getCommand('CommandName'), where "CommandName" is the
name of one of the following operations. Note: a command is a value that
encapsulates an operation and the parameters used to create an HTTP request.
EOT;
        $html->elem('p', null, $desc);
        $desc = <<<EOT
You can also create and send a command immediately using the magic methods
available on a client object: \$client->commandName(/* parameters */).
You can send the command asynchronously (returning a promise) by appending the
word "Async" to the operation name: \$client->commandNameAsync(/* parameters */).
EOT;
        $html->elem('p', null, $desc);
        $html->append($this->createHtmlForToc($service, $service->api->getOperations()));

        $this->createHtmlForPaginators($html, $service->api);
        $this->createHtmlForWaiters($html, $service->api);

        $html->section(2, 'Operations');
        foreach ($service->api->getOperations() as $opName => $operation) {
            $html->append($this->createHtmlForOperation(
                $service,
                $opName,
                $operation,
                isset($examples[$opName]) ? $examples[$opName] : []
            ));
        }

        $html->section(2, 'Shapes');
        $map = $service->api->getShapeMap();
        $keys = array_keys($service->api['shapes']);
        sort($keys);
        foreach ($keys as $name) {
            $shape = $map->resolve(['shape' => $name]);
            // Do not render top-level input or output shapes.
            if ($shape['type'] == 'structure'
                && !isset($this->skipMembers[$shape])
            ) {
                $html->section(3, $name, 'shape', 'method-title');
                $html->append($this->renderShape($service->docs, $shape));
            }
        }

        $this->writeThemeFile($service->serviceLink, $html->render());
    }

    private function createHtmlForToc(Service $service, array $operations)
    {
        $html = new HtmlDocument();
        $html->open('ul', 'methods-summary');
        foreach ($operations as $opName => $operation) {
            $item = 'slug($opName)
                . '">' . "{$opName} ( array \$params = [] )";

            if ($description = $service->docs->getOperationDocs($opName)) {
                $shortened = strip_tags($description);
                $shortened = strpos($shortened, '.') === false
                    ? $shortened
                    : substr($shortened, 0, strpos($shortened, '.') + 1);
                $item .= '

' . $shortened . '

';
            }

            $html->elem('li', null, $item);
        }
        $html->close();

        return $html;
    }

    private function writeThemeFile($name, $contents)
    {
        $name = str_replace('.html', '', $name);
        $name .= '.html';
        fwrite(STDOUT, "Writing file: {$name}.\n");
        $html = str_replace('{{ contents }}', $contents, $this->template);
        return (bool) file_put_contents("{$this->outputDir}/{$name}", $html);
    }

    private function replaceInner($name, $replace, $search = '{{ contents }}')
    {
        $name = str_replace('.html', '', $name);
        fwrite(STDOUT, "Updating file: {$name}.\n");
        $path = "{$this->outputDir}/{$name}.html";
        $contents = file_get_contents($path);
        $contents = str_replace($search, $replace, $contents);
        file_put_contents($path, $contents);
    }

    private function gatherServiceVersions()
    {
        $manifest = __DIR__ . '/../../../src/data/manifest.json';

        return json_decode(file_get_contents($manifest), true);
    }

    private function loadExamples($name, $version)
    {
        $path = __DIR__ . "/../../../src/data/{$name}/{$version}/examples-1.json";
        try {
            return \Aws\load_compiled_json($path)['examples'];
        } catch (\InvalidArgumentException $e) {
            return [];
        }
    }

    private function updateClients(array $services)
    {
        fwrite(STDOUT, "Updating client pages with service links\n");

        foreach ($services as $versions) {
            krsort($versions);
            $service = reset($versions);
            $html = '
Supported API Versions
';
            $html .= <<<EOT

This class uses a service description model that is associated at
runtime based on the version option given when constructing the
client. The version option will determine which API operations,
waiters, and paginators are available for a client. Creating a command or a
specific API operation can be done using magic methods (e.g.,
\$client->commandName(/** parameters */), or using the
$\client->getCommand method of the client.

EOT;

            $html .= '

';
            $latest = count($versions) > 1 ? ' (latest)' : '';
            foreach ($versions as $sv) {
                $html .= "
";
                $html .= "
serviceLink}\">{$sv->version} {$latest}
";
                $html .= "
";
                foreach (array_keys($sv->api->getOperations()) as $operation) {
                    $html .= "
";
                    $html .= "serviceLink}#" . strtolower($operation) ."\">$operation";
                    $html .= "
";
                }
                $html .= "
";
                $html .= "
";
                $latest = '';
            }
            $html .= '

';
            $this->replaceInner($service->clientLink, $html, '');
        }
    }

    private function updateExceptions(array $services)
    {
        fwrite(STDOUT, "Updating exception pages with modeled exception data...\n");

        foreach ($services as $versions) {
            krsort($versions);
            $service = reset($versions);
            $shapes = $service->api->getErrorShapes();
            if (count($shapes) > 0) {
                $html = new HtmlDocument;
                $html->section(2, 'Expected Exception Codes');
                $desc = <<<EOT
The following are the known exception codes and corresponding data shapes that
this service may return as part of an error response.
EOT;
                $html->elem('div', null, $desc);
                foreach ($shapes as $shape) {
                    if ($shape['type'] == 'structure'
                        && !isset($this->skipMembers[$shape])
                    ) {
                        $html->section(3, $shape->getName(), 'shape', 'method-title');

                        // Add error syntax
                        $outputShapes = new ShapeIterator($shape, $service->docs);
                        $outputExample = new ExampleBuilder($shape->getName(), false);
                        foreach ($outputShapes as $outputShape) {
                            $outputExample->addShape($outputShape);
                        }
                        $html->elem('pre', null, htmlentities($outputExample->getCode()));

                        // Add member details
                        $html->append($this->renderShape($service->docs, $shape));
                    }
                }
                $this->replaceInner($service->exceptionLink, $html->render(), '');
            }
        }
    }

    private function updateAliases(array $services, array $compatibleVersions)
    {
        fwrite(STDOUT, "Updating redirects for forward-compatible service versions\n");

        foreach ($compatibleVersions as $service => $aliasedVersions) {
            foreach ($aliasedVersions as $version => $aliases) {
                $redirectPage = <<<EOHTML
<!DOCTYPE html>

   <meta
       http-equiv="refresh"
       content="0; url={$services[$service][$version]->serviceLink}">

EOHTML;
                foreach ($aliases as $alias) {
                    $redirectFrom = str_replace($version, $alias, $services[$service][$version]->serviceLink);
                    file_put_contents("{$this->outputDir}/$redirectFrom", $redirectPage);
                }
            }
        }
    }

    private function updateSearch(array $services)
    {
        fwrite(STDOUT, "Updating search index\n");

        $broker = new Broker(new Broker\Backend\Memory());
        foreach ($this->sources as $sourceFile) {
            $broker->processFile($sourceFile);
        }
        $index = array_merge(
            $this->getServiceAutocompleteIndex($services),
            $this->getClassAutocompleteIndex($broker),
            $this->getFunctionAutocompleteIndex($broker)
        );
        $jsonIndex = json_encode($this->utf8Encode($index));
        $js = <<<EOJS
var AWS = AWS || {};
AWS.searchIndex = $jsonIndex;
EOJS;

        file_put_contents("{$this->outputDir}/searchIndex.js", $js);
    }

    private function utf8Encode($mixed)
    {
        if (is_array($mixed)) {
            return array_map([$this, 'utf8Encode'], $mixed);
        } elseif (is_string($mixed)) {
            return utf8_encode($mixed);
        } elseif (empty($mixed)) {
            return '';
        }

        throw new \InvalidArgumentException('Expecting string or array, got ' . gettype($mixed));
    }

    private function getServiceAutocompleteIndex(array $services)
    {
        $autoComplete = [];

        // Drop all but the latest version of each service from the array
        $services = array_map(function (array $versions) {
            return array_shift($versions);
        }, $services);
        // Add operations from latest version of each service to autocomplete index
        foreach ($services as $service) {
            foreach ($service->api->getOperations() as $operation => $def) {
                $autoComplete []= [
                    'name' => $service->namespace . '::' . lcfirst($operation),
                    'match' => $operation,
                    'link' => $service->serviceLink . '#' . strtolower($operation),
                    'description' => strip_tags($service->docs->getOperationDocs($operation)),
                ];
            }
        }

        return $autoComplete;
    }

    private function getClassAutocompleteIndex(Broker $broker)
    {
        $methodsToSkip = $this->getMethodsToSkip();

        $autoComplete = [];
        $classes = array_filter(
            array_values($broker->getClasses()),
            [$this, 'filterVisible']
        );
        foreach ($classes as $class) {
            // Add class to autocomplete index
            $autoComplete []= [
                'name' => $class->getName(),
                'match' => $class->getShortName(),
                'link' => 'class-' . str_replace('\\', '.', $class->getName()) . '.html',
                'description' => $this->shiftDocCommentLine($class->getDocComment()),
            ];

            $methods = array_filter(
                $class->getOwnMethods(\ReflectionMethod::IS_PUBLIC|\ReflectionMethod::IS_PROTECTED),
                [$this, 'filterVisible']
            );

            // Skip over methods implementing base interfaces or that start with
            // an underscore
            $methods = array_filter($methods, function (ReflectionMethod $method) use ($methodsToSkip) {
                $name = $method->getName();
                return !in_array($name, $methodsToSkip)
                    && $name{0} !== '_';
            });

            foreach ($methods as $method) {
                $autoComplete []= [
                    'name' => $class->getName() . '::' . $method->getName(),
                    'match' => $method->getName(),
                    'link' => 'class-' . str_replace('\\', '.', $class->getName()) . '.html'
                        . '#_' . $method->getName(),
                    'description' => '',
                ];
            }
        }

        return $autoComplete;
    }

    private function getMethodsToSkip()
    {
        $interfacesToSkip = [
            '\\ArrayAccess',
            '\\Countable',
            '\\Iterator',
            '\\IteratorAggregate',
            '\\JsonSerializable',
            '\\Serializable',
        ];

        $methodsToSkip = [];
        foreach ($interfacesToSkip as $interfaceToSkip) {
            $methodsToSkip = array_merge(
                $methodsToSkip,
                array_map(function (\ReflectionMethod $method) {
                    return $method->getName();
                }, (new \ReflectionClass($interfaceToSkip))->getMethods())
            );
        }

        return $methodsToSkip;
    }

    private function getFunctionAutocompleteIndex(Broker $broker)
    {
        $functions = array_filter(
            array_values($broker->getFunctions()),
            [$this, 'filterVisible']
        );

        return array_map(function (ReflectionFunction $function) {
            return [
                'name' => $function->getName(),
                'match' => $function->getShortName(),
                'link' => 'function-' . str_replace('\\', '.', $function->getName()) . '.html',
                'description' => $this->shiftDocCommentLine($function->getDocComment()),
            ];
        }, $functions);
    }

    private function filterVisible(ReflectionBase $reflected)
    {
        $annotations = $reflected->getAnnotations();

        return empty($annotations['internal'])
            && empty($annotations['deprecated']);
    }

    private function shiftDocCommentLine($comment)
    {
        $comment = array_map(function ($line) {
            return ltrim($line, '/* ');
        }, explode("\n", $comment));
        $comment = array_filter($comment, function ($line) {
            return !empty($line);
        });

        return array_shift($comment);
    }

    private function createHtmlForWaiters(HtmlDocument $html, Api $service)
    {
        $waiters = $service->getWaiters();

        if (!$waiters) {
            return;
        }

        $desc = <<<EOT
Waiters allow you to poll a resource until it enters into a desired state.
A waiter has a name used to describe what it does, and is associated with an
API operation. When creating a waiter, you can provide the API operation
parameters associated with the corresponding operation. Waiters can be
accessed using the
getWaiter(\$waiterName, \$operationParameters) method of a client object.
This client supports the following waiters:
EOT;

        $html->section(2, 'Waiters');
        $html->open('div', 'element-summary');
            $html->elem('p', null, $desc);
            $html->open('table', 'table table-condensed');
                $html->open('thead');
                    $html->open('tr');
                        $html->elem('th', null, 'Waiter name');
                        $html->elem('th', null, 'API Operation');
                        $html->elem('th', null, 'Delay');
                        $html->elem('th', null, 'Max Attempts');
                    $html->close();
                $html->close();
                $html->open('tbody');
                    foreach ($waiters as $name => $config) {
                        $html->open('tr');
                            $html->elem('td', null, $name);
                            $html->elem('td', null, '' . $config['operation'] . '');
                            $html->elem('td', null, $config['delay']);
                            $html->elem('td', null, $config['maxAttempts']);
                        $html->close();
                    }
                $html->close();
            $html->close();
        $html->close();
    }

    private function createHtmlForPaginators(HtmlDocument $html, Api $service)
    {
        $paginators = $service->getPaginators();

        if (!$paginators) {
            return;
        }

        $desc = <<<EOT
Paginators handle automatically iterating over paginated API results. Paginators
are associated with specific API operations, and they accept the parameters
that the corresponding API operation accepts. You can get a paginator from a
client class using
getPaginator(\$paginatorName, \$operationParameters). This client supports
the following paginators:
EOT;

        $html->section(2, 'Paginators');
        $html->open('div', 'element-summary');
            $html->elem('p', null, $desc);
            $html->open('ul');
                foreach ($paginators as $name => $config) {
                    $html->open('li');
                        $attr = ['href' => '#' . strtolower($name)];
                        $html->elem('a', $attr, $name);
                    $html->close();
                }
            $html->close();
        $html->close();
    }

    private function createHtmlForOperation(Service $service, $name, Operation $operation, $examples)
    {
        $html = new HtmlDocument;
        $html->open('div', 'operation-container');

        // Name
        $html->section(
            3,
            $html->glyph('cog') . ' ' . $name,
            null,
            'method-title',
            $name
        );

        // Code
        $html->elem(
            'pre',
            'opcode',
            '$result = $client->' . lcfirst($name) . '([/* ... */]);' . "\n"
            . '$promise = $client->' . lcfirst($name) . 'Async([/* ... */]);' . "\n"
        );

        // Description
        if ($description = $service->docs->getOperationDocs($name)) {
            $html->elem('div', 'operation-docs', $description);
        }

        // Parameters
        $input = $operation->getInput();
        $this->skipMembers->attach($input);
        $output = $operation->getOutput();
        $this->skipMembers->attach($output);

        $inputShapes = new ShapeIterator($input, $service->docs);
        $inputExample = new ExampleBuilder($name);
        foreach ($inputShapes as $shape) {
            $inputExample->addShape($shape);
        }

        $html
            ->elem('h4', null, 'Parameter Syntax')
            ->elem('pre', null, htmlentities($inputExample->getCode()))
            ->elem('h4', null, 'Parameter Details')
            ->append($this->renderShape($service->docs, $input, false));

        // Results
        $html->elem('h4', null, 'Result Syntax');

        if (!count($output->getMembers())) {
            $html->elem('pre', null, '[]');
            $html->elem('h4', null, 'Result Details');
            $html->elem('div', 'alert alert-info', 'The results for this operation are always empty.');
        } else {
            $outputShapes = new ShapeIterator($output, $service->docs);
            $eventStreamExample = null;
            foreach ($outputShapes as $shape) {
                if (!empty($shape['eventstream'])) {
                    $eventStreamExample = new EventStreamExampleBuilder($shape['param']);
                }
            }
            $outputExample = new ExampleBuilder($name, false);
            foreach ($outputShapes as $shape) {
                $outputExample->addShape($shape);
                if ($eventStreamExample) {
                    $eventStreamExample->addShape($shape);
                }
            }
            $html->elem('pre', null, htmlentities($outputExample->getCode()))
                ->elem('h4', null, 'Result Details')
                ->append($this->renderShape($service->docs, $output, false));
            if ($eventStreamExample) {
                $desc = <<<EOT
To use an EventParsingIterator, you will need to loop over the events it will
generate and check the top-level field to determine which type of event it is.
EOT;

                $html->elem('h5', null, 'Using an EventParsingIterator')
                    ->elem('p', null, $desc)
                    ->elem('pre', null, htmlentities($eventStreamExample->getCode()));
            }
        }

        // Errors
        $html->elem('h4', null, 'Errors');
        $errors = $operation->getErrors();
        if (!$errors) {
            $html->elem('p', null, 'There are no errors described for this operation.');
        } else {
            $html->open('ul');
            foreach ($errors as $error) {
                $desc = $service->docs->getErrorDocs($error->getName())
                    ?: 'This error does not currently have a description.';
                $html
                    ->open('li')
                        ->open('p')
                            ->elem(
                                'a',
                                [
                                    'href' => $service->exceptionLink . '#shape-'
                                        . strtolower($error->getName())
                                ],
                                $error['name'] . ': ')
                            ->elem('p', null, $desc)
                        ->close()
                    ->close();
            }
            $html->close();
        }

        // Examples
        if (!empty($examples)) {
            $generator = new CodeSnippetGenerator($service->api);
            $html->elem('h4', null, 'Examples');
            foreach ($examples as $number => $example) {
                $exampleNumber = $number + 1;
                $exampleId = $this->exampleSlug($name, $exampleNumber);
                $html->open('h5', ['id' => $exampleId]);
                $html->elem('span', null, 'Example ' . $exampleNumber . ': ' . $example['title']);
                $html->elem('a', ['href' => '#' . $exampleId], $html->glyph('link'));
                $html->close();
                $html->elem('p', null, $example['description']);
                $comments = $example['comments'];
                $html->elem('pre', null, $generator->generateInput(
                    $name,
                    isset($example['input']) ? $example['input'] : [],
                    isset($comments['input']) ? $comments['input'] : []
                ));
                if (isset($example['output'])) {
                    $html->elem('p', null, 'Result syntax:');
                    $html->elem('pre', null, $generator->generateOutput(
                        $name,
                        $example['output'],
                        isset($comments['output'])
                            ? $comments['output']
                            : []
                    ));
                }
            }

            $generatorIssues = $generator->getIssues();
            foreach ($generatorIssues as $shapeName => $levelIssues) {
                foreach ($levelIssues as $level => $messages) {
                    foreach (array_keys($messages) as $message) {
                        $generatorIssues[$shapeName][$level][$message] = $name;
                    }
                }
            }
            $this->logIssues(
                $service->api->getServiceName(),
                $service->api->getApiVersion(),
                $generatorIssues
            );
        }

        $html->close(); // operation-container

        return $html;
    }

    private function logIssues($serviceName, $serviceVersion, $issuesToLog)
    {
        if (!isset($this->issues[$serviceName][$serviceVersion])) {
            $this->issues[$serviceName][$serviceVersion] = [];
        }

        foreach ($issuesToLog as $shapeName=>$shapeIssues) {
            foreach ($shapeIssues as $level => $messages) {
                foreach ($messages as $message => $exampleName) {
                    $this->issues[$level][$serviceName][$serviceVersion][
                        $exampleName . ' has an issue - '
                        . $message . ' on ' . $shapeName
                    ] = true;
                }
            }
        }
    }

    private function renderShape(
        DocModel $docs,
        StructureShape $shape,
        $showTitle = true
    ) {
        $html = new HtmlDocument();

        $html->open('div', 'shape-container');

        // Disable show title for input and output parameters.
        if ($showTitle) {
            $desc = $docs->getShapeDocs($shape['name'], null, null);
            if ($desc) {
                $html->elem('h5', null, 'Description');
                $html->elem('div', 'shape-description', $desc);
            }
        }

        $html->elem('h5', null, 'Members');
        $html->open('dl', 'shape-members');

        $members = $shape->getMembers();
        ksort($members);
        foreach ($members as $name => $member) {
            $html->open('dt', 'param-def');
            $html->elem('span', 'term', $name);
            $html->close();
            $html->open('dd', 'param-def');
            $required = !empty($shape['required'])
                && in_array($name, $shape['required']);
            $html->append($this->describeParam($member, $required));
            $desc = $docs->getShapeDocs($member['name'], $shape['name'], $name);
            $html->elem('div', 'param-def-doc', $desc);
            $html->close();
        }

        $html->close();
        $html->close();

        return $html;
    }

    private function describeParam(AbstractModel $member, $required = false)
    {
        $html = new HtmlDocument();
        if ($member instanceof StructureShape) {
            $typeDesc = $this->getMemberText($member);
        } elseif ($member instanceof ListShape) {
            $typeDesc = 'Array of ' . $this->getMemberText($member->getMember()) . 's';
        } elseif ($member instanceof MapShape) {
            $typeDesc = 'Associative array of custom strings keys ('
                . $member->getKey()->getName() . ') to '
                . $this->getMemberText($member->getValue()) . 's';
        } else {
            $typeDesc = $this->getPrimitivePhpType($member);
        }

        $html->open('div', 'param-attributes')->open('ul');
        if ($required) {
            $html->elem('li', 'required', 'Required: Yes');
        }
        $html->elem('li', '', 'Type: ' . $typeDesc);
        $html->close();
        $html->close();

        return $html;
    }

    private function getMemberText(AbstractModel $member)
    {
        if ($member instanceof StructureShape) {
            if (!empty($member['eventstream'])) {
                return $this->getEventStreamMemberText($member);
            }
            if (!empty($member['document'])) {
                return $this->getDocumentText($member);
            }
            return $this->memberLink($member->getName()) . ' structure';
        } elseif ($member instanceof ListShape) {
            switch ($member->getMember()['type']) {
                case 'string': return 'strings';
                case 'double': return 'floats';
                case 'integer': return 'integers';
                case 'boolean': return 'booleans';
                case 'structure': return $this->getMemberText($member->getMember()) . 's';
            }
        } elseif ($member instanceof MapShape) {
            switch ($member->getValue()['type']) {
                case 'string': return 'strings';
                case 'double': return 'floats';
                case 'integer': return 'integers';
                case 'boolean': return 'booleans';
                case 'structure': return $this->getMemberText($member->getValue()) . 's';
            }
        }

        return $this->getPrimitivePhpType($member);
    }

    private function getEventStreamMemberText(StructureShape $member)
    {
        return 'EventParsingIterator supplying the following structures: '
            . implode(', ',
                array_map(
                    [$this, 'memberLink'],
                    array_reduce(
                        $member->getMembers(),
                        function ($carry, $item) {
                            $carry []= $item['name'];
                            return $carry;
                        },
                        []
                    )
                )
            );
    }

    private function getDocumentText(StructureShape $member)
    {
        return 'document (null|bool|string|numeric) or an (array|associative array)'
        . ' whose members are all valid documents';
    }

    private function getPrimitivePhpType($member)
    {
        switch ($member['type']) {
            case 'long': return 'long (int|float)';
            case 'integer': return 'int';
            case 'blob': return 'blob (string|resource|Psr\Http\Message\StreamInterface)';
            case 'char': return 'char (string)';
            case 'timestamp': return 'timestamp (string|DateTime or anything parsable by strtotime)';
            case 'string':
                if ($member['jsonvalue']){
                    return 'string (string|number|array|map or anything parsable by json_encode)';
                }
            default: return $member['type'];
        }
    }

    private function exampleSlug($name, $number)
    {
        return strtolower($name) . '-example-' . $number;
    }

    private function memberSlug($name)
    {
        return 'shape-' . strtolower($name);
    }

    private function memberLink($name)
    {
        return 'memberSlug($name) . '">' . $name . '';
    }

    private function updateSitemap()
    {
        fwrite(STDOUT, "Updating sitemap\n");

        $writer = new \SimpleXMLElement("<urlset></urlset>");
        $writer->addAttribute('xmlns', "http://www.sitemaps.org/schemas/sitemap/0.9");

        $linksToIndex = new \GlobIterator("{$this->outputDir}/*.html", \FilesystemIterator::CURRENT_AS_FILEINFO);
        foreach ($linksToIndex as $link) {
            $url = $writer->addChild('url');
            $url->addChild('loc', "{$this->baseUrl}{$link->getBasename()}");
        }

        $writer->asXML("{$this->outputDir}/sitemap.xml");
    }
}


<?php
namespace Aws\Build\Docs;

/**
 * @internal
 */
class EventStreamExampleBuilder
{
    private $buffer;
    private $shapeName;
    private $skipLevel;
    private $elseRequired;
    private $closed;
    private $tabLevel;

    public function __construct($shapeName)
    {
        $this->buffer = '';
        $this->shapeName = $shapeName;
        $this->skipLevel = 0;
        $this->elseRequired = false;
        $this->closed = true;
        $this->tabLevel = 1;
    }

    public function getCode()
    {
        $code = 'foreach($result[' . $this->shapeName . '] as $event) {' . "\n";
        $code .= $this->buffer;
        $code .= "\n}";

        return str_replace("\t", '    ', $code);
    }

    public function addShape(array $shape)
    {
        if ($this->tabLevel === 1
            && !empty($shape['param'])
            && $shape['param'] === $this->shapeName
        ) {
            $this->closed = false;
            return;
        }
        if ($this->closed) {
            return;
        }
        // Handle skips and closers
        if ($shape['name'] === 'closer') {
            $this->tabLevel--;
            if ($this->tabLevel === 2) {
                $this->tabLevel--;
                $this->buffer .= $this->tab() . '}';
                $this->elseRequired = true;
            } else if ($this->tabLevel === 0) {
                $this->closed = true;
            }
            return;
        }

        // Write the parameter key.
        if ($shape['param'] !== '<index>') {
            if ($this->tabLevel === 1) {
                if (!$this->elseRequired) {
                    $this->buffer .= $this->tab();
                } else {
                    $this->buffer .= ' else ';
                }
                $this->buffer .= 'if (isset($event[' . $shape['param'] . '])) {' . "\n";
                $this->tabLevel++;
                $this->buffer .= $this->tab() . '// Handle the ' . $shape['param'] . " event.\n";
            } else {
                $this->elseRequired = false;
            }
        }

        if ($shape['complex']) {
            $this->tabLevel++;
        }
    }

    private function tab()
    {
        return str_repeat("\t", $this->tabLevel);
    }
}


<?php
namespace Aws\Build\Docs;

/**
 * @internal
 */
class ExampleBuilder
{
    private $buffer;
    private $operation;
    private $skipLevel;
    private $tabLevel;
    private $isInput;

    public function __construct($operation, $isInput = true)
    {
        $this->buffer = '';
        $this->operation = $operation;
        $this->skipLevel = 0;
        $this->tabLevel = 1;
        $this->isInput = $isInput;
    }

    public function getCode()
    {
        if ($this->isInput) {
            $code = '$result = $client->'
                . lcfirst($this->operation)
                . "([\n{$this->buffer}]);\n";
        } else {
            $code = "[\n{$this->buffer}]";
        }

        return str_replace("\t", '    ', $code);
    }

    public function addShape(array $shape)
    {
        // Handle skips and closers
        if ($this->skipLevel) {
            $this->skipLevel--;
            return;
        } elseif ($shape['name'] === 'closer') {
            if ($shape['type'] === 'list' || $shape['type'] === 'map') {
                $this->buffer .= $this->tab() . "// ...\n";
            }
            $this->tabLevel--;
            $this->buffer .= $this->tab() . "],\n";
            return;
        }

        // Write the parameter key.
        $this->buffer .= $this->tab();
        if ($shape['param'] !== '<index>') {
            $this->buffer .= "{$shape['param']} => ";
        }

        // Write the parameter value.
        if ($shape['complex']) {
            if ($shape['recursive']) {
                $this->buffer .= "[...],";
            } elseif (!in_array($shape['complex'], ['structure', 'list', 'map', 'mixed'])) {
                $this->buffer .= "[" . $this->getSimpleValue(['type' => $shape['complex']]) . ", ...],";
                $this->skipLevel += 2;
            } else {
                $this->buffer .= "[";
                $this->tabLevel++;
            }
        } else {
            $this->buffer .= $this->getSimpleValue($shape) . ",";
        }

        $this->buffer .= $this->getTags($shape);

        $this->buffer .= "\n";
    }

    private function getSimpleValue(array $shape)
    {
        switch ($shape['type']) {
            case 'integer':
                return '<integer>';
            case 'string':
                return isset($shape['enum'])
                    ? "'" . implode('|', $shape['enum']) . "'"
                    : "'<string>'";
            case 'boolean':
                return 'true || false';
            case 'stream':
                return '<string || resource || Psr\\Http\\Message\\StreamInterface>';
            case 'timestamp':
                return $this->isInput
                    ? '<integer || string || DateTime>'
                    : '<DateTime>';
            case 'float':
                return '<float>';
            default:
                return '<value>';
        }
    }

    private function getTags(array $shape)
    {
        $tags = [];
        if ($this->isInput && $shape['required']) {
            $tags[] = 'REQUIRED';
        }
        if ($shape['recursive']) {
            $tags[] = 'RECURSIVE';
        }
        if (!empty($shape['eventstream'])) {
            $tags[] = 'EventParsingIterator';
        }

        return $tags ? ' // ' . implode(', ', $tags) : '';
    }

    private function tab()
    {
        return str_repeat("\t", $this->tabLevel);
    }
}


<?php
namespace Aws\Build\Docs;

/**
 * @internal
 */
class HtmlDocument
{
    /** @var array */
    private $lines;

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

    /** @var \SplStack */
    private $stack;

    public function __construct()
    {
        $this->lines = [];
        $this->currentLine = 0;
        $this->stack = new \SplStack();
    }

    public function render($baseTabLevel = 0)
    {
        return implode("\n", array_map(function ($line) use ($baseTabLevel) {
            return str_repeat("\t", $baseTabLevel) . $line;
        }, $this->lines));
    }

    public function section($num, $text, $anchorPrefix = null, $class = null, $label = null)
    {
        $anchor = $this->slug($anchorPrefix . '-' . $text);
        $ariaLabel = empty($label) ? null : 'aria-label="Go to ' . $label . '"';
        $link = ' ' . $this->glyph('link') . '';
        $attrs = ['id' => $anchor];
        if ($class) {
            $attrs['class'] = $class;
        }
        $this->elem("h{$num}", $attrs, $text . $link);

        return $this;
    }

    public function open($tag, $attr = null)
    {
        $startingLine = $this->currentLine;
        $this->elem($tag, $attr);
        $this->stack->push([$tag, $startingLine]);

        return $this;
    }

    public function close($collapse = false)
    {
        list($tag, $startLine) = $this->stack->pop();
        $this->lines[$this->currentLine++] = $this->getTab() . '</' . $tag . '>';

        if ($collapse) {
            $this->lines[$startLine] = $this->lines[$startLine] . implode('', array_map('trim',
                array_slice($this->lines, $startLine + 1, $this->currentLine - $startLine + 1)
            ));
            $this->currentLine = $startLine + 1;
            for ($i = $this->currentLine; isset($this->lines[$i]); $i++) {
                unset($this->lines[$i]);
            }
        }

        return $this;
    }

    public function elem($tag, $attr = null, $content = null)
    {
        $attr = $attr
            ? (is_string($attr) ? ['class' => $attr] : (array) $attr)
            : [];

        $buffer = $this->getTab() . '<' . $tag;
        foreach ($attr as $k => $v) {
            $buffer .= ' ' . $k . '="' . $v . '"';
        }
        $buffer .= '>';

        if ($content !== null) {
            $buffer .= $content . '</' . $tag . '>';
        }

        $this->lines[$this->currentLine++] = $buffer;

        return $this;
    }

    public function slug($text)
    {
        $sanitized = preg_replace('/[^A-Za-z0-9-]/', '', strip_tags($text));

        return strtolower(str_replace(' ', '-', trim($sanitized, ' -')));
    }

    public function glyph($type)
    {
        return '';
    }

    public function append(HtmlDocument $html)
    {
        $this->lines = array_merge($this->lines, $html->lines);
        $this->currentLine += count($html->lines);

        return $this;
    }

    private function getTab()
    {
        return str_repeat("\t", count($this->stack));
    }
}


<?php
namespace Aws\Build\Docs;

use Aws\Api\ApiProvider;

/**
 * Builds redirect map file across services
 *
 * @internal
 */
class RedirectMapBuilder
{
    /** @var string */
    private $outputDir;

    /** @var ApiProvider */
    private $apiProvider;

    public function __construct(ApiProvider $provider, $outputDir)
    {
        $this->apiProvider = $provider;
        $this->outputDir = $outputDir;
    }

    public function build()
    {
        $redirectEntry = [];
        $reWriteRulePrefix = "RewriteRule ^/goto/SdkForPHPV3/";
        $docPathPrefix = " /aws-sdk-php/v3/api/";
        $flags = " [L,R,NE]\n";
        $skipCount = 0;
        // Using latest version per service
        foreach ($this->gatherServiceVersions() as $name => $data) {
            $ns = $data['namespace'];
            $version = $data['versions']['latest'];
            list($api, $docModel) = call_user_func(
                "Aws\\{$ns}\\{$ns}Client::applyDocFilters",
                ApiProvider::resolve($this->apiProvider, 'api', $name, $version),
                ApiProvider::resolve($this->apiProvider, 'docs', $name, $version)
            );
            $service = new Service($api, $docModel);
            // Add rewrite rule per operation per service
            foreach ($service->api->getOperations() as $key => $definition) {
                // Skip rewrite rule if service doesn't has a uid in metadata
                if ($service->uid) {
                    $entry = $reWriteRulePrefix . $service->uid . '/' . $key . '$';
                    $entry .= $docPathPrefix . 'api-' . $service->slug . ".html#" . strtolower($key);

                    $redirectEntry []= $entry . $flags;
                    $skipCount++;
                }
            }
            // Fall back to service client main page if version not found
            if (!empty($service->uid)) {
                $servicePrefix = $service->uid;
            } else {
                $servicePrefix = $service->name;
            }
            $redirectEntry []= $reWriteRulePrefix . $servicePrefix . '(.*)'
                . $docPathPrefix . 'class-Aws.'. $service->namespace . '.'
                . $service->namespace . 'Client.html' . $flags;
            $skipCount++;
        }
        // Apply skip check at beginning if not PHP SDK related
        array_unshift($redirectEntry,
            "RewriteCond %{REQUEST_URI} !^\\/goto\\/SdkForPHPV3\\/.*$\n",
            "RewriteRule \".*\" \"-\" [S={$skipCount}]\n"
        );

        // Fall back to api main page if service not found
        $redirectEntry []= $reWriteRulePrefix . '(.*)' . $docPathPrefix . 'index.html' . $flags;

        // Redirect old /AWSSDKforPHP/ paths
        $reWriteRulePrefix = 'RewriteRule ^/AWSSDKforPHP/';
        array_unshift($redirectEntry,
            "RewriteCond %{REQUEST_URI} !^\\/AWSSDKforPHP\\/.*$\n",
            "RewriteRule \".*\" \"-\" [S={4}]\n",
            $reWriteRulePrefix . 'latest(.*) /aws-sdk-php/latest/index.html' . $flags,
            $reWriteRulePrefix . 'v3(.*) /aws-sdk-php/v3/api/index.html' . $flags,
            $reWriteRulePrefix . 'v2(.*) /aws-sdk-php/v2/api/index.html' . $flags,
            $reWriteRulePrefix . 'v1(.*) /aws-sdk-php/v1/index.html' . $flags
        );

        file_put_contents($this->outputDir, $redirectEntry);
    }

    private function gatherServiceVersions()
    {
        $manifest = __DIR__ . '/../../../src/data/manifest.json';

        return json_decode(file_get_contents($manifest), true);
    }
}


<?php
namespace Aws\Build\Docs;

use Aws\Api\Service as Api;
use Aws\Api\DocModel;
use Aws\Sdk;

/**
 * Serves as a DTO for information about a service required to build various
 * aspects of the docs.
 *
 * @internal
 */
class Service
{
    /** @var Api */
    public $api;

    /** @var string */
    public $client;

    /** @var string */
    public $clientName;

    /** @var string */
    public $clientLink;

    /** @var DocModel */
    public $docs;

    /** @var string */
    public $exceptionLink;

    /** @var string */
    public $fullNamespace;

    /** @var string */
    public $fullTitle;

    /** @var string */
    public $name;

    /** @var string */
    public $namespace;

    /** @var string */
    public $namespaceLink;

    /** @var string */
    public $serviceLink;

    /** @var string */
    public $shortTitle;

    /** @var string */
    public $slug;

    /** @var string */
    public $title;

    /** @var string */
    public $version;

    public function __construct(Api $api, DocModel $docs)
    {
        $this->name = $api->getServiceName();
        $this->api = $api;
        $this->namespace = $this->getServiceNamespace($this->name);
        $this->version = $api->getApiVersion();
        $this->docs = $docs;
        $this->slug = $this->name . '-' . $this->version;
        $this->uid = $api->getUid();
        $this->clientName = $this->namespace . 'Client';
        $this->client = 'Aws\\' . $this->namespace . '\\' . $this->clientName;
        $this->clientLink = 'class-Aws.' . $this->namespace . '.' . $this->namespace . 'Client.html';
        $this->exceptionLink = 'class-Aws.' . $this->namespace . '.Exception.' . $this->namespace . 'Exception.html';
        $this->namespaceLink = 'namespace-Aws.' . $this->namespace . '.html';
        $this->serviceLink = 'api-' . $this->slug . '.html';
        $this->shortTitle = $api->getMetadata('serviceAbbreviation');
        $this->title = $api->getServiceFullName();
        $this->fullNamespace = 'Aws\\' . $this->namespace;
        $this->fullTitle = $this->title . ' (' . $this->version . ')';
    }

    private function getServiceNamespace($service)
    {
        static $namespaces;

        if (!$namespaces) {
            // Get the namespaces from the directories.
            foreach (glob(__DIR__ . '/../../../src/*') as $dir) {
                $base = basename($dir);
                if (class_exists("Aws\\{$base}\\{$base}Client")) {
                    $namespaces[Sdk::getEndpointPrefix($base)] = $base;
                }
            }
        }

        if (!isset($namespaces[$service])) {
            throw new \RuntimeException("Could not determine the namespace for {$service}.");
        }

        return $namespaces[$service];
    }
}


<?php
namespace Aws\Build\Docs;

use Aws\Api\ListShape;
use Aws\Api\MapShape;
use Aws\Api\Shape;
use Aws\Api\StructureShape;
use Aws\Api\DocModel;

/**
 * @internal
 */
class ShapeIterator implements \Iterator
{
    private static $keys = ['name', 'param', 'type', 'docs', 'recursive',
        'required', 'min', 'max', 'enum', 'pattern', 'complex'];

    private static $types = [
        'structure' => 'structure',
        'map'       => 'map',
        'list'      => 'list',
        'integer'   => 'integer',
        'long'      => 'integer',
        'string'    => 'string',
        'character' => 'string',
        'byte'      => 'string',
        'blob'      => 'stream',
        'double'    => 'float',
        'float'     => 'float',
        'boolean'   => 'boolean',
        'timestamp' => 'timestamp',
    ];

    /** @var DocModel */
    private $docs;

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

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

    public function __construct(StructureShape $root, DocModel $docs)
    {
        $this->docs = $docs;
        $this->shapes = [];
        $this->index = 0;
        $this->walkStructure($root, [], []);
        $this->rewind();
    }

    #[\ReturnTypeWillChange]
    public function current()
    {
        return $this->shapes[$this->index];
    }

    #[\ReturnTypeWillChange]
    public function next()
    {
        $this->index++;
    }

    #[\ReturnTypeWillChange]
    public function key()
    {
        return $this->index;
    }

    #[\ReturnTypeWillChange]
    public function valid()
    {
        return isset($this->shapes[$this->index]);
    }

    #[\ReturnTypeWillChange]
    public function rewind()
    {
        $this->index = 0;
    }

    private function walkComplexMember(Shape $shape, array $visited, array $path)
    {
        // Store the path associated with this shape to aid in recursion detection.
        $visited[$shape->getName()] = $path;

        // Delegate traversal to the proper method
        if ($shape instanceof StructureShape) {
            $this->walkStructure($shape, $visited, $path);
        } elseif ($shape instanceof MapShape) {
            $this->walkMap($shape, $visited, $path);
        } elseif ($shape instanceof ListShape) {
            $this->walkList($shape, $visited, $path);
        } else {
            throw new \UnexpectedValueException('Expected a complex shape.');
        }

        // Remove the shape from visited collection. This is also to aid in recursion detection.
        unset($visited[$shape->getName()]);

        // Add a "closer shape" that will be yielded by the iterator. These
        // shapes indicate the closure of a complex shape, and make it easier
        // to code the visual representation of the shape tree.
        $this->shapes[$this->index++] = ['name' => 'closer', 'type' => $shape->getType()];
    }

    private function walkStructure(StructureShape $shape, array $visited, array $path)
    {
        $shapeMembers = $shape->getMembers();
        ksort($shapeMembers);
        foreach ($shapeMembers as $ref => $member) {
            // Create the shape data from the Shape object.
            $data = $this->createShapeData($member);
            $path[] = $data['param'] = "'{$ref}'";
            $data['docs'] = $this->docs->getShapeDocs($member->getName(), $shape->getName(), $ref);
            $data['recursive'] = $this->getRecursionPath($member, $visited);
            $data['path'] = $path;

            // For structures, we have to see if the member is required.
            if (isset($shape['required']) && in_array($ref, $shape['required'])) {
                $data['required'] = true;
            }

            // Save the shape data.
            $this->shapes[$this->index++] = $data;

            // If the member is complex and not recursive, traverse it too.
            if ($data['complex'] && !$data['recursive']) {
                $this->walkComplexMember($member, $visited, $path);
            }
        }
    }

    private function walkMap(MapShape $shape, array $visited, array $path)
    {
        $key = $shape->getKey();
        $value = $shape->getValue();

        // Create the shape data from the Shape object.
        $data = $this->createShapeData($value);
        $path[] = $data['param'] = "'<{$key['name']}>'";
        $data['docs'] = $this->docs->getShapeDocs($value->getName(), $shape->getName(), 'value');
        $data['recursive'] = $this->getRecursionPath($value, $visited);
        $data['path'] = $path;

        // Save the shape data.
        $this->shapes[$this->index++] = $data;

        // If the value is complex and not recursive, traverse it too.
        if ($data['complex'] && !$data['recursive']) {
            $this->walkComplexMember($value, $visited, $path);
        }
    }

    private function walkList(ListShape $shape, array $visited, array $path)
    {
        $member = $shape->getMember();

        // Create the shape data from the Shape object.
        $data = $this->createShapeData($member);
        $path[] = $data['param'] = "<index>";
        $data['docs'] = $this->docs->getShapeDocs($member->getName(), $shape->getName(), 'member');
        $data['recursive'] = $this->getRecursionPath($member, $visited);
        $data['path'] = $path;

        // Save the shape data.
        $this->shapes[$this->index++] = $data;

        // If the member is complex and not recursive, traverse it too.
        if ($data['complex'] && !$data['recursive']) {
            $this->walkComplexMember($member, $visited, $path);
        }
    }

    private function getRecursionPath(Shape $shape, array $visited)
    {
        return isset($visited[$shape->getName()])
            ? $visited[$shape->getName()]
            : null;
    }

    private function createShapeData(Shape $shape)
    {
        // Get basic data from the shape object
        $data = $shape->toArray();
        unset($data['members'], $data['member'], $data['key'], $data['value'], $data['required']);
        $data += array_fill_keys(self::$keys, null);

        // Determine the type.
        $data['type'] = self::$types[$shape->getType()];

        // Determine the subtype for complex types.
        if ($data['type'] === 'structure') {
            $data['complex'] = 'mixed';
        } elseif ($data['type'] === 'map') {
            /** @var MapShape $shape */
            $data['complex'] = self::$types[$shape->getValue()->getType()];
        } elseif ($data['type'] === 'list') {
            /** @var ListShape $shape */
            $data['complex'] = self::$types[$shape->getMember()->getType()];
        }

        return $data;
    }
}


<?php

namespace Aws\Test\Integ;

use Aws\Api\ApiProvider;
use Aws\Api\Service;
use Aws\AwsClient;
use Aws\ClientSideMonitoring\ConfigurationProvider;
use Aws\ClientSideMonitoring\Exception\ConfigurationException;
use Aws\Command;
use Aws\Credentials\Credentials;
use Aws\Exception\AwsException;
use Aws\MockHandler;
use Aws\MonitoringEventsInterface;
use Aws\Result;
use Aws\Sdk;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\Assert;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;

class ClientSideMonitoringContext extends TestCase
    implements Context, SnippetAcceptingContext
{

    /**
     * Child process ID used by the test UDP server
     * @var int
     */
    private static $childPid;

    /**
     * Original environment variables before class instantiated
     * @var array
     */
    private static $originalEnv;

    /**
     * Store output filename for the UDP server's received datagrams
     * @var string
     */
    private static $outputFile;

    /**
     * @var Sdk
     */
    private $sdk;

    private $defaultEnv = [];
    private $sharedConfig;
    private $testApiProvider;
    private $testData;
    private $testDir;
    private $testServices = [];

    private $configKeys = [
        'region' => 'region',
    ];

    private $udpConfig = [
        'address' => '127.0.0.1',
        'port' => 31000,
        'shutdown' => 'shutdown',
    ];

    public function __construct()
    {
        $this->testDir = __DIR__ . "/csm";
    }

    /**
     * @BeforeScenario
     */
    public static function prepare()
    {
        if (!extension_loaded('pcntl') || !function_exists('pcntl_fork')) {
            throw new \RuntimeException("Test skipped because the 'pcntl' extension is not loaded or enabled.");
        }
        if (!extension_loaded('sockets') || !function_exists('socket_create')) {
            throw new \RuntimeException("Test skipped because the 'sockets' extension is not loaded or enabled.");
        }

        self::$originalEnv = [
            'enabled' => getenv(ConfigurationProvider::ENV_ENABLED) ?: '',
            'port' => getenv(ConfigurationProvider::ENV_PORT) ?: '',
            'client_id' => getenv(ConfigurationProvider::ENV_CLIENT_ID) ?: '',
            'profile' => getenv(ConfigurationProvider::ENV_PROFILE) ?: '',
        ];

        $dir = sys_get_temp_dir() . '/.aws';
        if (!is_dir($dir)) {
            mkdir($dir, 0777, true);
        }
        self::$outputFile = $dir . '/test_datagrams.json';
    }

    /**
     * @AfterScenario
     */
    public static function cleanup()
    {
        putenv(ConfigurationProvider::ENV_ENABLED . '=' .
            self::$originalEnv['enabled']);
        putenv(ConfigurationProvider::ENV_PORT . '=' .
            self::$originalEnv['port']);
        putenv(ConfigurationProvider::ENV_CLIENT_ID . '=' .
            self::$originalEnv['client_id']);
        putenv(ConfigurationProvider::ENV_PROFILE . '=' .
            self::$originalEnv['profile']);

        if (file_exists(self::$outputFile)) {
            unlink(self::$outputFile);
        }

        if (!empty(self::$childPid) && posix_getpgid(self::$childPid) !== false) {
            if (posix_kill(self::$childPid, SIGTERM) === false) {
                posix_kill(self::$childPid, SIGKILL);
            };
        }
    }

    /**
     * @Given I have loaded a test manifest file called :filename
     */
    public function iHaveLoadedATestManifestFileCalled($filename)
    {
        $manifest = json_decode(
            file_get_contents("{$this->testDir}/data/{$filename}"),
            true
        );
        $provider = ApiProvider::manifest("{$this->testDir}/data/", $manifest);
        $serviceDirectories = glob("{$this->testDir}/data/*" , GLOB_ONLYDIR);
        foreach ($serviceDirectories as $directory) {
            $definition = $provider('api', basename($directory), 'latest');
            $service = new Service($definition, $provider);
            $this->testServices[$service->getServiceId()] = $service;
        }
        $this->testApiProvider = $provider;
    }

    /**
     * @Given I have loaded a test case file called :filename
     */
    public function iHaveLoadedATestCaseFileCalled($filename)
    {
        $this->testData = json_decode(
            file_get_contents("{$this->testDir}/{$filename}"),
            true
        );

        if (!empty($this->testData['defaults']['configuration']['environmentVariables'])) {
            foreach ($this->testData['defaults']['configuration']['environmentVariables']
                     as $key => $value) {
                $this->defaultEnv[$key] = $value;
            }
        }

        $sharedConfig = [
            'version' => 'latest'
        ];
        foreach ($this->testData['defaults']['configuration'] as $key => $value) {
            if (array_key_exists($key, $this->configKeys)) {
                $sharedConfig[$this->configKeys[$key]] = $value;
            }
        }
        if (isset($this->testData['defaults']['configuration']['accessKey'])) {
            $sharedConfig['credentials'] = $this->createCredentials(
                $this->testData['defaults']['configuration']
            );
        }
        $this->sharedConfig = $sharedConfig;
        $this->sdk = new Sdk($sharedConfig);
    }

    /**
     * @Then I successfully run the test cases against a test server
     */
    public function iSuccessfullyRunTheTestCasesAgainstATestServer()
    {
        foreach ($this->testData['cases'] as $case) {

            $pid = pcntl_fork();

            if ($pid === -1) {
                throw new \RuntimeException("Test failed because 'pcntl_fork' was unable to fork PHP process.");
            } elseif ($pid === 0) {
                try {
                    $this->startUdpServer();
                } catch (\Exception $e) {
                    exit();
                }
            } else {
                try {
                    self::$childPid = $pid;

                    usleep(500000);

                    if (isset($case['configuration']['environmentVariables'])) {
                        $this->clearAndSetEnv($case['configuration']['environmentVariables']);
                    } else {
                        $this->clearAndSetEnv($this->defaultEnv);
                    }

                    $events = [];
                    $params = $this->prepareTestCaseParams($case);

                    foreach ($case['apiCalls'] as $apiCall) {
                        $client = $this->createClientForApiCall($apiCall, $params);
                        $list = $client->getHandlerList();
                        $command = new Command($apiCall['operationName'], $apiCall['params']);
                        $request = new Request('POST', (string)$client->getEndpoint());
                        $handler = new MockHandler();
                        foreach ($apiCall['attemptResponses'] as $attemptResponse) {
                            $response = $this->generateResponse($attemptResponse, $command);
                            if ($response instanceof \Exception &&
                                !($response instanceof AwsException)) {
                                $handler->appendException($response);
                            } else {
                                $handler->append($response);
                            }
                        }
                        $list->setHandler($handler);
                        $handler = $list->resolve();

                        try {
                            /** @var Result $result */
                            $result = $handler($command, $request)->wait();
                            $events = array_merge($events, $result->getMonitoringEvents());
                        } catch (\Exception $e) {
                            if ($e instanceof MonitoringEventsInterface) {
                                $events = array_merge($events, $e->getMonitoringEvents());
                            }
                        }
                    }

                    $this->sendSocketShutdownMessage();
                    usleep(500000);

                    $this->compareMonitoringEvents(
                        $case['expectedMonitoringEvents'],
                        $events
                    );

                    $received = file_get_contents(self::$outputFile);
                    $receivedEvents = json_decode($received, true);
                    $this->compareMonitoringEvents(
                        $case['expectedMonitoringEvents'],
                        $receivedEvents
                    );
                } catch (\Exception $e) {
                    $this->sendSocketShutdownMessage();
                    throw $e;
                }
            }
        }
    }

    private function clearAndSetEnv($env)
    {
        putenv(ConfigurationProvider::ENV_ENABLED . '=');
        putenv(ConfigurationProvider::ENV_PORT . '=');
        putenv(ConfigurationProvider::ENV_CLIENT_ID . '=');
        putenv(ConfigurationProvider::ENV_PROFILE . '=');

        foreach ($env as $key => $value) {
            putenv("{$key}={$value}");
        }
    }

    private function compareMonitoringEvents($expected, $actual)
    {
        $this->assertSame(count($expected), count($actual));
        foreach ($expected as $index => $expectedEvent) {
            $actualEvent = $actual[$index];
            foreach ($expectedEvent as $key => $value) {
                $this->assertArrayHasKey($key, $actualEvent);
                $this->validateEventValue($value, $actualEvent[$key]);
            }
            $allowedFields = array_merge(
                array_keys($expectedEvent),
                array_keys($this->testData['defaults']['optionalEventFields'][$actualEvent['Type']])
            );
            foreach ($actualEvent as $key => $value) {
                $this->assertTrue(in_array($key, $allowedFields));
                if (in_array($key, $this->testData['defaults']['optionalEventFields'][$actualEvent['Type']])) {
                    $this->validateEventValue(
                        $this->testData['defaults']['optionalEventFields'][$actualEvent['Type']],
                        $value
                    );
                }
            }
        }
    }

    private function createClientForApiCall(array $apiCall, array $params)
    {
        /** @var Service $service */
        if (!empty($service = $this->testServices[$apiCall['serviceId']])) {
            $params['service'] = $service->getEndpointPrefix();
            $params['api_provider'] = $this->testApiProvider;
            $params += $this->sharedConfig;
            $params += ['retries' => 3];
            return new AwsClient($params);
        }

        return $this->sdk->createClient(
            $apiCall['serviceId'],
            $params
        );
    }

    private function createCredentials(array $configuration)
    {
        if (!empty($configuration['sessionToken'])) {
            $token = $configuration['sessionToken'];
        } else {
            $token = null;
        }

        return new Credentials(
            $configuration['accessKey'],
            'test-secret',
            $token
        );
    }

    private function generateResponse($attemptResponse, $command)
    {
        $transferStats = [
            'total_time' => !empty($attemptResponse['totalTime'])
                ? $attemptResponse['totalTime']
                : 0.12,
            'primary_ip' => !empty($attemptResponse['primaryIp'])
                ? $attemptResponse['primaryIp']
                : '12.34.56.78',
            'namelookup_time' => !empty($attemptResponse['namelookupTime'])
                ? $attemptResponse['namelookupTime']
                : 0.012,
        ];

        $headers = [];
        if (isset($attemptResponse['responseHeaders'])) {
            foreach($attemptResponse['responseHeaders'] as $header => $value) {
                $headers[$header] = $value;
            }
        }

        if (!empty($attemptResponse['errorCode'])) {
            $context = [
                'code' => $attemptResponse['errorCode'],
                'message' => $attemptResponse['errorMessage'],
                'response' => new Response($attemptResponse['httpStatus'], $headers),
                'transfer_stats' => $transferStats,
            ];

            return new AwsException($attemptResponse['errorMessage'],
                $command,
                $context
            );
        }
        if (!empty($attemptResponse['sdkException'])) {
            return new ConfigurationException(
                $attemptResponse['sdkException']['message'], 555
            );
        }
        if (!empty($attemptResponse['httpStatus'])) {
            $params = [
                '@metadata' => [
                    'statusCode' => $attemptResponse['httpStatus'],
                    'headers' => $headers,
                    'transferStats' => [
                        'http' => [
                            $transferStats
                        ]
                    ],
                ],
            ];
            return new Result($params);
        }

        throw new \InvalidArgumentException('attemptResponse data does not contain required fields.');
    }

    private function prepareTestCaseParams(array $case)
    {
        $params = [];
        if (!empty($case['configuration']['region'])) {
            $params['region'] = $case['configuration']['region'];
        }
        if (!empty($case['configuration']['accessKey'])) {
            $params['credentials'] = $this->createCredentials($case['configuration']);
        }
        return $params;
    }

    private function sendSocketShutdownMessage()
    {
        $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
        socket_clear_error($socket);
        socket_connect(
            $socket,
            $this->udpConfig['address'],
            $this->udpConfig['port']
        );
        socket_write(
            $socket,
            $this->udpConfig['shutdown'],
            strlen($this->udpConfig['shutdown'])
        );
        socket_close($socket);
    }

    private function startUdpServer()
    {
        set_time_limit(60);
        $localPort = 0;
        $remoteAddress = 0;
        $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
        socket_bind($socket, $this->udpConfig['address'], $this->udpConfig['port']);
        $events = [];

        while (true) {
            socket_recvfrom($socket, $buf, 8096, 0, $remoteAddress, $localPort);

            if ($buf == $this->udpConfig['shutdown']) {
                file_put_contents(self::$outputFile, json_encode($events));
                socket_close($socket);
                exit();
            }
            $events[] = json_decode($buf, true);
        }
    }

    private function validateEventValue($expected, $actual)
    {
        switch ($expected) {
            case "ANY_INT":
                $this->assertIsInt($actual);
                break;
            case "ANY_STR":
                $this->assertIsString('string', $actual);
                break;
            default:
                $this->assertEquals($expected, $actual);
        }
    }
}


<?php
namespace Aws;

use GuzzleHttp\Promise;

/**
 * A configuration provider is a function that returns a promise that is
 * fulfilled with a configuration object. This class provides base functionality
 * usable by specific configuration provider implementations
 */
abstract class AbstractConfigurationProvider
{
    const ENV_PROFILE = 'AWS_PROFILE';
    const ENV_CONFIG_FILE = 'AWS_CONFIG_FILE';

    public static $cacheKey;

    protected static $interfaceClass;
    protected static $exceptionClass;

    /**
     * Wraps a config provider and saves provided configuration in an
     * instance of Aws\CacheInterface. Forwards calls when no config found
     * in cache and updates cache with the results.
     *
     * @param callable $provider Configuration provider function to wrap
     * @param CacheInterface $cache Cache to store configuration
     * @param string|null $cacheKey (optional) Cache key to use
     *
     * @return callable
     */
    public static function cache(
        callable $provider,
        CacheInterface $cache,
        $cacheKey = null
    ) {
        $cacheKey = $cacheKey ?: static::$cacheKey;

        return function () use ($provider, $cache, $cacheKey) {
            $found = $cache->get($cacheKey);
            if ($found instanceof static::$interfaceClass) {
                return Promise\Create::promiseFor($found);
            }

            return $provider()
                ->then(function ($config) use (
                    $cache,
                    $cacheKey
                ) {
                    $cache->set($cacheKey, $config);
                    return $config;
                });
        };
    }

    /**
     * Creates an aggregate configuration provider that invokes the provided
     * variadic providers one after the other until a provider returns
     * configuration.
     *
     * @return callable
     */
    public static function chain()
    {
        $links = func_get_args();
        if (empty($links)) {
            throw new \InvalidArgumentException('No providers in chain');
        }

        return function () use ($links) {
            /** @var callable $parent */
            $parent = array_shift($links);
            $promise = $parent();
            while ($next = array_shift($links)) {
                $promise = $promise->otherwise($next);
            }
            return $promise;
        };
    }

    /**
     * Gets the environment's HOME directory if available.
     *
     * @return null|string
     */
    protected static function getHomeDir()
    {
        // On Linux/Unix-like systems, use the HOME environment variable
        if ($homeDir = getenv('HOME')) {
            return $homeDir;
        }

        // Get the HOMEDRIVE and HOMEPATH values for Windows hosts
        $homeDrive = getenv('HOMEDRIVE');
        $homePath = getenv('HOMEPATH');

        return ($homeDrive && $homePath) ? $homeDrive . $homePath : null;
    }

    /**
     * Gets default config file location from environment, falling back to aws
     * default location
     *
     * @return string
     */
    protected static function getDefaultConfigFilename()
    {
        if ($filename = getenv(self::ENV_CONFIG_FILE)) {
            return $filename;
        }
        return self::getHomeDir() . '/.aws/config';
    }

    /**
     * Wraps a config provider and caches previously provided configuration.
     *
     * @param callable $provider Config provider function to wrap.
     *
     * @return callable
     */
    public static function memoize(callable $provider)
    {
        return function () use ($provider) {
            static $result;
            static $isConstant;

            // Constant config will be returned constantly.
            if ($isConstant) {
                return $result;
            }

            // Create the initial promise that will be used as the cached value
            if (null === $result) {
                $result = $provider();
            }

            // Return config and set flag that provider is already set
            return $result
                ->then(function ($config) use (&$isConstant) {
                    $isConstant = true;
                    return $config;
                });
        };
    }

    /**
     * Reject promise with standardized exception.
     *
     * @param $msg
     * @return Promise\RejectedPromise
     */
    protected static function reject($msg)
    {
        $exceptionClass = static::$exceptionClass;
        return new Promise\RejectedPromise(new $exceptionClass($msg));
    }
}