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

Support JSON Logic Validation #471

Merged
merged 9 commits into from
Oct 29, 2024
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]
### Breaking Change
- The `ComponentInterface` has a new method, `advancedValidations()` which returns an invokable `ValidationInterface` instance.
### Added
- Support for JSON Logic into the component validation process.
### Fixes
- Fixed an issue where the `Currency` component failed validation when not required and given null values.
- Fixed an instance where the `$subject` parameter to `str_replace()` could have been null in `Textfield::processValidations()`, as this was deprecated in PHP 8.1.

## [v1.0.1] 2024-05-15
### Changed
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ There are a couple pieces to be aware of:
- **Forms** are shown when you render a form definition. The form can be read-write or read-only (to display a submitted form). Forms produce submissions in the form of key:value JSON documents.

## Supported Features
Formiojs offers a lot of functionality. Dynamic Forms for Laravel has implemented a limited subset of all its available features.
Formiojs offers a lot of functionality. Dynamic Forms for Laravel has implemented a limited subset of all its available features, including JSON Logic support for complex conditions to show/hide fields, calculate values, and validate data.

Most of the decisions not to include something were driven by what would give us a good minimum viable product. If there are missing features that you would like to see, please feel free to submit an issue to discuss including it.

Expand Down
9 changes: 9 additions & 0 deletions docs/upgrading.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Upgrading

## v1.1.0
This version adds a new `advancedValidations()` method to the `ComponentInterface`.

```php
public function advancedValidations(): ?ValidationInterface;
```

This is implemented in the `BaseComponent`. However, if you have implemented this interface elsewhere, you should update your implementations.

## v1.0.0
This version swaps to the Formiojs v5 release candidate and assumes Bootstrap v5 and FontAwesome 6 are in use. The package now assumes Laravel 11, Laravel Vite, and PHP 8.2+.

Expand Down
11 changes: 11 additions & 0 deletions src/Components/BaseComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use Northwestern\SysDev\DynamicForms\Errors\ConditionalNotImplemented;
use Northwestern\SysDev\DynamicForms\Errors\InvalidDefinitionError;
use Northwestern\SysDev\DynamicForms\Errors\ValidationNotImplementedError;
use Northwestern\SysDev\DynamicForms\Validation\JSONValidation;
use Northwestern\SysDev\DynamicForms\Validation\ValidationInterface;

/**
* Implements common functionality for all components.
Expand Down Expand Up @@ -282,6 +284,15 @@ public function validations(): array
return $this->validations;
}

public function advancedValidations(): ?ValidationInterface
{
if ($this->validation('json')) {
return new JSONValidation($this->validation('json'));
}

return null;
}

public function additional(string $key): mixed
{
return Arr::get($this->additional, $key);
Expand Down
6 changes: 6 additions & 0 deletions src/Components/ComponentInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Illuminate\Contracts\Support\MessageBag;
use Northwestern\SysDev\DynamicForms\Calculation\CalculationInterface;
use Northwestern\SysDev\DynamicForms\Conditional\ConditionalInterface;
use Northwestern\SysDev\DynamicForms\Validation\ValidationInterface;

interface ComponentInterface
{
Expand Down Expand Up @@ -140,6 +141,11 @@ public function validation(string $name): mixed;
*/
public function validations(): array;

/**
* Returns an invokable Validation instance.
*/
public function advancedValidations(): ?ValidationInterface;

/**
* Get other settings that are not used directly by the library, but may be present in the component schema.
*/
Expand Down
8 changes: 5 additions & 3 deletions src/Components/Inputs/Textfield.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ protected function processValidations(string $fieldKey, string $fieldLabel, mixe
$rules->add(new CheckWordCount(CheckWordCount::MODE_MAXIMUM, $this->validation('maxWords')));
}

// PHP needs the regexp armoured with slashes, so...
$pattern = sprintf('/%s/', str_replace('/', '\/', $this->validation('pattern')));
$rules->addIfNotNull(['regex', $pattern], $this->validation('pattern'));
if ($this->validation('pattern')) {
// PHP needs the regexp armoured with slashes, so...
$pattern = sprintf('/%s/', str_replace('/', '\/', $this->validation('pattern')));
$rules->add(['regex', $pattern], $this->validation('pattern'));
}

return $validator->make(
[$fieldKey => $submissionValue],
Expand Down
3 changes: 3 additions & 0 deletions src/Forms/ValidatedForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public function __construct(array $components, array $values)

foreach ($this->flatComponents as $component) {
$messageBag->merge($component->validate());
if ($component->advancedValidations()) {
$messageBag->merge($component->advancedValidations()($component, $this->valuesWhileProcessingForm()));
}
$transformedValues->put($component->key(), $component->submissionValue());
}

Expand Down
53 changes: 53 additions & 0 deletions src/Validation/JSONValidation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Northwestern\SysDev\DynamicForms\Validation;

use Illuminate\Support\MessageBag;
use Illuminate\Support\MessageBag as MessageBagImpl;
use JWadhams\JsonLogic;
use Northwestern\SysDev\DynamicForms\Components\ComponentInterface;
use Northwestern\SysDev\DynamicForms\Errors\InvalidDefinitionError;
use Northwestern\SysDev\DynamicForms\JSONLogic\JsonLogicHelpers;

class JSONValidation implements ValidationInterface
{
protected array $jsonLogic;

public function __construct(array $jsonLogic)
{
$this->jsonLogic = JsonLogicHelpers::convertDataVars($jsonLogic);
}

public function __invoke(ComponentInterface $component, array $submissionValues): MessageBag
{
$bag = new MessageBagImpl;

if (! $this->isValidCustomValidation($this->jsonLogic)) {
throw new InvalidDefinitionError(
'Custom JSON Logic validations must always use the "if" parameter. The first argument to the statement must be the "true" case, and the second should be the error to display if the validation fails.',
$component->key(),
);
}

$validationResult = JsonLogic::apply($this->jsonLogic, $submissionValues);

if ($validationResult !== true) {
$bag->add($component->key(), $validationResult);
}

return $bag;
}

/**
* Custom JSON Logic validations must always use the "if" parameter. The first argument to the statement
* must be the "true" case, and the second should be the error to display if the validation fails.
*
* {@link https://help.form.io/developers/form-development/form-evaluations#custom-validation-1}
*/
protected function isValidCustomValidation(array $jsonLogic): bool
{
return isset($jsonLogic['if']) &&
is_array($jsonLogic['if']) &&
count($jsonLogic['if']) === 3;
}
}
11 changes: 11 additions & 0 deletions src/Validation/ValidationInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Northwestern\SysDev\DynamicForms\Validation;

use Illuminate\Contracts\Support\MessageBag;
use Northwestern\SysDev\DynamicForms\Components\ComponentInterface;

interface ValidationInterface
{
public function __invoke(ComponentInterface $component, array $submissionValues): MessageBag;
}
2 changes: 2 additions & 0 deletions tests/Components/TestCases/BaseComponentTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public function testCanValidate(): void
* @covers ::defaultValue
* @covers ::validation
* @covers ::validations
* @covers ::advancedValidations
*/
public function testGetters(): void
{
Expand All @@ -62,6 +63,7 @@ public function testGetters(): void
$this->assertEquals('foo', $component->defaultValue());
$this->assertNull($component->validation('required'));
$this->assertEmpty($component->validations());
$this->assertNull($component->advancedValidations());
$this->assertTrue($component->additional('disabled'));
}

Expand Down
Loading
Loading