lucidiot's cybrecluster

Validation d'attributs OpenSearch dans une XSD

Lucidiot Informatique 2021-12-19
Essayer de rendre une spécification plus solide en utilisant beaucoup trop d'expressions régulières.


Je me suis récemment mis à jouer avec SeaMonkey, un navigateur maintenu par la Mozilla Foundation et pas la Mozilla Corporation. La différence est importante en ce moment vu que Mozilla (l'entreprise) semble de plus en plus s'éloigner des objectifs de Mozilla (la fondation) et Firefox commence à perdre son orientation de protecteur de la vie privée.

SeaMonkey est un navigateur plutôt ancien qui inclut une suite d'outils qu'on retrouve aussi dans les versions de Mozilla (le navigateur), l'ancêtre de Firefox : un client IRC appelé ChatZilla, un client mail, un client NNTP (assez similaire à des forums), un carnet d'adresses, et un éditeur HTML.

Je joue avec SeaMonkey en partie pour des raisons sur lesquelles je reviendrai dans un autre article qui fera probablement peur à beaucoup de gens dans le domaine de l'informatique ; quand j'en parle un peu sur IRC, certains ont déjà des frissons. Ils n'aiment pas me voir m'amuser !

Comme SeaMonkey est toujours officiellement maintenu (même si aucune des deux Mozilla n'y accorde vraiment du temps de développement, donc rien n'avance), ses extensions sont toujours disponibles. Elles sont distribuées sur le site des extensions de Thunderbird, puisque Thunderbird aussi est maintenu par la fondation et pas la corporation. Un certain nombre de ces extensions incluent des moteurs de recherche.

Firefox, tous ses ancêtres et toutes ses dérivées ont la fâcheuse habitude de ne jamais permettre à l'utilisateur de configurer ses propres moteurs de recherche. Chromium autorise tout le monde à écrire des URLs où il faut placer un %s, qui indique où sera placée votre requête de recherche dans l'URL, mais Firefox n'oserait jamais laisser faire ça. À la place, il y a deux grandes façons d'ajouter des moteurs de recherche :

MozSearch est une spécification créée par Mozilla dont la documentation a récemment été supprimée de leurs sites, donc elle n'est plus que sur la Wayback Machine. Cette spécification n'a été conçue que pour permettre à Mozilla eux-mêmes d'ajouter des moteurs de recherche par défaut. Par exemple, le CNRTL est ajouté pour les utilisateurs français afin d'avoir un dictionnaire.

Bien que la spécification soit interne, il est permis d'utiliser des balises HTML pour indiquer que son site dispose d'un fichier XML décrivant un moteur de recherche. Le navigateur pourra ensuite indiquer à l'utilisateur qu'un moteur de recherche est disponible et lui proposer de l'ajouter. Cette détection existe encore dans la dernière version de Firefox, mais il faut faire un clic droit dans la barre d'adresse sur un site compatible. Vous pouvez par exemple essayer ça sur SoundCloud.

Cette spécification a été il y a fort longtemps repérée par une filiale d'Amazon appellée A9. A9 existe encore aujourd'hui, mais je ne crois pas qu'elle aie ne serait-ce qu'un site officiel. À l'époque, A9 visait à innover dans le monde des moteurs de recherche.

Pour cela, A9 a construit un « méta-moteur de recherche » : en visitant a9.com, on pouvait ajouter presque 700 moteurs de recherche et faire des recherches dans tous les moteurs en même temps, avec des pages qui affichaient les résultats de recherche de façon plutôt standard.

Standard est le mot important ici, puisque pour faire fonctionner tout ça, et pour permettre à n'importe qui d'ajouter son propre site à A9, une nouvelle spécification étendant assez fortement MozSearch a été créée : OpenSearch.

Introduction à OpenSearch

« We want OpenSearch to do for search what RSS has done for content. »

A9

Avec une citation pareille sur la page d'accueil de la spécification telle qu'elle était en 2005, rien de tel pour m'intéresser.

OpenSearch a vécu un long périple depuis ce temps. Initialement hébergée sur le site de A9, OpenSearch a obtenu son propre domaine avant que le site ne soit finalement tué en 2019 pour être repris par... OpenSearch, un système d'indexation et de recherche écrit en Java et sous licence Apache 2.0.

OpenSearch est aujourd'hui une spécification relativement morte, alors qu'elle est encore utilisée et toujours accessible dans les navigateurs récents. En fait, OpenSearch a vécu le même destin que RSS, donc je suppose qu'ils ont tenus leurs promesses : aujourd'hui, plus personne ne s'intéresse à l'interopérabilité ou au Web 3.01, et je me retrouve encore à jouer les archéologues numériques et à déterrer le standard pour le remettre au propre des années après.

Cette spécification est bien plus étendue que MozSearch et permet de décrire des cas plus variés avec notamment un support de l'autocomplétion ou le renvoi de résultats de recherche sous forme de flux RSS ou Atom. Ces flux peuvent contenir des attributs supplémentaires pour les résultats, par exemple pour indiquer un score de pertinence du résultat ou proposer d'autres requêtes de recherche alternatives qui contiennent des corrections orthographiques, des sujets liés, etc.

On va s'intéresser aujourd'hui à une extension optionelle de la spécification en particuler, OpenSearch Geo.

L'extension Geo permet d'optionnellement rechercher des lieux par géocodage, ou filter par une position ou une forme géométrique, et permet aux moteurs de recherche de répondre en incluant dans leurs résultats des données GeoRSS pour associer un résultat de recherche à une position ou une zone d'une carte.

XSD

Seule la spécification principale d'OpenSearch est au format Markdown, encore lisible sur GitHub. Le reste du contenu du dépôt utilise encore la syntaxe MediaWiki, puisque l'ancien site utilisait MediaWiki. Avec le format MediaWiki, il faut s'amuser à soit lire directement la syntaxe, soit envoyer ça sur un vrai wiki, soit le convertir avec un outil comme Pandoc. Et en plus d'être assez longues et dans un format moins courant, toutes ces spécifications ne sont pas très bien formatées et il est difficile pour un implémenteur de déterminer s'il suit vraiment chaque spécification ou non.

Bref, j'ai trouvé moult excuses pour rouvrir Visual Studio 2005 et écrire quelques fichiers XSD pour définir de façon plus formelle OpenSearch et toutes ses extensions et potentiellement permettre une validation automatique. Et entre autres, j'ai donc créé un fichier XSD pour OpenSearch Geo.

J'avais d'abord regardé si je ne pouvais pas trouver des XSD existants. Un développeur chez Microsoft avait publié en 2008 un article annonçant une XSD pour OpenSearch. 2008 était dans la période où Microsoft était vraiment à fond sur le XML, où ils avaient un site XML Developer Center ainsi qu'une équipe dédiée à RSS. L'article contient un lien vers un fichier sur Windows Live SkyDrive. La Wayback Machine a conservé la page décrivant le fichier mais on ne peut hélas pas le télécharger. J'ai donc dû tout écrire moi-même.

XSD est un format parmi d'autres pour décrire la structure d'un fichier XML et permettre sa validation. On connaît le plus souvent les DTD, un autre langage qui permet de décrire des structures XML, mais les DTD ne permettent quasiment aucune validation du contenu des éléments ou des attributs. XSD permet ce genre de validation, mais étant une spécification du W3C, XSD est aussi fichtrement complexe et a beaucoup de limitations. Il existe d'autres alternatives comme RELAX NG, mais j'ai pris l'habitude d'utiliser des XSD et j'ai des outils pour travailler avec, donc je fais avec.

Template de recherche

OpenSearch fonctionne avec un système de templates : dans un fichier XML spécifiant un moteur de recherche, on indique une ou plusieurs URL qui peuvent avoir des utilisations différentes (recherches, suggestions de recherches, exemples permettant à un développeur de tester le moteur de recherche, flux RSS, etc.). Prenons une description OpenSearch pour DuckDuckGo par exemple :

<?xml version="1.0" encoding="utf-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
  <ShortName>DuckDuckGo</ShortName>
  <Description>Privacy-first search engine</Description>
  <LongName>DuckDuckGo Search</LongName>
  <InputEncoding>UTF-8</InputEncoding>
  <Developer>Brainshit</Developer>
  <Contact>webmaster@brainshit.fr</Contact>
  <Url type="text/html" template="https://duckduckgo.com/?q={searchTerms}" ></Url>
  <Url type="application/x-suggestions+json" rel="suggestions" template="https://duckduckgo.com/ac/?q={searchTerms}&type=list" ></Url>
  <Url type="application/opensearchdescription+xml" rel="self" template="https://brainshit.fr/example.xml" ></Url>
  <Query role="example" searchTerms="duck" ></Query>
</OpenSearchDescription>

On a ici trois URL : une URL de recherche, une URL de suggestions et l'URL du fichier de description lui-même. Avoir l'URL du fichier de description permet à un navigateur de re-télécharger le fichier régulièrement pour faire des mises à jour automatiques.

Vous remarquerez la présence de {searchTerms} dans les URL de recherche et de suggestions. C'est là que les clients OpenSearch vont placer les termes de la recherche. Il existe d'autres paramètres comme le numéro de page pour récupérer plusieurs pages de résultats, le nombre maximum de résultats souhaités, la langue, etc.

OpenSearch définit une syntaxe relativement complexe pour définir des paramètres comme cela dans les URLs, pour par exemple permettre des paramètres optionnels. Il y a aussi une gestion des namespaces XML au sein des paramètres qui permet à des extensions à OpenSearch d'ajouter leurs propres paramètres.

On retrouve ces mêmes paramètres dans l'élément Query où on propose un exemple de recherche. Le seul paramètre ici est searchTerms, et on suggère de rechercher duck pour tester le moteur.

Dans les réponses sous forme de fichiers XML OpenSearch, RSS ou Atom, il est aussi possible aux moteurs de renvoyer d'autres éléments Query avec d'autres paramètres, par exemple pour indiquer la requête qu'ils viennent d'exécuter et permettre au client de vérifier qu'il n'y a pas eu d'erreur, ou pour proposer des recherches similaires ou des corrections orthographiques. Dans tous les cas, le fait que les attributs existent sur Query fait que je peux m'amuser à définir chacun de ces paramètres dans mon fichier XSD.

Étendre la spécification OpenSearch avec XSD

Je vais vous passer un bon nombre des détails de mes vrais XSD pour OpenSearch et simplifier la définition de l'élément Query à juste l'attribut searchTerms pour l'instant :

<xs:schema
  targetNamespace="http://a9.com/-/spec/opensearch/1.1/"
  xmlns="http://a9.com/-/spec/opensearch/1.1/" 
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
  <xs:element name="Query" minOccurs="0" maxOccurs="unbounded">
    <xs:annotation>
      <xs:documentation>
        Defines a search query that can be performed by search clients. 
      </xs:documentation>
    </xs:annotation>
    <xs:attribute name="searchTerms" type="xs:string" use="optional">
      <xs:annotation>
        <xs:documentation>
          Contains the value representing the searchTerms as an OpenSearch 1.1 parameter name.
        </xs:documentation>
      </xs:annotation>
    </xs:attribute>
  </xs:element>
</xs:schema>

On définit ici un élément Query qu'on peut se permettre d'utiliser de 0 à un nombre infini de fois. Il a une ligne de documentation, et un attribute optionnel searchTerms, une chaîne de caractères arbitraire avec là aussi une ligne de documentation. J'écris mes fichiers XSD pour réécrire la documentation d'anciennes spécifications donc il est logique que j'y documente des choses.

Si vous n'avez jamais vu une XSD, sachez que targetNamespace est l'espace de noms XML pour lequel on définit notre spécification. Puisque XML aime être verbeux, on doit réutiliser cet espace nom dans xmlns pour indiquer que tout nom qui n'est pas qualifié (c'est-à-dire tout nom qui n'a pas de préfixe d'espace de noms devant lui, comme Query) appartient à notre espace de noms. À l'inverse, on utilise xmlns:xs pour définir que les éléments de XML Schema Definition soient dans le préfixe xs:. Il faut donc ensuite utiliser des éléments aux noms qualifiés avec le préfixe xs: pour y référer.

OpenSearch utilise pour son moteur de templates le même principe de noms qualifiés. Si on voulait par exemple utiliser le paramètre name, qui permet d'indiquer un lieu comme Tour Eiffel dans la recherche (et que ce nom devienne ensuite une position utilisée pour faire une recherche près de la Tour Eiffel), on ajoutera l'espace de noms de l'extension Geo et on ajoutera son préfixe devant name :

<OpenSearchDescription
  xmlns="http://a9.com/-/spec/opensearch/1.1/"
  xmlns:geo="http://a9.com/-/opensearch/extensions/geo/1.0/"
>
  <!-- ... plein d'autres choses ... -->
  <Url type="text/html" template="https://brainshit.fr/?q={searchTerms}&name={geo:name?}" ></Url>
  <Query role="example" searchTerms="resto pas cher" geo:name="Tour Eiffel" ></Query>
</OpenSearchDescription>

On utilise geo:name dans le template exactement comme on s'en sert dans un attribut XML classique. Notez le ? dans le template qui indique un paramètre optionnel.

Pour pouvoir définir avec des XSD un attribut comme celui-là, il y a plusieurs stratégies. J'ai voulu faire le choix le plus strict mais pas le plus flexible en important l'extension Geo dans la spécification originale et en permettant d'ajouter ses attributs :

<xs:schema
  targetNamespace="http://a9.com/-/spec/opensearch/1.1/"
  xmlns="http://a9.com/-/spec/opensearch/1.1/" 
  xmlns:geo="http://a9.com/-/opensearch/extensions/geo/1.0/"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
  <xs:import
    namespace="http://a9.com/-/opensearch/extensions/geo/1.0/"
    schemaLocation="geo.xsd"
  ></xs:import>

  <xs:element name="Query" minOccurs="0" maxOccurs="unbounded">
    <!-- ... documentation et attributs précédents ... -->
    <xs:attributeGroup ref="geo:QueryAttributes" ></xs:attributeGroup>
  <xs:element>
</xs:schema>

On ajoute de nouveau le xmlns:geo pour définir un préfixe. On utilise ensuite xs:import qui permet d'indiquer un autre schéma XSD qu'il faudra utiliser pour comprendre l'espace de noms de l'extension Geo. Il existe aussi xs:include, mais celui-là permet de copier un schéma à l'intérieur d'un autre, dans un seul et même espace de noms ; ici, on veut garder les choses bien rangées à leur place. On pointe vers geo.xsd, qui serait le nom du fichier où j'ai écrit la spécification de l'extension Geo.

On utilise ensuite un attributeGroup. Il est possible en XSD de définir des groupes d'attributs qu'on peut réutiliser ailleurs. Dans mon fichier geo.xsd, je définis donc ce groupe d'attributs, qu'on va pouvoir remplir ensuite avec chaque attribut de l'extension Geo :

<xs:schema
  targetNamespace="http://a9.com/-/opensearch/extensions/geo/1.0/"
  xmlns="http://a9.com/-/opensearch/extensions/geo/1.0/"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
  <xs:attributeGroup name="QueryAttributes">
    <xs:annotation>
      <xs:documentation>
        Additional attributes defined by the OpenSearch Geo extension for the Query element.
      </xs:documentation>
    </xs:annotation>
    <!-- insérer des attributs ici -->
  </xs:attributeGroup>
</xs:schema>

Maintenant que tout le code boilerplate est en place, on va pouvoir commencer à s'approcher du véritable sujet de l'article.

Les paramètres de OpenSearch Geo

geo:name
Un nom à géocoder, c'est-à-dire à traduire en des coordonnées qui permettront une recherche relative à un point ou une zone géographique décrite par ce nom.
geo:lat
La latitude en WGS84 (en format de coordonnées GPS classique) d'une position exacte à utiliser pour la recherche.
geo:lon
La longitude en WGS84 (en format de coordonnées GPS classique) d'une position exacte à utiliser pour la recherche. Notons que la spécification n'interdit nulle part de n'indiquer que la latitude ou que la longitude sans donner les deux, ce qui n'a pas vraiment de sens.
geo:radius
Un rayon en mètres autour du point désigné par geo:name ou par la combinaison de geo:lat et geo:lon où la recherche devrait se restreindre. Par exemple, on peut chercher un restaurant à au plus 1 kilomètre de la tour Eiffel.
geo:box
Un rectangle défini par deux paires de coordonnées GPS qui permet de restreindre les résultats à une recherche spécifique. Par exemple, 8.42,2.31,1.12,2.03.
geo:relation
Comment filtrer les résultats de recherche : ne donner que ceux qui sont contenus dans la zone spécifiée (contains), ceux qui croisent la zone spécifiée même s'ils ne sont pas entièrement dedans (intersects) ou exclure ceux qui ne sont pas du tout dans la zone spécifiée (disjoint).
geo:uid
Un identifiant étrange et mal documenté qui semble être laissé là pour permettre de faire des requêtes avec des identifiants personnalisés définis par le moteur de recherche lui-même. Pas très intéressant.
geo:geometry
Une géométrie arbitraire : un point, une ligne, un polygone, plusieurs points, plusieurs lignes, ou plusieurs polygones. La géométrie est représentée à l'aide du format Well-Known Text.

Pour commencer, on va se dépêcher de définir tous les attributs assez rapidement, avec peu de validation. Pour raccourcir un peu, on va faire ce que beaucoup trop de développeurs font et ne pas écrire de documentation.

<xs:attribute name="name" type="xs:string" use="optional" ></xs:attribute>
<xs:attribute name="lat" type="xs:decimal" use="optional" ></xs:attribute>
<xs:attribute name="lon" type="xs:decimal" use="optional" ></xs:attribute>
<xs:attribute name="radius" type="xs:nonNegativeInteger" use="optional" ></xs:attribute>
<xs:attribute name="box" type="xs:string" use="optional" ></xs:attribute>
<xs:attribute name="relation" type="xs:string" use="optional" ></xs:attribute>
<xs:attribute name="uid" type="xs:string" use="optional" ></xs:attribute>
<xs:attribute name="geometry" type="xs:string" use="optional" ></xs:attribute>

On utilise trois types ici : xs:string pour les chaînes de caractères (donc en fait n'importe quelle valeur valide d'un attribut XML), xs:decimal pour des nombres à virgule, et xs:nonNegativeInteger pour un nombre entier non-négatif, c'est à dire supérieur ou égal à zéro. Un xs:positiveInteger n'autoriserait pas le zéro.

On va maintenant ajouter un peu plus de validation pour la latitude, la longitude, la relation et le rectangle englobant.

Validation des coordonnées

On va commencer par le plus rapide, la latitude et la longitude. Dans le format WGS84, qui décrit les coordonnées utilisées par le système GPS, la latitude varie entre -90 et 90 degrés, et la longitude entre -180 et 180 degrés. Pour pouvoir restreindre nos nombres décimaux à ces valeurs, il va falloir définir des types d'attribut personnalisés. On va en définir deux basés sur le type de nombre décimal et indiquer nos valeurs minimales et maximales.

<xs:simpleType name="Latitude">
  <xs:restriction base="xs:decimal">
    <xs:minInclusive value="-90" ></xs:minInclusive>
    <xs:maxInclusive value="90" ></xs:maxInclusive>
  </xs:restriction>
</xs:simpleType>

<xs:simpleType name="Longitude">
  <xs:restriction base="xs:decimal">
    <xs:minInclusive value="-180" ></xs:minInclusive>
    <xs:maxInclusive value="180" ></xs:maxInclusive>
  </xs:restriction>
</xs:simpleType>

Il est possible de se baser sur d'autres types en XSD de plusieurs façons, par exemple en faisant une union pour permettre plusieurs types à la fois, et nous utilisons ici une restriction pour... restreindre le type décimal à nos valeurs. On définit des minimums et maximums inclusifs, c'est-à-dire que -90 reste une latitude valide, mais pas -90.0000000001.

Validation de la relation

On va utiliser un autre type d'extension de type XSD pour restreindre relation aux trois chaînes de caractères possibles. On va de nouveau utiliser xs:restriction, mais cette fois-ci on va définir une énumération :

<xs:simpleType name="Relation">
  <xs:restriction base="xs:string">
    <xs:enumeration value="intersects" ></xs:enumeration>
    <xs:enumeration value="contains" ></xs:enumeration>
    <xs:enumeration value="disjoint" ></xs:enumeration>
  </xs:restriction>
</xs:simpleType>

Maintenant que nos trois types personnalisés sont définis, on peut les utiliser dans la définition des attributs :

<xs:attribute name="lat" type="Latitude" use="optional" ></xs:attribute>
<xs:attribute name="lon" type="Longitude" use="optional" ></xs:attribute>
<xs:attribute name="relation" type="Relation" use="optional" ></xs:attribute>

Avec ces restrictions supplémentaires, un éditeur XML tel que Oxygen pourrait alerter d'une valeur incorrecte. Un générateur de documentation pourrait aussi indiquer ces restrictions fermes, encodées dans un fichier lisible par un programme et donc vérifiables de façon plus formelle qu'une spécification classique. Ou un humain suffisamment fou pour lire un schéma XSD pourrait directement interpréter tout ça. Je ne ferais pas de commentaires sur ceux encore plus fous qui écrivent un article sur comment écrire ce genre de schémas, ou ceux qui lisent ces articles.

Validation du rectangle englobant

Le rectangle englobant est un peu plus compliqué à définir en XSD. Il est représenté par quatre nombres décimaux, dans l'ordre suivant : latitude sud, longitude ouest, latitude nord, longitude est. Les quatre nombres sont reliés entre eux par des virgules.

Il n'existe pas vraiment de moyen de dire à XSD quelque chose comme « cherche une Latitude, puis une virgule, puis une Longitude, etc. » Le seul système capable de gérer ça serait d'appliquer une restriction sur la chaîne de caractères et d'y appliquer une expression régulière avec <xs:pattern value="..." ></xs:pattern>.

On peut d'abord représenter un nombre décimal seul comme ceci : [+-]?\d+(?:\.\d+)?.

On propose d'abord un signe optionnel, car s'il n'y a pas de signe on sait que c'est positif, et un + devant un nombre n'est quand même pas interdit, avec [+-]?. Autrement dit, zéro ou une seule fois un caractère parmi + et -.

Ensuite, on requiert au moins un chiffre avec \d+. On peut en avoir plus d'un.

Pour gérer les chiffres après la virgule, on utilise (?:\.\d+)?. Le \. signifie une virgule, puisque les virgules sont représentées par des points en informatique. On préfixe d'une controblique parce que . seul signifie « n'importe quel caractère » dans une expression régulière. On retrouve notre « au moins un chiffre » avec \d+.

On encapsule toute cette expression dans un groupe non-capturant qu'on accepte zéro ou une fois, c'est-à-dire (?: ... )?. Un groupe classique () peut faire apparaître dans les résultats d'une recherche par expression régulière une sous-partie spécifique de l'expression, ce qui peut s'avérer utile quand on veut par exemple découper un morceau de texte en plusieurs parties. On appelle ça une capture. Quand on utilise un groupe mais pas pour sa fonctionnalité de capture, on peut le démarrer par ?: ; ? signifie juste qu'on veut paramétrer le groupe, et : dit de rendre le groupe non-capturant. C'est une bonne pratique de rendre tous les groupes qu'on ne voulait pas capturer non-capturants, pour réduire la confusion quand on exploite l'expression ailleurs.

En utilisant un groupe, on autorisera 1.0 ou 1 mais pas 1.. Cette dernière syntaxe existe, et je m'en suis servi dans des calculs pour exporter une carte OpenStreetMap en SQL, mais elle est suffisamment peu courante pour qu'on ne s'en préoccupe pas ici.

Pour valider un rectangle englobant entier, la façon la plus simple est de copier-coller et d'ajouter des virgules ! On obtiendrait donc [+-]?\d+(?:\.\d+)?,[+-]?\d+(?:\.\d+)?,[+-]?\d+(?:\.\d+)?,[+-]?\d+(?:\.\d+)?.

Mais il manque quelque chose qui est quand même assez dommage. On a ajouté précédemment des restrictions pour limiter la latitude et la longitude à leurs valeurs acceptables ; on pourrait bien faire ça ici aussi ?

Validation numérique avec une expression régulière

J'ai déjà fait ce genre de validations dans une autre longue expression régulière que j'avais construit pour valider mes propres données de scans de réseaux Wi-Fi. On va découper un peu nos deux restrictions :

Ça semble être une condition parfaite pour utiliser encore une autre fonctionnalité des groupes qui n'est pas la capture : les alternatives. Si on utilise | dans un groupe, on sépare deux possibilités : par exemple, (lo+l|mdr) acceptera lol, looooooooool, et mdr. On va donc avoir une validation de nombre qui ressemble à [+-]?(?: ... | ... ). On peut garder le signe hors de nos alternatives, car peu importe le signe, la validation des chiffres derrière sera la même.

On peut commencer par valider les minimums et maximums. Pour 90 virgule zéro, ce sera 90(?:\.0+)?. On autorise ainsi 90, mais aussi 90.0000000. Pour 180, 180(?:\.0+)?.

Pour les autres nombres, on peut encore découper : d'abord, on a entre 0.0000 et 9.9999. Pour ça, on peut juste reprendre notre expression originale mais n'autoriser qu'exactement un seul chiffre avant la virgule : \d(?:\.\d+)?.

Ensuite, pour valider la latitude à deux chiffres, on ira de 00.0000 à 89.9999. On autorise à commencer par un zéro non significatif. Le chiffre des dizaines ne variera donc que entre zéro et huit, donc on peut l'exprimer avec [0-8]. Pour combiner avec la latitude à un seul chiffre, on va juste rendre le chiffre des dizaines optionnel : [0-8]?\d(?:\.\d+).

Pour la longitude, même principe. On reprend la même validation pour un seul chiffre avant la virgule. Pour un second chiffre cette fois, on pourrait avoir 00.0000 à 99.9999, donc on va continuer à utiliser \d une deuxième fois. Mais pour les nombres à trois chiffres, on a entre 000.0000 et 179.9999. On va encore devoir découper : on a 000.0000 à 099.9999 puis 100.0000 à 179.9999. On a donc 0\d\d(?:\.\d+)?, et 1[0-7]\d(?:\.\d+)?.

Pour combiner donc les quatre possibilités de longitude (un chiffre, deux chiffres, trois en dessous de 100, trois au dessus de 100), on va encore utiliser une alternative. On peut déjà combiner les trois premières possibilités avec 0?\d?\d(?:\.\d+)?. Pour la quatrième, on met notre alternative seulement sur les chiffres des centaines et des dizaines : (?:1[0-7]|0?\d)?\d(?:\.\d+)?. On garde le chiffre des unités à part, tout seul, la seule partie vraiment obligatoire de l'expression puisqu'il nous faut bien au moins un chiffre.

On va maintenant assembler nos deux alternatives pour la latitude et la longitude. Les expressions régulières finales pour ces deux types sont donc :


On peut maintenant combiner toutes ces expressions à nouveau avec des virgules : une latitude, puis une longitude, puis une latitude, puis une longitude, et nous pouvons placer tout ça dans un xs:pattern.

<xs:simpleType name="Box">
  <xs:restriction base="xs:string">
    <xs:pattern value="[+-]?(?:90(?:\.0+)?|[0-8]?\d(?:\.\d+)),[+-]?(?:180(?:\.0+)?|(?:1[0-7]|0?\d)?\d(?:\.\d+)?),[+-]?(?:90(?:\.0+)?|[0-8]?\d(?:\.\d+)),[+-]?(?:180(?:\.0+)?|(?:1[0-7]|0?\d)?\d(?:\.\d+)?)" ></xs:pattern>
  </xs:restriction>
</xs:simpleType>

Et notre dernier morceau de XSD pour aujourd'hui ! On peut utiliser ce « simple » type pour notre attribut original :

<xs:attribute name="box" type="Box" use="optional" ></xs:attribute>

Un problème des expressions régulières en XSD

Comme je l'ai dit plus haut, XSD souffre de moult limitations ; je me suis heurté à certaines dont je n'ai pas du tout parlé en écrivant les spécifications d'OpenSearch ou celles d'autres formats. XSD n'est pas capable de se définir lui-même, et n'est pas capable de définir XHTML, RSS ou OpenSearch lui-même correctement. Sa version 1.1, sortie près de 10 ans après la version 1.0, contient quelques améliorations mais n'est que très rarement prise en charge.

Une des limitations qu'il est intéressant de mentionner ici est que j'aurai pu, avec les expressions régulières d'autres languages comme PHP, Perl, Ruby, etc., utiliser des sous-routines. Les sous-routines permettent de définir des morceaux d'expression regulière qu'on veut réutiliser plusieurs fois, et de leur donner un nom. Du coup, j'aurais pu avoir une sous-routine de latitude et une sous-routine de longitude.

La syntaxe varie selon les languages, donc je vais utiliser celle de PCRE, une librairie assez courante qu'on retrouve notamment dans PHP et qu'on peut installer et utiliser dans beaucoup d'autres langages :

(?P<latitude>[+-]?(?:90(?:\.0+)?|[0-8]?\d(?:\.\d+))),(?P<longitude>[+-]?(?:180(?:\.0+)?|(?:1[0-7]|0?\d)?\d(?:\.\d+)?)),(?P>latitude),(?P>longitude)

Cela peut éviter le copier-coller et permet de donner des noms. Je n'ai même pas accès en XSD aux fonctionnalités de commentaires avec (?#mon commentaire), alors qu'ils ne coûtent pas grand chose à implémenter dans la plupart des langages si vous en êtes déjà à implémenter un moteur d'expressions régulières.

Conclusion

Je m'étais intéressé au sujet d'OpenSearch initialement seulement parce que je voulais modifier deux extensions officielles pour SeaMonkey de DuckDuckGo, DuckDuckGo Lite et DuckDuckGo HTML. Ces extensions ne sont plus maintenues et ces deux moteurs de recherche spéciaux ont des URLs légèrement différentes, ce qui fait que les recherches ne fonctionnent plus avec ces extensions. La modification fait cinq caractères sur chaque fichier. Donc pour dix caractères au total, j'écris des tonnes de XML et de XSD puis plusieurs milliers de mots. C'est beau.

J'ai mis en ligne une liste de fichiers OpenSearch que vous pouvez utiliser si vous souhaitez ajouter ces deux moteurs mis à jour, ou alors un moteur de recherche FTP, Gopher, un service météo, un dictionnaire, ou la Wayback Machine.

Je travaille en ce moment (entre tous mes autres projets) à un projet concernant le défunt méta-moteur de A9, donc vous pouvez vous attendre plus tard à revoir un article qui le concerne.

Mais avant ça, vous remarquerez peut-être que bien que j'aie fait une belle validation pour la plupart des paramètres, je n'ai rien fait pour geometry ! Ça, on y reviendra une prochaine fois, car il va me falloir aborder encore d'autres notions. Nous nous y prendrons de plein fouet la limitation mentionnée ci-dessus, ainsi que d'autres.


  1. Je parle ici du Web sémantique. Le Web 3.0 est celui des machines parlant aux machines, capables d'explorer les pages Web d'elles-mêmes pour trouver des informations. C'est dans ce contexte qu'existe Wikidata, ainsi qu'OpenGraph, une autre spécification qui se cache derrière les aperçus magiques avec une miniature et un résumé que vous voyez lorsque vous publiez un lien sur des réseaux sociaux. Il faut faire une distinction claire avec le Web 3.0 de 2021, parfois abrégé Web3, un buzzword utilisé par des idiots qui croient en la blockchain et veulent que le Web tout entier soit basé sur la blockchain. Il existe même une entreprise appelée RSS3 qui réinvente RSS en JSON avec de l'Ethereum au milieu, alors que RSS 3.0 existe ainsi que JSON Feed. J'ai hâte que tout ce petit monde s'effondre. ↩︎


Commentaires

Il n'y a pour l'instant aucun commentaire. Soyez le premier !