diff --git a/README.md b/README.md index 727feb6..f386a54 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,13 @@ A package MUST use these names for these root-level files: A package SHOULD include a root-level file indicating the licensing and copyright terms of the package contents. +## Validator + +Quickly validate your project's compliance by following these steps: + +- Install package in your project: `composer require pds/skeleton @dev` +- Run the validator: `./vendor/pds/skeleton/bin/validate` + ## Root-Level Directories ### bin/ diff --git a/bin/validate b/bin/validate new file mode 100755 index 0000000..e628a96 --- /dev/null +++ b/bin/validate @@ -0,0 +1,291 @@ +#!/usr/bin/env php + + $line) { + if (is_dir($line)) { + $lines[$i] .= "/"; + } + } + $validator = new ComplianceValidator(); + $results = $validator->validate($lines); + $validator->outputResults($results); +} + +class ComplianceValidator +{ + const STATE_OPTIONAL_NOT_PRESENT = 1; + const STATE_CORRECT_PRESENT = 2; + const STATE_REQUIRED_NOT_PRESENT = 3; + const STATE_INCORRECT_PRESENT = 4; + + public function validate($lines) + { + $complianceTests = [ + "Command-line executables" => $this->checkBin($lines), + "Configuration files" => $this->checkConfig($lines), + "Documentation files" => $this->checkDocs($lines), + "Web server files" => $this->checkPublic($lines), + "Other resource files" => $this->checkResources($lines), + "PHP source code" => $this->checkSrc($lines), + "Test code" => $this->checkTests($lines), + "Package managers" => $this->checkVendor($lines), + "Log of changes between releases" => $this->checkChangelog($lines), + "Guidelines for contributors" => $this->checkContributing($lines), + "Licensing information" => $this->checkLicense($lines), + "Information about the package itself" => $this->checkReadme($lines), + ]; + + $results = []; + foreach ($complianceTests as $label => $complianceResult) { + $state = $complianceResult[0]; + $expected = $complianceResult[1]; + $actual = $complianceResult[2]; + $results[$expected] = [ + 'label' => $label, + 'state' => $state, + 'expected' => $expected, + 'actual' => $actual, + ]; + } + return $results; + } + + public function outputResults($results) + { + foreach ($results as $result) { + $this->outputResultLine($result['label'], $result['state'], $result['expected'], $result['actual']); + } + } + + protected function outputResultLine($label, $complianceState, $expected, $actual) + { + $messages = [ + self::STATE_OPTIONAL_NOT_PRESENT => "Optional {$expected} not present", + self::STATE_CORRECT_PRESENT => "Correct {$actual} present", + self::STATE_INCORRECT_PRESENT => "Incorrect {$actual} present", + self::STATE_REQUIRED_NOT_PRESENT => "Required {$expected} not present", + ]; + echo $this->colorConsoleText("- " . $label . ": " . $messages[$complianceState], $complianceState) . PHP_EOL; + } + + protected function colorConsoleText($text, $complianceState) + { + $colors = [ + self::STATE_OPTIONAL_NOT_PRESENT => "\033[43;30m", + self::STATE_CORRECT_PRESENT => "\033[42;30m", + self::STATE_INCORRECT_PRESENT => "\033[41m", + self::STATE_REQUIRED_NOT_PRESENT => "\033[41m", + ]; + if (!array_key_exists($complianceState, $colors)) { + return $text; + } + return $colors[$complianceState] . " " . $text . " \033[0m"; + } + + protected function checkDir($lines, $pass, array $fail) + { + foreach ($lines as $line) { + $line = trim($line); + if ($line == $pass) { + return [self::STATE_CORRECT_PRESENT, $pass, $line]; + } + if (in_array($line, $fail)) { + return [self::STATE_INCORRECT_PRESENT, $pass, $line]; + } + } + return [self::STATE_OPTIONAL_NOT_PRESENT, $pass, null]; + } + + protected function checkFile($lines, $pass, array $fail) + { + foreach ($lines as $line) { + $line = trim($line); + if (preg_match("/^{$pass}(\.[a-z]+)?$/", $line)) { + return [self::STATE_CORRECT_PRESENT, $pass, $line]; + } + foreach ($fail as $regex) { + if (preg_match($regex, $line)) { + return [self::STATE_INCORRECT_PRESENT, $pass, $line]; + } + } + } + return [self::STATE_OPTIONAL_NOT_PRESENT, $pass, null]; + } + + protected function checkVendor($lines, $pass = 'vendor/') + { + foreach ($lines as $line) { + $line = trim($line); + if ($line == $pass) { + return [self::STATE_CORRECT_PRESENT, $pass, $line]; + } + } + return [self::STATE_REQUIRED_NOT_PRESENT, $pass, null]; + } + + protected function checkChangelog($lines) + { + return $this->checkFile($lines, 'CHANGELOG', [ + '/^.*CHANGLOG.*$/i', + '/^.*CAHNGELOG.*$/i', + '/^WHATSNEW(\.[a-z]+)?$/i', + '/^RELEASE((_|-)?NOTES)?(\.[a-z]+)?$/i', + '/^RELEASES(\.[a-z]+)?$/i', + '/^CHANGES(\.[a-z]+)?$/i', + '/^CHANGE(\.[a-z]+)?$/i', + '/^HISTORY(\.[a-z]+)?$/i', + ]); + } + + protected function checkContributing($lines) + { + return $this->checkFile($lines, 'CONTRIBUTING', [ + '/^DEVELOPMENT(\.[a-z]+)?$/i', + '/^README\.CONTRIBUTING(\.[a-z]+)?$/i', + '/^DEVELOPMENT_README(\.[a-z]+)?$/i', + '/^CONTRIBUTE(\.[a-z]+)?$/i', + '/^HACKING(\.[a-z]+)?$/i', + ]); + } + + protected function checkLicense($lines) + { + return $this->checkFile($lines, 'LICENSE', [ + '/^.*EULA.*$/i', + '/^.*(GPL|BSD).*$/i', + '/^([A-Z-]+)?LI(N)?(S|C)(E|A)N(S|C)(E|A)(_[A-Z_]+)?(\.[a-z]+)?$/i', + '/^COPY(I)?NG(\.[a-z]+)?$/i', + '/^COPYRIGHT(\.[a-z]+)?$/i', + ]); + } + + protected function checkReadme($lines) + { + return $this->checkFile($lines, 'README', [ + '/^USAGE(\.[a-z]+)?$/i', + '/^SUMMARY(\.[a-z]+)?$/i', + '/^DESCRIPTION(\.[a-z]+)?$/i', + '/^IMPORTANT(\.[a-z]+)?$/i', + '/^NOTICE(\.[a-z]+)?$/i', + '/^GETTING(_|-)STARTED(\.[a-z]+)?$/i', + ]); + } + + protected function checkBin($lines) + { + return $this->checkDir($lines, 'bin/', [ + 'cli/', + 'scripts/', + 'console/', + 'shell/', + 'script/', + ]); + } + + protected function checkConfig($lines) + { + return $this->checkDir($lines, 'config/', [ + 'etc/', + 'settings/', + 'configuration/', + 'configs/', + '_config/', + 'conf/', + ]); + } + + protected function checkDocs($lines) + { + return $this->checkDir($lines, 'docs/', [ + 'manual/', + 'documentation/', + 'usage/', + 'doc/', + 'guide/', + 'phpdoc/', + ]); + } + + protected function checkPublic($lines) + { + return $this->checkDir($lines, 'public/', [ + 'assets/', + 'static/', + 'html/', + 'httpdocs/', + 'media/', + 'docroot/', + 'css/', + 'fonts/', + 'styles/', + 'style/', + 'js/', + 'javascript/', + 'images/', + 'site/', + 'mysite/', + 'img/', + 'web/', + 'pub/', + 'webroot/', + 'www/', + 'htdocs/', + 'asset/', + 'public_html/', + 'publish/', + 'pages/', + ]); + } + + protected function checkSrc($lines) + { + return $this->checkDir($lines, 'src/', [ + 'exception/', + 'exceptions/', + 'src-files/', + 'traits/', + 'interfaces/', + 'common/', + 'sources/', + 'php/', + 'inc/', + 'libraries/', + 'autoloads/', + 'autoload/', + 'source/', + 'includes/', + 'include/', + 'lib/', + 'libs/', + 'library/', + 'code/', + 'classes/', + 'func/', + ]); + } + + protected function checkTests($lines) + { + return $this->checkDir($lines, 'tests/', [ + 'test/', + 'unit-tests/', + 'phpunit/', + 'testing/', + ]); + } + + protected function checkResources($lines) + { + return $this->checkDir($lines, 'resources/', [ + 'Resources/', + 'res/', + 'resource/', + 'Resource/', + 'ressources/', + 'Ressources/', + ]); + } +} diff --git a/tests/ComplianceValidatorTest.php b/tests/ComplianceValidatorTest.php new file mode 100644 index 0000000..57c4826 --- /dev/null +++ b/tests/ComplianceValidatorTest.php @@ -0,0 +1,69 @@ +testValidate_WithIncorrectBin_ReturnsIncorrectBin(); +$tester->testValidate_WithoutVendor_ReturnsMissingVendor(); + +echo "Errors: {$tester->numErrors}" . PHP_EOL; + +class ComplianceValidatorTest +{ + public $numErrors = 0; + + public function testValidate_WithIncorrectBin_ReturnsIncorrectBin() + { + $paths = [ + 'cli/', + 'vendor/', + ]; + + $validator = new ComplianceValidator(); + $results = $validator->validate($paths); + + foreach ($results as $expected => $result) { + if ($expected == "bin/") { + if ($result['state'] != ComplianceValidator::STATE_INCORRECT_PRESENT) { + $this->numErrors++; + echo __FUNCTION__ . ": Expected state of {$result['expected']} to be STATE_INCORRECT_PRESENT" . PHP_EOL; + } + continue; + } + if ($expected == "vendor/") { + if ($result['state'] != ComplianceValidator::STATE_CORRECT_PRESENT) { + $this->numErrors++; + echo __FUNCTION__ . ": Expected state of {$result['expected']} to be STATE_CORRECT_PRESENT" . PHP_EOL; + } + continue; + } + if ($result['state'] != ComplianceValidator::STATE_OPTIONAL_NOT_PRESENT) { + $this->numErrors++; + echo __FUNCTION__ . ": Expected state of {$result['expected']} to be STATE_OPTIONAL_NOT_PRESENT" . PHP_EOL; + continue; + } + } + } + + public function testValidate_WithoutVendor_ReturnsMissingVendor() + { + $paths = [ + 'bin/', + ]; + + $validator = new ComplianceValidator(); + $results = $validator->validate($paths); + + foreach ($results as $expected => $result) { + if ($expected == "vendor/") { + if ($result['state'] != ComplianceValidator::STATE_REQUIRED_NOT_PRESENT) { + $this->numErrors++; + echo __FUNCTION__ . ": Expected state of {$result['expected']} to be STATE_REQUIRED_NOT_PRESENT" . PHP_EOL; + } + continue; + } + } + } +} \ No newline at end of file