Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Review: Add validator spec via code generation #100 #221

Merged
merged 19 commits into from
Jun 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 3 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
*.php diff=php

# Mark generated files so diffs are hidden by default.
/resources/**/* linguist-generated=true
/tests/spec/**/* linguist-generated=true
/src/Validator/**/* linguist-generated=true
/resources/**/* linguist-generated=true
/tests/spec/**/* linguist-generated=true

# Exclude build & test files from dist archives.
/.github export-ignore
Expand Down
1 change: 1 addition & 0 deletions bin/src/Validator/SpecGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function generate($jsonSpec, $rootNamespace, $destination)
$this->generateEntityClass('Tag', $fileManager);
$this->generateEntityClass('TagWithExtensionSpec', $fileManager, 'interface');
$this->generateEntityClass('ExtensionSpec', $fileManager, 'trait');
$this->generateEntityClass('Identifiable', $fileManager, 'interface');
$this->generateEntityClass('IterableSection', $fileManager, 'interface');
$this->generateEntityClass('Iteration', $fileManager, 'trait');
$this->generateErrorCodeInterface($jsonSpec, $fileManager);
Expand Down
18 changes: 18 additions & 0 deletions bin/src/Validator/SpecGenerator/ClassNames.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,22 @@ protected function getClassNameFromId($id)

return $className;
}

/**
* Get the short name for a provided class name.
*
* @param string $class Class name to get the short name for.
* @return string Short name of the provided class name.
*/
private function getShortName($class)
{
$class = ltrim($class, '\\');

if (strpos($class, 'AmpProject\\') === 0) {
$partials = array_filter(explode('\\', $class));
$class = array_pop($partials);
}

return $class;
}
}
11 changes: 0 additions & 11 deletions bin/src/Validator/SpecGenerator/ConstantNames.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,4 @@ private function getAttributeConstant($attribute)

return "Attribute::{$attribute}";
}

/**
* Get the error code constant.
*
* @param string $errorCode Error code to get the constant for.
* @return string Error code constant.
*/
private function getErrorCodeConstant($errorCode)
{
return "ErrorCode::{$errorCode}";
}
}
37 changes: 30 additions & 7 deletions bin/src/Validator/SpecGenerator/Dumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ public function dump($value, $level, $parentKeys = [])
*/
public function dumpWithKey($key, $value, $level, $parentKeys = [])
{
if (is_string($value) && strpos($value, '[\'') === 0) {
$json = json_decode(str_replace('\'', '"', $value), true);
if (json_last_error() === JSON_ERROR_NONE) {
$value = $json;
}
}

$value = $this->replaceConstants($key, $value, $parentKeys);

if (is_string($key)) {
Expand Down Expand Up @@ -292,9 +299,14 @@ private function replaceConstants($specRule, $value, $parentKeys)
case 'alternativeNames':
case 'disabledBy':
case 'enabledBy':
case 'mandatoryAnyof':
case 'mandatoryOneof':
$attributes = [];
foreach ($value as $attribute) {
$attributes[] = $this->getAttributeConstant($this->getConstantName($attribute));
if (is_string($attribute) && strpos($attribute, '[') !== 0) {
$attribute = $this->getAttributeConstant($this->getConstantName($attribute));
}
$attributes[] = $attribute;
}
return $attributes;
case 'ampLayout':
Expand All @@ -311,6 +323,16 @@ private function replaceConstants($specRule, $value, $parentKeys)
$attributeLists[] = "AttributeList\\{$className}::ID";
}
return $attributeLists;
case 'attrs':
$attributes = [];
foreach ($value as $name => $specRules) {
$key = $name;
if (strpos($name, '[') !== 0) {
$key = $this->getAttributeConstant($this->getConstantName($name));
}
$attributes[$key] = $specRules;
}
return $attributes;
case 'declarationList':
$declarationLists = [];
foreach ($value as $declarationList) {
Expand All @@ -328,17 +350,18 @@ private function replaceConstants($specRule, $value, $parentKeys)
}
return $formats;
case 'name':
if (empty($parentKeys[0])) {
if (! is_string($value) || empty($parentKeys[0])) {
return $value;
}
if ($parentKeys[0] === 'atRuleSpec') {
return $this->getAtRuleConstant($this->getConstantName($value));
}
if ($parentKeys[0] !== 'extensionSpec' && $parentKeys[0] !== 'properties') {
$constant = $this->getAttributeConstant($this->getConstantName($value));
if (strpos($value, '[') !== 0) {
return $constant;
}
if (
$parentKeys[0] !== 'extensionSpec'
&& $parentKeys[0] !== 'properties'
&& strpos($value, '[') !== 0
) {
return $this->getAttributeConstant($this->getConstantName($value));
}
return $value;
case 'protocol':
Expand Down
19 changes: 1 addition & 18 deletions bin/src/Validator/SpecGenerator/FileManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

final class FileManager
{
use ClassNames;

/**
* Directory that contains the source for the namespace root.
Expand Down Expand Up @@ -272,24 +273,6 @@ private function maybeAddImport(PhpNamespace $namespace, $class)
$namespace->addUse($fqcn, $alias);
}

/**
* Get the short name for a provided class name.
*
* @param string $class Class name to get the short name for.
* @return string Short name of the provided class name.
*/
private function getShortName($class)
{
$class = ltrim($class, '\\');

if (strpos($class, 'AmpProject\\') === 0) {
$partials = array_filter(explode('\\', $class));
$class = array_pop($partials);
}

return $class;
}

/**
* Get the fully qualified class name for a provided class name.
*
Expand Down
99 changes: 99 additions & 0 deletions bin/src/Validator/SpecGenerator/MagicPropertyAnnotations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace AmpProject\Tooling\Validator\SpecGenerator;

trait MagicPropertyAnnotations
{

/**
* Get the magic property annotations for a given spec.
*
* @param array $spec Spec to get the magic property annotations for.
* @return string[] Array of magic property annotations.
*/
private function getMagicPropertyAnnotations($spec)
{
$annotations = [];

foreach ($spec as $key => $value) {
if (! is_string($key)) {
continue;
}

if (strpos($key, '[') === 0) {
$key = trim($key, '[]') . '_binding';
}

$key = lcfirst(str_replace('-', '', ucwords($key, '-')));

$type = $this->getPropertyType($value);
$annotations[] = "@property-read {$type} \${$key}";
}

return $annotations;
}

/**
* Get the type of a property by examining its value.
*
* @param mixed $value Value to examine to determine the type.
schlessera marked this conversation as resolved.
Show resolved Hide resolved
* @return string Property type.
*/
private function getPropertyType($value)
{
if (is_array($value)) {
$type = $this->getCommonArrayValueType($value);

if ($type !== false) {
return "array<{$type}>";
}

return 'array';
}

if (is_bool($value)) {
return 'bool';
}

if (is_string($value)) {
return 'string';
}

if (is_int($value)) {
return 'int';
}

if (is_float($value)) {
return 'float';
}

if (is_object($value)) {
return 'object';
}

return 'mixed';
}

/**
* Get the common type for all values of an array.
*
* @param array $values Array of values to determine the common type of.
* @return string|false Common type of the array, or false if it could not be determined.
*/
private function getCommonArrayValueType($values)
{
$commonType = false;

foreach ($values as $value) {
$type = $this->getPropertyType($value);

if ($commonType !== false && $commonType !== $type) {
return false;
}

$commonType = $type;
}

return $commonType;
}
}
15 changes: 11 additions & 4 deletions bin/src/Validator/SpecGenerator/Section/AttributeLists.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use AmpProject\Tooling\Validator\SpecGenerator\ClassNames;
use AmpProject\Tooling\Validator\SpecGenerator\ConstantNames;
use AmpProject\Tooling\Validator\SpecGenerator\FileManager;
use AmpProject\Tooling\Validator\SpecGenerator\MagicPropertyAnnotations;
use AmpProject\Tooling\Validator\SpecGenerator\Section;
use AmpProject\Tooling\Validator\SpecGenerator\Template;
use Nette\PhpGenerator\ClassType;
Expand All @@ -14,6 +15,7 @@ final class AttributeLists implements Section
{
use ClassNames;
use ConstantNames;
use MagicPropertyAnnotations;

/**
* Process a section.
Expand All @@ -32,7 +34,7 @@ public function process(FileManager $fileManager, $spec, PhpNamespace $namespace
$namespace->addUse("{$fileManager->getRootNamespace()}\\Spec\\IterableSection");
$namespace->addUse("{$fileManager->getRootNamespace()}\\Spec\\Iteration");

$this->data = $this->adaptSpec($spec);
$data = $this->adaptSpec($spec);

$class->addImplement("{$fileManager->getRootNamespace()}\\Spec\\IterableSection");
$class->addTrait(
Expand All @@ -46,7 +48,7 @@ public function process(FileManager $fileManager, $spec, PhpNamespace $namespace
->addComment("Cache of instantiated AttributeList objects.\n\n@var array<Spec\\AttributeList>");

$attributeLists = [];
foreach ($this->data as $key => $value) {
foreach ($data as $key => $value) {
$className = $this->generateAttributeListSpecificClass($key, $value, $fileManager);

$attributeLists["AttributeList\\{$className}::ID"] = "AttributeList\\{$className}::class";
Expand Down Expand Up @@ -101,13 +103,13 @@ private function generateAttributeListSpecificClass($attributeListId, $jsonSpec,

$className = $this->getClassNameFromId($attributeListId);

$namespace->addUse("{$fileManager->getRootNamespace()}\\Spec\\SpecRule");
$namespace->addUse("{$fileManager->getRootNamespace()}\\Spec\\AttributeList");

/** @var ClassType $class */
$class = $namespace->addClass($className)
->setFinal()
->addExtend('AmpProject\Validator\Spec\AttributeList');
->addExtend('AmpProject\Validator\Spec\AttributeList')
->addImplement('AmpProject\Validator\Spec\Identifiable');

$class->addConstant('ID', $attributeListId)
->addComment("ID of the attribute list.\n\n@var string");
Expand All @@ -120,6 +122,11 @@ private function generateAttributeListSpecificClass($attributeListId, $jsonSpec,
$class->addConstant('ATTRIBUTES', $attributes)
->addComment("Array of attributes.\n\n@var array<array>");

$classComment = "Attribute list class {$className}.\n\n";
$classComment .= "@package ampproject/amp-toolbox.\n\n";
$classComment .= implode("\n", $this->getMagicPropertyAnnotations($jsonSpec));
$class->addComment($classComment);

$fileManager->saveFile($file, "Spec/AttributeList/{$className}.php");

return $className;
Expand Down
10 changes: 9 additions & 1 deletion bin/src/Validator/SpecGenerator/Section/CssRulesets.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use AmpProject\Tooling\Validator\SpecGenerator\ClassNames;
use AmpProject\Tooling\Validator\SpecGenerator\ConstantNames;
use AmpProject\Tooling\Validator\SpecGenerator\FileManager;
use AmpProject\Tooling\Validator\SpecGenerator\MagicPropertyAnnotations;
use AmpProject\Tooling\Validator\SpecGenerator\Section;
use AmpProject\Tooling\Validator\SpecGenerator\Template;
use Nette\PhpGenerator\ClassType;
Expand All @@ -14,6 +15,7 @@ final class CssRulesets implements Section
{
use ClassNames;
use ConstantNames;
use MagicPropertyAnnotations;

/**
* Process a section.
Expand Down Expand Up @@ -146,14 +148,20 @@ private function generateCssRulesetSpecificClass($ruleset, $jsonSpec, FileManage
/** @var ClassType $class */
$class = $namespace->addClass($className)
->setFinal()
->addExtend('AmpProject\Validator\Spec\CssRuleset');
->addExtend('AmpProject\Validator\Spec\CssRuleset')
->addImplement('AmpProject\Validator\Spec\Identifiable');

$class->addConstant('ID', $ruleset)
->addComment("ID of the ruleset.\n\n@var string");

$class->addConstant('SPEC', $jsonSpec)
->addComment("Array of spec rules.\n\n@var array");

$classComment = "CSS ruleset class {$className}.\n\n";
$classComment .= "@package ampproject/amp-toolbox.\n\n";
$classComment .= implode("\n", $this->getMagicPropertyAnnotations($jsonSpec));
$class->addComment($classComment);

$fileManager->saveFile($file, "Spec/CssRuleset/{$className}.php");

return $className;
Expand Down
Loading