Skip to content

Commit

Permalink
Merge pull request #1097 from nextcloud/dev/303-polymophism-in-filters
Browse files Browse the repository at this point in the history
Dev/303 polymophism in filters
  • Loading branch information
christianlupus authored Jul 22, 2022
2 parents d2b08d5 + 73eef78 commit 5057949
Show file tree
Hide file tree
Showing 42 changed files with 2,760 additions and 346 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/dependabot-approve-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@ on:

jobs:
auto-approve-merge:
if: github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest

steps:
# Github actions bot approve
- uses: hmarr/auto-approve-action@v2
if: github.actor == 'dependabot[bot]'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

# Nextcloud bot approve and merge request
- uses: ahmadnassri/action-dependabot-auto-merge@v2
if: github.actor == 'dependabot[bot]'
with:
target: minor
github-token: ${{ secrets.COOKBOOK_BOT_TOKEN }}

- run: "echo 'Skipping PR auto-merge as it is no dependabot PR'"
if: github.actor != 'dependabot[bot]'
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## [Unreleased]

### Changed
- Parsing of JSON recipe objects in a cascade of filters
[#1097](https://github.com/nextcloud/cookbook/pull/1097) @christianlupus

### Fixed
- Prevent slow loading of recipes due to iteration over all files
[#1072](https://github.com/nextcloud/cookbook/pull/1072) @christianlupus
Expand Down
9 changes: 9 additions & 0 deletions lib/Exception/InvalidDurationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace OCA\Cookbook\Exception;

class InvalidDurationException extends \Exception {
public function __construct($message = null, $code = null, $previous = null) {
parent::__construct($message, $code, $previous);
}
}
9 changes: 9 additions & 0 deletions lib/Exception/InvalidRecipeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace OCA\Cookbook\Exception;

class InvalidRecipeException extends \Exception {
public function __construct($message = null, $code = null, $previous = null) {
parent::__construct($message, $code, $previous);
}
}
36 changes: 36 additions & 0 deletions lib/Helper/Filter/AbstractJSONFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace OCA\Cookbook\Helper\Filter;

use OCA\Cookbook\Exception\InvalidRecipeException;
use OCP\Files\File;

/**
* An abstract filter on a recipe JSON.
*
* A filter should have a single purpose that is serves and implement this interface
*/
abstract class AbstractJSONFilter {
/**
* Filter the given recipe according to the filter class specification.
*
* This function can make changes to the recipe array to carry out the needed changes.
* In order to signal if the JSON file needs updating, the return value must be true if and only if the recipe was changed.
*
* @param array $json The recipe data as array
* @return bool true, if and only if the recipe was changed
* @throws InvalidRecipeException if the recipe was not correctly filtered
*/
abstract public function apply(array &$json): bool;

protected function setJSONValue(array &$json, string $key, $value): bool {
if (!array_key_exists($key, $json)) {
$json[$key] = $value;
return true;
} elseif ($json[$key] !== $value) {
$json[$key] = $value;
return true;
}
return false;
}
}
44 changes: 44 additions & 0 deletions lib/Helper/Filter/JSON/CleanCategoryFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace OCA\Cookbook\Helper\Filter\JSON;

use OCA\Cookbook\Helper\Filter\AbstractJSONFilter;
use OCA\Cookbook\Helper\TextCleanupHelper;

/**
* Clean the category of a recipe.
*
* A recipe must have at most one category.
* This category must be a string.
* Recipes without category are assigned the empty string for the category.
*/
class CleanCategoryFilter extends AbstractJSONFilter {
/** @var TextCleanupHelper */
private $textCleaner;

public function __construct(TextCleanupHelper $cleanupHelper) {
$this->textCleaner = $cleanupHelper;
}

public function apply(array &$json): bool {
if (! isset($json['recipeCategory'])) {
$json['recipeCategory'] = '';
return true;
}

$cache = $json['recipeCategory'];

if (is_array($json['recipeCategory'])) {
reset($json['recipeCategory']);
$json['recipeCategory'] = current($json['recipeCategory']);
}

if (!is_string($json['recipeCategory'])) {
$json['recipeCategory'] = '';
}

$json['recipeCategory'] = $this->textCleaner->cleanUp($json['recipeCategory'], true, true);

return $cache !== $json['recipeCategory'];
}
}
104 changes: 104 additions & 0 deletions lib/Helper/Filter/JSON/ExtractImageUrlFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace OCA\Cookbook\Helper\Filter\JSON;

use OCA\Cookbook\Helper\Filter\AbstractJSONFilter;
use OCP\IL10N;
use OCP\ILogger;

/**
* Select the best image URL.
*
* This filter makes a few sanity checks on the recipe image URL.
* It tries to guess the image URL with the highest resolution heuristically.
*
* It ensures that the image property is no object but a pure URL.
*/
class ExtractImageUrlFilter extends AbstractJSONFilter {
/** @var IL10N */
private $l;

/** @var ILogger */
private $logger;

public function __construct(IL10N $l, ILogger $logger) {
$this->l = $l;
$this->logger = $logger;
}

public function apply(array &$json): bool {
if (!isset($json['image']) || !$json['image']) {
$json['image'] = '';
return true;
}

if (is_string($json['image'])) {
// We do not change anything here
return false;
}

if (!is_array($json['image'])) {
// It is neither a plain string nor a JSON object
$this->logger->info($this->l->t('The given image for the recipe %s cannot be parsed. Aborting and skipping it.', [$json['name']]));
$json['image'] = '';
return true;
}

// We have an array of images or an object
if (isset($json['image']['url'])) {
// We have a single object. Use it.
$json['image'] = $json['image']['url'];
return true;
}

// We have an array of images. Extract the URLs
$images = array_map(function ($x) {
if (is_string($x)) {
return $x;
}
if (is_array($x) && isset($x['url']) && $x['url'] && is_string($x['url'])) {
return $x['url'];
}
return null;
}, $json['image']);
$images = array_filter($images);

if (count($images) === 1) {
reset($images);
$json['image'] = current($images);
return true;
}

if (count($images) === 0) {
$this->logger->info($this->l->t('No valid recipe was left after heuristics of recipe %s.', [$json['name']]));
$json['image'] = '';
return true;
}

$maxSum = -1;
$changed = false;

foreach ($images as $img) {
$regex = preg_match_all('/\d+/', $img, $matches);

if ($regex !== false && $regex > 0) {
$sum = array_sum(array_map(fn ($x) => ((int) $x), $matches[0]));

if ($sum > $maxSum) {
$json['image'] = $img;
$maxSum = $sum;
$changed = true;
}
}
}

if (!$changed) {
$this->logger->info($this->l->t('Heuristics failed for image extraction of recipe %s.', [$json['name']]));

reset($images);
$json['image'] = current($images);
}

return true;
}
}
53 changes: 53 additions & 0 deletions lib/Helper/Filter/JSON/FixDescriptionFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace OCA\Cookbook\Helper\Filter\JSON;

use OCP\IL10N;
use OCP\ILogger;
use OCA\Cookbook\Helper\Filter\AbstractJSONFilter;
use OCA\Cookbook\Helper\TextCleanupHelper;

/**
* Fix the description of a recipe.
*
* The description must be present in all cases.
* If is is not present, this filter will insert the empty string instead.
*
* Apart from that the description is cleaned from malicious chars.
*/
class FixDescriptionFilter extends AbstractJSONFilter {
private const NUTRITION = 'description';

/** @var IL10N */
private $l;

/** @var ILogger */
private $logger;

/** @var TextCleanupHelper */
private $textCleaner;

public function __construct(
IL10N $l,
ILogger $logger,
TextCleanupHelper $textCleanupHelper
) {
$this->l = $l;
$this->logger = $logger;
$this->textCleaner = $textCleanupHelper;
}

public function apply(array &$json): bool {
if (!isset($json[self::NUTRITION])) {
$json[self::NUTRITION] = '';
return true;
}

$description = $this->textCleaner->cleanUp($json[self::NUTRITION], false);
$description = trim($description);

$changed = $description !== $json[self::NUTRITION];
$json[self::NUTRITION] = $description;
return $changed;
}
}
69 changes: 69 additions & 0 deletions lib/Helper/Filter/JSON/FixDurationsFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace OCA\Cookbook\Helper\Filter\JSON;

use OCA\Cookbook\Exception\InvalidDurationException;
use OCP\IL10N;
use OCP\ILogger;
use OCA\Cookbook\Helper\Filter\AbstractJSONFilter;
use OCA\Cookbook\Helper\ISO8601DurationHelper;

/**
* Fix the durations of a recipe.
*
* The filter works on the prepTime, cookTime and totalTime entries, individually.
*
* If a time is not present or not able to read, it is set to null.
* The time is parsed and reformatted as ISO 8601 duration.
*/
class FixDurationsFilter extends AbstractJSONFilter {
private const DURATIONS = 'durations';

/** @var IL10N */
private $l;

/** @var ILogger */
private $logger;

/** @var ISO8601DurationHelper */
private $iso8601DurationHelper;

public function __construct(
IL10N $l,
ILogger $logger,
ISO8601DurationHelper $isoHelper
) {
$this->l = $l;
$this->logger = $logger;
$this->iso8601DurationHelper = $isoHelper;
}

public function apply(array &$json): bool {
$changed = false;

$this->fixDate($json, 'prepTime', $changed);
$this->fixDate($json, 'cookTime', $changed);
$this->fixDate($json, 'totalTime', $changed);

return $changed;
}

private function fixDate(array &$json, string $type, bool &$changed): void {
if (!isset($json[$type])) {
$json[$type] = null;
$changed = true;
return;
}

$orig = $json[$type];
try {
$json[$type] = $this->iso8601DurationHelper->parseDuration($json[$type]);
} catch (InvalidDurationException $ex) {
$json[$type] = null;
}

if ($orig !== $json[$type]) {
$changed = true;
}
}
}
Loading

0 comments on commit 5057949

Please sign in to comment.