From 36a6981329e5965f70b6aad2dbb4e46b89c50fc2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 30 Jun 2018 16:46:51 +0200 Subject: [PATCH] Halfway rule API. --- app/Api/V1/Controllers/RuleController.php | 69 +++++++-- app/Api/V1/Requests/RuleRequest.php | 144 ++++++++++++++++++ app/Models/Rule.php | 16 +- app/Repositories/Rule/RuleRepository.php | 10 ++ .../Rule/RuleRepositoryInterface.php | 7 + app/Validation/FireflyValidator.php | 125 +++++++++++++-- resources/lang/en_US/validation.php | 2 + routes/api.php | 13 ++ 8 files changed, 361 insertions(+), 25 deletions(-) create mode 100644 app/Api/V1/Requests/RuleRequest.php diff --git a/app/Api/V1/Controllers/RuleController.php b/app/Api/V1/Controllers/RuleController.php index e3d6fedb78..2ce2e9a8ee 100644 --- a/app/Api/V1/Controllers/RuleController.php +++ b/app/Api/V1/Controllers/RuleController.php @@ -23,13 +23,30 @@ declare(strict_types=1); 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 Illuminate\Http\JsonResponse; 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 { + /** @var RuleRepositoryInterface */ + private $ruleRepository; + public function __construct() { parent::__construct(); @@ -38,7 +55,9 @@ class RuleController extends Controller /** @var User $user */ $user = auth()->user(); - // todo add local repositories. + $this->ruleRepository = app(RuleRepositoryInterface::class); + $this->ruleRepository->setUser($user); + return $next($request); } ); @@ -67,7 +86,28 @@ class RuleController extends Controller */ 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. * * @param Request $request - * @param string $object + * @param Rule $rule * * @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 */ - 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; } /** diff --git a/app/Api/V1/Requests/RuleRequest.php b/app/Api/V1/Requests/RuleRequest.php new file mode 100644 index 0000000000..ebbc4bbd99 --- /dev/null +++ b/app/Api/V1/Requests/RuleRequest.php @@ -0,0 +1,144 @@ +. + */ + +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')); + } + } +} \ No newline at end of file diff --git a/app/Models/Rule.php b/app/Models/Rule.php index 4d368acec2..36d67bb89d 100644 --- a/app/Models/Rule.php +++ b/app/Models/Rule.php @@ -25,6 +25,7 @@ namespace FireflyIII\Models; use Carbon\Carbon; use FireflyIII\User; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; @@ -64,6 +65,7 @@ class Rule extends Model 'active' => 'boolean', 'order' => 'int', 'stop_processing' => 'boolean', + 'id' => 'int', 'strict' => 'boolean', ]; /** @var array */ @@ -98,18 +100,18 @@ class Rule extends Model /** * @codeCoverageIgnore - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @return BelongsTo */ - public function ruleGroup() + public function ruleGroup(): BelongsTo { return $this->belongsTo(RuleGroup::class); } /** * @codeCoverageIgnore - * @return \Illuminate\Database\Eloquent\Relations\HasMany + * @return HasMany */ - public function ruleTriggers() + public function ruleTriggers(): HasMany { return $this->hasMany(RuleTrigger::class); } @@ -117,16 +119,16 @@ class Rule extends Model /** * @param $value */ - public function setDescriptionAttribute($value) + public function setDescriptionAttribute($value): void { $this->attributes['description'] = e($value); } /** * @codeCoverageIgnore - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @return BelongsTo */ - public function user() + public function user(): BelongsTo { return $this->belongsTo(User::class); } diff --git a/app/Repositories/Rule/RuleRepository.php b/app/Repositories/Rule/RuleRepository.php index 06d29aefa0..26a1cedb92 100644 --- a/app/Repositories/Rule/RuleRepository.php +++ b/app/Repositories/Rule/RuleRepository.php @@ -80,6 +80,16 @@ class RuleRepository implements RuleRepositoryInterface return $rule; } + /** + * Get all the users rules. + * + * @return Collection + */ + public function getAll(): Collection + { + return $this->user->rules()->with(['ruleGroup'])->get(); + } + /** * FIxXME can return null. * diff --git a/app/Repositories/Rule/RuleRepositoryInterface.php b/app/Repositories/Rule/RuleRepositoryInterface.php index 5fcdd4c89d..cefbf46a95 100644 --- a/app/Repositories/Rule/RuleRepositoryInterface.php +++ b/app/Repositories/Rule/RuleRepositoryInterface.php @@ -54,6 +54,13 @@ interface RuleRepositoryInterface */ public function find(int $ruleId): Rule; + /** + * Get all the users rules. + * + * @return Collection + */ + public function getAll(): Collection; + /** * @return RuleGroup */ diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 1f9571e99d..15dcc33618 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -225,6 +225,8 @@ class FireflyValidator extends Validator } /** + * TODO lots of if-else because of API calls. + * * @param $attribute * * @return bool @@ -234,12 +236,27 @@ class FireflyValidator extends Validator // get the index from a string like "rule-action-value.2". $parts = explode('.', $attribute); $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. // check if rule-action-value matches the thing. - - if (\is_array($this->data['rule-action'])) { - $name = $this->data['rule-action'][$index] ?? 'invalid'; - $value = $this->data['rule-action-value'][$index] ?? false; + if (\is_array($actions)) { + $name = $this->getRuleActionName($index); + $value = $this->getRuleActionValue($index); switch ($name) { 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 * * @return bool @@ -280,20 +299,60 @@ class FireflyValidator extends Validator // get the index from a string like "rule-trigger-value.2". $parts = explode('.', $attribute); $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. // check if rule-value matches the thing. - if (\is_array($this->data['rule-trigger'])) { + if (\is_array($triggers)) { $name = $this->getRuleTriggerName($index); $value = $this->getRuleTriggerValue($index); // break on some easy checks: switch ($name) { case 'amount_less': + case 'amount_more': + case 'amount_exactly': $result = is_numeric($value); if (false === $result) { 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; case 'transaction_type': $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 * * @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 * * @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; } /** diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php index 5fcc19c535..38371f9d97 100644 --- a/resources/lang/en_US/validation.php +++ b/resources/lang/en_US/validation.php @@ -46,6 +46,8 @@ return [ 'belongs_to_user' => 'The value of :attribute is unknown.', 'accepted' => 'The :attribute must be accepted.', '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.', 'model_id_invalid' => 'The given ID seems invalid for this model.', 'more' => ':attribute must be larger than zero.', diff --git a/routes/api.php b/routes/api.php index e9beddb78a..f7f967d3d9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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( ['middleware' => ['auth:api', 'bindings'], 'namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'currencies', 'as' => 'api.v1.currencies.'], function () {