-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from carsdotcom/initial-commit
Initial Commit
- Loading branch information
Showing
28 changed files
with
9,641 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ | |
node_modules/ | ||
npm-debug.log | ||
yarn-error.log | ||
.ackrc | ||
.idea/ | ||
|
||
# Laravel 4 specific | ||
bootstrap/compiled.php | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,20 @@ | ||
# laravel-json-schema | ||
Use JsonSchema in Laravel apps | ||
|
||
## Purpose | ||
|
||
This library builds on the _outstanding_ JsonSchema validator [opis/json-schema](https://opis.io/json-schema/) | ||
|
||
The entire intent of this library is to make JsonSchema feel like a first class citizen in a Laravel project. | ||
|
||
It adds a new config file, `config/json-schema.php` to configure your root directory for self-hosted schema files. | ||
|
||
It adds a Facade, `SchemaValidator` that can be used to instantiate the validator with appropriate loaders. | ||
|
||
It adds new PhpUnit assertions in `JsonSchemaAssertions` like validating that a mixed item validates for a specific schema. | ||
|
||
Most interestingly, it lets you use JsonSchema to validate incoming Requests bodies, and/or validate your own outgoing response bodies, all using JsonSchema schemas that you can then export into OpenAPI documentation. | ||
|
||
## Coming Soon | ||
|
||
More documentation will be coming soon, including some more projects that build on this, including Guzzle outgoing- and incoming-body validation, and a new kind of Laravel Model that persists to JSON instead of to a relational database. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?php | ||
/** | ||
* Generate JsonSchema files automatically for anything in the App\Enums namespace | ||
* that implements the trait GeneratesSchema | ||
*/ | ||
|
||
namespace Carsdotcom\JsonSchemaValidation\Console\Commands\Schemas; | ||
|
||
use Carsdotcom\JsonSchemaValidation\Helpers\FindClasses; | ||
use Carsdotcom\JsonSchemaValidation\Traits\GeneratesSchemaTrait; | ||
use Carsdotcom\JsonSchemaValidation\Traits\VerboseLineTrait; | ||
use Illuminate\Console\Command; | ||
|
||
class Generate extends Command | ||
{ | ||
use VerboseLineTrait; | ||
|
||
/** | ||
* The name and signature of the console command. | ||
* | ||
* @var string | ||
*/ | ||
protected $signature = 'schemas:generate'; | ||
|
||
/** | ||
* The console command description. | ||
* | ||
* @var string | ||
*/ | ||
protected $description = 'Generate Json Schema files'; | ||
|
||
/** | ||
* Execute the console command. | ||
*/ | ||
public function handle() | ||
{ | ||
FindClasses::inAppPath('Enums') | ||
->filter(fn($class) => in_array(GeneratesSchemaTrait::class, class_uses_recursive($class), true)) | ||
->each(fn($class) => $class::generateSchema()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
/** | ||
* Handy interface for "Does this implement ValidatesWithJsonSchema" | ||
*/ | ||
|
||
namespace Carsdotcom\JsonSchemaValidation\Contracts; | ||
|
||
use Exception; | ||
|
||
/** | ||
* Interface CanValidate | ||
* @package Carsdotcom\JsonSchemaValidation\Contracts | ||
*/ | ||
interface CanValidate | ||
{ | ||
/** | ||
* Does this object pass its own standard for validation? | ||
* @return true | ||
* @throws Exception if the data is invalid. Exact exception is up to the implementation, | ||
* but should implement HasExtendedExceptionData like JsonSchemaValidationException | ||
*/ | ||
public function validateOrThrow(string $exceptionMessage = null): bool; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?php | ||
|
||
/** | ||
* The implementing exception has extra data it would like to apply when being serialized by app/Exceptions/Handler.php | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Carsdotcom\JsonSchemaValidation\Contracts; | ||
|
||
/** | ||
* Class HasExtendedExceptionData | ||
* @package Carsdotcom\JsonSchemaValidation\Contracts | ||
*/ | ||
interface HasExtendedExceptionData | ||
{ | ||
/** | ||
* Returns an array of extra properties that can be merged onto the json representation being assembled | ||
* @return array | ||
*/ | ||
public function getExtendedData(): array; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<?php | ||
|
||
/** | ||
* Exception thrown by failing JsonSchema validation. | ||
* Formats errors into the same structure you'd get from | ||
* Laravel ValidationException->validator->errors() | ||
*/ | ||
|
||
namespace Carsdotcom\JsonSchemaValidation\Exceptions; | ||
|
||
use Carsdotcom\JsonSchemaValidation\Contracts\HasExtendedExceptionData; | ||
use Opis\JsonSchema\Errors\ErrorFormatter; | ||
use Opis\JsonSchema\Errors\ValidationError; | ||
use RuntimeException; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
/** | ||
* Class JsonSchemaValidationException | ||
* @package Carsdotcom\JsonSchemaValidation\Exceptions | ||
*/ | ||
class JsonSchemaValidationException extends RuntimeException implements HasExtendedExceptionData | ||
{ | ||
protected ValidationError $error; | ||
protected int $failureHttpStatusCode = Response::HTTP_BAD_REQUEST; | ||
|
||
/** | ||
* @param string $message | ||
* @param ValidationError $error | ||
* @param \Throwable|null $previous | ||
* @param int $code | ||
*/ | ||
public function __construct( | ||
string $message, | ||
ValidationError $error, | ||
\Throwable $previous = null, | ||
int $code = Response::HTTP_BAD_REQUEST, | ||
) { | ||
$this->error = $error; | ||
$this->failureHttpStatusCode = $code; | ||
|
||
parent::__construct(message: $message, code: $code, previous: $previous); | ||
} | ||
|
||
/** | ||
* @return array formatted like message bag | ||
*/ | ||
public function errors(): array | ||
{ | ||
return (new ErrorFormatter())->format($this->error, true, null, [$this, 'formatErrorKey']); | ||
} | ||
|
||
/** | ||
* Returns extra properties that will be appended to this exception when rendered by app/Exceptions/Handler.php | ||
* @return array | ||
*/ | ||
public function getExtendedData(): array | ||
{ | ||
return ['errors' => $this->errors()]; | ||
} | ||
|
||
/** | ||
* We format our error keys using dot-notation, Opis by default would use Json-pointer | ||
* @param ValidationError $error | ||
* @return string | ||
*/ | ||
public static function formatErrorKey(ValidationError $error): string | ||
{ | ||
$path = $error->data()->fullPath(); | ||
if (!$path) { | ||
return 'root element'; | ||
} | ||
return implode('.', $path); | ||
} | ||
|
||
/** | ||
* Used in Handler::determineMessageStringAndStatusCode() to determine return http status code | ||
* @return int | ||
*/ | ||
public function getStatusCode(): int | ||
{ | ||
return $this->failureHttpStatusCode; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
<?php | ||
/** | ||
* Helper to find classes. | ||
* This is useful in places where we use folder structure as an organizing convention, | ||
* like email notifications being customized by an Account in App\Notifications\ | ||
* or the complete set of analytics events in App\Events\ | ||
*/ | ||
|
||
namespace Carsdotcom\JsonSchemaValidation\Helpers; | ||
|
||
use Illuminate\Support\Collection; | ||
use Illuminate\Support\Str; | ||
use ReflectionClass; | ||
use Symfony\Component\Finder\Finder; | ||
|
||
class FindClasses | ||
{ | ||
/** | ||
* Returns all the classes in a subfolder of Laravel's app_path(), recursively | ||
* @param string $subfolder | ||
* @return Collection | ||
*/ | ||
public static function inAppPath(string $subfolder): Collection | ||
{ | ||
$fullPath = Str::finish(app_path($subfolder), '/'); | ||
|
||
return collect( | ||
(new Finder()) | ||
->in($fullPath) | ||
->files() | ||
->name('*.php'), | ||
) | ||
->map(static function (string $filename) { | ||
// Remove app path from the left, and .php from the right | ||
$className = substr($filename, strlen(app_path()), -4); | ||
|
||
// Add App (root of the namespace) and change / to \ | ||
return 'App' . str_replace('/', '\\', $className); | ||
}) | ||
->filter(static function (string $className) { | ||
return class_exists($className, true) && !(new ReflectionClass($className))->isAbstract(); | ||
}) | ||
->values(); // Restore 0-index no gaps | ||
} | ||
|
||
/** | ||
* Get one class if you know its folder (or any parent folder) and the class name. | ||
* @param string $subfolder | ||
* @param string $name | ||
* @return string|null | ||
*/ | ||
public static function byPathAndName(string $subfolder, string $name): ?string | ||
{ | ||
return self::inAppPath($subfolder)->first(function ($item) use ($name) { | ||
return Str::endsWith($item, '\\' . $name); | ||
}); | ||
} | ||
|
||
public static function byPathAndType(string $subfolder, string $implements): Collection | ||
{ | ||
return self::inAppPath($subfolder)->filter(fn($item) => is_a($item, $implements, true)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
<?php | ||
|
||
/** | ||
* Given an object, or a full class name and namespace (e.g. thing::class or ModelNotFoundException->getModel()) | ||
* strip off the name space, and turn the camel case into separated words. | ||
*/ | ||
|
||
declare(strict_types=0); | ||
|
||
namespace Carsdotcom\JsonSchemaValidation\Helpers; | ||
|
||
class FriendlyClassName | ||
{ | ||
public function __invoke(string|object $class): string | ||
{ | ||
$class = is_object($class) ? get_class($class) : $class; | ||
|
||
if (str_contains($class, '@anonymous')) { | ||
$parent = get_parent_class($class); | ||
if (!$parent || $parent === 'stdClass') { | ||
return 'Anonymous Class'; | ||
} | ||
return 'Anonymous Descendent of ' . (new self())($parent); | ||
} | ||
|
||
// Get just the class name (strip namespace) | ||
$className = class_basename($class); | ||
|
||
// Replace punctuation with spaces (especially _ ) | ||
$className = preg_replace('/[^a-z]+/i', ' ', $className); | ||
|
||
// Insert spaces in camel case names | ||
$className = preg_replace('/([a-z])([A-Z])/', '$1 $2', $className); | ||
|
||
return $className; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<?php | ||
|
||
/** | ||
* Facade for the Schema Validator service | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Carsdotcom\JsonSchemaValidation; | ||
|
||
use Illuminate\Support\Facades\Facade; | ||
use Symfony\Component\HttpFoundation\Response; | ||
|
||
/** | ||
* @method static array|object getSchemaContents(string $relativeUri, bool $associative = true) | ||
* @method static void putSchemaContents(string $relativeUri, array|object $schema) | ||
* @method static string registerRawSchema(bool|object|string $schema) | ||
* @method static bool validate(array|object $data, string $schema) | ||
* @method static bool validateOrThrow(string|array|object $data, string $schema, string $exceptionMessage = null, bool $appendValidationDescriptions = false, int $failureHttpStatusCode = Response::HTTP_BAD_REQUEST) | ||
*/ | ||
class SchemaValidator extends Facade | ||
{ | ||
/** | ||
* @return string | ||
*/ | ||
public static function getFacadeAccessor(): string | ||
{ | ||
return SchemaValidatorService::class; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php | ||
|
||
namespace Carsdotcom\JsonSchemaValidation; | ||
|
||
use Carsdotcom\JsonSchemaValidation\Console\Commands\Schemas\Generate; | ||
use Illuminate\Contracts\Support\DeferrableProvider; | ||
use Illuminate\Support\ServiceProvider; | ||
|
||
class SchemaValidatorProvider extends ServiceProvider implements DeferrableProvider | ||
{ | ||
public function register() | ||
{ | ||
$this->app->singleton(SchemaValidatorService::class, function () { | ||
return new SchemaValidatorService(); | ||
}); | ||
} | ||
|
||
/** | ||
* Get the services provided by the provider. (So Laravel can defer loading until one is requested) | ||
* | ||
* @return array | ||
*/ | ||
public function provides() | ||
{ | ||
return [SchemaValidatorService::class]; | ||
} | ||
|
||
public function boot() | ||
{ | ||
$this->publishes([ | ||
__DIR__ . '/../config/json-schema.php' => config_path('json-schema.php'), | ||
]); | ||
|
||
if ($this->app->runningInConsole()) { | ||
$this->commands([ | ||
Generate::class, | ||
]); | ||
} | ||
} | ||
} |
Oops, something went wrong.