<?php

namespace Joli\Jane\OpenApi\SchemaParser;

use Joli\Jane\OpenApi\Exception\ParseFailureException;
use Joli\Jane\OpenApi\Model\OpenApi;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Yaml\Yaml;

class SchemaParser
{
    const OPEN_API_MODEL    = "Joli\\Jane\\OpenApi\\Model\\OpenApi";
    const EXCEPTION_MESSAGE = "Could not parse \"%s\", is it a valid specification?";
    const CONTENT_TYPE_JSON = 'json';
    const CONTENT_TYPE_YAML = 'yaml';
    /**
     * @var SerializerInterface
     */
    private $serializer;

    /**
     * SchemaParser constructor.
     *
     * @param SerializerInterface $serializer
     */
    public function __construct(SerializerInterface $serializer)
    {
        $this->serializer = $serializer;
    }

    /**
     * Parse an file into a OpenAPI Schema model
     *
     * @param string $openApiSpec
     *
     * @return OpenApi
     *
     * @throws ParseFailureException
     */
    public function parseSchema($openApiSpec)
    {
        $openApiSpecContents = file_get_contents($openApiSpec);
        $schemaClass         = self::OPEN_API_MODEL;
        $schema              = null;
        $jsonException       = null;
        $yamlException       = null;

        try {
            return $this->serializer->deserialize(
                $openApiSpecContents,
                $schemaClass,
                self::CONTENT_TYPE_JSON,
                [
                    'document-origin' => $openApiSpec
                ]
            );
        } catch (\Exception $exception) {
            $jsonException = $exception;
        }

        $content = Yaml::parse($openApiSpecContents, Yaml::PARSE_OBJECT | Yaml::PARSE_OBJECT_FOR_MAP | Yaml::PARSE_DATETIME | Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE);
        $openApiSpecContents = json_encode($content);

        return $this->serializer->deserialize(
            $openApiSpecContents,
            $schemaClass,
            self::CONTENT_TYPE_JSON,
            [
                'document-origin' => $openApiSpec
            ]
        );
    }
}


<?php

namespace Joli\Jane\OpenApi\Guesser\OpenApiSchema;

use Joli\Jane\Guesser\JsonSchema\ItemsGuesser as BaseItemsGuesser;
use Joli\Jane\OpenApi\Model\Schema;

class ItemsGuesser extends BaseItemsGuesser
{
    /**
     * {@inheritDoc}
     */
    public function supportObject($object)
    {
        return (
            ($object instanceof Schema)
            && (
                $object->getItems() instanceof Schema
                ||
                (
                    is_array($object->getItems())
                    &&
                    count($object->getItems()) > 0
                )
            )
        );
    }
}


<?php

namespace Joli\Jane\OpenApi\Generator;

use Doctrine\Common\Inflector\Inflector;
use Joli\Jane\Generator\Context\Context;
use Joli\Jane\Runtime\Reference;
use Joli\Jane\OpenApi\Model\BodyParameter;
use Joli\Jane\OpenApi\Model\FormDataParameterSubSchema;
use Joli\Jane\OpenApi\Model\HeaderParameterSubSchema;
use Joli\Jane\OpenApi\Model\PathParameterSubSchema;
use Joli\Jane\OpenApi\Model\QueryParameterSubSchema;
use Joli\Jane\OpenApi\Operation\Operation;
use Joli\Jane\Reference\Resolver;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

trait InputGeneratorTrait
{
    /**
     * @var Parameter\BodyParameterGenerator
     */
    protected $bodyParameterGenerator;

    /**
     * @var Parameter\FormDataParameterGenerator
     */
    protected $formDataParameterGenerator;

    /**
     * @var Parameter\HeaderParameterGenerator
     */
    protected $headerParameterGenerator;

    /**
     * @var Parameter\PathParameterGenerator
     */
    protected $pathParameterGenerator;

    /**
     * @var Parameter\QueryParameterGenerator
     */
    protected $queryParameterGenerator;

    /**
     * @return DenormalizerInterface
     */
    abstract protected function getDenormalizer();

    /**
     * Create the query param statements and documentation
     *
     * @param Operation $operation
     *
     * @return array
     */
    protected function createQueryParamStatements(Operation $operation)
    {
        $queryParamDocumentation = [];
        $queryParamVariable = new Expr\Variable('queryParam');
        $queryParamStatements = [
            new Expr\Assign($queryParamVariable, new Expr\New_(new Name('QueryParam')))
        ];

        if ($operation->getOperation()->getParameters()) {
            foreach ($operation->getOperation()->getParameters() as $parameter) {
                if ($parameter instanceof Reference) {
                    $parameter = $this->resolveParameter($parameter);
                }

                if ($parameter instanceof FormDataParameterSubSchema) {
                    $queryParamStatements = array_merge($queryParamStatements, $this->formDataParameterGenerator->generateQueryParamStatements($parameter, $queryParamVariable));
                    $queryParamDocumentation[] = $this->formDataParameterGenerator->generateQueryDocParameter($parameter);
                }

                if ($parameter instanceof HeaderParameterSubSchema) {
                    $queryParamStatements = array_merge($queryParamStatements, $this->headerParameterGenerator->generateQueryParamStatements($parameter, $queryParamVariable));
                    $queryParamDocumentation[] = $this->headerParameterGenerator->generateQueryDocParameter($parameter);
                }

                if ($parameter instanceof QueryParameterSubSchema) {
                    $queryParamStatements = array_merge($queryParamStatements, $this->queryParameterGenerator->generateQueryParamStatements($parameter, $queryParamVariable));
                    $queryParamDocumentation[] = $this->queryParameterGenerator->generateQueryDocParameter($parameter);
                }
            }
        }

        return [$queryParamDocumentation, $queryParamStatements, $queryParamVariable];
    }

    /**
     * Create parameters for the method and their documentation
     *
     * @param Operation $operation
     * @param string[]  $queryParamDocumentation
     * @param Context   $context
     *
     * @return array
     */
    protected function createParameters(Operation $operation, $queryParamDocumentation, Context $context)
    {
        $documentationParams = [];
        $methodParameters = [];

        if ($operation->getOperation()->getParameters()) {
            foreach ($operation->getOperation()->getParameters() as $key => $parameter) {
                if ($parameter instanceof Reference) {
                    $parameter = $this->resolveParameter($parameter);
                }

                if ($parameter instanceof PathParameterSubSchema) {
                    $methodParameters[] = $this->pathParameterGenerator->generateMethodParameter($parameter, $context, $operation->getReference() . '/parameters/' . $key);
                    $documentationParams[] = sprintf(' * @param %s', $this->pathParameterGenerator->generateDocParameter($parameter, $context, $operation->getReference() . '/parameters/' . $key));
                }
            }

            foreach ($operation->getOperation()->getParameters() as $key => $parameter) {
                if ($parameter instanceof Reference) {
                    $parameter = $this->resolveParameter($parameter);
                }

                if ($parameter instanceof BodyParameter) {
                    $methodParameters[] = $this->bodyParameterGenerator->generateMethodParameter($parameter, $context, $operation->getReference() . '/parameters/' . $key);
                    $documentationParams[] = sprintf(' * @param %s', $this->bodyParameterGenerator->generateDocParameter($parameter, $context, $operation->getReference() . '/parameters/' . $key));
                }
            }
        }

        if (!empty($queryParamDocumentation)) {
            $documentationParams[] = " * @param array  \$parameters {";
            $documentationParams   = array_merge($documentationParams, array_map(function ($doc) {
                return " *     " . $doc;
            }, $queryParamDocumentation));
            $documentationParams[] = " * }";
        } else {
            $documentationParams[] = " * @param array  \$parameters List of parameters";
        }

        $documentationParams[] = " * @param string \$fetch      Fetch mode (object or response)";

        $methodParameters[] = new Param('parameters', new Expr\Array_());
        $methodParameters[] = new Param('fetch', new Expr\ConstFetch(new Name('self::FETCH_OBJECT')));

        return [$documentationParams, $methodParameters];
    }

    /**
     * Create all statements around url transformation
     *
     * @param Operation $operation
     * @param Expr\Variable $queryParamVariable
     *
     * @return array
     */
    protected function createUrlStatements(Operation $operation, $queryParamVariable)
    {
        $urlVariable = new Expr\Variable('url');
        // url = /path
        $statements = [
            new Expr\Assign($urlVariable, new Scalar\String_($operation->getPath()))
        ];

        if ($operation->getOperation()->getParameters()) {
            foreach ($operation->getOperation()->getParameters() as $parameter) {
                if ($parameter instanceof Reference) {
                    $parameter = $this->resolveParameter($parameter);
                }

                if ($parameter instanceof PathParameterSubSchema) {
                    // $url = str_replace('{param}', $param, $url)
                    $statements[] = new Expr\Assign($urlVariable, new Expr\FuncCall(new Name('str_replace'), [
                        new Arg(new Scalar\String_('{' . $parameter->getName() . '}')),
                        new Arg(new Expr\FuncCall(new Name('urlencode'), [
                            new Arg(new Expr\Variable(Inflector::camelize($parameter->getName()))),
                        ])),
                        new Arg($urlVariable)
                    ]));
                }
            }
        }

        // url = url . ? . $queryParam->buildQueryString
        $statements[] = new Expr\Assign($urlVariable, new Expr\BinaryOp\Concat(
            $urlVariable,
            new Expr\BinaryOp\Concat(
                new Scalar\String_('?'),
                new Expr\MethodCall($queryParamVariable, 'buildQueryString', [new Arg(new Expr\Variable('parameters'))])
            )
        ));

        return [$statements, $urlVariable];
    }

    /**
     * Create body statements
     *
     * @param Operation $operation
     * @param Expr\Variable $queryParamVariable
     * @param Context $context
     *
     * @return array
     */
    protected function createBodyStatements(Operation $operation, $queryParamVariable, Context $context)
    {
        $bodyParameter = null;
        $bodyVariable = new Expr\Variable('body');
        $parameterKey = 0;

        if ($operation->getOperation()->getParameters()) {
            foreach ($operation->getOperation()->getParameters() as $key => $parameter) {
                if ($parameter instanceof BodyParameter) {
                    $bodyParameter = $parameter;
                    $parameterKey = $key;
                }
            }
        }

        if (null === $bodyParameter) {
            // $body = $queryParam->buildFormDataString($parameters);
            return [[
                new Expr\Assign($bodyVariable, new Expr\MethodCall($queryParamVariable, 'buildFormDataString', [new Arg(new Expr\Variable('parameters'))]))
            ], $bodyVariable];
        }

        // $body = $this->serializer->serialize($parameter);
        if ($bodyParameter->getSchema() instanceof Reference || $context->getRegistry()->hasClass($operation->getReference() . '/parameters/' . $parameterKey)) {
            return [
                [
                    new Expr\Assign(
                        $bodyVariable,
                        new Expr\MethodCall(
                            new Expr\PropertyFetch(new Expr\Variable('this'), 'serializer'),
                            'serialize',
                            [
                                new Arg(new Expr\Variable(Inflector::camelize($bodyParameter->getName()))),
                                new Arg(new Scalar\String_('json'))
                            ]
                        )
                    )
                ],
                $bodyVariable
            ];
        }

        // $body = $parameter
        return [[
            new Expr\Assign($bodyVariable, new Expr\Variable(Inflector::camelize($bodyParameter->getName())))
        ], $bodyVariable];
    }

    /**
     * Create headers statements
     *
     * @param Operation $operation
     * @param Expr\Variable $queryParamVariable
     *
     * @return array
     */
    protected function createHeaderStatements(Operation $operation, $queryParamVariable)
    {
        $headerVariable = new Expr\Variable('headers');

        $headers = [
            new Expr\ArrayItem(
                new Scalar\String_($operation->getHost()),
                new Scalar\String_('Host')
            ),
        ];

        $produces = $operation->getOperation()->getProduces();

        if ($produces && in_array("application/json", $produces)) {
            $headers[]
                = new Expr\ArrayItem(
                    new Expr\Array_(
                        [
                        new Expr\ArrayItem(
                            new Scalar\String_("application/json")
                        ),
                        ]
                    ),
                    new Scalar\String_('Accept')
                );
        }

        $consumes = $operation->getOperation()->getProduces();

        if ($operation->getOperation()->getParameters() && $consumes) {
            $bodyParameters = array_filter(
                $operation->getOperation()->getParameters(),
                function ($parameter) {
                    return $parameter instanceof BodyParameter;
                }
            );

            if (count($bodyParameters) > 0 && in_array("application/json", $consumes)) {
                $headers[]
                    = new Expr\ArrayItem(
                        new Scalar\String_("application/json"),
                        new Scalar\String_('Content-Type')
                    );
            }
        }

        return [
            [
                new Expr\Assign(
                    $headerVariable,
                    new Expr\FuncCall(new Name('array_merge'), [
                        new Arg(
                            new Expr\Array_(
                                $headers
                            )
                        ),
                        new Arg(new Expr\MethodCall($queryParamVariable, 'buildHeaders', [new Arg(new Expr\Variable('parameters'))]))
                    ])
                )
            ],
            $headerVariable
        ];
    }

    /**
     * @param Reference $parameter
     *
     * @return BodyParameter|HeaderParameterSubSchema|FormDataParameterSubSchema|QueryParameterSubSchema|PathParameterSubSchema
     */
    protected function resolveParameter(Reference $parameter)
    {
        return $parameter->resolve(function ($value) {
            if (isset($value->{'in'}) and $value->{'in'} == 'body') {
                return $this->getDenormalizer()->denormalize($value, 'Joli\\Jane\\OpenApi\\Model\\BodyParameter');
            }
            if (isset($value->{'in'}) and $value->{'in'} == 'header') {
                return $this->getDenormalizer()->denormalize($value, 'Joli\\Jane\\OpenApi\\Model\\HeaderParameterSubSchema');
            }
            if (isset($value->{'in'}) and $value->{'in'} == 'formData') {
                return $this->getDenormalizer()->denormalize($value, 'Joli\\Jane\\OpenApi\\Model\\FormDataParameterSubSchema');
            }
            if (isset($value->{'in'}) and $value->{'in'} == 'query') {
                return $this->getDenormalizer()->denormalize($value, 'Joli\\Jane\\OpenApi\\Model\\QueryParameterSubSchema');
            }
            if (isset($value->{'in'}) and $value->{'in'} == 'path') {
                return $this->getDenormalizer()->denormalize($value, 'Joli\\Jane\\OpenApi\\Model\\PathParameterSubSchema');
            }

            return $value;
        });
    }
}


<?php

namespace Joli\Jane\OpenApi\Operation;

use Doctrine\Common\Inflector\Inflector;

class OperationCollection extends \ArrayObject
{
    /**
     * @param Operation $operation
     *
     * @return self
     */
    public function addOperation(Operation $operation)
    {
        $id    = $operation->getMethod().$operation->getPath();
        $group = 'Default';

        if ($operation->getOperation()->getOperationId()) {
            $id = $operation->getOperation()->getOperationId();
        }

        if ($operation->getOperation()->getTags() !== null && count($operation->getOperation()->getTags()) > 0) {
            $group = $operation->getOperation()->getTags()[0];
        }

        $group = trim(preg_replace('/[^a-z0-9 ]+/iu', '', $group));
        $group = Inflector::classify($group);

        if (!isset($this[$group])) {
            $this[$group] = [];
        }

        $this[$group][$id] = $operation;

        return $this;
    }
}


<?php

namespace Joli\Jane\OpenApi\Generator\Parameter;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;

class HeaderParameterGenerator extends NonBodyParameterGenerator
{
    /**
     * {@inheritdoc}
     */
    public function generateQueryParamStatements($parameter, Expr $queryParamVariable)
    {
        $statements = parent::generateQueryParamStatements($parameter, $queryParamVariable);
        $statements[] = new Expr\MethodCall($queryParamVariable, 'setHeaderParameters', [
            new Node\Arg(new Expr\Array_([new Expr\ArrayItem(new Scalar\String_($parameter->getName()))])),
        ]);

        return $statements;
    }
}


<?php

namespace Joli\Jane\OpenApi\Generator\Parameter;

use Doctrine\Common\Inflector\Inflector;
use Joli\Jane\Generator\Context\Context;
use Joli\Jane\OpenApi\Model\FormDataParameterSubSchema;
use Joli\Jane\OpenApi\Model\HeaderParameterSubSchema;
use Joli\Jane\OpenApi\Model\PathParameterSubSchema;
use Joli\Jane\OpenApi\Model\QueryParameterSubSchema;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;

abstract class NonBodyParameterGenerator extends ParameterGenerator
{
    /**
     * {@inheritdoc}
     *
     * @param $parameter PathParameterSubSchema|HeaderParameterSubSchema|FormDataParameterSubSchema|QueryParameterSubSchema
     */
    public function generateMethodParameter($parameter, Context $context, $reference)
    {
        $name = Inflector::camelize($parameter->getName());
        $methodParameter = new Node\Param($name);

        if (!$parameter->getRequired() || $parameter->getDefault() !== null) {
            $methodParameter->default = $this->getDefaultAsExpr($parameter);
        }

        return $methodParameter;
    }

    /**
     * {@inheritdoc}
     *
     * @param $parameter PathParameterSubSchema|HeaderParameterSubSchema|FormDataParameterSubSchema|QueryParameterSubSchema
     */
    public function generateQueryParamStatements($parameter, Expr $queryParamVariable)
    {
        $statements = [];

        if (!$parameter->getRequired() || $parameter->getDefault() !== null) {
            $statements[] = new Expr\MethodCall($queryParamVariable, 'setDefault', [
                new Node\Arg(new Scalar\String_($parameter->getName())),
                new Node\Arg($this->getDefaultAsExpr($parameter)),
            ]);
        }

        if ($parameter->getRequired() && $parameter->getDefault() === null) {
            $statements[] = new Expr\MethodCall($queryParamVariable, 'setRequired', [new Node\Arg(new Scalar\String_($parameter->getName()))]);
        }

        return $statements;
    }

    /**
     * Generate a default value as an Expr.
     *
     * @param $parameter PathParameterSubSchema|HeaderParameterSubSchema|FormDataParameterSubSchema|QueryParameterSubSchema
     *
     * @return Expr
     */
    protected function getDefaultAsExpr($parameter)
    {
        return $this->parser->parse('<?php '.var_export($parameter->getDefault(), true).';')[0];
    }

    /**
     * {@inheritdoc}
     *
     * @param $parameter PathParameterSubSchema|HeaderParameterSubSchema|FormDataParameterSubSchema|QueryParameterSubSchema
     */
    public function generateDocParameter($parameter, Context $context, $reference)
    {
        return sprintf('%s $%s %s', $this->convertParameterType($parameter->getType()), Inflector::camelize($parameter->getName()), $parameter->getDescription() ?: '');
    }

    /**
     * @param $parameter PathParameterSubSchema|HeaderParameterSubSchema|FormDataParameterSubSchema|QueryParameterSubSchema
     *
     * @return string
     */
    public function generateQueryDocParameter($parameter)
    {
        return sprintf('@var %s $%s %s', $this->convertParameterType($parameter->getType()), $parameter->getName(), $parameter->getDescription() ?: '');
    }

    public function convertParameterType($type)
    {
        $convertArray = [
            'string' => 'string',
            'number' => 'float',
            'boolean' => 'bool',
            'integer' => 'int',
            'array' => 'array',
            'file' => '\Psr\Http\Message\StreamInterface',
        ];

        return $convertArray[$type];
    }
}


<?php

namespace Joli\Jane\OpenApi;

use Fitbug\SymfonySerializer\YamlEncoderDecoder\YamlDecode;
use Fitbug\SymfonySerializer\YamlEncoderDecoder\YamlEncode;
use Fitbug\SymfonySerializer\YamlEncoderDecoder\YamlEncoder;
use Joli\Jane\Encoder\RawEncoder;
use Joli\Jane\Generator\Context\Context;
use Joli\Jane\Generator\File;
use Joli\Jane\Generator\ModelGenerator;
use Joli\Jane\Generator\Naming;
use Joli\Jane\Generator\NormalizerGenerator;
use Joli\Jane\Guesser\ChainGuesser;
use Joli\Jane\OpenApi\Generator\ClientGenerator;
use Joli\Jane\OpenApi\Generator\GeneratorFactory;
use Joli\Jane\OpenApi\Guesser\OpenApiSchema\GuesserFactory;
use Joli\Jane\OpenApi\Model\OpenApi;
use Joli\Jane\OpenApi\Normalizer\NormalizerFactory;
use Joli\Jane\OpenApi\SchemaParser\SchemaParser;
use Joli\Jane\Registry;
use Joli\Jane\Schema;
use PhpCsFixer\Config;
use PhpCsFixer\ConfigInterface;
use PhpCsFixer\Console\ConfigurationResolver;
use PhpCsFixer\Differ\NullDiffer;
use PhpCsFixer\Error\ErrorsManager;
use PhpCsFixer\Finder;
use PhpCsFixer\Runner\Runner;
use PhpCsFixer\ToolInfo;
use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter;
use PhpParser\PrettyPrinterAbstract;
use Symfony\Component\Serializer\Encoder\JsonDecode;
use Symfony\Component\Serializer\Encoder\JsonEncode;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Serializer;
use PhpCsFixer\Cache\NullCacheManager;
use PhpCsFixer\Linter\Linter;

class JaneOpenApi
{
    const VERSION = '1.0.x';

    /**
     * @var SchemaParser
     */
    private $schemaParser;
    /**
     * @var Generator\ClientGenerator
     */
    private $clientGenerator;

    /**
     * @var \PhpParser\PrettyPrinterAbstract
     */
    private $prettyPrinter;

    /**
     * @var ModelGenerator
     */
    private $modelGenerator;

    /**
     * @var NormalizerGenerator
     */
    private $normalizerGenerator;

    /**
     * @var ChainGuesser
     */
    private $chainGuesser;

    /**
     * @var ConfigInterface
     */
    private $fixerConfig;

    /**
     * JaneOpenApi constructor.
     *
     * @param SchemaParser          $schemaParser
     * @param ChainGuesser          $chainGuesser
     * @param ModelGenerator        $modelGenerator
     * @param NormalizerGenerator   $normalizerGenerator
     * @param ClientGenerator       $clientGenerator
     * @param PrettyPrinterAbstract $prettyPrinter
     * @param ConfigInterface|null  $fixerConfig
     */
    public function __construct(
        SchemaParser $schemaParser,
        ChainGuesser $chainGuesser,
        ModelGenerator $modelGenerator,
        NormalizerGenerator $normalizerGenerator,
        ClientGenerator $clientGenerator,
        PrettyPrinterAbstract $prettyPrinter,
        ConfigInterface $fixerConfig = null
    ) {
        $this->schemaParser        = $schemaParser;
        $this->clientGenerator     = $clientGenerator;
        $this->prettyPrinter       = $prettyPrinter;
        $this->modelGenerator      = $modelGenerator;
        $this->normalizerGenerator = $normalizerGenerator;
        $this->chainGuesser        = $chainGuesser;
        $this->fixer               = $fixerConfig;
    }

    /**
     * Return a list of class guessed
     *
     * @param Registry $registry A registry
     * @param string $name
     *
     * @return Context
     */
    public function createContext(Registry $registry, $name)
    {
        $schemas = array_values($registry->getSchemas());

        /** @var Schema $schema */
        foreach ($schemas as $schema) {
            $openApiSpec = $this->schemaParser->parseSchema($schema->getOrigin());
            $this->chainGuesser->guessClass($openApiSpec, $schema->getRootName(), $schema->getOrigin() . '#', $registry);
            $schema->setParsed($openApiSpec);
        }

        foreach ($registry->getSchemas() as $schema) {
            foreach ($schema->getClasses() as $class) {
                $properties = $this->chainGuesser->guessProperties($class->getObject(), $schema->getRootName(), $class->getReference(), $registry);

                foreach ($properties as $property) {
                    $property->setType($this->chainGuesser->guessType($property->getObject(), $property->getName(), $property->getReference(), $registry));
                }

                $class->setProperties($properties);
            }
        }

        return new Context($registry);
    }

    /**
     * Generate a list of files
     *
     * @param Registry $registry A registry
     *
     * @return File[]
     */
    public function generate(Registry $registry)
    {
        /** @var OpenApi $openApi */
        $context = $this->createContext($registry, 'Client');

        $files   = [];

        foreach ($registry->getSchemas() as $schema) {
            $context->setCurrentSchema($schema);

            $files = array_merge($files, $this->modelGenerator->generate($schema, $schema->getRootName(), $context));
            $files = array_merge($files, $this->normalizerGenerator->generate($schema, $schema->getRootName(), $context));
            $clients = $this->clientGenerator->generate($schema->getParsed(), $schema->getNamespace(), $context, $schema->getOrigin() . '#');

            foreach ($clients as $node) {
                $files[] = new File($schema->getDirectory() . DIRECTORY_SEPARATOR . 'Resource' . DIRECTORY_SEPARATOR . $node->stmts[2]->name . '.php', $node, '');
            }
        }

        return $files;
    }

    /**
     * Print files
     *
     * @param File[] $files
     * @param Registry $registry
     */
    public function printFiles($files, $registry)
    {
        foreach ($files as $file) {
            if (!file_exists(dirname($file->getFilename()))) {
                mkdir(dirname($file->getFilename()), 0755, true);
            }

            file_put_contents($file->getFilename(), $this->prettyPrinter->prettyPrintFile([$file->getNode()]));
        }

        foreach ($registry->getSchemas() as $schema) {
            $this->fix($schema->getDirectory());
        }
    }

    /**
     * Use php cs fixer to have a nice formatting of generated files
     *
     * @param string $directory
     *
     * @return array|void
     */
    protected function fix($directory)
    {
        if (!class_exists('PhpCsFixer\Config')) {
            return;
        }

        /** @var Config $fixerConfig */
        $fixerConfig = $this->fixerConfig;

        if (null === $fixerConfig) {
            $fixerConfig = Config::create()
                ->setRiskyAllowed(true)
                ->setRules(
                    array(
                        '@Symfony' => true,
                        'array_syntax' => array('syntax' => 'short'),
                        'simplified_null_return' => false,
                        'ordered_imports' => true,
                        'phpdoc_order' => true,
                        'binary_operator_spaces' => array('align_equals'=>true),
                        'concat_space' => false,
                        'yoda_style' => false,
                        'header_comment' => [
                            'header' => <<<EOH
This file has been auto generated by Jane,

Do no edit it directly.
EOH
                            ,
                        ]
                    )
                );
        }
        $resolverOptions = array('allow-risky' => true);
        $resolver = new ConfigurationResolver($fixerConfig, $resolverOptions, $directory, new ToolInfo());

        $finder = new Finder();
        $finder->in($directory);
        $fixerConfig->setFinder($finder);

        $runner = new Runner(
            $resolver->getConfig()->getFinder(),
            $resolver->getFixers(),
            new NullDiffer(),
            null,
            new ErrorsManager(),
            new Linter(),
            false,
            new NullCacheManager()
        );

        return $runner->fix();
    }

    public static function build(array $options = [])
    {
        $encoders        = [
            new JsonEncoder(
                new JsonEncode(),
                new JsonDecode(false)
            ),
            new YamlEncoder(
                new YamlEncode(),
                new YamlDecode(false, true, true, true)
            ),
        ];
        $normalizers     = NormalizerFactory::create();
        $serializer      = new Serializer($normalizers, $encoders);
        $schemaParser    = new SchemaParser($serializer);
        $clientGenerator = GeneratorFactory::build($serializer);
        $prettyPrinter   = new StandardPrettyPrinter();
        $naming          = new Naming();
        $modelGenerator  = new ModelGenerator($naming);
        $normGenerator   = new NormalizerGenerator($naming, isset($options['reference']) ? $options['reference'] : false);

        return new self(
            $schemaParser,
            GuesserFactory::create($serializer, $options),
            $modelGenerator,
            $normGenerator,
            $clientGenerator,
            $prettyPrinter
        );
    }
}


<?php

namespace Joli\Jane\OpenApi\Generator;

use Joli\Jane\Generator\Context\Context;
use Joli\Jane\OpenApi\Model\Response;
use Joli\Jane\Runtime\Reference;
use Joli\Jane\OpenApi\Generator\Parameter\BodyParameterGenerator;
use Joli\Jane\OpenApi\Generator\Parameter\FormDataParameterGenerator;
use Joli\Jane\OpenApi\Generator\Parameter\HeaderParameterGenerator;
use Joli\Jane\OpenApi\Generator\Parameter\PathParameterGenerator;
use Joli\Jane\OpenApi\Generator\Parameter\QueryParameterGenerator;
use Joli\Jane\OpenApi\Operation\Operation;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Scalar;
use PhpParser\Comment;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

class OperationGenerator
{
    use OutputGeneratorTrait;
    use InputGeneratorTrait;

    /**
     * @var DenormalizerInterface
     */
    protected $denormalizer;

    public function __construct(DenormalizerInterface $denormalizer, BodyParameterGenerator $bodyParameterGenerator, FormDataParameterGenerator $formDataParameterGenerator, HeaderParameterGenerator $headerParameterGenerator, PathParameterGenerator $pathParameterGenerator, QueryParameterGenerator $queryParameterGenerator)
    {
        $this->denormalizer               = $denormalizer;
        $this->bodyParameterGenerator     = $bodyParameterGenerator;
        $this->formDataParameterGenerator = $formDataParameterGenerator;
        $this->headerParameterGenerator   = $headerParameterGenerator;
        $this->pathParameterGenerator     = $pathParameterGenerator;
        $this->queryParameterGenerator    = $queryParameterGenerator;
    }

    /**
     * Generate a method for an operation
     *
     * @param string    $name
     * @param Operation $operation
     * @param Context   $context
     *
     * @return Stmt\ClassMethod
     */
    public function generate($name, Operation $operation, Context $context)
    {
        // Input
        list($queryParamDocumentation, $queryParamStatements, $queryParamVariable) = $this->createQueryParamStatements($operation);
        list($documentationParameters, $parameters) = $this->createParameters($operation, $queryParamDocumentation, $context);
        list($urlStatements, $urlVariable) = $this->createUrlStatements($operation, $queryParamVariable);
        list($bodyStatements, $bodyVariable) = $this->createBodyStatements($operation, $queryParamVariable, $context);
        list($headerStatements, $headerVariable) = $this->createHeaderStatements($operation, $queryParamVariable);

        $statements = array_merge($queryParamStatements, $urlStatements, $headerStatements, $bodyStatements, [
            // $request = $this->messageFactory->createRequest('method', $url, $headers, $body);
            new Expr\Assign(new Expr\Variable('request'), new Expr\MethodCall(
                new Expr\PropertyFetch(new Expr\Variable('this'), 'messageFactory'),
                'createRequest',
                [
                    new Arg(new Scalar\String_($operation->getMethod())),
                    new Arg($urlVariable),
                    new Arg($headerVariable),
                    new Arg($bodyVariable)
                ]
            )),
            // $response = $this->httpClient->sendRequest($request);
            new Expr\Assign(new Expr\Variable('promise'), new Expr\MethodCall(
                new Expr\PropertyFetch(new Expr\Variable('this'), 'httpClient'),
                'sendAsyncRequest',
                [new Arg(new Expr\Variable('request'))]
            )),
            // if ($fetch === self::FETCH_PROMISE) { return $promise }
            new Stmt\If_(
                new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('self::FETCH_PROMISE')), new Expr\Variable('fetch')),
                [
                    'stmts' => [
                        new Stmt\Return_(new Expr\Variable('promise'))
                    ]
                ]
            ),
            // $response = $promise->wait();
            new Expr\Assign(new Expr\Variable('response'), new Expr\MethodCall(
                new Expr\Variable('promise'),
                'wait'
            )),
        ]);

        // Output
        $outputStatements = [];
        $outputTypes = ["\\Psr\\Http\\Message\\ResponseInterface"];

        if ($operation->getOperation()->getResponses()) {
            foreach ($operation->getOperation()->getResponses() as $status => $response) {
                if ($response instanceof Reference) {
                    list(, $response) = $this->resolve($response, Response::class);
                }

                list($outputType, $ifStatus) = $this->createResponseDenormalizationStatement(
                    $status,
                    $response->getSchema(),
                    $context,
                    $operation->getReference() . '/responses/' . $status
                );

                if (null !== $outputType) {
                    if (!in_array($outputType, $outputTypes)) {
                        $outputTypes[] = $outputType;
                    }

                    $outputStatements[] = $ifStatus;
                }
            }
        }

        if (!empty($outputStatements)) {
            $statements[] = new Stmt\If_(
                new Expr\BinaryOp\Equal(new Expr\ConstFetch(new Name('self::FETCH_OBJECT')), new Expr\Variable('fetch')),
                [
                    'stmts' => $outputStatements
                ]
            );
        }

        // return $response
        $statements[] = new Stmt\Return_(new Expr\Variable('response'));
        $documentation = array_merge(
            [
                '/**',
                sprintf(" * %s", $operation->getOperation()->getDescription()),
                ' *',
            ],
            $documentationParameters,
            [
                ' *',
                ' * @return ' . implode('|', $outputTypes),
                ' */'
            ]
        );

        return new Stmt\ClassMethod($name, [
            'type'     => Stmt\Class_::MODIFIER_PUBLIC,
            'params'   => $parameters,
            'stmts'    => $statements
        ], [
            'comments' => [new Comment\Doc(implode("\n", $documentation))]
        ]);
    }

    /**
     * @return DenormalizerInterface
     */
    protected function getDenormalizer()
    {
        return $this->denormalizer;
    }
}


<?php

require_once dirname(__FILE__) . '/scorer.php';

/**
 * A single test result.
 */
abstract class SimpleResult
{
    public $time;
    public $breadcrumb;
    public $message;

    /**
     * Records the test result as public members.
     *
     * @param array  $breadcrumb Test stack at the time of the event.
     * @param string $message    The messsage to the human.
     */
    public function __construct($breadcrumb, $message)
    {
        list($this->time, $this->breadcrumb, $this->message) =
                array(time(), $breadcrumb, $message);
    }
}

/**
 * A single pass captured for later.
 */
class SimpleResultOfPass extends SimpleResult
{
}

/**
 * A single failure captured for later.
 */
class SimpleResultOfFail extends SimpleResult
{
}

/**
 * A single exception captured for later.
 */
class SimpleResultOfException extends SimpleResult
{
}

/**
 * Array-based test recorder.
 * Returns an array with timestamp, status, test name and message for each pass and failure.
 */
class Recorder extends SimpleReporterDecorator
{
    public $results = array();

    /**
     * Stashes the pass as a SimpleResultOfPass for later retrieval.
     *
     * @param string $message    Pass message to be displayed eventually.
     */
    public function paintPass($message)
    {
        parent::paintPass($message);

        $this->results[] = new SimpleResultOfPass(parent::getTestList(), $message);
    }

    /**
     * Stashes the fail as a SimpleResultOfFail for later retrieval.
     *
     * @param string $message    Failure message to be displayed eventually.
     */
    public function paintFail($message)
    {
        parent::paintFail($message);

        $this->results[] = new SimpleResultOfFail(parent::getTestList(), $message);
    }

    /**
     * Stashes the exception as a SimpleResultOfException for later retrieval.
     *
     * @param string $message    Exception message to be displayed eventually.
     */
    public function paintException($message)
    {
        parent::paintException($message);

        $this->results[] = new SimpleResultOfException(parent::getTestList(), $message);
    }
}


<?php

require_once dirname(__FILE__) . '/socket.php';

/**
 * Single post parameter.
 */
class SimpleEncodedPair
{
    private $key;
    private $value;

    /**
     * Stashes the data for rendering later.
     *
     * @param string $key       Form element name.
     * @param string $value     Data to send.
     */
    public function __construct($key, $value)
    {
        $this->key   = $key;
        $this->value = $value;
    }

    /**
     * The pair as a single string.
     *
     * @return string        Encoded pair.
     */
    public function asRequest()
    {
        return urlencode($this->key) . '=' . urlencode($this->value);
    }

    /**
     * The MIME part as a string.
     *
     * @return string        MIME part encoding.
     */
    public function asMime()
    {
        $part = 'Content-Disposition: form-data; ';
        $part .= 'name="' . $this->key . "\"\r\n";
        $part .= "\r\n" . $this->value;

        return $part;
    }

    /**
     * Is this the value we are looking for?
     *
     * @param string $key    Identifier.
     *
     * @return bool       True if matched.
     */
    public function isKey($key)
    {
        return $key == $this->key;
    }

    /**
     * Is this the value we are looking for?
     *
     * @return string       Identifier.
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * Is this the value we are looking for?
     *
     * @return string       Content.
     */
    public function getValue()
    {
        return $this->value;
    }
}

/**
 * Single post parameter.
 */
class SimpleAttachment
{
    private $key;
    private $content;
    private $filename;

    /**
     * Stashes the data for rendering later.
     *
     * @param string $key          Key to add value to.
     * @param string $content      Raw data.
     * @param hash $filename       Original filename.
     */
    public function __construct($key, $content, $filename)
    {
        $this->key      = $key;
        $this->content  = $content;
        $this->filename = $filename;
    }

    /**
     * The pair as a single string.
     *
     * @return string        Encoded pair.
     */
    public function asRequest()
    {
        return '';
    }

    /**
     * The MIME part as a string.
     *
     * @return string        MIME part encoding.
     */
    public function asMime()
    {
        $part = 'Content-Disposition: form-data; ';
        $part .= 'name="' . $this->key . '"; ';
        $part .= 'filename="' . $this->filename . '"';
        $part .= "\r\nContent-Type: " . $this->deduceMimeType();
        $part .= "\r\n\r\n" . $this->content;

        return $part;
    }

    /**
     * Attempts to figure out the MIME type from the file extension and the content.
     *
     * @return string        MIME type.
     */
    protected function deduceMimeType()
    {
        if ($this->isOnlyAscii($this->content)) {
            return 'text/plain';
        }

        return 'application/octet-stream';
    }

    /**
     * Tests each character is in the range 0-127.
     *
     * @param string $ascii    String to test.
     */
    protected function isOnlyAscii($ascii)
    {
        for ($i = 0, $length = strlen($ascii); $i < $length; $i++) {
            if (ord($ascii[$i]) > 127) {
                return false;
            }
        }

        return true;
    }

    /**
     * Is this the value we are looking for?
     *
     * @param string $key    Identifier.
     *
     * @return bool       True if matched.
     */
    public function isKey($key)
    {
        return $key == $this->key;
    }

    /**
     * Is this the value we are looking for?
     *
     * @return string       Identifier.
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * Is this the value we are looking for?
     *
     * @return string       Content.
     */
    public function getValue()
    {
        return $this->filename;
    }
}

/**
 * Bundle of GET/POST parameters. Can include repeated parameters.
 */
class SimpleEncoding
{
    private $request;

    /**
     * Starts empty.
     *
     * @param array $query       Hash of parameters. Multiple values are as lists on a single key.
     */
    public function __construct($query = false)
    {
        if (! $query) {
            $query = array();
        }
        $this->clear();
        $this->merge($query);
    }

    /**
     * Empties the request of parameters.
     */
    public function clear()
    {
        $this->request = array();
    }

    /**
     * Adds a parameter to the query.
     *
     * @param string $key            Key to add value to.
     * @param string/array $value    New data.
     */
    public function add($key, $value)
    {
        if ($value === false) {
            return;
        }
        if (is_array($value)) {
            foreach ($value as $item) {
                $this->addPair($key, $item);
            }
        } else {
            $this->addPair($key, $value);
        }
    }

    /**
     * Adds a new value into the request.
     *
     * @param string $key            Key to add value to.
     * @param string/array $value    New data.
     */
    protected function addPair($key, $value)
    {
        $this->request[] = new SimpleEncodedPair($key, $value);
    }

    /**
     * Adds a MIME part to the query. Does nothing for a form encoded packet.
     *
     * @param string $key          Key to add value to.
     * @param string $content      Raw data.
     * @param hash $filename       Original filename.
     */
    public function attach($key, $content, $filename)
    {
        $this->request[] = new SimpleAttachment($key, $content, $filename);
    }

    /**
     * Adds a set of parameters to this query.
     *
     * @param array/SimpleQueryString $query  Multiple values are as lists on a single key.
     */
    public function merge($query)
    {
        if (is_object($query)) {
            $this->request = array_merge($this->request, $query->getAll());
        } elseif (is_array($query)) {
            foreach ($query as $key => $value) {
                $this->add($key, $value);
            }
        }
    }

    /**
     * Accessor for single value.
     *
     * @return string/array    False if missing, string if present and array if multiple entries.
     */
    public function getValue($key)
    {
        $values = array();
        foreach ($this->request as $pair) {
            if ($pair->isKey($key)) {
                $values[] = $pair->getValue();
            }
        }
        if (count($values) == 0) {
            return false;
        } elseif (count($values) == 1) {
            return $values[0];
        } else {
            return $values;
        }
    }

    /**
     * Accessor for listing of pairs.
     *
     * @return array        All pair objects.
     */
    public function getAll()
    {
        return $this->request;
    }

    /**
     * Renders the query string as a URL encoded request part.
     *
     * @return string        Part of URL.
     */
    protected function encode()
    {
        $statements = array();
        foreach ($this->request as $pair) {
            if ($statement = $pair->asRequest()) {
                $statements[] = $statement;
            }
        }

        return implode('&', $statements);
    }
}

/**
 * Bundle of GET parameters. Can include repeated parameters.
 */
class SimpleGetEncoding extends SimpleEncoding
{
    /**
     * Starts empty.
     *
     * @param array $query       Hash of parameters. Multiple values are as lists on a single key.
     */
    public function __construct($query = false)
    {
        parent::__construct($query);
    }

    /**
     * HTTP request method.
     *
     * @return string        Always GET.
     */
    public function getMethod()
    {
        return 'GET';
    }

    /**
     * Writes no extra headers.
     *
     * @param SimpleSocket $socket        Socket to write to.
     */
    public function writeHeadersTo(&$socket)
    {
    }

    /**
     * No data is sent to the socket as the data is encoded into the URL.
     *
     * @param SimpleSocket $socket        Socket to write to.
     */
    public function writeTo(&$socket)
    {
    }

    /**
     * Renders the query string as a URL encoded request part for attaching to a URL.
     *
     * @return string        Part of URL.
     */
    public function asUrlRequest()
    {
        return $this->encode();
    }
}

/**
 * Bundle of URL parameters for a HEAD request.
 */
class SimpleHeadEncoding extends SimpleGetEncoding
{
    /**
     * Starts empty.
     *
     * @param array $query       Hash of parameters. Multiple values are as lists on a single key.
     */
    public function __construct($query = false)
    {
        parent::__construct($query);
    }

    /**
     * HTTP request method.
     *
     * @return string        Always HEAD.
     */
    public function getMethod()
    {
        return 'HEAD';
    }
}

/**
 * Bundle of URL parameters for a DELETE request.
 */
class SimpleDeleteEncoding extends SimpleGetEncoding
{
    /**
     * Starts empty.
     *
     * @param array $query       Hash of parameters. Multiple values are as lists on a single key.
     */
    public function __construct($query = false)
    {
        parent::__construct($query);
    }

    /**
     * HTTP request method.
     *
     * @return string        Always DELETE.
     */
    public function getMethod()
    {
        return 'DELETE';
    }
}

/**
 * Bundles an entity-body for transporting a raw content payload with the request.
 */
class SimpleEntityEncoding extends SimpleEncoding
{
    private $content_type;
    private $body;

    public function __construct($query = false, $content_type = false)
    {
        $this->content_type = $content_type;
        if (is_string($query)) {
            $this->body = $query;
            parent::__construct();
        } else {
            parent::__construct($query);
        }
    }

    /**
     * Returns the media type of the entity body
     *
     * @return string
     */
    public function getContentType()
    {
        if (!$this->content_type) {
            return ($this->body) ? 'text/plain' : 'application/x-www-form-urlencoded';
        }

        return $this->content_type;
    }

    /**
     * Dispatches the form headers down the socket.
     *
     * @param SimpleSocket $socket        Socket to write to.
     */
    public function writeHeadersTo(&$socket)
    {
        $socket->write('Content-Length: ' . (integer) strlen($this->encode()) . "\r\n");
        $socket->write('Content-Type: ' .  $this->getContentType() . "\r\n");
    }

    /**
     * Dispatches the form data down the socket.
     *
     * @param SimpleSocket $socket        Socket to write to.
     */
    public function writeTo(&$socket)
    {
        $socket->write($this->encode());
    }

    /**
     * Renders the request body
     *
     * @return Encoded entity body
     */
    protected function encode()
    {
        return ($this->body) ? $this->body : parent::encode();
    }
}

/**
 * Bundle of POST parameters. Can include repeated parameters.
 */
class SimplePostEncoding extends SimpleEntityEncoding
{
    /**
     * Starts empty.
     *
     * @param array $query       Hash of parameters. Multiple values are as lists on a single key.
     */
    public function __construct($query = false, $content_type = false)
    {
        if (is_array($query) and $this->hasMoreThanOneLevel($query)) {
            $query = $this->rewriteArrayWithMultipleLevels($query);
        }
        parent::__construct($query, $content_type);
    }

    public function hasMoreThanOneLevel($query)
    {
        foreach ($query as $key => $value) {
            if (is_array($value)) {
                return true;
            }
        }

        return false;
    }

    public function rewriteArrayWithMultipleLevels($query)
    {
        $query_ = array();
        foreach ($query as $key => $value) {
            if (is_array($value)) {
                foreach ($value as $sub_key => $sub_value) {
                    $query_[$key . '[' . $sub_key . ']'] = $sub_value;
                }
            } else {
                $query_[$key] = $value;
            }
        }
        if ($this->hasMoreThanOneLevel($query_)) {
            $query_ = $this->rewriteArrayWithMultipleLevels($query_);
        }

        return $query_;
    }

    /**
     * HTTP request method.
     *
     * @return string        Always POST.
     */
    public function getMethod()
    {
        return 'POST';
    }

    /**
     * Renders the query string as a URL encoded request part for attaching to a URL.
     *
     * @return string        Part of URL.
     */
    public function asUrlRequest()
    {
        return '';
    }
}

/**
 * Encoded entity body for a PUT request.
 */
class SimplePutEncoding extends SimpleEntityEncoding
{
    /**
     * Starts empty.
     *
     * @param array $query       Hash of parameters. Multiple values are as lists on a single key.
     */
    public function __construct($query = false, $content_type = false)
    {
        parent::__construct($query, $content_type);
    }

    /**
     * HTTP request method.
     *
     * @return string        Always PUT.
     */
    public function getMethod()
    {
        return 'PUT';
    }
}

/**
 * Bundle of POST parameters in the multipart format. Can include file uploads.
 */
class SimpleMultipartEncoding extends SimplePostEncoding
{
    private $boundary;

    /**
     * Starts empty.
     *
     * @param array $query       Hash of parameters. Multiple values are as lists on a single key.
     */
    public function __construct($query = false, $boundary = false)
    {
        parent::__construct($query);
        $this->boundary = ($boundary === false ? uniqid('st') : $boundary);
    }

    /**
     * Dispatches the form headers down the socket.
     *
     * @param SimpleSocket $socket        Socket to write to.
     */
    public function writeHeadersTo(&$socket)
    {
        $socket->write('Content-Length: ' . (integer) strlen($this->encode()) . "\r\n");
        $socket->write('Content-Type: multipart/form-data; boundary=' . $this->boundary . "\r\n");
    }

    /**
     * Dispatches the form data down the socket.
     *
     * @param SimpleSocket $socket        Socket to write to.
     */
    public function writeTo(&$socket)
    {
        $socket->write($this->encode());
    }

    /**
     * Renders the query string as a URL encoded request part.
     *
     * @return string        Part of URL.
     */
    public function encode()
    {
        $stream = '';
        foreach ($this->getAll() as $pair) {
            $stream .= '--' . $this->boundary . "\r\n";
            $stream .= $pair->asMime() . "\r\n";
        }
        $stream .= '--' . $this->boundary . "--\r\n";

        return $stream;
    }
}


<?php

require_once dirname(__FILE__) . '/page.php';
require_once dirname(__FILE__) . '/encoding.php';

/**
 * Creates tags and widgets given HTML tag attributes.
 */
class SimpleTagBuilder
{
    /**
     * Factory for the tag objects.
     * Creates the appropriate tag object for the incoming tag name and attributes.
     *
     * @param string $name        HTML tag name.
     * @param hash $attributes    Element attributes.
     *
     * @return SimpleTag          Tag object.
     */
    public function createTag($name, $attributes)
    {
        static $map = array(
            'a'        => 'SimpleAnchorTag',
            'title'    => 'SimpleTitleTag',
            'base'     => 'SimpleBaseTag',
            'button'   => 'SimpleButtonTag',
            'textarea' => 'SimpleTextAreaTag',
            'option'   => 'SimpleOptionTag',
            'label'    => 'SimpleLabelTag',
            'form'     => 'SimpleFormTag',
            'frame'    => 'SimpleFrameTag'
        );

        $attributes = $this->keysToLowerCase($attributes);
        if (array_key_exists($name, $map)) {
            $tag_class = $map[$name];

            return new $tag_class($attributes);
        } elseif ($name === 'select') {
            return $this->createSelectionTag($attributes);
        } elseif ($name === 'input') {
            return $this->createInputTag($attributes);
        }

        return new SimpleTag($name, $attributes);
    }

    /**
     * Factory for selection fields.
     *
     * @param hash $attributes    Element attributes.
     *
     * @return SimpleTag          Tag object.
     */
    protected function createSelectionTag($attributes)
    {
        if (isset($attributes['multiple'])) {
            return new MultipleSelectionTag($attributes);
        }

        return new SimpleSelectionTag($attributes);
    }

    /**
     * Factory for input tags.
     *
     * @param hash $attributes    Element attributes.
     *
     * @return SimpleTag          Tag object.
     */
    protected function createInputTag($attributes)
    {
        if (! isset($attributes['type'])) {
            return new SimpleTextTag($attributes);
        }
        $type = strtolower(trim($attributes['type']));
        $map  = array(
            'submit'   => 'SimpleSubmitTag',
            'image'    => 'SimpleImageSubmitTag',
            'checkbox' => 'SimpleCheckboxTag',
            'radio'    => 'SimpleRadioButtonTag',
            'text'     => 'SimpleTextTag',
            'hidden'   => 'SimpleTextTag',
            'password' => 'SimpleTextTag',
            'file'     => 'SimpleUploadTag'
        );
        if (array_key_exists($type, $map)) {
            $tag_class = $map[$type];

            return new $tag_class($attributes);
        }

        return false;
    }

    /**
     * Make the keys lower case for case insensitive look-ups.
     *
     * @param hash $map   Hash to convert.
     *
     * @return hash       Unchanged values, but keys lower case.
     */
    protected function keysToLowerCase($map)
    {
        $lower = array();
        foreach ($map as $key => $value) {
            $lower[strtolower($key)] = $value;
        }

        return $lower;
    }
}

/**
 * HTML or XML tag.
 */
class SimpleTag
{
    private $name;
    private $attributes;
    private $content;

    /**
     * Starts with a named tag with attributes only.
     *
     * @param string $name        Tag name.
     * @param hash $attributes    Attribute names and string values. Note that the keys must have
     * been converted to lower case.
     */
    public function __construct($name, $attributes)
    {
        $this->name       = strtolower(trim($name));
        $this->attributes = $attributes;
        $this->content    = '';
    }

    /**
     * Check to see if the tag can have both start and end tags with content in between.
     *
     * @return bool        True if content allowed.
     */
    public function expectEndTag()
    {
        return true;
    }

    /**
     * The current tag should not swallow all content for itself as it's searchable page content.
     * Private content tags are usually widgets that contain default values.
     *
     * @return bool        False as content is available to other tags by default.
     */
    public function isPrivateContent()
    {
        return false;
    }

    /**
     * Appends string content to the current content.
     *
     * @param string $content        Additional text.
     */
    public function addContent($content)
    {
        $this->content .= (string) $content;

        return $this;
    }

    /**
     * Adds an enclosed tag to the content.
     *
     * @param SimpleTag $tag    New tag.
     */
    public function addTag($tag)
    {
    }

    /**
     * Adds multiple enclosed tags to the content.
     *
     * @param array            List of SimpleTag objects to be added.
     */
    public function addTags($tags)
    {
        foreach ($tags as $tag) {
            $this->addTag($tag);
        }
    }

    /**
     * Accessor for tag name.
     *
     * @return string       Name of tag.
     */
    public function getTagName()
    {
        return $this->name;
    }

    /**
     * List of legal child elements.
     *
     * @return array        List of element names.
     */
    public function getChildElements()
    {
        return array();
    }

    /**
     * Accessor for an attribute.
     *
     * @param string $label    Attribute name.
     *
     * @return string          Attribute value.
     */
    public function getAttribute($label)
    {
        $label = strtolower($label);
        if (! isset($this->attributes[$label])) {
            return false;
        }

        return (string) $this->attributes[$label];
    }

    /**
     * Sets an attribute.
     *
     * @param string $label    Attribute name.
     *
     * @return string $value   New attribute value.
     */
    protected function setAttribute($label, $value)
    {
        $this->attributes[strtolower($label)] = $value;
    }

    /**
     * Accessor for the whole content so far.
     *
     * @return string       Content as big raw string.
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * Accessor for content reduced to visible text. Acts like a text mode browser, normalising
     * space and reducing images to their alt text.
     *
     * @return string       Content as plain text.
     */
    public function getText()
    {
        return SimplePage::normalise($this->content);
    }

    /**
     * Test to see if id attribute matches.
     *
     * @param string $id        ID to test against.
     *
     * @return bool          True on match.
     */
    public function isId($id)
    {
        return ($this->getAttribute('id') == $id);
    }
}

/**
 * Base url.
 */
class SimpleBaseTag extends SimpleTag
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('base', $attributes);
    }

    /**
     * Base tag is not a block tag.
     *
     * @return bool       false
     */
    public function expectEndTag()
    {
        return false;
    }
}

/**
 * Page title.
 */
class SimpleTitleTag extends SimpleTag
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('title', $attributes);
    }
}

/**
 * Link.
 */
class SimpleAnchorTag extends SimpleTag
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('a', $attributes);
    }

    /**
     * Accessor for URL as string.
     *
     * @return string    Forced to string.
     */
    public function getHref()
    {
        $url = $this->getAttribute('href');
        if (is_bool($url)) {
            $url = '';
        }

        return $url;
    }
}

/**
 * Form element.
 */
class SimpleWidget extends SimpleTag
{
    private $is_set;
    private $label;
    private $value;

    /**
     * Starts with a named tag with attributes only.
     *
     * @param string $name        Tag name.
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($name, $attributes)
    {
        parent::__construct($name, $attributes);
        $this->is_set = false;
        $this->label  = false;
        $this->value  = false;
    }

    /**
     * Accessor for name submitted as the key in GET/POST privateiables hash.
     *
     * @return string        Parsed value.
     */
    public function getName()
    {
        return $this->getAttribute('name');
    }

    /**
     * Accessor for default value parsed with the tag.
     *
     * @return string        Parsed value.
     */
    public function getDefault()
    {
        return $this->getAttribute('value');
    }

    /**
     * Accessor for currently set value or default if none.
     *
     * @return string      Value set by form or default if none.
     */
    public function getValue()
    {
        if (! $this->is_set) {
            return $this->getDefault();
        }

        return $this->value;
    }

    /**
     * Sets the current form element value.
     *
     * @param string $value       New value.
     *
     * @return bool            True if allowed.
     */
    public function setValue($value)
    {
        $this->value  = $value;
        $this->is_set = true;

        return true;
    }

    /**
     * Resets the form element value back to the default.
     */
    public function resetValue()
    {
        $this->is_set = false;
    }

    /**
     * Allows setting of a label externally, say by a label tag.
     *
     * @param string $label    Label to attach.
     */
    public function setLabel($label)
    {
        $this->label = trim($label);

        return $this;
    }

    /**
     * Reads external or internal label.
     *
     * @param string $label    Label to test.
     *
     * @return bool         True is match.
     */
    public function isLabel($label)
    {
        return $this->label == trim($label);
    }

    /**
     * Dispatches the value into the form encoded packet.
     *
     * @param SimpleEncoding $encoding    Form packet.
     */
    public function write($encoding)
    {
        if ($this->getName()) {
            $encoding->add($this->getName(), $this->getValue());
        }
    }
}

/**
 * Text, password and hidden field.
 */
class SimpleTextTag extends SimpleWidget
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('input', $attributes);
        if ($this->getAttribute('value') === false) {
            $this->setAttribute('value', '');
        }
    }

    /**
     * Tag contains no content.
     *
     * @return bool        False.
     */
    public function expectEndTag()
    {
        return false;
    }

    /**
     * Sets the current form element value. Cannot change the value of a hidden field.
     *
     * @param string $value       New value.
     *
     * @return bool            True if allowed.
     */
    public function setValue($value)
    {
        if ($this->getAttribute('type') === 'hidden') {
            return false;
        }

        return parent::setValue($value);
    }
}

/**
 * Submit button as input tag.
 */
class SimpleSubmitTag extends SimpleWidget
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('input', $attributes);
        if ($this->getAttribute('value') === false) {
            $this->setAttribute('value', 'Submit');
        }
    }

    /**
     * Tag contains no end element.
     *
     * @return bool        False.
     */
    public function expectEndTag()
    {
        return false;
    }

    /**
     * Disables the setting of the button value.
     *
     * @param string $value       Ignored.
     *
     * @return bool            True if allowed.
     */
    public function setValue($value)
    {
        return false;
    }

    /**
     * Value of browser visible text.
     *
     * @return string        Visible label.
     */
    public function getLabel()
    {
        return $this->getValue();
    }

    /**
     * Test for a label match when searching.
     *
     * @param string $label     Label to test.
     *
     * @return bool          True on match.
     */
    public function isLabel($label)
    {
        return trim($label) == trim($this->getLabel());
    }
}

/**
 * Image button as input tag.
 */
class SimpleImageSubmitTag extends SimpleWidget
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('input', $attributes);
    }

    /**
     * Tag contains no end element.
     *
     * @return bool        False.
     */
    public function expectEndTag()
    {
        return false;
    }

    /**
     * Disables the setting of the button value.
     *
     * @param string $value       Ignored.
     *
     * @return bool            True if allowed.
     */
    public function setValue($value)
    {
        return false;
    }

    /**
     * Value of browser visible text.
     *
     * @return string        Visible label.
     */
    public function getLabel()
    {
        if ($this->getAttribute('title')) {
            return $this->getAttribute('title');
        }

        return $this->getAttribute('alt');
    }

    /**
     * Test for a label match when searching.
     *
     * @param string $label     Label to test.
     *
     * @return bool          True on match.
     */
    public function isLabel($label)
    {
        return trim($label) == trim($this->getLabel());
    }

    /**
     * Dispatches the value into the form encoded packet.
     *
     * @param SimpleEncoding $encoding    Form packet.
     * @param int $x                  X coordinate of click.
     * @param int $y                  Y coordinate of click.
     */
    public function write($encoding, $x = 1, $y = 1)
    {
        if ($this->getName()) {
            $encoding->add($this->getName() . '.x', $x);
            $encoding->add($this->getName() . '.y', $y);
        } else {
            $encoding->add('x', $x);
            $encoding->add('y', $y);
        }
    }
}

/**
 * Submit button as button tag.
 */
class SimpleButtonTag extends SimpleWidget
{
    /**
     * Starts with a named tag with attributes only.
     * Defaults are very browser dependent.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('button', $attributes);
    }

    /**
     * Check to see if the tag can have both start and end tags with content in between.
     *
     * @return bool        True if content allowed.
     */
    public function expectEndTag()
    {
        return true;
    }

    /**
     * Disables the setting of the button value.
     *
     * @param string $value       Ignored.
     *
     * @return bool            True if allowed.
     */
    public function setValue($value)
    {
        return false;
    }

    /**
     * Value of browser visible text.
     *
     * @return string        Visible label.
     */
    public function getLabel()
    {
        return $this->getContent();
    }

    /**
     * Test for a label match when searching.
     *
     * @param string $label     Label to test.
     *
     * @return bool          True on match.
     */
    public function isLabel($label)
    {
        return trim($label) == trim($this->getLabel());
    }
}

/**
 * Content tag for text area.
 */
class SimpleTextAreaTag extends SimpleWidget
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('textarea', $attributes);
    }

    /**
     * Accessor for starting value.
     *
     * @return string        Parsed value.
     */
    public function getDefault()
    {
        return $this->wrap(html_entity_decode($this->getContent(), ENT_QUOTES));
    }

    /**
     * Applies word wrapping if needed.
     *
     * @param string $value      New value.
     *
     * @return bool            True if allowed.
     */
    public function setValue($value)
    {
        return parent::setValue($this->wrap($value));
    }

    /**
     * Test to see if text should be wrapped.
     *
     * @return bool        True if wrapping on.
     */
    public function wrapIsEnabled()
    {
        if ($this->getAttribute('cols')) {
            $wrap = $this->getAttribute('wrap');
            if (($wrap === 'physical') || ($wrap === 'hard')) {
                return true;
            }
        }

        return false;
    }

    /**
     * Performs the formatting that is peculiar to this tag.
     *
     * @todo  There is strange behaviour in this one, including stripping a leading new line.
     *        Go figure. I am using Firefox as a guide.
     *
     * @param string $text    Text to wrap.
     *
     * @return string         Text wrapped with carriage returns and line feeds
     */
    protected function wrap($text)
    {
        $text = str_replace("\r\r\n", "\r\n", str_replace("\n", "\r\n", $text));
        $text = str_replace("\r\n\n", "\r\n", str_replace("\r", "\r\n", $text));
        if (strncmp($text, "\r\n", strlen("\r\n")) == 0) {
            $text = substr($text, strlen("\r\n"));
        }
        if ($this->wrapIsEnabled()) {
            return wordwrap(
                    $text,
                    (integer) $this->getAttribute('cols'),
                    "\r\n");
        }

        return $text;
    }

    /**
     * The content of textarea is not part of the page.
     *
     * @return bool        True.
     */
    public function isPrivateContent()
    {
        return true;
    }
}

/**
 * File upload widget.
 */
class SimpleUploadTag extends SimpleWidget
{
    /**
     * Starts with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('input', $attributes);
    }

    /**
     * Tag contains no content.
     *
     * @return bool        False.
     */
    public function expectEndTag()
    {
        return false;
    }

    /**
     * Dispatches the value into the form encoded packet.
     *
     * @param SimpleEncoding $encoding    Form packet.
     */
    public function write($encoding)
    {
        if (! file_exists($this->getValue())) {
            return;
        }
        $encoding->attach(
                $this->getName(),
                implode('', file($this->getValue())),
                basename($this->getValue()));
    }
}

/**
 * Drop down widget.
 */
class SimpleSelectionTag extends SimpleWidget
{
    private $options;
    private $choice;

    /**
     * Starts with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('select', $attributes);
        $this->options = array();
        $this->choice  = false;
    }

    /**
     * Adds an option tag to a selection field.
     *
     * @param SimpleOptionTag $tag     New option.
     */
    public function addTag($tag)
    {
        if ($tag->getTagName() === 'option') {
            $this->options[] = $tag;
        }
    }

    /**
     * Text within the selection element is ignored.
     *
     * @param string $content        Ignored.
     */
    public function addContent($content)
    {
        return $this;
    }

    /**
     * Scans options for defaults. If none, then the first option is selected.
     *
     * @return string        Selected field.
     */
    public function getDefault()
    {
        for ($i = 0, $count = count($this->options); $i < $count; $i++) {
            if ($this->options[$i]->getAttribute('selected') !== false) {
                return $this->options[$i]->getDefault();
            }
        }
        if ($count > 0) {
            return $this->options[0]->getDefault();
        }

        return '';
    }

    /**
     * Can only set allowed values.
     *
     * @param string $value       New choice.
     *
     * @return bool            True if allowed.
     */
    public function setValue($value)
    {
        for ($i = 0, $count = count($this->options); $i < $count; $i++) {
            if ($this->options[$i]->isValue($value)) {
                $this->choice = $i;

                return true;
            }
        }

        return false;
    }

    /**
     * Accessor for current selection value.
     *
     * @return string      Value attribute or content of opton.
     */
    public function getValue()
    {
        if ($this->choice === false) {
            return $this->getDefault();
        }

        return $this->options[$this->choice]->getValue();
    }
}

/**
 * Drop down widget.
 */
class MultipleSelectionTag extends SimpleWidget
{
    private $options;
    private $values;

    /**
     * Starts with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('select', $attributes);
        $this->options = array();
        $this->values  = false;
    }

    /**
     * Adds an option tag to a selection field.
     *
     * @param SimpleOptionTag $tag     New option.
     */
    public function addTag($tag)
    {
        if ($tag->getTagName() === 'option') {
            $this->options[] = &$tag;
        }
    }

    /**
     * Text within the selection element is ignored.
     *
     * @param string $content        Ignored.
     */
    public function addContent($content)
    {
        return $this;
    }

    /**
     * Scans options for defaults to populate the value array().
     *
     * @return array        Selected fields.
     */
    public function getDefault()
    {
        $default = array();
        for ($i = 0, $count = count($this->options); $i < $count; $i++) {
            if ($this->options[$i]->getAttribute('selected') !== false) {
                $default[] = $this->options[$i]->getDefault();
            }
        }

        return $default;
    }

    /**
     * Can only set allowed values.
     * Any illegal value will result in a failure, but all correct values will be set.
     *
     * @param array $desired      New choices.
     *
     * @return bool            True if all allowed.
     */
    public function setValue($desired)
    {
        $achieved = array();
        foreach ($desired as $value) {
            $success = false;
            for ($i = 0, $count = count($this->options); $i < $count; $i++) {
                if ($this->options[$i]->isValue($value)) {
                    $achieved[] = $this->options[$i]->getValue();
                    $success    = true;
                    break;
                }
            }
            if (! $success) {
                return false;
            }
        }
        $this->values = $achieved;

        return true;
    }

    /**
     * Accessor for current selection value.
     *
     * @return array      List of currently set options.
     */
    public function getValue()
    {
        if ($this->values === false) {
            return $this->getDefault();
        }

        return $this->values;
    }
}

/**
 * Option for selection field.
 */
class SimpleOptionTag extends SimpleWidget
{
    /**
     * Stashes the attributes.
     */
    public function __construct($attributes)
    {
        parent::__construct('option', $attributes);
    }

    /**
     * Does nothing.
     *
     * @param string $value      Ignored.
     *
     * @return bool           Not allowed.
     */
    public function setValue($value)
    {
        return false;
    }

    /**
     * Test to see if a value matches the option.
     *
     * @param string $compare    Value to compare with.
     *
     * @return bool           True if possible match.
     */
    public function isValue($compare)
    {
        $compare = trim($compare);
        if (trim($this->getValue()) == $compare) {
            return true;
        }

        return trim(strip_tags($this->getContent())) == $compare;
    }

    /**
     * Accessor for starting value. Will be set to the option label if no value exists.
     *
     * @return string        Parsed value.
     */
    public function getDefault()
    {
        if ($this->getAttribute('value') === false) {
            return strip_tags($this->getContent());
        }

        return $this->getAttribute('value');
    }

    /**
     * The content of options is not part of the page.
     *
     * @return bool        True.
     */
    public function isPrivateContent()
    {
        return true;
    }
}

/**
 * Radio button.
 */
class SimpleRadioButtonTag extends SimpleWidget
{
    /**
     * Stashes the attributes.
     *
     * @param array $attributes        Hash of attributes.
     */
    public function __construct($attributes)
    {
        parent::__construct('input', $attributes);
        if ($this->getAttribute('value') === false) {
            $this->setAttribute('value', 'on');
        }
    }

    /**
     * Tag contains no content.
     *
     * @return bool        False.
     */
    public function expectEndTag()
    {
        return false;
    }

    /**
     * The only allowed value sn the one in the "value" attribute.
     *
     * @param string $value      New value.
     *
     * @return bool           True if allowed.
     */
    public function setValue($value)
    {
        if ($value === false) {
            return parent::setValue($value);
        }
        if ($value != $this->getAttribute('value')) {
            return false;
        }

        return parent::setValue($value);
    }

    /**
     * Accessor for starting value.
     *
     * @return string        Parsed value.
     */
    public function getDefault()
    {
        if ($this->getAttribute('checked') !== false) {
            return $this->getAttribute('value');
        }

        return false;
    }
}

/**
 * Checkbox widget.
 */
class SimpleCheckboxTag extends SimpleWidget
{
    /**
     * Starts with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('input', $attributes);
        if ($this->getAttribute('value') === false) {
            $this->setAttribute('value', 'on');
        }
    }

    /**
     * Tag contains no content.
     *
     * @return bool        False.
     */
    public function expectEndTag()
    {
        return false;
    }

    /**
     * The only allowed value in the one in the "value" attribute.
     * The default for this attribute is "on".
     * If this widget is set to true, then the usual value will be taken.
     *
     * @param string $value      New value.
     *
     * @return bool           True if allowed.
     */
    public function setValue($value)
    {
        if ($value === false) {
            return parent::setValue($value);
        }
        if ($value === true) {
            return parent::setValue($this->getAttribute('value'));
        }
        if ($value != $this->getAttribute('value')) {
            return false;
        }

        return parent::setValue($value);
    }

    /**
     * Accessor for starting value. The default value is "on".
     *
     * @return string        Parsed value.
     */
    public function getDefault()
    {
        if ($this->getAttribute('checked') !== false) {
            return $this->getAttribute('value');
        }

        return false;
    }
}

/**
 * A group of multiple widgets with some shared behaviour.
 */
class SimpleTagGroup
{
    private $widgets = array();

    /**
     * Adds a tag to the group.
     *
     * @param SimpleWidget $widget
     */
    public function addWidget($widget)
    {
        $this->widgets[] = $widget;
    }

    /**
     * Accessor to widget set.
     *
     * @return array        All widgets.
     */
    protected function &getWidgets()
    {
        return $this->widgets;
    }

    /**
     * Accessor for an attribute.
     *
     * @param string $label    Attribute name.
     *
     * @return bool         Always false.
     */
    public function getAttribute($label)
    {
        return false;
    }

    /**
     * Fetches the name for the widget from the first member.
     *
     * @return string        Name of widget.
     */
    public function getName()
    {
        if (count($this->widgets) > 0) {
            return $this->widgets[0]->getName();
        }
    }

    /**
     * Scans the widgets for one with the appropriate ID field.
     *
     * @param string $id        ID value to try.
     *
     * @return bool          True if matched.
     */
    public function isId($id)
    {
        for ($i = 0, $count = count($this->widgets); $i < $count; $i++) {
            if ($this->widgets[$i]->isId($id)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Scans the widgets for one with the appropriate attached label.
     *
     * @param string $label     Attached label to try.
     *
     * @return bool          True if matched.
     */
    public function isLabel($label)
    {
        for ($i = 0, $count = count($this->widgets); $i < $count; $i++) {
            if ($this->widgets[$i]->isLabel($label)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Dispatches the value into the form encoded packet.
     *
     * @param SimpleEncoding $encoding    Form packet.
     */
    public function write($encoding)
    {
        $encoding->add($this->getName(), $this->getValue());
    }
}

/**
 * A group of tags with the same name within a form.
 */
class SimpleCheckboxGroup extends SimpleTagGroup
{
    /**
     * Accessor for current selected widget or false if none.
     *
     * @return string/array     Widget values or false if none.
     */
    public function getValue()
    {
        $values  = array();
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getValue() !== false) {
                $values[] = $widgets[$i]->getValue();
            }
        }

        return $this->forceValues($values);
    }

    /**
     * Accessor for starting value that is active.
     *
     * @return string/array      Widget values or false if none.
     */
    public function getDefault()
    {
        $values  = array();
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getDefault() !== false) {
                $values[] = $widgets[$i]->getDefault();
            }
        }

        return $this->forceValues($values);
    }

    /**
     * Accessor for current set values.
     *
     * @param string/array/boolean $values   Either a single string, a hash or false for nothing
     * set.
     *
     * @return bool                       True if all values can be set.
     */
    public function setValue($values)
    {
        $values = $this->makeArray($values);
        if (! $this->valuesArePossible($values)) {
            return false;
        }
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            $possible = $widgets[$i]->getAttribute('value');
            if (in_array($widgets[$i]->getAttribute('value'), $values)) {
                $widgets[$i]->setValue($possible);
            } else {
                $widgets[$i]->setValue(false);
            }
        }

        return true;
    }

    /**
     * Tests to see if a possible value set is legal.
     *
     * @param string/array/boolean $values Either a single string, a hash or false for nothing set.
     *
     * @return bool False if trying to set a missing value.
     */
    protected function valuesArePossible($values)
    {
        $matches = array();
        $widgets = &$this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            $possible = $widgets[$i]->getAttribute('value');
            if (in_array($possible, $values)) {
                $matches[] = $possible;
            }
        }

        return ($values == $matches);
    }

    /**
     * Converts the output to an appropriate format.
     * This means that no values is false, a single
     * value is just that value and only two or more are contained in an array.
     *
     * @param array $values           List of values of widgets.
     *
     * @return string/array/boolean   Expected format for a tag.
     */
    protected function forceValues($values)
    {
        if (count($values) == 0) {
            return false;
        } elseif (count($values) == 1) {
            return $values[0];
        } else {
            return $values;
        }
    }

    /**
     * Converts false or string into array. The opposite of the coercian method.
     *
     * @param string/array/boolean $value  A single item is converted to a one item list. False
     * gives an empty list.
     *
     * @return array                       List of values, possibly empty.
     */
    protected function makeArray($value)
    {
        if ($value === false) {
            return array();
        }
        if (is_string($value)) {
            return array($value);
        }

        return $value;
    }
}

/**
 * A group of tags with the same name within a form.
 * Used for radio buttons.
 */
class SimpleRadioGroup extends SimpleTagGroup
{
    /**
     * Each tag is tried in turn until one is successfully set.
     * The others will be unchecked if successful.
     *
     * @param string $value      New value.
     *
     * @return bool           True if any allowed.
     */
    public function setValue($value)
    {
        if (! $this->valueIsPossible($value)) {
            return false;
        }
        $index   = false;
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if (! $widgets[$i]->setValue($value)) {
                $widgets[$i]->setValue(false);
            }
        }

        return true;
    }

    /**
     * Tests to see if a value is allowed.
     *
     * @param string    Attempted value.
     *
     * @return bool  True if a valid value.
     */
    protected function valueIsPossible($value)
    {
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getAttribute('value') == $value) {
                return true;
            }
        }

        return false;
    }

    /**
     * Accessor for current selected widget or false if none.
     *
     * @return string/boolean   Value attribute or content of opton.
     */
    public function getValue()
    {
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getValue() !== false) {
                return $widgets[$i]->getValue();
            }
        }

        return false;
    }

    /**
     * Accessor for starting value that is active.
     *
     * @return string/boolean      Value of first checked widget or false if none.
     */
    public function getDefault()
    {
        $widgets = $this->getWidgets();
        for ($i = 0, $count = count($widgets); $i < $count; $i++) {
            if ($widgets[$i]->getDefault() !== false) {
                return $widgets[$i]->getDefault();
            }
        }

        return false;
    }
}

/**
 * Tag to keep track of labels.
 */
class SimpleLabelTag extends SimpleTag
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('label', $attributes);
    }

    /**
     * Access for the ID to attach the label to.
     *
     * @return string        For attribute.
     */
    public function getFor()
    {
        return $this->getAttribute('for');
    }
}

/**
 * Tag to aid parsing the form.
 */
class SimpleFormTag extends SimpleTag
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('form', $attributes);
    }
}

/**
 * Tag to aid parsing the frames in a page.
 */
class SimpleFrameTag extends SimpleTag
{
    /**
     * Starts with a named tag with attributes only.
     *
     * @param hash $attributes    Attribute names and string values.
     */
    public function __construct($attributes)
    {
        parent::__construct('frame', $attributes);
    }

    /**
     * Tag contains no content.
     *
     * @return bool        False.
     */
    public function expectEndTag()
    {
        return false;
    }
}