Loading src/Schema/DataPreProcessor/ObjectCoercionDataPreProcessor.php 0 → 100644 +24 −0 Original line number Diff line number Diff line <?php namespace Drupal\patternkit\Schema\DataPreProcessor; use Swaggest\JsonSchema\DataPreProcessor; use Swaggest\JsonSchema\Schema; /** * Preprocess data to cast arrays to objects if desired by the schema. */ class ObjectCoercionDataPreProcessor implements DataPreProcessor { /** * {@inheritdoc} */ public function process($data, Schema $schema, $import = TRUE) { if (property_exists($schema, 'type') && $schema->type == 'object' && is_array($data)) { $data = (object) $data; } return $data; } } src/Schema/SchemaFactory.php +77 −3 Original line number Diff line number Diff line Loading @@ -5,7 +5,9 @@ namespace Drupal\patternkit\Schema; use Drupal\patternkit\Exception\SchemaException; use Drupal\patternkit\Exception\SchemaReferenceException; use Drupal\patternkit\Exception\SchemaValidationException; use Drupal\patternkit\Schema\DataPreProcessor\ObjectCoercionDataPreProcessor; use Swaggest\JsonSchema\Context; use Swaggest\JsonSchema\DataPreProcessor; use Swaggest\JsonSchema\Exception; use Swaggest\JsonSchema\InvalidValue; use Swaggest\JsonSchema\RemoteRefProvider; Loading @@ -28,14 +30,37 @@ class SchemaFactory { */ protected RemoteRefProvider $refProvider; /** * A data preprocessor to configure on all schema instances. * * @var \Swaggest\JsonSchema\DataPreProcessor */ protected DataPreProcessor $dataPreProcessor; /** * The default context to use for schema operations. * * @var \Swaggest\JsonSchema\Context */ protected Context $defaultContext; /** * Create a new Schema instance. * * @param \Swaggest\JsonSchema\RemoteRefProvider $refProvider * The ref provider for loading schema references. * @param \Swaggest\JsonSchema\DataPreProcessor|null $dataPreProcessor * (Optional) A data preprocessor to use on all Schema instances. Defaults * to an ObjectCoercionDataPreProcessor if none is provided. * * @see \Drupal\patternkit\Schema\DataPreProcessor\ObjectCoercionDataPreProcessor */ public function __construct(RemoteRefProvider $refProvider) { public function __construct(RemoteRefProvider $refProvider, ?DataPreProcessor $dataPreProcessor = NULL) { $this->refProvider = $refProvider; $this->dataPreProcessor = $dataPreProcessor ?? new ObjectCoercionDataPreProcessor(); $this->defaultContext = new Context($refProvider); $this->defaultContext->setDataPreProcessor($this->dataPreProcessor); } /** Loading @@ -43,6 +68,10 @@ class SchemaFactory { * * @param string $schema * The schema JSON for injection into the schema instance. * @param \Swaggest\JsonSchema\Context|null $context * (Optional) Context configuration for schema processing and operations. * In most cases, this is not needed, but it may be provided for detailed * customization of how a Schema instance should behave. * * @return \Swaggest\JsonSchema\SchemaContract * A configured Schema instance. Loading @@ -54,9 +83,27 @@ class SchemaFactory { * Throws an exception if the given value fails to validate against the * provided schema. */ public function createInstance(string $schema): SchemaContract { public function createInstance(string $schema, ?Context $context = NULL): SchemaContract { // Configure defaults on the provided context options if one is provided. if ($context !== NULL) { // Don't override the remote ref provider if one is already configured // for use. if ($context->getRemoteRefProvider() === NULL) { $context->setRemoteRefProvider($this->refProvider); } // Don't override the data preprocessor if one is already configured // for use. if ($context->getDataPreProcessor() === NULL) { $context->setDataPreProcessor($this->dataPreProcessor); } } else { $context = $this->getDefaultContext(); } try { return Schema::import(json_decode($schema), new Context($this->refProvider)); return Schema::import($this->decodeJson($schema), $context); } catch (InvalidValue $invalidValueException) { // Wrap the exception in our own class for abstraction. Loading @@ -78,4 +125,31 @@ class SchemaFactory { } } /** * Get a configured default context for use in schema operations. * * @return \Swaggest\JsonSchema\Context * A configured default context for schema operations. */ public function getDefaultContext(): Context { // Clone the default context to avoid capturing processing state data during // usage of the returned instance in schema operations. return clone($this->defaultContext); } /** * Encapsulate the process for decoding provided JSON values. * * @param string $schema * The JSON string value to be decoded. * * @return mixed * The decoded JSON value. * * @throws \JsonException */ protected function decodeJson(string $schema) { return json_decode($schema, FALSE, 512, JSON_THROW_ON_ERROR); } } src/Schema/SchemaHelper.php +9 −2 Original line number Diff line number Diff line Loading @@ -31,7 +31,10 @@ class SchemaHelper { */ public static function isCompositionSchema(SchemaContract $schema): bool { foreach (['anyOf', 'oneOf', 'allOf'] as $constraint) { if (isset($schema->$constraint)) { // Test like this instead of using isset() or empty() to avoid triggering // an exception if we're working with a Wrapper instance. // @see \Swaggest\JsonSchema\Wrapper::__isset if (property_exists($schema, $constraint) && $schema->$constraint !== NULL) { return TRUE; } } Loading Loading @@ -71,7 +74,11 @@ class SchemaHelper { $value = is_array($value) && !array_is_list($value) ? static::castArrayToObject($value) : $value; try { $validationResult = $schema->in($value); // Get a globally configured execution context for use in schema // operations. This will ensure expected options as well as configured // remote reference providers and data preprocessors are available. $context = \Drupal::service('patternkit.schema.schema_factory')->getDefaultContext(); $validationResult = $schema->in($value, $context); } catch (InvalidValue $exception) { // Set the parent document path for context on the exception. Loading tests/src/Traits/JsonDecodeTrait.php 0 → 100644 +29 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\patternkit\Traits; /** * A helper trait to consistently decode JSON strings throughout test classes. * * This trait is only intended for use in tests. */ trait JsonDecodeTrait { /** * Encapsulate the process for decoding provided JSON values. * * @param string $schema * The JSON string value to be decoded. * @param bool $associative * Whether to cast JSON objects as arrays when decoding. * * @return mixed * The decoded JSON value. * * @throws \JsonException */ protected function decodeJson(string $schema, bool $associative = FALSE) { return json_decode($schema, $associative, 512, JSON_THROW_ON_ERROR); } } tests/src/Traits/SchemaHelperTestTrait.php 0 → 100644 +139 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\patternkit\Traits; use Drupal\patternkit\Schema\DataPreProcessor\ObjectCoercionDataPreProcessor; use Drupal\patternkit\Schema\SchemaFactory; use Drupal\Tests\patternkit\Unit\Schema\TestPatternkitRefProvider; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; use Swaggest\JsonSchema\Context; use Swaggest\JsonSchema\DataPreProcessor; use Swaggest\JsonSchema\RemoteRefProvider; /** * A helper trait to assist unit tests with dependencies on SchemaHelper. * * This trait is only intended for use in tests. Implementation of the * tearDown() method assumes the implementing class inherits from * \PHPUnit\Framework\TestCase for a parent tearDown() implementation to extend. * * @see \Drupal\patternkit\Schema\SchemaHelper */ trait SchemaHelperTestTrait { use ProphecyTrait; /** * A prophecy mock for the schema factory service. * * @var \Prophecy\Prophecy\ObjectProphecy */ protected ObjectProphecy $schemaFactory; /** * A reference provider for loading schemas from. * * @var \Drupal\Tests\patternkit\Unit\Schema\TestPatternkitRefProvider */ protected TestPatternkitRefProvider $refProvider; /** * A data preprocessor to configure in schema contexts. * * @var \Swaggest\JsonSchema\DataPreProcessor */ protected DataPreProcessor $dataPreProcessor; /** * A default schema context to use for schema operations. * * @var \Swaggest\JsonSchema\Context */ protected Context $context; /** * Prepares a mocked schema factory service. * * Prepares a mocked schema factory service with configuration to return a * configured schema context from the getDefaultContext() method. If a * container is provided as an argument, the mock will be registered as the * 'patternkit.schema.schema_factory' service as well. * * Note: If the container is provided to this method, the mock wil be * instantiated and registered, so further manipulation of it in the class * variable will not be reflected in the registered service. If further * customization and mocking of the service is needed, it is recommended to * not provide the container as an argument and register it separately. * * @param \Psr\Container\ContainerInterface|null $container * (Optional) A container to register the mocked service to. * * @return \Prophecy\Prophecy\ObjectProphecy * The configured prophecy for the schema factory service. */ protected function setUpSchemaFactory(?ContainerInterface $container = NULL): ObjectProphecy { $this->schemaFactory = $this->prophesize(SchemaFactory::class); $this->schemaFactory->getDefaultContext()->willReturn($this->getSchemaContext()); if (isset($container)) { $container->set('patternkit.schema.schema_factory', $this->schemaFactory->reveal()); } return $this->schemaFactory; } /** * Get the default remote reference provider for schema contexts. * * @return \Swaggest\JsonSchema\RemoteRefProvider * A remote reference provider for schema contexts. */ protected function refProvider(): RemoteRefProvider { if (!isset($this->refProvider)) { $this->refProvider = new TestPatternkitRefProvider(); } return $this->refProvider; } /** * Get the default data preprocessor for schema contexts. * * @return \Swaggest\JsonSchema\DataPreProcessor * A date preprocessor to use in schema contexts. */ protected function dataPreProcessor(): DataPreProcessor { if (!isset($this->dataPreProcessor)) { $this->dataPreProcessor = new ObjectCoercionDataPreProcessor(); } return $this->dataPreProcessor; } /** * Get a configured schema context for schema operations. * * @return \Swaggest\JsonSchema\Context * A configured schema context for schema operations. */ protected function getSchemaContext(): Context { if (!isset($this->context)) { $this->context = new Context($this->refProvider()); $this->context->setDataPreProcessor($this->dataPreProcessor()); } // Clone the context to avoid persisting state of schema operations. return clone($this->context); } /** * {@inheritdoc} */ public function tearDown(): void { parent::tearDown(); $this->tearDownProphecy(); } } Loading
src/Schema/DataPreProcessor/ObjectCoercionDataPreProcessor.php 0 → 100644 +24 −0 Original line number Diff line number Diff line <?php namespace Drupal\patternkit\Schema\DataPreProcessor; use Swaggest\JsonSchema\DataPreProcessor; use Swaggest\JsonSchema\Schema; /** * Preprocess data to cast arrays to objects if desired by the schema. */ class ObjectCoercionDataPreProcessor implements DataPreProcessor { /** * {@inheritdoc} */ public function process($data, Schema $schema, $import = TRUE) { if (property_exists($schema, 'type') && $schema->type == 'object' && is_array($data)) { $data = (object) $data; } return $data; } }
src/Schema/SchemaFactory.php +77 −3 Original line number Diff line number Diff line Loading @@ -5,7 +5,9 @@ namespace Drupal\patternkit\Schema; use Drupal\patternkit\Exception\SchemaException; use Drupal\patternkit\Exception\SchemaReferenceException; use Drupal\patternkit\Exception\SchemaValidationException; use Drupal\patternkit\Schema\DataPreProcessor\ObjectCoercionDataPreProcessor; use Swaggest\JsonSchema\Context; use Swaggest\JsonSchema\DataPreProcessor; use Swaggest\JsonSchema\Exception; use Swaggest\JsonSchema\InvalidValue; use Swaggest\JsonSchema\RemoteRefProvider; Loading @@ -28,14 +30,37 @@ class SchemaFactory { */ protected RemoteRefProvider $refProvider; /** * A data preprocessor to configure on all schema instances. * * @var \Swaggest\JsonSchema\DataPreProcessor */ protected DataPreProcessor $dataPreProcessor; /** * The default context to use for schema operations. * * @var \Swaggest\JsonSchema\Context */ protected Context $defaultContext; /** * Create a new Schema instance. * * @param \Swaggest\JsonSchema\RemoteRefProvider $refProvider * The ref provider for loading schema references. * @param \Swaggest\JsonSchema\DataPreProcessor|null $dataPreProcessor * (Optional) A data preprocessor to use on all Schema instances. Defaults * to an ObjectCoercionDataPreProcessor if none is provided. * * @see \Drupal\patternkit\Schema\DataPreProcessor\ObjectCoercionDataPreProcessor */ public function __construct(RemoteRefProvider $refProvider) { public function __construct(RemoteRefProvider $refProvider, ?DataPreProcessor $dataPreProcessor = NULL) { $this->refProvider = $refProvider; $this->dataPreProcessor = $dataPreProcessor ?? new ObjectCoercionDataPreProcessor(); $this->defaultContext = new Context($refProvider); $this->defaultContext->setDataPreProcessor($this->dataPreProcessor); } /** Loading @@ -43,6 +68,10 @@ class SchemaFactory { * * @param string $schema * The schema JSON for injection into the schema instance. * @param \Swaggest\JsonSchema\Context|null $context * (Optional) Context configuration for schema processing and operations. * In most cases, this is not needed, but it may be provided for detailed * customization of how a Schema instance should behave. * * @return \Swaggest\JsonSchema\SchemaContract * A configured Schema instance. Loading @@ -54,9 +83,27 @@ class SchemaFactory { * Throws an exception if the given value fails to validate against the * provided schema. */ public function createInstance(string $schema): SchemaContract { public function createInstance(string $schema, ?Context $context = NULL): SchemaContract { // Configure defaults on the provided context options if one is provided. if ($context !== NULL) { // Don't override the remote ref provider if one is already configured // for use. if ($context->getRemoteRefProvider() === NULL) { $context->setRemoteRefProvider($this->refProvider); } // Don't override the data preprocessor if one is already configured // for use. if ($context->getDataPreProcessor() === NULL) { $context->setDataPreProcessor($this->dataPreProcessor); } } else { $context = $this->getDefaultContext(); } try { return Schema::import(json_decode($schema), new Context($this->refProvider)); return Schema::import($this->decodeJson($schema), $context); } catch (InvalidValue $invalidValueException) { // Wrap the exception in our own class for abstraction. Loading @@ -78,4 +125,31 @@ class SchemaFactory { } } /** * Get a configured default context for use in schema operations. * * @return \Swaggest\JsonSchema\Context * A configured default context for schema operations. */ public function getDefaultContext(): Context { // Clone the default context to avoid capturing processing state data during // usage of the returned instance in schema operations. return clone($this->defaultContext); } /** * Encapsulate the process for decoding provided JSON values. * * @param string $schema * The JSON string value to be decoded. * * @return mixed * The decoded JSON value. * * @throws \JsonException */ protected function decodeJson(string $schema) { return json_decode($schema, FALSE, 512, JSON_THROW_ON_ERROR); } }
src/Schema/SchemaHelper.php +9 −2 Original line number Diff line number Diff line Loading @@ -31,7 +31,10 @@ class SchemaHelper { */ public static function isCompositionSchema(SchemaContract $schema): bool { foreach (['anyOf', 'oneOf', 'allOf'] as $constraint) { if (isset($schema->$constraint)) { // Test like this instead of using isset() or empty() to avoid triggering // an exception if we're working with a Wrapper instance. // @see \Swaggest\JsonSchema\Wrapper::__isset if (property_exists($schema, $constraint) && $schema->$constraint !== NULL) { return TRUE; } } Loading Loading @@ -71,7 +74,11 @@ class SchemaHelper { $value = is_array($value) && !array_is_list($value) ? static::castArrayToObject($value) : $value; try { $validationResult = $schema->in($value); // Get a globally configured execution context for use in schema // operations. This will ensure expected options as well as configured // remote reference providers and data preprocessors are available. $context = \Drupal::service('patternkit.schema.schema_factory')->getDefaultContext(); $validationResult = $schema->in($value, $context); } catch (InvalidValue $exception) { // Set the parent document path for context on the exception. Loading
tests/src/Traits/JsonDecodeTrait.php 0 → 100644 +29 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\patternkit\Traits; /** * A helper trait to consistently decode JSON strings throughout test classes. * * This trait is only intended for use in tests. */ trait JsonDecodeTrait { /** * Encapsulate the process for decoding provided JSON values. * * @param string $schema * The JSON string value to be decoded. * @param bool $associative * Whether to cast JSON objects as arrays when decoding. * * @return mixed * The decoded JSON value. * * @throws \JsonException */ protected function decodeJson(string $schema, bool $associative = FALSE) { return json_decode($schema, $associative, 512, JSON_THROW_ON_ERROR); } }
tests/src/Traits/SchemaHelperTestTrait.php 0 → 100644 +139 −0 Original line number Diff line number Diff line <?php namespace Drupal\Tests\patternkit\Traits; use Drupal\patternkit\Schema\DataPreProcessor\ObjectCoercionDataPreProcessor; use Drupal\patternkit\Schema\SchemaFactory; use Drupal\Tests\patternkit\Unit\Schema\TestPatternkitRefProvider; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; use Swaggest\JsonSchema\Context; use Swaggest\JsonSchema\DataPreProcessor; use Swaggest\JsonSchema\RemoteRefProvider; /** * A helper trait to assist unit tests with dependencies on SchemaHelper. * * This trait is only intended for use in tests. Implementation of the * tearDown() method assumes the implementing class inherits from * \PHPUnit\Framework\TestCase for a parent tearDown() implementation to extend. * * @see \Drupal\patternkit\Schema\SchemaHelper */ trait SchemaHelperTestTrait { use ProphecyTrait; /** * A prophecy mock for the schema factory service. * * @var \Prophecy\Prophecy\ObjectProphecy */ protected ObjectProphecy $schemaFactory; /** * A reference provider for loading schemas from. * * @var \Drupal\Tests\patternkit\Unit\Schema\TestPatternkitRefProvider */ protected TestPatternkitRefProvider $refProvider; /** * A data preprocessor to configure in schema contexts. * * @var \Swaggest\JsonSchema\DataPreProcessor */ protected DataPreProcessor $dataPreProcessor; /** * A default schema context to use for schema operations. * * @var \Swaggest\JsonSchema\Context */ protected Context $context; /** * Prepares a mocked schema factory service. * * Prepares a mocked schema factory service with configuration to return a * configured schema context from the getDefaultContext() method. If a * container is provided as an argument, the mock will be registered as the * 'patternkit.schema.schema_factory' service as well. * * Note: If the container is provided to this method, the mock wil be * instantiated and registered, so further manipulation of it in the class * variable will not be reflected in the registered service. If further * customization and mocking of the service is needed, it is recommended to * not provide the container as an argument and register it separately. * * @param \Psr\Container\ContainerInterface|null $container * (Optional) A container to register the mocked service to. * * @return \Prophecy\Prophecy\ObjectProphecy * The configured prophecy for the schema factory service. */ protected function setUpSchemaFactory(?ContainerInterface $container = NULL): ObjectProphecy { $this->schemaFactory = $this->prophesize(SchemaFactory::class); $this->schemaFactory->getDefaultContext()->willReturn($this->getSchemaContext()); if (isset($container)) { $container->set('patternkit.schema.schema_factory', $this->schemaFactory->reveal()); } return $this->schemaFactory; } /** * Get the default remote reference provider for schema contexts. * * @return \Swaggest\JsonSchema\RemoteRefProvider * A remote reference provider for schema contexts. */ protected function refProvider(): RemoteRefProvider { if (!isset($this->refProvider)) { $this->refProvider = new TestPatternkitRefProvider(); } return $this->refProvider; } /** * Get the default data preprocessor for schema contexts. * * @return \Swaggest\JsonSchema\DataPreProcessor * A date preprocessor to use in schema contexts. */ protected function dataPreProcessor(): DataPreProcessor { if (!isset($this->dataPreProcessor)) { $this->dataPreProcessor = new ObjectCoercionDataPreProcessor(); } return $this->dataPreProcessor; } /** * Get a configured schema context for schema operations. * * @return \Swaggest\JsonSchema\Context * A configured schema context for schema operations. */ protected function getSchemaContext(): Context { if (!isset($this->context)) { $this->context = new Context($this->refProvider()); $this->context->setDataPreProcessor($this->dataPreProcessor()); } // Clone the context to avoid persisting state of schema operations. return clone($this->context); } /** * {@inheritdoc} */ public function tearDown(): void { parent::tearDown(); $this->tearDownProphecy(); } }