Verified Commit 1a2f1f95 authored by Cyril Vazquez's avatar Cyril Vazquez
Browse files

WIP transformation

parent 59060842
Pipeline #15890 failed with stages
in 36 seconds
<?php
/*
* Copyright (C) 2022 Maarch
*
* This file is part of Maarch RM.
*
* Maarch RM 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.
*
* Maarch RM 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 Maarch RM. If not, see <http://www.gnu.org/licenses/>.
*/
namespace dependency\Transformation;
/**
* The transformation engine
*
* @author Cyril VAZQUEZ <cyril.vazquez@maarch.org>
*/
class Processor
{
/**
* @var JsonPath
*/
protected $jsonPath;
/**
* @var mixed
*/
protected $source;
/**
* Applies a set of rules on a source object
*
* @param object $source
* @param object $template
*/
public function transform($template, $source)
{
$this->jsonPath = \laabs::newService('dependency/json/Jsonpath', $source);
return $this->process($template);
}
protected function process($instruction, $context = null)
{
if (is_string($instruction)) {
if (substr($instruction, 0, 2) == '$.' || substr($instruction, 0, 2) == '@.') {
return $this->processValue($instruction, $context);
}
return $instruction;
}
return $this->processTemplate($instruction, $context);
}
protected function processTemplate($template, $context = null)
{
switch ($template->type) {
case 'object':
return $this->processObject($template, $context);
case 'array':
return $this->processArray($template, $context);
}
}
protected function processObject($template, $context = null)
{
$target = (object) [];
if (isset($template->select)) {
$items = $this->select($template->select, $context);
} else {
$items = [$context];
}
if (isset($template->properties)) {
foreach ($template->properties as $name => $property) {
if (!$this->isExpression($name)) {
$target->{$name} = $this->process($property, $context);
} else {
foreach ($items as $item) {
$pname = $this->process($name, $item);
$pvalue = $this->process($property, $item);
$target->{$pname} = $pvalue;
}
}
}
}
return $target;
}
protected function processTemplateProperties($templates, $target, $context = null)
{
foreach ($templates as $template) {
}
}
protected function processArray($template, $context = null)
{
$items = $this->select($template->select, $context);
if (!isset($template->items)) {
return $items;
}
$target = [];
foreach ($items as $item) {
$target[] = $this->process($template->items, $item);
}
return $target;
}
protected function isExpression($value)
{
return (
is_string($value)
&& (substr($value, 0, 2) == '$.' || substr($value, 0, 2) == '@.')
);
}
protected function processValue($path, $context = null)
{
$results = $this->jsonPath->query($path, $context);
return reset($results);
}
protected function select($path, $context = null)
{
return $this->jsonPath->query($path, $context);
}
}
Transformation
==============
Le composant permet de transformer des structures de données
grâce à des modèles paramétrés.
Usage
-----
La classe `Processor` fournit le moteur d'application des modèles
aux données d'entrée pour fournir les données de sortie.
Son usage est très simple : une fois le moteur instancié,
la méthode `transform` reçoit en paramètres les données en entrée
et le modèle de données;
il retourne les données transformées selon le modèle.
```
$transformationProcessor = new Processor();
$b = $transformationProcessor->transform($a, $template);
```
Modèles
-------
Un modèle est un document qui décrit la structure des données attendues en sortie
et les méthodes d'évaluation, principalement par utilisation des données fournies
en entrée.
Il existe deux types de modèles :
- `object` pour les objets
- `array` pour les tableaux indexés
Le type est fourni dans la propriété `type` du modèle.
Les modèles utilisent deux instructions principales pour accéder aux données d'entrée
et fournir les données de sortie:
- `select` retourne un jeu de résultats sous la forme d'un tableau.
Les résultats fournis peuvent ensuite être utilisés tels quels ou soumis à un nouveau
modèle de transformation.
- `value` retourne la première valeur trouvée d'après une expression de requête
ou une valeur constante
### Modèle d'objet
Le modèle de type `object` décrit une structure qui sera retournée sous la forme d'un objet de
classe standard de base `stdClass`.
Il fournit la liste des définitions de propriétés à évaluer avec la structure
`properties`, qui fait correspondre les noms des propriétés en sortie avec des
modèles de transformation.
```
type: object
properties:
{value} : {template} | {expression}
```
### Modèle de tableau
Le modèle de type `array` décrit une structure qui sera retournée sous la forme d'un tableau
indexé.
Il fournit une requête de sélection des éléments à intégrer au tableau via une
instruction `select`:
```
type: array
select: {expression}
```
Il peut fournir la définitions des éléments à évaluer avec la structure
`items`.
Chaque élément de données sélectionné par l'instruction `select` se verra
appliquer le modèle.
```
type: array
select: {expression}
items: {template} | {expression}
```
Valeurs
-------
L'instruction `value` fournit une requête de sélection de la valeur de sortie
via une expression :
```
value: {expression}
```
Les expressions sont le plus souvent utilisées pour fournir des valeurs
de propriétés ou éléments de tableaux scalaires.
Exemples
--------
### Objet simple
Le modèle suivant va prodire un objet comportant 2 propriétés issues des données
sources de type objet, mais en modifiant les noms :
```
type: object
properties:
targetProperty1: $.sourceProperty1
targetProperty2: $.sourceProperty2
```
### Objet avec propriétés nommées dynamiquement
Le modèle suivant va prodire un objet comportant 2 propriétés dont **le nom et
les valeurs** sont fournies par des expressions:
```
type: object
properties:
$.path.to.sourcePropertyName1: $.path.to.sourcePropertyValue1
$.path.to.sourcePropertyName2: $.path.to.sourcePropertyValue2
```
### Objet avec propriétés dynamiques
Le modèle suivant va prodire un objet comportant autant de propriétés que de résultats
fournis par l'expression `select`, dont le nom et les valeurs sont fournies par des expressions:
```
type: object
select: $.path.to.array.of.items
properties:
@.relative.path.to.sourcePropertyNames: @.relative.path.to.sourcePropertyValues
```
### Tableau de valeurs de propriété d'objet
```
type: array
select: $.sourceArray[*]
items: @.sourcePropertyName
```
<?php
/*
* Copyright (C) 2021 Maarch
*
* This file is part of dependency json.
*
* Dependency json is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dependency json 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with dependency json. If not, see <http://www.gnu.org/licenses/>.
*/
namespace dependency\json;
/**
* A jsonPath implementation for PHP
* Based on https://github.com/Peekmo/JsonPath
*/
class JsonPath
{
private $data = null;
private $result = [];
private $keywords = ['=', ')', '!', '<', '>'];
public function __construct($data)
{
$this->data = $data;
}
public function query($expr, $context = null)
{
if (is_null($context) || substr($expr, 0, 2) == '$.') {
$context = $this->data;
} else {
$expr = substr_replace($expr, '$.', 0, 2);
}
$x = $this->normalize($expr);
if ($x) {
$expr = preg_replace("/^\\$;/", "", $x);
$this->trace($expr, $context, "$");
return $this->result;
}
}
// normalize path expression
private function normalize($expression)
{
// Replaces filters by #0 #1...
$expression = preg_replace_callback(
array("/[\['](\??\(.*?\))[\]']/", "/\['(.*?)'\]/"),
array(&$this, "tempFilters"),
$expression
);
// ; separator between each elements
$expression = preg_replace(
array("/'?\.'?|\['?/", "/;;;|;;/", "/;$|'?\]|'$/"),
array(";", ";..;", ""),
$expression
);
// Restore filters
$expression = preg_replace_callback("/#([0-9]+)/", array(&$this, "restoreFilters"), $expression);
$this->result = array(); // result array was temporarily used as a buffer ..
return $expression;
}
/**
* Pushs the filter into the list
* @param string $filter
* @return string
*/
private function tempFilters($filter)
{
$f = $filter[1];
$elements = explode('\'', $f);
// Hack to make "dot" works on filters
for ($i=0, $m=0; $i<count($elements); $i++) {
if ($m%2 == 0) {
if ($i > 0 && substr($elements[$i-1], 0, 1) == '\\') {
continue;
}
$e = explode('.', $elements[$i]);
$str = ''; $first = true;
foreach ($e as $substr) {
if ($first) {
$str = $substr;
$first = false;
continue;
}
$end = null;
if (false !== $pos = $this->strpos_array($substr, $this->keywords)) {
list($substr, $end) = array(substr($substr, 0, $pos), substr($substr, $pos, strlen($substr)));
}
$str .= '[' . $substr . ']';
if (null !== $end) {
$str .= $end;
}
}
$elements[$i] = $str;
}
$m++;
}
return "[#" . (array_push($this->result, implode('\'', $elements)) - 1) . "]";
}
/**
* Get a filter back
* @param string $filter
* @return mixed
*/
private function restoreFilters($filter)
{
return $this->result[$filter[1]];
}
/**
* Builds json path expression
* @param string $path
* @return string
*/
private function asPath($path)
{
$expr = explode(";", $path);
$fullPath = "$";
for ($i = 1, $n = count($expr); $i < $n; $i++) {
$fullPath .= preg_match("/^[0-9*]+$/", $expr[$i]) ? ("[" . $expr[$i] . "]") : ("['" . $expr[$i] . "']");
}
return $fullPath;
}
private function store($p, $v)
{
if ($p) {
$this->result[] = $v;
}
return !!$p;
}
private function trace($expr, $val, $path)
{
if ($expr !== "") {
$x = explode(";", $expr);
$loc = array_shift($x);
$x = implode(";", $x);
if (is_array($val) && array_key_exists($loc, $val)) {
$this->trace($x, $val[$loc], $path . ";" . $loc);
}
else if (is_object($val) && property_exists($val, $loc)) {
$this->trace($x, $val->{$loc}, $path . ";" . $loc);
}
else if ($loc == "*") {
$this->walk($loc, $x, $val, $path, array(&$this, "_callback_03"));
}
else if ($loc === "..") {
$this->trace($x, $val, $path);
$this->walk($loc, $x, $val, $path, array(&$this, "_callback_04"));
}
else if (preg_match("/^\(.*?\)$/", $loc)) { // [(expr)]
$this->trace($this->evalx($loc, $val, substr($path, strrpos($path, ";") + 1)) . ";" . $x, $val, $path);
}
else if (preg_match("/^\?\(.*?\)$/", $loc)) { // [?(expr)]
$this->walk($loc, $x, $val, $path, array(&$this, "_callback_05"));
}
else if (preg_match("/^(-?[0-9]*):(-?[0-9]*):?(-?[0-9]*)$/", $loc)) { // [start:end:step] phyton slice syntax
$this->slice($loc, $x, $val, $path);
}
else if (preg_match("/,/", $loc)) { // [name1,name2,...]
for ($s = preg_split("/'?,'?/", $loc), $i = 0, $n = count($s); $i < $n; $i++)
$this->trace($s[$i] . ";" . $x, $val, $path);
}
} else {
$this->store($path, $val);
}
}
private function _callback_03($m, $l, $x, $v, $p)
{
$this->trace($m . ";" . $x, $v, $p);
}
private function _callback_04($m, $l, $x, $v, $p)
{
if (is_array($v)) {
if (is_array($v[$m]) || is_object($v[$m])) {
$this->trace("..;" . $x, $v[$m], $p . ";" . $m);
}
} elseif (is_object($v)) {
if (is_array($v->{$m}) || is_object($v->{$m})) {
$this->trace("..;" . $x, $v->{$m}, $p . ";" . $m);
}
}
}
private function _callback_05($m, $l, $x, $v, $p)
{
if ($this->evalx(preg_replace("/^\?\((.*?)\)$/", "$1", $l), $v[$m])) {
$this->trace($m . ";" . $x, $v, $p);
}
}
private function walk($loc, $expr, $val, $path, $f)
{
if ($val) {
foreach ($val as $m => $v) {
call_user_func($f, $m, $loc, $expr, $val, $path);
}
}
}
private function slice($loc, $expr, $v, $path)
{
$s = explode(":", preg_replace("/^(-?[0-9]*):(-?[0-9]*):?(-?[0-9]*)$/", "$1:$2:$3", $loc));
$len = count($v);
$start = (int)$s[0] ? $s[0] : 0;
$end = (int)$s[1] ? $s[1] : $len;
$step = (int)$s[2] ? $s[2] : 1;
$start = ($start < 0) ? max(0, $start + $len) : min($len, $start);
$end = ($end < 0) ? max(0, $end + $len) : min($len, $end);
for ($i = $start; $i < $end; $i += $step) {
$this->trace($i . ";" . $expr, $v, $path);
}
}
/**
* @param string $x filter
* @param array $v node
*
* @param string $vname
* @return string
*/
private function evalx($x, $v, $vname = null)
{
$name = "";
$expr = preg_replace(array("/\\$/", "/@/"), array("\$this->data", "\$v"), $x);
if (is_object($v)) {
$expr = preg_replace("#\[([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\]#", "->$1", $expr);
} else {
$expr = preg_replace("#\[([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\]#", "['$1']", $expr);
}
$res = eval("\$name = $expr;");
if ($res === false) {
print("(jsonPath) SyntaxError: " . $expr);
} else {
return $name;
}
}
private function toObject($array)
{
//$o = (object)'';
$o = new \stdClass();
foreach ($array as $key => $value) {
if (is_array($value)) {
$value = $this->toObject($value);
}
$o->$key = $value;
}
return $o;
}
/**
* Search one of the given needs in the array
* @param string $haystack
* @param array $needles
* @return bool|string
*/
private function strpos_array($haystack, array $needles)
{
$closer = 10000;
foreach($needles as $needle) {
if (false !== $pos = strpos($haystack, $needle)) {
if ($pos < $closer) {
$closer = $pos;
}
}
}
return 10000 === $closer ? false : $closer;
}
}
......@@ -1124,34 +1124,11 @@ class ArchiveTransfer extends abstractMessage
return $mapping->inputs->{$namespace};
}
protected function applyTransformationRules($source, $rules, $target = null)
protected function applyTransformationRules($archiveDescription, $rules, $archive)
{
$jsonPath = \laabs::newService('dependency/json/Jsonpath', $source);
$transformationProcessor = \laabs::newService('dependency/Transformation/Processor');