Halfway rule API.

This commit is contained in:
James Cole 2018-06-30 16:46:51 +02:00
parent 7abcdea816
commit 36a6981329
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
8 changed files with 361 additions and 25 deletions

View File

@ -23,13 +23,30 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers; namespace FireflyIII\Api\V1\Controllers;
use FireflyIII\Api\V1\Requests\RuleRequest;
use FireflyIII\Models\Rule;
use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
use FireflyIII\Transformers\PiggyBankTransformer;
use FireflyIII\Transformers\RuleTransformer;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Validation\Validator;
use League\Fractal\Manager;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection;
use League\Fractal\Resource\Item;
use League\Fractal\Serializer\JsonApiSerializer;
/**
* Class RuleController
*/
class RuleController extends Controller class RuleController extends Controller
{ {
/** @var RuleRepositoryInterface */
private $ruleRepository;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
@ -38,7 +55,9 @@ class RuleController extends Controller
/** @var User $user */ /** @var User $user */
$user = auth()->user(); $user = auth()->user();
// todo add local repositories. $this->ruleRepository = app(RuleRepositoryInterface::class);
$this->ruleRepository->setUser($user);
return $next($request); return $next($request);
} }
); );
@ -67,7 +86,28 @@ class RuleController extends Controller
*/ */
public function index(Request $request): JsonResponse public function index(Request $request): JsonResponse
{ {
// todo implement. // create some objects:
$manager = new Manager;
$baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
// types to get, page size:
$pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
// get list of budgets. Count it and split it.
$collection = $this->ruleRepository->getAll();
$count = $collection->count();
$rules = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
// make paginator:
$paginator = new LengthAwarePaginator($rules, $count, $pageSize, $this->parameters->get('page'));
$paginator->setPath(route('api.v1.piggy_banks.index') . $this->buildParams());
// present to user.
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new FractalCollection($rules, new RuleTransformer($this->parameters), 'rules');
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
} }
@ -75,13 +115,23 @@ class RuleController extends Controller
* List single resource. * List single resource.
* *
* @param Request $request * @param Request $request
* @param string $object * @param Rule $rule
* *
* @return JsonResponse * @return JsonResponse
*/ */
public function show(Request $request, string $object): JsonResponse public function show(Request $request, Rule $rule): JsonResponse
{ {
// todo implement me. $manager = new Manager();
// add include parameter:
$include = $request->get('include') ?? '';
$manager->parseIncludes($include);
$baseUrl = $request->getSchemeAndHttpHost() . '/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new Item($rule, new RuleTransformer($this->parameters), 'rules');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
} }
@ -92,10 +142,11 @@ class RuleController extends Controller
* *
* @return JsonResponse * @return JsonResponse
*/ */
public function store(Request $request): JsonResponse public function store(RuleRequest $request): JsonResponse
{ {
// todo replace code and replace request object. print_r($request->getAll());
print_r($request->all());
exit;
} }
/** /**

View File

@ -0,0 +1,144 @@
<?php
/**
* RuleRequest.php
* Copyright (c) 2018 thegrumpydictator@gmail.com
*
* This file is part of Firefly III.
*
* Firefly III is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Firefly III 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests;
use Illuminate\Validation\Validator;
/**
* Class RuleRequest
*/
class RuleRequest extends Request
{
/**
* @return bool
*/
public function authorize(): bool
{
// Only allow authenticated users
return auth()->check();
}
/**
* @return array
*/
public function getAll(): array
{
$data = [
'title' => $this->string('title'),
'rule_group_id' => $this->integer('rule_group_id'),
'rule_group_title' => $this->string('rule_group_title'),
'trigger' => $this->string('trigger'),
'strict' => $this->boolean('strict'),
'stop_processing' => $this->boolean('stop_processing'),
'active' => $this->boolean('active'),
'rule-triggers' => [],
'rule-actions' => [],
];
foreach ($this->get('rule-triggers') as $trigger) {
$data['rule-triggers'][] = [
'name' => $trigger['name'],
'value' => $trigger['value'],
];
}
}
/**
* @return array
*/
public function rules(): array
{
$validTriggers = array_keys(config('firefly.rule-triggers'));
$validActions = array_keys(config('firefly.rule-actions'));
// some actions require text:
$contextActions = implode(',', config('firefly.rule-actions-text'));
$rules = [
'title' => 'required|between:1,100|uniqueObjectForUser:rules,title',
'description' => 'between:1,5000|nullable',
'rule_group_id' => 'required|belongsToUser:rule_groups|required_without:rule_group_title',
'rule_group_title' => 'nullable|between:1,255|required_without:rule_group_id|belongsToUser:rule_groups,title',
'trigger' => 'required|in:store-journal,update-journal',
'rule-triggers.*.name' => 'required|in:' . implode(',', $validTriggers),
'rule-triggers.*.value' => 'required|min:1|ruleTriggerValue', //
'rule-actions.*.name' => 'required|in:' . implode(',', $validActions),
'rule-actions.*.value' => 'required_if:rule-action.*.type,' . $contextActions . '|ruleActionValue',
'strict' => 'required|boolean',
'stop_processing' => 'required|boolean',
'active' => 'required|boolean',
];
return $rules;
}
/**
* Configure the validator instance.
*
* @param Validator $validator
*
* @return void
*/
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator) {
$this->atLeastOneTrigger($validator);
$this->atLeastOneAction($validator);
}
);
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*
* @param Validator $validator
*/
protected function atLeastOneAction(Validator $validator): void
{
$data = $validator->getData();
$repetitions = $data['rule-actions'] ?? [];
// need at least one transaction
if (\count($repetitions) === 0) {
$validator->errors()->add('title', trans('validation.at_least_one_action'));
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*
* @param Validator $validator
*/
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
$repetitions = $data['rule-triggers'] ?? [];
// need at least one transaction
if (\count($repetitions) === 0) {
$validator->errors()->add('title', trans('validation.at_least_one_trigger'));
}
}
}

View File

@ -25,6 +25,7 @@ namespace FireflyIII\Models;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -64,6 +65,7 @@ class Rule extends Model
'active' => 'boolean', 'active' => 'boolean',
'order' => 'int', 'order' => 'int',
'stop_processing' => 'boolean', 'stop_processing' => 'boolean',
'id' => 'int',
'strict' => 'boolean', 'strict' => 'boolean',
]; ];
/** @var array */ /** @var array */
@ -98,18 +100,18 @@ class Rule extends Model
/** /**
* @codeCoverageIgnore * @codeCoverageIgnore
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @return BelongsTo
*/ */
public function ruleGroup() public function ruleGroup(): BelongsTo
{ {
return $this->belongsTo(RuleGroup::class); return $this->belongsTo(RuleGroup::class);
} }
/** /**
* @codeCoverageIgnore * @codeCoverageIgnore
* @return \Illuminate\Database\Eloquent\Relations\HasMany * @return HasMany
*/ */
public function ruleTriggers() public function ruleTriggers(): HasMany
{ {
return $this->hasMany(RuleTrigger::class); return $this->hasMany(RuleTrigger::class);
} }
@ -117,16 +119,16 @@ class Rule extends Model
/** /**
* @param $value * @param $value
*/ */
public function setDescriptionAttribute($value) public function setDescriptionAttribute($value): void
{ {
$this->attributes['description'] = e($value); $this->attributes['description'] = e($value);
} }
/** /**
* @codeCoverageIgnore * @codeCoverageIgnore
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @return BelongsTo
*/ */
public function user() public function user(): BelongsTo
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }

View File

@ -80,6 +80,16 @@ class RuleRepository implements RuleRepositoryInterface
return $rule; return $rule;
} }
/**
* Get all the users rules.
*
* @return Collection
*/
public function getAll(): Collection
{
return $this->user->rules()->with(['ruleGroup'])->get();
}
/** /**
* FIxXME can return null. * FIxXME can return null.
* *

View File

@ -54,6 +54,13 @@ interface RuleRepositoryInterface
*/ */
public function find(int $ruleId): Rule; public function find(int $ruleId): Rule;
/**
* Get all the users rules.
*
* @return Collection
*/
public function getAll(): Collection;
/** /**
* @return RuleGroup * @return RuleGroup
*/ */

View File

@ -225,6 +225,8 @@ class FireflyValidator extends Validator
} }
/** /**
* TODO lots of if-else because of API calls.
*
* @param $attribute * @param $attribute
* *
* @return bool * @return bool
@ -234,12 +236,27 @@ class FireflyValidator extends Validator
// get the index from a string like "rule-action-value.2". // get the index from a string like "rule-action-value.2".
$parts = explode('.', $attribute); $parts = explode('.', $attribute);
$index = $parts[\count($parts) - 1]; $index = $parts[\count($parts) - 1];
if ($index === 'value') {
// user is coming from API.
$index = $parts[\count($parts) - 2];
}
$index = (int)$index;
// get actions from $this->data
$actions = [];
if (isset($this->data['rule-action']) && \is_array($this->data['rule-action'])) {
$actions = $this->data['rule-action'];
}
if (isset($this->data['rule-actions']) && \is_array($this->data['rule-actions'])) {
$actions = $this->data['rule-actions'];
}
// loop all rule-actions. // loop all rule-actions.
// check if rule-action-value matches the thing. // check if rule-action-value matches the thing.
if (\is_array($actions)) {
if (\is_array($this->data['rule-action'])) { $name = $this->getRuleActionName($index);
$name = $this->data['rule-action'][$index] ?? 'invalid'; $value = $this->getRuleActionValue($index);
$value = $this->data['rule-action-value'][$index] ?? false;
switch ($name) { switch ($name) {
default: default:
@ -271,6 +288,8 @@ class FireflyValidator extends Validator
} }
/** /**
* TODO This method uses a lot of if-then to handle the API calls as well. Fix.
*
* @param $attribute * @param $attribute
* *
* @return bool * @return bool
@ -280,20 +299,60 @@ class FireflyValidator extends Validator
// get the index from a string like "rule-trigger-value.2". // get the index from a string like "rule-trigger-value.2".
$parts = explode('.', $attribute); $parts = explode('.', $attribute);
$index = $parts[\count($parts) - 1]; $index = $parts[\count($parts) - 1];
// if the index is not a number, then we might be dealing with an API $attribute
// which is formatted "rule-triggers.0.value"
if ($index === 'value') {
$index = $parts[\count($parts) - 2];
}
$index = (int)$index;
// get triggers from $this->data
$triggers = [];
if (isset($this->data['rule-trigger']) && \is_array($this->data['rule-trigger'])) {
$triggers = $this->data['rule-trigger'];
}
if (isset($this->data['rule-triggers']) && \is_array($this->data['rule-triggers'])) {
$triggers = $this->data['rule-triggers'];
}
// loop all rule-triggers. // loop all rule-triggers.
// check if rule-value matches the thing. // check if rule-value matches the thing.
if (\is_array($this->data['rule-trigger'])) { if (\is_array($triggers)) {
$name = $this->getRuleTriggerName($index); $name = $this->getRuleTriggerName($index);
$value = $this->getRuleTriggerValue($index); $value = $this->getRuleTriggerValue($index);
// break on some easy checks: // break on some easy checks:
switch ($name) { switch ($name) {
case 'amount_less': case 'amount_less':
case 'amount_more':
case 'amount_exactly':
$result = is_numeric($value); $result = is_numeric($value);
if (false === $result) { if (false === $result) {
return false; return false;
} }
break;
case 'from_account_starts':
case 'from_account_ends':
case 'from_account_is':
case 'from_account_contains':
case 'to_account_starts':
case 'to_account_ends':
case 'to_account_is':
case 'to_account_contains':
case 'description_starts':
case 'description_ends':
case 'description_contains':
case 'description_is':
case 'category_is':
case 'budget_is':
case 'tag_is':
case 'currency_is':
case 'notes_contain':
case 'notes_start':
case 'notes_end':
case 'notes_are':
return \strlen($value) > 0;
break; break;
case 'transaction_type': case 'transaction_type':
$count = TransactionType::where('type', $value)->count(); $count = TransactionType::where('type', $value)->count();
@ -489,23 +548,71 @@ class FireflyValidator extends Validator
} }
/** /**
* TODO this method needs a lot of logic to be able to handle API calls. Fix that.
*
* @param int $index * @param int $index
* *
* @return string * @return string
*/ */
private function getRuleTriggerName($index): string private function getRuleActionName(int $index): string
{ {
return $this->data['rule-trigger'][$index] ?? 'invalid'; $name = $this->data['rule-action'][$index] ?? 'invalid';
if (!isset($this->data['rule-action'][$index])) {
$name = $this->data['rule-actions'][$index]['name'] ?? 'invalid';
}
return $name;
} }
/** /**
* TODO this method needs a lot of logic to be able to handle API calls. Fix that.
*
* @param int $index * @param int $index
* *
* @return string * @return string
*/ */
private function getRuleTriggerValue($index): string private function getRuleActionValue(int $index): string
{ {
return $this->data['rule-trigger-value'][$index] ?? ''; $value = $this->data['rule-action-value'][$index] ?? '';
if (!isset($this->data['rule-action-value'][$index])) {
$value = $this->data['rule-actions'][$index]['value'] ?? '';
}
return $value;
}
/**
* TODO this method needs a lot of logic to be able to handle API calls. Fix that.
*
* @param int $index
*
* @return string
*/
private function getRuleTriggerName(int $index): string
{
$name = $this->data['rule-trigger'][$index] ?? 'invalid';
if (!isset($this->data['rule-trigger'][$index])) {
$name = $this->data['rule-triggers'][$index]['name'] ?? 'invalid';
}
return $name;
}
/**
* TODO this method needs a lot of logic to be able to handle API calls. Fix that.
*
* @param int $index
*
* @return string
*/
private function getRuleTriggerValue(int $index): string
{
$value = $this->data['rule-trigger-value'][$index] ?? '';
if (!isset($this->data['rule-trigger-value'][$index])) {
$value = $this->data['rule-triggers'][$index]['value'] ?? '';
}
return $value;
} }
/** /**

View File

@ -46,6 +46,8 @@ return [
'belongs_to_user' => 'The value of :attribute is unknown.', 'belongs_to_user' => 'The value of :attribute is unknown.',
'accepted' => 'The :attribute must be accepted.', 'accepted' => 'The :attribute must be accepted.',
'bic' => 'This is not a valid BIC.', 'bic' => 'This is not a valid BIC.',
'at_least_one_trigger' => 'Rule must have at least one trigger',
'at_least_one_action' => 'Rule must have at least one action',
'base64' => 'This is not valid base64 encoded data.', 'base64' => 'This is not valid base64 encoded data.',
'model_id_invalid' => 'The given ID seems invalid for this model.', 'model_id_invalid' => 'The given ID seems invalid for this model.',
'more' => ':attribute must be larger than zero.', 'more' => ':attribute must be larger than zero.',

View File

@ -206,6 +206,19 @@ Route::group(
} }
); );
Route::group(
['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'rules', 'as' => 'api.v1.rules.'],
function () {
// Rules API routes:
Route::get('', ['uses' => 'RuleController@index', 'as' => 'index']);
Route::post('', ['uses' => 'RuleController@store', 'as' => 'store']);
Route::get('{rule}', ['uses' => 'RuleController@show', 'as' => 'show']);
Route::put('{rule}', ['uses' => 'RuleController@update', 'as' => 'update']);
Route::delete('{rule}', ['uses' => 'RuleController@delete', 'as' => 'delete']);
}
);
Route::group( Route::group(
['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'currencies', 'as' => 'api.v1.currencies.'], ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'currencies', 'as' => 'api.v1.currencies.'],
function () { function () {