Si vous constatez la perte de tickets, de commentaires ou de projets, veuillez consulter la page https://status.framasoft.org/incident/626.

Commit 10d349b7 authored by Blaise de Carné 's avatar Blaise de Carné

Merge branch 'release/1.1.0'

parents 1f0ee9de 266c31a5
Pipeline #55520 passed with stage
in 22 seconds
......@@ -6,4 +6,6 @@
/vendor/
/.idea/
/var/
/bin/
\ No newline at end of file
/bin/
tests/dataset.hdt.index
node_modules
\ No newline at end of file
......@@ -4,6 +4,20 @@ Tous les changements notables apportés à ce projet seront documentés dans ce
Le format est basé sur [Keep a Changelog](http://keepachangelog.com/fr/1.0.0/)
et ce projet adhère à [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## En cours
## [1.1.0] - 2018-06-04
### Ajouts
- Gestion des propriétés multilingues
- Ajout d'un argument lang
### Modifications
- Ajout d'un tri par défaut sur la requête SPARQL (uri ASC)
- Ajout d'un pré-traitement d'ordonnancement dans le QueryHydrator
- Ajout d'une option pour choisir le graph par défaut dans le VirtuosoResolver
- Modification du filtre uri : utilisation de VALUES à la place de BIND
## [1.0.0] - 2018-04-24
### Ajouts
......
......@@ -3,7 +3,7 @@
"license": "MIT",
"keywords": ["graphql","datatourisme","api","sparql","opendata"],
"type": "library",
"version": "1.0.0",
"version": "1.1.0",
"homepage": "https://www.datatourisme.gouv.fr",
"authors": [
{
......@@ -43,14 +43,19 @@
},
"scripts": {
"clean": "vendor/bin/php-cs-fixer fix -v --using-cache=no",
"generate_doc": [
"graphql-markdown http://localhost:8080 > docs/api/reference.md",
"sed -i -e \"s/# Schema Types/# Référence/g\" docs/api/reference.md",
"sed -i -e \"s/href=\\\"#/href=\\\"#\\/api/reference?id=/g\" docs/api/reference.md",
"sed -i -e \"s/id=pointofinterest_sort/id=pointofinterest/g\" docs/api/reference.md",
"sed -i -e \"s/id=pointofinterest_filter/id=pointofinterest/g\" docs/api/reference.md",
"sed -i -e \"s/<th align=\\\"right\\\">Argument<\\\/th>//g\" docs/api/reference.md",
"sed -i -e \"s/<th align=\\\"left\\\">Field<\\\/th>/<th align=\\\"left\\\" colspan=\\\"2\\\">Field<\\\/th>/g\" docs/api/reference.md"
"doc-serve": "docsify serve docs",
"graphql-endpoint": "php -S 127.0.0.1:8080 -t tests/",
"doc-generate": [
"node_modules/.bin/graphql-markdown http://127.0.0.1:8080/endpoint.php > docs/api/reference.md",
"sed -i -e \"s/# Schema Types/# Référence/g\" docs/api/reference.md",
"sed -i -e \"s/href=\\\"#/href=\\\"#\\/api\\/reference?id=/g\" docs/api/reference.md",
"sed -i -e \"s/id=pointofinterest_sort/id=pointofinterest/g\" docs/api/reference.md",
"sed -i -e \"s/id=pointofinterest_filter/id=pointofinterest/g\" docs/api/reference.md",
"sed -i -e \"s/<th align=\\\"right\\\">Argument<\\\/th>//g\" docs/api/reference.md",
"sed -i -e \"s/<th align=\\\"left\\\">Field<\\\/th>/<th align=\\\"left\\\" colspan=\\\"2\\\">Field<\\\/th>/g\" docs/api/reference.md",
"sed -i -e \"s/<details>//g\" docs/api/reference.md",
"sed -i -e \"s/<\\/details>//g\" docs/api/reference.md",
"sed -i -e \"s/<summary><strong>Table of Contents<\\/strong><\\/summary>//g\" docs/api/reference.md"
]
}
}
......@@ -8,8 +8,9 @@ services:
volumes:
- ./tests/RWStore.properties:/docker-entrypoint-initdb.d/kb/RWStore.properties
- ./tests/dataset.ttl:/docker-entrypoint-initdb.d/kb/data/dataset.ttl
virtuoso:
image: tenforce/virtuoso:1.3.1-virtuoso7.2.2
image: tenforce/virtuoso:1.3.1-virtuoso7.2.4
ports:
- 8890:8890
volumes:
......
......@@ -25,7 +25,8 @@ Voici la liste des arguments disponibles :
],
sort: [
# ... <- Liste des règles de tri des résultats
]
],
lang: "fr"
) {
# ...
}
......@@ -264,4 +265,10 @@ Cette requête extrait tous les identifiants des **POI** par ordre décroissant
}
}
}
```
\ No newline at end of file
```
## lang
**lang** permet de limiter les propriétés traduisibles renvoyées dans la réponse aux
seules valeurs de la langue sélectionnée. La liste des codes acceptés est définie par
la norme **ISO 639-1** : [https://fr.wikipedia.org/wiki/Liste_des_codes_ISO_639-1].
# Champs
## Généralités
Le principe essentiel de GraphQL est d'adapter la réponse aux champs demandés par l'utilisateur dans le corps de la
requête :
......@@ -36,7 +38,6 @@ pour connaître la liste des champs d'un POI : [PointOfInterest](api/reference.m
la spécification GraphQL n'autorisant pas cette notation, l'API DATAtourisme remplace le double point par un `_`.
Exemple: `rdfs:label` devient `rdfs_label`.
** Exemple **
```graphql
......@@ -45,9 +46,12 @@ Exemple: `rdfs:label` devient `rdfs_label`.
total
results {
_uri # <- Identifiant du POI
rdfs_label # <- Nom du POI
hasDescription {
shortDescription # <- Description du POI
dc_identifier # <- Référence du POI
isLocatedAt {
schema_geo {
schema_latitude # <- Latitude du POI
schema_longitude # <- Longitude du POI
}
}
}
}
......@@ -64,29 +68,44 @@ Exemple: `rdfs:label` devient `rdfs_label`.
"results": [
{
"_uri": "https://data.datatourisme.gouv.fr/21/5430055a-2d35-3ce7-adc9-ebecb9f5cbfd",
"rdfs_label": ["Jouer aux échecs"],
"hasDescription": [{
"shortDescription": [
"Rendez-vous le 1er et le 3ème vendredi du mois à la maison des Associations, cours apprentissage, perfectionnement, rencontres amicales, tournois."
"dc_identifier": ["FMACHA0100957599"],
"isLocatedAt": [{
"schema_geo": [{
"schema_latitude": [
49.376388888889
],
"schema_longitude": [
6.1580555555556
]
}]
}]
},
{
"_uri": "https://data.datatourisme.gouv.fr/21/ce824246-dd4f-3f08-ab90-974a409f67d6",
"rdfs_label": ["Le sens de la fête"],
"hasDescription": [{
"shortDescription": [
"Comédie de Eric Toledano et Olivier Nakache. Avec Jean-Pierre Bacri, Gilles Lellouche, Eye Haidara."
"dc_identifier": ["FMACHA0100957578"],
"isLocatedAt": [{
"schema_geo": [{
"schema_latitude": [
48.693055555556
],
"schema_longitude": [
6.1794444444445
]
}
]},
}]
}]
},
{
"_uri": "https://data.datatourisme.gouv.fr/31/00024f0a-74b7-34fd-80f0-eec5cbf61189",
"rdfs_label": ["Randonnée"],
"hasDescription": [{
"shortDescription": [
"Randonnée par les Randonneurs du Montet"
"dc_identifier": ["FMACHA0100705801"],
"isLocatedAt": [{
"schema_geo": [{
"schema_latitude": [
49.360329728326
],
"schema_longitude": [
6.1624977836914
]
}]
}]
}
]
......@@ -95,3 +114,54 @@ Exemple: `rdfs:label` devient `rdfs_label`.
}
```
## Propriétés multilingues
Certaines propriétés (rdf:label, :shortDescription) sont multilingues : chaque valeur peut se retrouver traduite
en plusieurs langues.
Dans ce cas précis, leur structure est décomposée afin de connaître la langue associée à chacune des valeurs :
** Exemple **
```graphql
{
poi {
total
results {
_uri
rdfs_label {
value
lang
}
}
}
}
```
** Résultat **
```json
{
"data": {
"poi": {
"total": 1308,
"results": [
{
"_uri": "https://data.datatourisme.gouv.fr/21/5430055a-2d35-3ce7-adc9-ebecb9f5cbfd",
"rdfs_label": [
{ "value": "Jouer aux échecs", lang: "fr" },
{ "value": "Play chess", lang: "en" }
]
},
{
"_uri": "https://data.datatourisme.gouv.fr/21/ce824246-dd4f-3f08-ab90-974a409f67d6",
"rdfs_label": [
{ "value": "Randonnée", lang: "fr" },
{ "value": "Hiking", lang: "en" }
]
}
]
}
}
}
```
This diff is collapsed.
{
"name": "datatourisme-api",
"version": "1.1.0",
"repository": "https://framagit.org/datatourisme/api",
"author": "Conjecto <contact@conjecto.com>",
"license": "MIT",
"dependencies": {
"graphql-markdown": "^3.2.0"
}
}
......@@ -11909,7 +11909,7 @@ fields:
description: null
ebucore:abstract:
name: "a pour abstraction"
type: "xsd:string"
type: "rdf:langString"
description: "fournit un description courte."
schema:openingHoursSpecification:
name: "a pour horaires d'ouverture"
......@@ -12065,7 +12065,7 @@ fields:
description: "L'age minimum requis."
dc:description:
name: "Description longue"
type: "xsd:string"
type: "rdf:langString"
description: "Description longue de la ressource."
:appliesOnDay:
name: "s'applique sur le jour de la semaine"
......@@ -12272,7 +12272,7 @@ fields:
\ objets point séparés par un caractère espace."
rdfs:label:
name: "Nom"
type: "xsd:string"
type: "rdf:langString"
description: "Etiquette courte décrivant la ressource."
:agreementLicense:
name: "Numéro d'immatriculation au registre des opérateurs de voyages"
......@@ -12411,7 +12411,7 @@ fields:
\ composant de celui-ci). Ex: un court de golf dans un camping."
ebucore:title:
name: "a pour titre"
type: "xsd:string"
type: "rdf:langString"
description: "Titre de la ressource."
:hasPricingMode:
name: "a pour mode de tarification"
......@@ -12499,7 +12499,7 @@ fields:
\ spécifier une distance. Sinon, préférer la relation directe isCloseTo."
:shortDescription:
name: "Description courte"
type: "xsd:string"
type: "rdf:langString"
description: "Utiliser cette propriété pour attacher une description courte qui\
\ pourra être utilisée sur des supports de publication l'espace est un critè\
re important."
......@@ -12546,7 +12546,7 @@ fields:
description: "L'age maximum suggéré."
ebucore:comments:
name: "description"
type: "xsd:string"
type: "rdf:langString"
description: "La description de la ressource."
:hasHikeAudience:
name: "est un parcours dédié à l'audience"
......
<?php
/**
* Created by PhpStorm.
* User: blaise
* Date: 25/05/18
* Time: 18:32
*/
namespace Datatourisme\Api\Processor;
use Datatourisme\Api\Schema\Type\Object\LangStringType;
use Youshido\GraphQL\Field\FieldInterface;
use Youshido\GraphQL\Parser\Ast\Interfaces\FieldInterface as AstFieldInterface;
/**
* Class Processor
* @package Datatourisme\Api\Processor
*/
class Processor extends \Youshido\GraphQL\Execution\Processor
{
protected function resolveObject(FieldInterface $field, AstFieldInterface $ast, $parentValue = null, $fromObject = false)
{
// hack to handle legacy langstring behavior
// will be deprecated in 2.0
if($field->getType() instanceof LangStringType && !count($ast->getFields())) {
return $parentValue;
}
return parent::resolveObject($field, $ast, $parentValue, $fromObject); // TODO: Change the autogenerated stub
}
}
\ No newline at end of file
......@@ -13,7 +13,6 @@ use Datatourisme\Api\Exception\RuntimeException;
use Datatourisme\Api\Resolver\ResolverInterface;
use Datatourisme\Api\Schema\Compiler\SchemaCompiler;
use Datatourisme\Api\Schema\SchemaLoader;
use Youshido\GraphQL\Execution\Processor;
class ProcessorBuilder
{
......@@ -50,7 +49,7 @@ class ProcessorBuilder
/**
* @var array
*/
protected $scalarTypes = array();
protected $typesMap = array();
/**
* ProcessorBuilder constructor.
......@@ -102,6 +101,7 @@ class ProcessorBuilder
/**
* @return SchemaLoader
* @throws \Exception
*/
public function getLoader()
{
......@@ -163,9 +163,9 @@ class ProcessorBuilder
*
* @return $this
*/
public function addScalarType(string $name, string $className)
public function addTypeMap(string $name, string $className)
{
$this->scalarTypes[$name] = $className;
$this->typesMap[$name] = $className;
return $this;
}
......@@ -174,10 +174,11 @@ class ProcessorBuilder
* @return Processor
*
* @throws RuntimeException
* @throws \Exception
*/
public function getProcessor(): Processor
{
$this->getCompiler()->setScalarTypes($this->scalarTypes);
$this->getCompiler()->setTypesMap($this->typesMap);
$schema = $this->getLoader()->load($this->schemaFile);
// add entry points
......
......@@ -28,7 +28,7 @@ class HdtResolver extends SparqlResolver
throw new IOException('File '.$this->HDTBin.' not found');
}
}
$sparql = str_replace("\n"," ", $sparql);
$sparql = str_replace("\n", ' ', $sparql);
$result = shell_exec('java -jar '.$this->HDTBin.' '.$this->endpoint.' "'.addslashes($sparql).'"');
if ('Parse error' == substr($result, 0, 11)) {
......
<?php
/**
* This file is part of the DATAtourisme project.
*
* @author Conjecto <contact@conjecto.com>
*
* For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
*/
namespace Datatourisme\Api\Resolver\SparqlResolver\Collection;
class OrderCollection extends AbstractCollection
{
public function __toString()
{
if (!$this->count()) {
return '';
}
return implode(' ', $this->getArrayCopy());
}
}
......@@ -27,7 +27,7 @@ use Datatourisme\Api\Resolver\SparqlResolver\Filter\Logical\AndOperator;
use Datatourisme\Api\Resolver\SparqlResolver\Filter\Logical\NotOperator;
use Datatourisme\Api\Resolver\SparqlResolver\Filter\Logical\OrOperator;
use Datatourisme\Api\Resolver\SparqlResolver\Filter\LogicalFilterInterface;
use Datatourisme\Api\Resolver\SparqlResolver\Sparql\Functions\Bind;
use Datatourisme\Api\Resolver\SparqlResolver\Sparql\Functions\Values;
use Datatourisme\Api\Resolver\SparqlResolver\Sparql\Triplet;
use Datatourisme\Api\Resolver\SparqlResolver\Utils\SparqlUtils;
use Youshido\GraphQL\Field\InputFieldInterface;
......@@ -77,7 +77,7 @@ class FilterGenerator
}
if ($uri) {
$set->add(new Bind($subject, '<'.$uri.'>'));
$set->add(new Values($subject, ['<'.$uri.'>']));
}
return $set;
......
......@@ -11,6 +11,7 @@ namespace Datatourisme\Api\Resolver\SparqlResolver\QueryBuilder;
use Datatourisme\Api\Resolver\SparqlResolver\Collection\Collection;
use Datatourisme\Api\Resolver\SparqlResolver\Collection\FlatCollection;
use Datatourisme\Api\Resolver\SparqlResolver\Collection\OrderCollection;
class OrderClause
{
......@@ -29,7 +30,7 @@ class OrderClause
*/
public function __construct()
{
$this->conditions = new Collection();
$this->conditions = new OrderCollection();
$this->patterns = new FlatCollection();
}
......
......@@ -131,11 +131,11 @@ class QueryBuilder
$query->add(new OrderBy($this->orderBy));
}
if (null != $this->limit) {
if (null !== $this->limit) {
$query->add(new Limit($this->limit));
}
if (null != $this->offset) {
if (null !== $this->offset) {
$query->add(new Offset($this->offset));
}
......
......@@ -61,6 +61,9 @@ class QueryGenerator
$queryBuilder->orderBy($orderClause->getConditions());
$queryBuilder->offset('0');
// dump($queryBuilder->getQuery());
// die;
return $queryBuilder;
}
......
......@@ -51,6 +51,8 @@ class SortGenerator
$recursiveBuild($subject, $outputTypeDef, $sort);
}
$orderClause->getConditions()->add('?res');
return $orderClause;
}
}
......@@ -10,16 +10,12 @@
namespace Datatourisme\Api\Resolver\SparqlResolver;
use Datatourisme\Api\Resolver\SparqlResolver\Utils\SparqlUtils;
use EasyRdf\Graph;
use EasyRdf\Literal;
use EasyRdf\Resource;
use Youshido\GraphQL\Parser\Ast\ArgumentValue\InputList;
class QueryHydrator
{
/**
* @param $entryPoint
* @param array $arguments
* @param array $args
* @param $astFields
* @param string $sparqlResult
*
......@@ -27,7 +23,7 @@ class QueryHydrator
*
* @throws \EasyRdf_Exception
*/
public function generate(array $arguments, $astFields, $sparqlResult = '')
public function generate(array $args, $astFields, $sparqlResult = '')
{
/** @var resource[] $listRes */
$listRes = [];
......@@ -39,12 +35,19 @@ class QueryHydrator
$listRes = $graph->allOfType('urn:resource');
}
// first sort by uri
usort($listRes, function ($a, $b) {
return strcmp($a->getUri(), $b->getUri());
});
// hydrate results
$tabResult = [];
foreach ($listRes as $res) {
$tabResult[] = $this->consume($astFields, $res);
$tabResult[] = $this->consume($astFields, $res, $args);
}
$sortList = (isset($arguments['sort'])) ? $arguments['sort'] : [];
// re-apply sorts
$sortList = (isset($args['sort'])) ? $args['sort'] : [];
$tabResult = $this->sortResult($sortList, $tabResult);
return $tabResult;
......@@ -56,31 +59,43 @@ class QueryHydrator
*
* @return mixed
*/
private function consume(array $fields, $res)
private function consume(array $fields, $res, $args)
{
$tabResult = [];
foreach ($fields as $field) {
$fieldName = $field->getName();
if ('_uri' == $fieldName) {
$tabResult['_uri'] = $res->getUri();
continue;
}
$property = new \EasyRdf_Resource(SparqlUtils::expandUri($fieldName));
if($res) {
foreach ($fields as $field) {
$fieldName = $field->getName();
if ($field->hasFields()) {
$tabResult[$fieldName] = [];
$listRes = $res->all($property);
foreach ($listRes as $resourceValue) {
$tabResult[$fieldName][] = $this->consume($field->getFields(), $resourceValue);
if ('_uri' == $fieldName) {
$tabResult['_uri'] = $res->getUri();
continue;
}
} else {
if ($res && $res->hasProperty($property)) {
$property = new \EasyRdf_Resource(SparqlUtils::expandUri($fieldName));
if ($field->hasFields()) {
$tabResult[$fieldName] = [];
$listRes = $res->all($property);
foreach ($listRes as $resourceValue) {
if(isset($args['lang']) && $resourceValue instanceof \EasyRdf_Literal && $resourceValue->getLang() && $resourceValue->getLang() != $args['lang']) {
continue;
}
$tabResult[$fieldName][] = $this->consume($field->getFields(), $resourceValue, $args);
}
} elseif($res instanceof \EasyRdf_Literal) {
switch($fieldName) {
case "value": $tabResult[$fieldName] = $res->getValue(); break;
case "lang": $tabResult[$fieldName] = $res->getLang(); break;
}
} elseif($res instanceof \EasyRdf_Resource && $res->hasProperty($property)) {
$tabResult[$fieldName] = [];
/** @var \EasyRdf_Resource $res */
$propertyValues = $res->all($property);
foreach ($propertyValues as $value) {
if ($value instanceof \EasyRdf_Literal) {
if(isset($args['lang']) && $value->getLang() && $value->getLang() != $args['lang']) {
continue;
}
$tabResult[$fieldName][] = $value->getValue();
} elseif ($value instanceof \EasyRdf_Resource) {
if (!$value->isBNode()) {
......
......@@ -14,6 +14,16 @@ use Datatourisme\Api\Resolver\SparqlResolver\SparqlResolver;
class VirtuosoResolver extends SparqlResolver
{
protected $defaultGraphUri = '';
/**
* @param $defaultGraphUri
*/
public function setDefaultGraphUri($defaultGraphUri)
{
$this->defaultGraphUri = $defaultGraphUri;
}
/**
* @param $sparql
* @param bool $count
......@@ -28,7 +38,7 @@ class VirtuosoResolver extends SparqlResolver
curl_setopt($ch, CURLOPT_URL, $this->endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, '&query='.urlencode($sparql));
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['default-graph-uri' => $this->defaultGraphUri, 'query' => $sparql]));
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept:text/plain'));
$result = curl_exec($ch);
......
......@@ -79,6 +79,7 @@ abstract class AbstractSchema extends \Youshido\GraphQL\Schema\AbstractSchema
'from' => new IntType(),
'sort' => new ListType(new $sortTypeClass()),
'filters' => new ListType(new $filterTypeClass()),
'lang' => new StringType()
],
'resolve' => function ($values, $args, ResolveInfo $info) use ($resolver, $entrypoint) {
return $resolver->resolve($entrypoint, $args, $info);
......
......@@ -35,7 +35,7 @@ class FieldCompiler extends AbstractCompiler
$type = 'new ListType(new Type_'.SchemaCompiler::normalize($def['type']).'())';
} else {
$uri = $this->prefixMap->expandUri($def['type']);
$type = 'new ListType(new \\'.$this->schemaCompiler->resolveScalarType($uri).'())';
$type = 'new ListType(new \\'.$this->schemaCompiler->resolveType($uri).'())';
}
$className = 'Field_'.SchemaCompiler::normalize($name);
......
......@@ -9,9 +9,10 @@
namespace Datatourisme\Api\Schema\Compiler;
use Datatourisme\Api\Schema\Field\Scalar\BooleanType;
use Datatourisme\Api\Schema\Field\Scalar\FloatType;
use Datatourisme\Api\Schema\Field\Scalar\IntType;
use Datatourisme\Api\Schema\Type\Object\LangStringType;
use Datatourisme\Api\Schema\Type\Scalar\BooleanType;
use Datatourisme\Api\Schema\Type\Scalar\FloatType;
use Datatourisme\Api\Schema\Type\Scalar\IntType;
use Symfony\Component\Yaml\Yaml;
use Youshido\GraphQL\Type\Scalar\DateTimeType;
use Youshido\GraphQL\Type\Scalar\StringType;
......@@ -20,9 +21,9 @@ class SchemaCompiler
{
private $twig;
private $scalarTypes = array();
private $typesMap = array();