gdbots/pbjc-php

View on GitHub
src/Generator/Generator.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Gdbots\Pbjc\Generator;

use Gdbots\Common\Util\StringUtils;
use Gdbots\Pbjc\CompileOptions;
use Gdbots\Pbjc\EnumDescriptor;
use Gdbots\Pbjc\FieldDescriptor;
use Gdbots\Pbjc\Generator\Twig\StringExtension;
use Gdbots\Pbjc\SchemaDescriptor;
use Gdbots\Pbjc\SchemaStore;
use Gdbots\Pbjc\Util\OutputFile;

abstract class Generator
{
    const TEMPLATE_DIR = __DIR__ . '/Twig/';
    const LANGUAGE = 'unknown';
    const EXTENSION = '.unk';
    const MANIFEST = 'pbj-schemas';

    /** @var CompileOptions */
    protected $compileOptions;

    /** @var \Twig_Environment */
    protected $twig;

    /**
     * @param CompileOptions $compileOptions
     */
    public function __construct(CompileOptions $compileOptions)
    {
        $this->compileOptions = $compileOptions;
    }

    /**
     * Generates code for the given SchemaDescriptor.
     *
     * Produces files for (varies by language):
     * - message class (the concrete class - curie major)
     * - message interface (curie)
     * - mixin (the schema fields that are "mixed" into the message)
     * - mixin interface (curie)
     * - mixin major interface (curie major for the mixin)
     * - mixin trait (any methods provided by insertion points)
     *
     * @param SchemaDescriptor $schema
     *
     * @return GeneratorResponse
     */
    public function generateSchema(SchemaDescriptor $schema)
    {
        $response = new GeneratorResponse();

        foreach ($schema->getFields() as $field) {
            $this->updateFieldOptions($schema, $field);
        }

        if ($schema->isMixinSchema()) {
            $this->generateMixin($schema, $response);
            $this->generateMixinInterface($schema, $response);
            $this->generateMixinMajorInterface($schema, $response);
            $this->generateMixinTrait($schema, $response);
        } else {
            $this->generateMessage($schema, $response);
            $this->generateMessageInterface($schema, $response);
        }

        return $response;
    }

    /**
     * Generates code for an Enum.
     *
     * @param EnumDescriptor $enum
     *
     * @return GeneratorResponse
     */
    public function generateEnum(EnumDescriptor $enum)
    {
        return new GeneratorResponse();
    }

    /**
     * Generates a manifest of all messages the store provides.
     * This is used to configure the MessageResolver.
     *
     * @param SchemaDescriptor[] $schemas
     *
     * @return GeneratorResponse
     */
    public function generateManifest(array $schemas)
    {
        $response = new GeneratorResponse();
        $manifests = ['all' => []];

        /** @var SchemaDescriptor $schema */
        foreach ($schemas as $schema) {
            $id = $schema->getId();
            $curie = $id->getCurie();
            $pkg = "{$id->getVendor()}-{$id->getPackage()}";
            $category = $id->getCategory() ? "category-{$id->getCategory()}" : '';
            if (!isset($manifests[$pkg])) {
                $manifests[$pkg] = [];
            }

            if (!isset($manifests[$category])) {
                $manifests[$category] = [];
            }

            if ($schema->isMixinSchema()) {
                continue;
            }

            if (isset($manifests['all'][$id->getCurieWithMajorRev()])) {
                continue;
            }

            if (!SchemaStore::hasOtherSchemaMajorRev($id)) {
                $manifests['all'][$curie] = $schema;
                $manifests[$pkg][$curie] = $schema;
                $manifests[$category][$curie] = $schema;
                continue;
            }

            if ($schema->isLatestVersion()) {
                $manifests['all'][$curie] = $schema;
                $manifests[$pkg][$curie] = $schema;
                $manifests[$category][$curie] = $schema;
            }

            $manifests['all'][$id->getCurieWithMajorRev()] = $schema;
            $manifests[$pkg][$id->getCurieWithMajorRev()] = $schema;
            $manifests[$category][$id->getCurieWithMajorRev()] = $schema;

            /** @var SchemaDescriptor $s */
            foreach (SchemaStore::getOtherSchemaMajorRev($schema->getId()) as $s) {
                $spkg = "{$s->getId()->getVendor()}-{$s->getId()->getPackage()}";
                $scategory = "category-{$s->getId()->getCategory()}";
                $manifests['all'][$s->getId()->getCurieWithMajorRev()] = $s;
                $manifests[$spkg][$s->getId()->getCurieWithMajorRev()] = $s;
                $manifests[$scategory][$s->getId()->getCurieWithMajorRev()] = $s;
            }
        }

        foreach ($manifests as $group => $schemas) {
            if ('all' !== $group) {
                // we are getting rid of manifest groups... quick n dirty removal here.
                // more complete refactor to come later
                continue;
            }

            $filename = 'all' === $group ? static::MANIFEST : $group;

            // delete invalid schemas
            foreach ($schemas as $key => $schema) {
                if (!SchemaStore::getSchemaById($schema->getId(), true)) {
                    unset($schemas[$key]);
                }
            }

            if (empty($schemas)) {
                continue;
            }

            ksort($schemas);

            $manifest = [
                'version' => '0.1',
                'curies'  => [],
                'classes' => [],
                'mixins'  => [],
            ];

            $id = 0;

            /** @var SchemaDescriptor $schema */
            foreach ($schemas as $curie => $schema) {
                $manifest['curies'][$curie] = $id;
                $manifest['classes'][$id] = $this->schemaToNativeClassPath($schema);
                foreach ($schema->getMixins() as $mixin) {
                    $mixinId = $mixin->getId()->getCurieWithMajorRev();
                    if (!isset($manifest['mixins'][$mixinId])) {
                        $manifest['mixins'][$mixinId] = [];
                    }

                    $manifest['mixins'][$mixinId][] = $id;
                }
                ++$id;
            }

            ksort($manifest['mixins']);

            $response->addFile(
                $this->generateOutputFile('manifest.twig', $filename, [
                    'schemas' => $schemas,
                    'manifest' => $manifest,
                ])
            );
        }

        return $response;
    }

    /**
     * Returns the class name to be used for the given SchemaDescriptor.
     *
     * @param SchemaDescriptor $schema
     * @param bool             $withMajor
     *
     * @return string
     */
    public function schemaToClassName(SchemaDescriptor $schema, $withMajor = false)
    {
        $className = StringUtils::toCamelFromSlug($schema->getId()->getMessage());
        if (!$withMajor) {
            return $className;
        }

        return "{$className}V{$schema->getId()->getVersion()->getMajor()}";
    }

    /**
     * Returns a fully qualified class name to be used for the given SchemaDescriptor.
     * Use this in generated code to avoid name collisions.
     *
     * @param SchemaDescriptor $schema
     * @param bool             $withMajor
     *
     * @return string
     */
    public function schemaToFqClassName(SchemaDescriptor $schema, $withMajor = false)
    {
        $id = $schema->getId();
        $vendor = StringUtils::toCamelFromSlug($id->getVendor());
        $package = StringUtils::toCamelFromSlug(str_replace('.', '-', $id->getPackage()));
        return "{$vendor}{$package}{$this->schemaToClassName($schema, $withMajor)}";
    }

    /**
     * Returns the class name to be used for the given EnumDescriptor.
     *
     * @param EnumDescriptor $enum
     *
     * @return string
     */
    public function enumToClassName(EnumDescriptor $enum)
    {
        return StringUtils::toCamelFromSlug($enum->getId()->getName());
    }

    /**
     * Returns the native package name for the SchemaDescriptor as
     * looked up in compile options or created automatically.
     *
     * @param SchemaDescriptor $schema
     *
     * @return string
     */
    public function schemaToNativePackage(SchemaDescriptor $schema)
    {
        $id = $schema->getId();
        return $this->getNativePackage($id->getVendor(), $id->getPackage());
    }

    /**
     * Returns the native package name for the EnumDescriptor as
     * looked up in compile options or created automatically.
     *
     * @param EnumDescriptor $enum
     *
     * @return string
     */
    public function enumToNativePackage(EnumDescriptor $enum)
    {
        $id = $enum->getId();
        return $this->getNativePackage($id->getVendor(), $id->getPackage());
    }

    /**
     * Returns the native namespace for the SchemaDescriptor
     * by combining native package and curie.
     *
     * @example
     *  es6: import Article from '@acme/schemas/acme/blog/node';
     *  php: use Acme\Schemas\Blog\Node;
     *
     * @param SchemaDescriptor $schema
     *
     * @return string
     */
    public function schemaToNativeNamespace(SchemaDescriptor $schema)
    {
    }

    /**
     * Returns the native class path for the SchemaDescriptor
     * by combining native namespace and class name with major.
     *
     * @example
     *  es6: @acme/schemas/acme/blog/node/ArticleV1
     *  php: Acme\Schemas\Blog\Node\ArticleV1
     *
     * @param SchemaDescriptor $schema
     *
     * @return string
     */
    public function schemaToNativeClassPath(SchemaDescriptor $schema)
    {
        $path = $this->schemaToNativeNamespace($schema);
        $class = $this->schemaToClassName($schema, true);
        $delim = 'php' === static::LANGUAGE ? '\\' : '/';
        return "{$path}{$delim}{$class}";
    }

    /**
     * Returns the native namespace for the EnumDescriptor
     * by combining native package and curie.
     *
     * @example
     *  es6: import SomeEnum from '@acme/schemas/acme/blog/enums';
     *  php: use Acme\Schemas\Blog\Enum;
     *
     * @param EnumDescriptor $enum
     *
     * @return string
     */
    public function enumToNativeNamespace(EnumDescriptor $enum)
    {
    }

    /**
     * Generate a message (the concrete class)
     *
     * @param SchemaDescriptor  $schema
     * @param GeneratorResponse $response
     */
    protected function generateMessage(SchemaDescriptor $schema, GeneratorResponse $response)
    {
    }

    /**
     * Generates a message interface.
     *
     * @param SchemaDescriptor  $schema
     * @param GeneratorResponse $response
     */
    protected function generateMessageInterface(SchemaDescriptor $schema, GeneratorResponse $response)
    {
    }

    /**
     * Generates a mixin (schema fields "mixed" into messages).
     *
     * @param SchemaDescriptor  $schema
     * @param GeneratorResponse $response
     */
    protected function generateMixin(SchemaDescriptor $schema, GeneratorResponse $response)
    {
    }

    /**
     * Generates a mixin interface.
     *
     * @param SchemaDescriptor  $schema
     * @param GeneratorResponse $response
     */
    protected function generateMixinInterface(SchemaDescriptor $schema, GeneratorResponse $response)
    {
    }

    /**
     * Generates a mixin major (as in curie major) interface.
     *
     * @param SchemaDescriptor  $schema
     * @param GeneratorResponse $response
     */
    protected function generateMixinMajorInterface(SchemaDescriptor $schema, GeneratorResponse $response)
    {
    }

    /**
     * Generates a mixin trait (the methods provided by a mixin).
     *
     * @param SchemaDescriptor  $schema
     * @param GeneratorResponse $response
     */
    protected function generateMixinTrait(SchemaDescriptor $schema, GeneratorResponse $response)
    {
    }

    /**
     * Adds and updates field php options.
     *
     * @param SchemaDescriptor $schema
     * @param FieldDescriptor  $field
     */
    protected function updateFieldOptions(SchemaDescriptor $schema, FieldDescriptor $field)
    {
    }

    /**
     * @param string $template
     * @param string $file
     * @param array  $parameters
     *
     * @return OutputFile
     */
    protected function generateOutputFile($template, $file, array $parameters)
    {
        $template = sprintf('%s/%s', static::LANGUAGE, $template);
        $content = $this->render($template, $parameters);
        $ext = static::EXTENSION;
        $addNewLine = static::LANGUAGE !== 'json-schema';
        return new OutputFile(
            "{$this->compileOptions->getOutput()}/{$file}$ext",
            trim($content) . ($addNewLine ? PHP_EOL : '')
        );
    }

    /**
     * @param string $vendor
     * @param string $package
     *
     * @return ?string
     */
    protected function getNativePackage($vendor, $package)
    {
        $packages = $this->compileOptions->getPackages();
        $vendorPackage = "{$vendor}:{$package}";

        if (isset($packages[$vendorPackage])) {
            return $packages[$vendorPackage];
        }

        if (isset($packages[$vendor])) {
            return $packages[$vendor];
        }

        return null;
    }

    /**
     * @param array $imports
     *
     * @return string
     */
    protected function optimizeImports(array $imports)
    {
        $imports = array_map('trim', $imports);
        $imports = array_filter($imports);
        $imports = array_unique($imports);
        asort($imports);
        return implode(PHP_EOL, $imports);
    }

    /**
     * @param string $template
     * @param array  $parameters
     *
     * @return string
     */
    protected function render($template, array $parameters)
    {
        $twig = $this->getTwig();
        $parameters['compile_options'] = $this->compileOptions;
        return $twig->render($template, $parameters);
    }

    /**
     * Get the twig environment that will render skeletons.
     *
     * @return \Twig_Environment
     */
    protected function getTwig()
    {
        if (null === $this->twig) {
            $this->twig = new \Twig_Environment(new \Twig_Loader_Filesystem(self::TEMPLATE_DIR), [
                'debug'            => true,
                'cache'            => false,
                'strict_variables' => true,
                'autoescape'       => false,
            ]);

            $this->twig->addExtension(new StringExtension());

            $class = sprintf(
                '\Gdbots\Pbjc\Generator\Twig\%sGeneratorExtension',
                StringUtils::toCamelFromSlug(static::LANGUAGE)
            );

            $this->twig->addExtension(new $class($this->compileOptions, $this));
        }

        return $this->twig;
    }
}