swaggest/php-code-builder

View on GitHub
src/Markdown/TypeBuilder.php

Summary

Maintainability
F
5 days
Test Coverage
<?php

namespace Swaggest\PhpCodeBuilder\Markdown;

use Swaggest\CodeBuilder\TableRenderer;
use Swaggest\JsonSchema\Schema;
use Swaggest\PhpCodeBuilder\PhpCode;

class TypeBuilder
{
    const EXAMPLES = 'examples';
    const EXAMPLE = 'example';

    /** @var \SplObjectStorage */
    private $processed;

    public $trimNamePrefix = [
        '#/definitions'
    ];

    public $addNamePrefix = '';

    /**
     * Map of type name to type doc.
     * @var array<string,string>
     */
    public $types = [];

    public $uniqueTypeSchemas = [];

    public $file = '';

    public $confluence = false;

    public function __construct()
    {
        $this->processed = new \SplObjectStorage();
    }

    public function anchorLink($destinationHeader, $anchor = null)
    {
        if ($this->confluence) {
            $l = str_replace('`', '', $destinationHeader);
            $l = str_replace(' ', '-', $l);
            if (!is_string($l)) {
                return '#';
            }

            $l = urlencode($l);

            return '#' . $l;
        }

        if (!empty($anchor)) {
            $l = strtolower($anchor);
        } else {
            $l = strtolower($destinationHeader);
        }

        return '#' . $l;
    }

    public function header($text, $anchor = null)
    {
        if ($this->confluence) {
            return $text;
        }

        if (!empty($anchor)) {
            $l = strtolower($anchor);
        } else {
            $l = strtolower($text);
        }
        return '<a id="' . $l . '">' . '</a> ' . $text;
    }

    /**
     * @param Schema|boolean|null $schema
     * @param string $path
     * @return string
     */
    public function getTypeString($schema, $path = '')
    {
        if ($schema === null) {
            return '';
        }

        $schema = Schema::unboolSchema($schema);

        $isOptional = false;
        $isObject = false;
        $isArray = false;
        $isBoolean = false;
        $isString = false;
        $isNumber = false;

        if ($schema->const !== null) {
            return '`' . var_export($schema->const, true) . '`';
        }

        if (!empty($schema->enum)) {
            $res = '';
            if ($this->confluence) {
                foreach ($schema->enum as $value) {
                    $res .= '`' . var_export($value, true) . '`, ';
                }
                return substr($res, 0, -2);
            } else {
                foreach ($schema->enum as $value) {
                    $res .= '<br>`' . var_export($value, true) . '`, ';
                }
                return substr($res, 4, -2);
            }
        }

        if (!empty($schema->getFromRefs())) {
            $refs = $schema->getFromRefs();
            $path = $refs[0];
        }

        $type = $schema->type;
        if ($type === null) {
            $type = [];

            if (!empty($schema->properties) || !empty($schema->additionalProperties) || !empty($schema->patternProperties)) {
                $type[] = Schema::OBJECT;
            }

            if (!empty($schema->items) || !empty($schema->additionalItems)) {
                $type[] = Schema::_ARRAY;
            }
        }

        if (!is_array($type)) {
            $type = [$type];
        }

        $or = [];

        if ($schema->oneOf !== null) {
            foreach ($schema->oneOf as $i => $item) {
                $or[] = $this->getTypeString($item, $path . '/oneOf/' . $i);
            }
        }

        if ($schema->anyOf !== null) {
            foreach ($schema->anyOf as $i => $item) {
                $or[] = $this->getTypeString($item, $path . '/anyOf/' . $i);
            }
        }

        if ($schema->allOf !== null) {
            foreach ($schema->allOf as $i => $item) {
                $or[] = $this->getTypeString($item, $path . '/allOf/' . $i);
            }
        }

        if ($schema->then !== null) {
            $or[] = $this->getTypeString($schema->then, $path . '/then');
        }

        if ($schema->else !== null) {
            $or[] = $this->getTypeString($schema->else, $path . '/else');
        }

        foreach ($type as $i => $t) {
            switch ($t) {
                case Schema::NULL:
                    $isOptional = true;
                    break;

                case Schema::OBJECT:
                    $isObject = true;
                    break;

                case Schema::_ARRAY:
                    $isArray = true;
                    break;

                case Schema::NUMBER:
                case Schema::INTEGER:
                    $isNumber = true;
                    break;

                case Schema::STRING:
                    $isString = true;
                    break;

                case Schema::BOOLEAN:
                    $isBoolean = true;
                    break;

            }
        }


        $namedTypeAdded = false;
        if (!empty($schema->properties) || $this->hasConstraints($schema)) {
            if ($this->processed->contains($schema)) {
                $or [] = $this->processed->offsetGet($schema);
                $namedTypeAdded = true;
            } else {
                if ($schema instanceof Schema) {
                    $typeName = $this->typeName($schema, $path);
                    $this->makeTypeDef($schema, $path);

                    $or [] = $typeName;
                    $namedTypeAdded = true;
                }
            }
        }

        if ($isObject) {
            $typeAdded = false;

            if ($namedTypeAdded) {
                $typeAdded = true;
            }

            if ($schema->additionalProperties instanceof Schema) {
                $typeName = $this->getTypeString($schema->additionalProperties, $path . '/additionalProperties');
                $or [] = "`Map<String,`$typeName`>`";
                $typeAdded = true;
            }

            if (!empty($schema->patternProperties)) {
                foreach ($schema->patternProperties as $pattern => $propertySchema) {
                    if ($propertySchema instanceof Schema) {
                        $typeName = $this->getTypeString($propertySchema, $path . '/patternProperties/' . $pattern);
                        $or [] = $typeName;
                        $typeAdded = true;
                    }
                }
            }

            if (!$typeAdded) {
                $or [] = '`Object`';
            }
        }

        if ($isArray) {
            $typeAdded = false;

            if ($schema->items instanceof Schema) {
                $typeName = $this->getTypeString($schema->items, $path . '/items');
                $or [] = "`Array<`$typeName`>`";
                $typeAdded = true;
            }

            if ($schema->additionalItems instanceof Schema) {
                $typeName = $this->getTypeString($schema->additionalItems, $path . '/additionalItems');
                $or [] = "`Array<`$typeName`>`";
                $typeAdded = true;
            }

            if (!$typeAdded) {
                $or [] = '`Array`';
            }
        }

        if ($isOptional) {
            $or [] = '`null`';
        }

        if ($isString) {
            $or [] = '`String`';
        }

        if ($isNumber) {
            $or [] = '`Number`';
        }

        if ($isBoolean) {
            $or [] = '`Boolean`';
        }

        if ($schema->format !== null) {
            $or [] = 'Format: `' . $schema->format . '`';
        }

        $res = '';
        foreach ($or as $item) {
            if (!empty($item) && $item !== '*') {
                $res .= ', ' . $item;
            }
        }

        if ($res !== '') {
            $res = substr($res, 2);
        } else {
            $res = '`*`';
        }

        if (empty($res)) {
            $res = '';
        }

        $res = str_replace('``', '', $res);

        return $res;
    }

    private function typeName(Schema $schema, $path, $raw = false)
    {
        if ($fromRefs = $schema->getFromRefs()) {
            $path = $fromRefs[count($fromRefs) - 1];
        }

        foreach ($this->trimNamePrefix as $prefix) {
            if ($prefix === substr($path, 0, strlen($prefix))) {
                $path = substr($path, strlen($prefix));
            }
        }

        if (($path === '#' || empty($path)) && !empty($schema->title)) {
            $path = $schema->title;
        }

        $name = PhpCode::makePhpName($this->addNamePrefix . '_' . $path, false);

        if ($raw) {
            return $name;
        }

        if ($this->confluence) {
            return '[' . $name . '](' . $this->anchorLink($name) . ')';
        }

        return '[`' . $name . '`](' . $this->anchorLink($name) . ')';
    }

    private static function constraints()
    {
        static $constraints;

        if ($constraints === null) {
            $names = Schema::names();
            $constraints = [
                $names->multipleOf,
                $names->maximum,
                $names->exclusiveMaximum,
                $names->minimum,
                $names->exclusiveMinimum,
                $names->maxLength,
                $names->minLength,
                $names->pattern,
                $names->maxItems,
                $names->minItems,
                $names->uniqueItems,
                $names->maxProperties,
                $names->minProperties,
            ];
        }

        return $constraints;
    }

    /**
     * @param Schema $schema
     */
    private function hasConstraints($schema)
    {
        foreach (self::constraints() as $name) {
            if ($schema->$name !== null) {
                return true;
            }
        }

        return false;
    }

    public function renderTypeDef(Schema $schema, $typeName, $path)
    {
        $head = '';
        if (!empty($schema->title) && $schema->title != $typeName) {
            $head .= $schema->title . "\n";
        }

        if (!empty($schema->description)) {
            $head .= $schema->description . "\n";
        }

        $examples = [];
        if (!empty($schema->{self::EXAMPLES})) {
            $examples = $schema->{self::EXAMPLES};
        }

        if (!empty($schema->{self::EXAMPLE})) {
            $examples[] = $schema->{self::EXAMPLE};
        }

        if (!empty($examples)) {
            $head .= "Example:\n\n";
            foreach ($examples as $example) {
                $head .= <<<MD
```json
$example
```


MD;

            }
        }

        $res = <<<MD


### {$this->header($typeName)}
$head

MD;


        $rows = [];
        foreach (self::constraints() as $name) {
            if ($schema->$name !== null) {
                $value = $schema->$name;

                if ($value instanceof Schema) {
                    $value = $this->typeName($value, $path . '/' . $name);
                }

                $rows [] = [
                    'Constraint' => $name,
                    'Value' => $value,
                ];
            }
        }
        $tr = TableRenderer::create(new \ArrayIterator($rows))
            ->stripEmptyColumns()
            ->setColDelimiter('|')
            ->setHeadRowDelimiter('-')
            ->setOutlineVertical(true)
            ->setShowHeader();

        if (!$this->confluence) {
            $tr->multilineCellDelimiter('<br>');
        }

        $res .= $tr;

        $res .= "\n\n";

        $rows = [];
        $hasDescription = false;
        if (!empty($schema->properties)) {
            foreach ($schema->properties as $propertyName => $propertySchema) {
                $typeString = $this->getTypeString($propertySchema, $path . '/' . $propertyName);
                $desc = $this->description($propertySchema);
                if (!empty($desc)) {
                    $hasDescription = true;
                }
                $isRequired = false;
                if (!empty($schema->required)) {
                    $isRequired = in_array($propertyName, $schema->required);
                }
                $rows [] = array(
                    'Property' => '`' . $propertyName . '`' . ($isRequired ? ' (required)' : ''),
                    'Type' => $typeString,
                    'Description' => $desc,
                );
            }

            if (!$hasDescription) {
                foreach ($rows as &$row) {
                    unset($row['Description']);
                }
            }

            $tr = TableRenderer::create(new \ArrayIterator($rows))
                ->stripEmptyColumns()
                ->setColDelimiter('|')
                ->setHeadRowDelimiter('-')
                ->setOutlineVertical(true)
                ->setShowHeader();

            if (!$this->confluence) {
                $tr->multilineCellDelimiter('<br>');
            }

            $res .= $tr;
        }

        $res .= <<<MD

MD;

        return $res;
    }

    private function makeTypeDef(Schema $schema, $path)
    {
        $tn = $this->typeName($schema, $path, true);
        $typeName = $this->typeName($schema, $path);
        $this->processed->attach($schema, $typeName);

        $res = $this->renderTypeDef($schema, $tn, $path);

        if (isset($this->uniqueTypeSchemas[$res])) {
            return $this->uniqueTypeSchemas[$res];
        }

        $this->types[$typeName] = $res;
        $this->uniqueTypeSchemas[$res] = $typeName;
        $this->file .= $res;

        return $typeName;
    }

    public function sortTypes()
    {
        ksort($this->types);
    }

    public function tableOfContents()
    {
        if (count($this->types) === 0) {
            return '';
        }

        $res = '# Types' . "\n\n";

        foreach ($this->types as $name => $doc) {
            $res .= '  * ' . $name . "\n";
        }

        $res .= "\n\n";

        return $res;
    }

    private function trim($s)
    {
        if (empty($s)) {
            return '';
        }

        return trim($s);
    }

    private function description(Schema $schema)
    {
        $res = str_replace("\n", " ", $this->trim($schema->title));
        if (!is_string($res)) {
            return '';
        }

        if ($this->trim($schema->description)) {
            if ($res) {
                $res .= ". ";
            }

            $res .= str_replace("\n", " ", $this->trim($schema->description));
        }
        if ($res) {
            return rtrim($res, '.') . '.';
        }

        return '';
    }
}