feat: support action expression parsing, validation, and evaluation

This commit is contained in:
Michael Thomas
2024-03-06 17:50:16 -05:00
parent 068191e08c
commit daddee7806
9 changed files with 633 additions and 2 deletions

View File

@@ -0,0 +1,89 @@
<?php
/*
* ExpressionController.php
* Copyright (c) 2024 Michael Thomas
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Rule;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Rule\ValidateExpressionRequest;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\TransactionRules\Expressions\ActionExpressionEvaluator;
use FireflyIII\TransactionRules\Factory\ExpressionLanguageFactory;
use FireflyIII\User;
use Illuminate\Http\JsonResponse;
use Symfony\Component\ExpressionLanguage\SyntaxError;
/**
* Class ExpressionController
*/
class ExpressionController extends Controller
{
private RuleRepositoryInterface $ruleRepository;
/**
* RuleController constructor.
*
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
/** @var User $user */
$user = auth()->user();
$this->ruleRepository = app(RuleRepositoryInterface::class);
$this->ruleRepository->setUser($user);
return $next($request);
}
);
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/rules/validateExpression
*
* @param ValidateExpressionRequest $request
*
* @return JsonResponse
*/
public function validateExpression(ValidateExpressionRequest $request): JsonResponse
{
$expr = $request->getExpression();
$expressionLanguage = ExpressionLanguageFactory::get();
$evaluator = new ActionExpressionEvaluator($expressionLanguage, $expr);
try {
$evaluator->lint();
return response()->json([
"valid" => true,
]);
} catch (SyntaxError $e) {
return response()->json([
"valid" => false,
"error" => $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* ValidateExpressionRequest.php
* Copyright (c) 2024 Michael Thomas
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Rule;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
/**
* Class TestRequest
*/
class ValidateExpressionRequest extends FormRequest
{
use ConvertsDataTypes;
use ChecksLogin;
/**
* @return string
*/
public function getExpression(): string
{
return $this->convertString("expression");
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* ActionExpressionEvaluator.php
* Copyright (c) 2024 Michael Thomas
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\TransactionRules\Expressions;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;
class ActionExpressionEvaluator
{
private static array $NAMES = array("transaction");
private string $expr;
private bool $isExpression;
private ExpressionLanguage $expressionLanguage;
public function __construct(ExpressionLanguage $expressionLanguage, string $expr)
{
$this->expressionLanguage = $expressionLanguage;
$this->expr = $expr;
$this->isExpression = self::isExpression($expr);
}
private static function isExpression(string $expr): bool
{
return str_starts_with($expr, "=");
}
public function isValid(): bool
{
if (!$this->isExpression) {
return true;
}
try {
$this->lint(array());
return true;
} catch (SyntaxError $e) {
return false;
}
}
private function lintExpression(string $expr): void
{
$this->expressionLanguage->lint($expr, self::$NAMES);
}
public function lint(): void
{
if (!$this->isExpression) {
return;
}
$this->lintExpression(substr($this->expr, 1));
}
private function evaluateExpression(string $expr, array $journal): string
{
$result = $this->expressionLanguage->evaluate($expr, [
"transaction" => $journal
]);
return strval($result);
}
public function evaluate(array $journal): string
{
if (!$this->isExpression) {
return $this->expr;
}
return $this->evaluateExpression(substr($this->expr, 1), $journal);
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* ActionExpressionLanguageProvider.php
* Copyright (c) 2024 Michael Thomas
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\TransactionRules\Expressions;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
class ActionExpressionLanguageProvider implements ExpressionFunctionProviderInterface
{
public function getFunctions(): array
{
return [
ExpressionFunction::fromPhp("substr"),
ExpressionFunction::fromPhp("strlen")
];
}
}

View File

@@ -19,6 +19,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\TransactionRules\Factory;
@@ -27,6 +28,8 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\RuleAction;
use FireflyIII\Support\Domain;
use FireflyIII\TransactionRules\Actions\ActionInterface;
use FireflyIII\TransactionRules\Expressions\ActionExpressionEvaluator;
use FireflyIII\TransactionRules\Factory\ExpressionLanguageFactory;
use Illuminate\Support\Facades\Log;
/**
@@ -56,7 +59,10 @@ class ActionFactory
$class = self::getActionClass($action->action_type);
Log::debug(sprintf('self::getActionClass("%s") = "%s"', $action->action_type, $class));
return new $class($action);
$expressionLanguage = ExpressionLanguageFactory::get();
$expressionEvaluator = new ActionExpressionEvaluator($expressionLanguage, $action->action_value);
return new $class($action, $expressionEvaluator);
}
/**

View File

@@ -0,0 +1,45 @@
<?php
/**
* ExpressionLanguageFactory.php
* Copyright (c) 2024 Michael Thomas
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\TransactionRules\Factory;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use FireflyIII\TransactionRules\Expressions\ActionExpressionLanguageProvider;
class ExpressionLanguageFactory
{
protected static ExpressionLanguage $expressionLanguage;
private static function constructExpressionLanguage(): ExpressionLanguage
{
$expressionLanguage = new ExpressionLanguage();
$expressionLanguage->registerProvider(new ActionExpressionLanguageProvider());
return $expressionLanguage;
}
public static function get(): ExpressionLanguage
{
return self::$expressionLanguage ??= self::constructExpressionLanguage();
}
}