Sfoglia il codice sorgente

rename service Url into UrlUtils, refactor HydraDenormalizer

Olivier Massot 2 anni fa
parent
commit
099297b178

+ 4 - 4
components/Layout/AlertBar/Cotisation.vue

@@ -48,7 +48,7 @@ Barre d'alerte qui s'affiche pour donner l'état de la cotisation
 <script setup lang="ts">
 import {useOrganizationProfileStore} from "~/stores/organizationProfile";
 import {Ref} from "vue";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import {ALERT_STATE_COTISATION} from "~/types/enum/enums";
 import {useAsyncData} from "#app";
 import {useEntityFetch} from "~/composables/data/useEntityFetch";
@@ -113,19 +113,19 @@ const goOn = (type: ALERT_STATE_COTISATION) => {
       if (!organizationProfile.id) {
         throw new Error('missing organization id')
       }
-      window.location.href = Url.join(baseLegacyUrl, '/cotisation/cotisation_steps', organizationProfile.id, 'steps/1')
+      window.location.href = UrlUtils.join(baseLegacyUrl, '/cotisation/cotisation_steps', organizationProfile.id, 'steps/1')
       break;
     case ALERT_STATE_COTISATION.INVOICE :
       if (!cotisationYear.value) {
         throw new Error('no cotisation year defined')
       }
       window.open(
-          Url.join(baseLegacyUrl, 'cotisation/invoice', cotisationYear.value),
+          UrlUtils.join(baseLegacyUrl, 'cotisation/invoice', cotisationYear.value),
           '_blank'
       )
       break;
     case ALERT_STATE_COTISATION.INSURANCE :
-      window.location.href = Url.join(baseLegacyUrl, 'cotisation/insuranceedit')
+      window.location.href = UrlUtils.join(baseLegacyUrl, 'cotisation/insuranceedit')
       break;
     case ALERT_STATE_COTISATION.ADVERTISINGINSURANCE :
       window.open(

+ 2 - 2
components/Layout/AlertBar/SuperAdmin.vue

@@ -23,7 +23,7 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur est un super admin en mode sw
 
 <script setup lang="ts">
   import {useAccessProfileStore} from "~/stores/accessProfile";
-  import Url from "~/services/utils/url";
+  import UrlUtils from "~/services/utils/urlUtils";
   import {ComputedRef} from "@vue/reactivity";
 
   const runtimeConfig = useRuntimeConfig()
@@ -41,7 +41,7 @@ Barre d'alerte qui s'affiche lorsque l'utilisateur est un super admin en mode sw
     const originalAccessId = accessProfile.originalAccess ? accessProfile.originalAccess.id : null
 
     if (show && orgId && originalAccessId) {
-      return Url.join(baseLegacyUrl, 'switch_user', orgId, originalAccessId, 'exit')
+      return UrlUtils.join(baseLegacyUrl, 'switch_user', orgId, originalAccessId, 'exit')
     }
     return ''
   })

+ 5 - 5
components/Layout/Header/Notification.vue

@@ -101,7 +101,7 @@ import {ComputedRef, Ref, ref} from "@vue/reactivity";
 import {useEntityFetch} from "~/composables/data/useEntityFetch";
 import {Pagination} from "~/types/data";
 import {useEntityManager} from "~/composables/data/useEntityManager";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import ArrayUtils from "~/services/utils/arrayUtils";
 
 const accessProfileStore = useAccessProfileStore()
@@ -241,14 +241,14 @@ const download = (link: string) => {
   const url_parts: Array<string> = link.split('/api');
 
   if(accessProfileStore.originalAccess)
-    url_parts[0] = Url.join('api', String(accessProfileStore.originalAccess.id), String(accessProfileStore.id))
+    url_parts[0] = UrlUtils.join('api', String(accessProfileStore.originalAccess.id), String(accessProfileStore.id))
   else
-    url_parts[0] = Url.join('api', String(accessProfileStore.id))
+    url_parts[0] = UrlUtils.join('api', String(accessProfileStore.id))
 
-  window.open(Url.join(runtimeConfig.baseUrlLegacy, url_parts.join('')));
+  window.open(UrlUtils.join(runtimeConfig.baseUrlLegacy, url_parts.join('')));
 }
 
-const notificationUrl = Url.join(runtimeConfig.baseUrlAdminLegacy, 'notifications/list/')
+const notificationUrl = UrlUtils.join(runtimeConfig.baseUrlAdminLegacy, 'notifications/list/')
 
 </script>
 

+ 2 - 2
components/Layout/SubHeader/Breadcrumbs.vue

@@ -9,7 +9,7 @@ import {useRouter, useRuntimeConfig} from "#app";
 import {computed, ComputedRef} from "@vue/reactivity";
 import {AnyJson} from "~/types/data";
 import {useI18n} from "vue-i18n";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 
 const runtimeConfig = useRuntimeConfig()
 const i18n = useI18n()
@@ -23,7 +23,7 @@ const items: ComputedRef<Array<AnyJson>> = computed(() => {
     href: runtimeConfig.baseUrlAdminLegacy
   })
 
-  const pathPart: Array<string> = Url.split(router.currentRoute.value.path)
+  const pathPart: Array<string> = UrlUtils.split(router.currentRoute.value.path)
 
   let path: string = ''
 

+ 2 - 2
components/Ui/Input/AutocompleteWithAPI.vue

@@ -29,7 +29,7 @@ d'une api)
 <script setup lang="ts">
 
 import {Ref, ref, toRefs} from "@vue/reactivity";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import {FetchOptions} from "ohmyfetch";
 import {useFetch} from "#app";
 import {watch} from "@vue/runtime-core";
@@ -112,7 +112,7 @@ if (props.data) {
   const ids:Array<any> = []
 
   for(const uri of props.remoteUri){
-    ids.push(Url.extractIdFromUri(uri as string))
+    ids.push(UrlUtils.extractIdFromUri(uri as string))
   }
 
   const options: FetchOptions = { method: 'GET', query: {key: 'id', value: ids.join(',')} }

+ 2 - 2
components/Ui/Input/Image.vue

@@ -64,7 +64,7 @@ import {ref, Ref} from "@vue/reactivity";
 import {AnyJson} from "~/types/enum/data";
 import {File} from '~/models/Core/File'
 import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import {useImageFetch} from "~/composables/data/useImageFetch";
 import {onUnmounted, watch, WatchStopHandle} from "@vue/runtime-core";
 import {useEntityManager} from "~/composables/data/useEntityManager";
@@ -111,7 +111,7 @@ const defaultSize = ({ imageSize, visibleArea }: any) => {
 // Si l'id est renseigné, on récupère l'Item File afin d'avoir les informations de config, le nom, etc.
 if (props.imageId && props.imageId > 0) {
   const { apiRequestService } = useAp2iRequestService()
-  const result: any = await apiRequestService.get(Url.join('api/files', '' + props.imageId))
+  const result: any = await apiRequestService.get(UrlUtils.join('api/files', '' + props.imageId))
 
   const config = JSON.parse(result.data.config)
   coordinates.value.left = config.x

+ 2 - 2
components/Ui/ItemFromUri.vue

@@ -18,7 +18,7 @@ Espace permettant de récupérer un item via une uri et de gérer son affichage
 // TODO: renommer en EntityFromUri? voir si ce component est encore nécessaire, ou si ça ne peut pas être une méthode de l'entity manager
 
 import {Query} from "pinia-orm";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import {useEntityFetch} from "~/composables/data/useEntityFetch";
 import {object} from "@ucast/core";
 import {computed, ComputedRef} from "@vue/reactivity";
@@ -46,7 +46,7 @@ const props = defineProps({
   }
 })
 
-const id = Url.extractIdFromUri(props.uri)
+const id = UrlUtils.extractIdFromUri(props.uri)
 if (id === null) {
   throw new Error('Uri parsing error : no id found')
 }

+ 2 - 2
composables/form/useValidation.ts

@@ -1,6 +1,6 @@
 import  {useI18n} from 'vue-i18n'
 import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import {Ref} from "@vue/reactivity";
 
 /**
@@ -19,7 +19,7 @@ export function useValidation() {
     const validateSiret = async (siret: string) => {
 
       const { apiRequestService } = useAp2iRequestService()
-      const response: any = await apiRequestService.get(Url.join('/api/siret-checking', siret))
+      const response: any = await apiRequestService.get(UrlUtils.join('/api/siret-checking', siret))
 
       if (typeof response === 'undefined') {
         siretError.value = false

+ 0 - 0
composables/layout/useRedirectToLogin.ts


+ 3 - 3
pages/organization/index.vue

@@ -454,7 +454,7 @@ import {useExtensionPanel} from "~/composables/layout/useExtensionPanel";
 import {useRoute} from "#app";
 import { useValidation } from "~/composables/form/useValidation";
 import {useEntityManager} from "~/composables/data/useEntityManager";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import {Organization} from "~/models/Organization/Organization";
 import {useI18nUtils} from "~/composables/utils/useI18nUtils";
 import {ContactPoint} from "~/models/Core/ContactPoint";
@@ -502,13 +502,13 @@ const formatPhoneNumber = (number: string): string => {
 const getIdsFromUris = (uris: Array<string>) => {
   const ids:Array<any> = []
   for(const uri of uris){
-    ids.push(Url.extractIdFromUri(uri))
+    ids.push(UrlUtils.extractIdFromUri(uri))
   }
   return ids
 }
 
 // TODO: voir si l'extraction de cette id ne pourrait pas être faite en amont, au niveau des post-processors
-const getIdFromUri = (uri: string) => Url.extractIdFromUri(uri)
+const getIdFromUri = (uri: string) => UrlUtils.extractIdFromUri(uri)
 
 const models = () => {
   return {

+ 8 - 8
services/data/entityManager.ts

@@ -1,6 +1,6 @@
 import ApiRequestService from "./apiRequestService";
 import {Repository, useRepo} from "pinia-orm";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import ModelNormalizer from "./serializer/normalizer/modelNormalizer";
 import HydraDenormalizer from "./serializer/denormalizer/hydraDenormalizer";
 import ApiModel from "~/models/ApiModel";
@@ -133,7 +133,7 @@ class EntityManager {
         }
 
         // Else, get the object from the API
-        const url = Url.join('api', model.entity, String(id))
+        const url = UrlUtils.join('api', model.entity, String(id))
 
         const response = await this.apiRequestService.get(url)
 
@@ -155,9 +155,9 @@ class EntityManager {
     public async fetchCollection(model: typeof ApiResource, parent: ApiResource | null, query: AssociativeArray = []): Promise<Collection> {
         let url
         if (parent !== null) {
-            url = Url.join('api', parent.entity, '' + parent.id, model.entity)
+            url = UrlUtils.join('api', parent.entity, '' + parent.id, model.entity)
         } else {
-            url = Url.join('api', model.entity)
+            url = UrlUtils.join('api', model.entity)
         }
 
         const response = await this.apiRequestService.get(url, query)
@@ -206,13 +206,13 @@ class EntityManager {
         // Recast in case class definition has been "lost"
         entity = this.cast(model, entity)
 
-        let url = Url.join('api', model.entity)
+        let url = UrlUtils.join('api', model.entity)
         let response
 
         const data = ModelNormalizer.normalize(entity)
 
         if (!entity.isNew()) {
-            url = Url.join(url, String(entity.id))
+            url = UrlUtils.join(url, String(entity.id))
             response = await this.apiRequestService.put(url, data)
         } else {
             delete data.id
@@ -236,7 +236,7 @@ class EntityManager {
      * @param data
      */
     public async patch(model: typeof ApiModel, id: number, data: AssociativeArray) {
-        let url = Url.join('api', model.entity, ''+id)
+        let url = UrlUtils.join('api', model.entity, ''+id)
 
         const body = JSON.stringify(data)
         const response = await this.apiRequestService.put(url, body)
@@ -255,7 +255,7 @@ class EntityManager {
 
         // If object has been persisted to the datasource, send a delete request
         if (!entity.isNew()) {
-            const url = Url.join('api', model.entity, String(entity.id))
+            const url = UrlUtils.join('api', model.entity, String(entity.id))
             await this.apiRequestService.delete(url)
         }
 

+ 2 - 2
services/data/enumManager.ts

@@ -1,5 +1,5 @@
 import ApiRequestService from "./apiRequestService";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import HydraDenormalizer from "~/services/data/serializer/denormalizer/hydraDenormalizer";
 import {Enum} from "~/types/data.d";
 import {VueI18n} from "vue-i18n";
@@ -14,7 +14,7 @@ class EnumManager {
     }
 
     public async fetch(enumName: string): Promise<Enum> {
-        const url = Url.join('api', 'enum', enumName)
+        const url = UrlUtils.join('api', 'enum', enumName)
 
         const response = await this.apiRequestService.get(url)
 

+ 39 - 112
services/data/serializer/denormalizer/hydraDenormalizer.ts

@@ -1,144 +1,71 @@
-import {AnyJson, ApiResponse, HydraMetadata} from "~/types/data";
-import Url from "~/services/utils/url";
-import {METADATA_TYPE} from "~/types/enum/data";
+import {AnyJson, ApiResponse, HydraMetadata} from '~/types/data'
+import UrlUtils from '~/services/utils/urlUtils'
+import {METADATA_TYPE} from '~/types/enum/data'
 
 /**
- * Classe permettant d'assurer la dé-normalization d'un objet Hydra en JSON
+ * Classe permettant d'assurer la dénormalization d'un objet Hydra en JSON
  */
 class HydraDenormalizer {
   /**
    * Parse une réponse Hydra pour retourner son équivalent en Json
    *
-   * @param {AnyJson} data
+   * @param {AnyJson} hydraData
    * @return {AnyJson} réponse parsée
    */
-  public static denormalize (data: AnyJson): ApiResponse {
-    if (data['hydra:member']) {
-      return HydraDenormalizer.parseCollection(data)
-    }
-
-    return HydraDenormalizer.parseItem(data)
-  }
-
-  /**
-   * Parse une réponse Hydra contenant un item
-   *
-   * @param hydraData
-   * @private
-   */
-  private static parseItem (hydraData: AnyJson): ApiResponse {
+  public static denormalize(hydraData: AnyJson): ApiResponse {
     return {
-      data: hydraData,
-      metadata: HydraDenormalizer.constructMetadataForItem(hydraData)
+      data: HydraDenormalizer.getData(hydraData),
+      metadata: HydraDenormalizer.getMetadata(hydraData)
     }
   }
 
-  /**
-   * Méthode de parsing appelé si on est dans un GET Collection
-   *
-   * @param {AnyJson} hydraData
-   */
-  private static parseCollection (hydraData: AnyJson): ApiResponse {
-    const collectionResponse:ApiResponse = {
-      data:hydraData['hydra:member'],
-      metadata : HydraDenormalizer.constructMetadataForCollection(hydraData)
-    }
-
-    // collectionResponse.order = {}
-    // collectionResponse.search = {}
-
-    // Populate href property for all elements of the collection
-    for (const key in collectionResponse.data) {
-      const value = collectionResponse.data[key]
-      HydraDenormalizer.populateAllData(value)
-    }
-
-    // if (typeof (hydraData['hydra:search']) !== 'undefined') {
-    //   const collectionSearch = hydraData['hydra:search']['hydra:mapping']
-    //   for (const key in collectionSearch) {
-    //     const value = collectionSearch[key]
-    //     if (value.variable.indexOf('filter[order]') === 0) {
-    //       collectionResponse.order[value.property] = value
-    //     } else if (value.variable.indexOf('filter[where]') === 0) {
-    //       collectionResponse.search[value.property] = value
-    //     }
-    //   }
-    // }
-
-    return collectionResponse
-  }
-
-  /**
-   * Génère les metadonnées d'un item
-   *
-   * @param {AnyJson} hydraData
-   */
-  private static constructMetadataForItem (hydraData: AnyJson): AnyJson {
-    const metadata: HydraMetadata = {}
-
-    // if (hydraData['hydra:previous']) {
-    //   const iriParts = hydraData['hydra:previous'].split('/')
-    //   hydraData.previous = iriParts[iriParts.length - 1]
-    // }
-    // if (hydraData['hydra:next']) {
-    //   const iriParts = hydraData['hydra:next'].split('/')
-    //   hydraData.next = iriParts[iriParts.length - 1]
-    // }
-    // if (hydraData['hydra:totalItems']) {
-    //   hydraData.totalItems = hydraData['hydra:totalItems']
-    // }
-    // if (hydraData['hydra:itemPosition']) {
-    //   hydraData.itemPosition = hydraData['hydra:itemPosition']
-    // }
-
-    metadata.type = METADATA_TYPE.ITEM
-
-    return metadata
-
+  protected static getData(hydraData: AnyJson): AnyJson {
+    return hydraData['@type'] === 'hydra:Collection' ? hydraData['hydra:member'] : hydraData
   }
 
   /**
-   * Génère les métadonnées d'une collection
+   * Génère les métadonnées d'un item ou d'une collection
    *
    * @param data
-   * @private
+   * @protected
    */
-  private static  constructMetadataForCollection(data:AnyJson){
+  protected static getMetadata(data: AnyJson): HydraMetadata {
+    if (data['@type'] !== 'hydra:Collection') {
+      // A single item, no metadata
+      return { type: METADATA_TYPE.ITEM }
+    }
+
     const metadata: HydraMetadata = {
       totalItems: data['hydra:totalItems']
     }
 
-    if(data['hydra:view']){
-      metadata.firstPage = HydraDenormalizer.getPageFromUri(data['hydra:view']['hydra:first'], 1) as number
-      metadata.lastPage = HydraDenormalizer.getPageFromUri(data['hydra:view']['hydra:last'], 1) as number
-      metadata.nextPage = HydraDenormalizer.getPageFromUri(data['hydra:view']['hydra:next']) ?? undefined
-      metadata.previousPage = HydraDenormalizer.getPageFromUri(data['hydra:view']['hydra:previous']) ?? undefined
+    if (data['hydra:view']) {
+      /**
+       * Extract the page number from the IRIs in the hydra:view section
+       */
+      const extractPageNumber = (pos: string, default_: number | undefined=undefined): number | undefined => {
+        const iri = data['hydra:view']['hydra:' + pos]
+        if (!iri) {
+          return default_
+        }
+        return UrlUtils.getParameter(
+            data['hydra:view']['hydra:' + pos],
+            'page',
+            default_
+        ) as number | undefined
+      }
+
+      // TODO: utile d'ajouter la page en cours?
+      metadata.firstPage = extractPageNumber('first', 1)
+      metadata.lastPage = extractPageNumber('last', 1)
+      metadata.nextPage = extractPageNumber('next')
+      metadata.previousPage = extractPageNumber('previous')
     }
 
     metadata.type = METADATA_TYPE.COLLECTION
 
     return metadata
   }
-
-  /**
-   * Hydrate l'objet JSON de façon récursive (afin de gérer les objet nested)
-   *
-   * @param {AnyJson} data
-   */
-  private static populateAllData (data: AnyJson): void {
-    for (const key in data) {
-      const value = data[key]
-      if (value instanceof Object) {
-        HydraDenormalizer.populateAllData(value)
-      }
-    }
-  }
-
-  private static getPageFromUri(uri: string, default_: number | null = null): number | null {
-    const url = 'https://foo' + uri // Pour que l'uri partielle soit parsée, on doit y ajouter une url de base bidon
-    const page = Url.getParameter(url, 'page')
-    return page ? parseInt(page) : default_
-  }
 }
 
 export default HydraDenormalizer

+ 2 - 2
services/menuBuilder/abstractMenuBuilder.ts

@@ -1,7 +1,7 @@
 import {IconItem, MenuBuilder, MenuGroup, MenuItem, MenuItems} from '~/types/layout'
 import {MENU_LINK_TYPE} from "~/types/enum/layout";
 import {RuntimeConfig} from "@nuxt/schema";
-import Url from "~/services/utils/url";
+import UrlUtils from "~/services/utils/urlUtils";
 import {AnyAbility} from "@casl/ability";
 import {AccessProfile, organizationState} from "~/types/interfaces";
 
@@ -87,7 +87,7 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
     switch(type) {
       case MENU_LINK_TYPE.V1:
         const v1BaseURL = this.runtimeConfig.baseUrlAdminLegacy ?? this.runtimeConfig.public.baseUrlAdminLegacy
-        url = Url.join(v1BaseURL, to ?? '')
+        url = UrlUtils.join(v1BaseURL, to ?? '')
         break;
       default:
         url = to

+ 0 - 94
services/utils/url.ts

@@ -1,94 +0,0 @@
-/**
- * Classe permettant de construire une URL pour l'interrogation d'une API externe
- */
-export default class Url {
-  /**
-   * Concatenate a base url and a tail
-   * @param base
-   * @param tails
-   * @private
-   */
-  public static join (base: string|number, ...tails: Array<string|number>): string {
-    let url = String(base)
-    tails.forEach((tail: string|number) => {
-      url = url.replace(/^|\/$/g, '') + '/' + String(tail).replace(/^\/?|$/g, '')
-    })
-    return url
-  }
-
-  /**
-   * Prepend the 'https://' part if neither 'http://' of 'https://' is present, else: does nothing
-   *
-   * @param url
-   */
-  public static prependHttps (url: string): string {
-    if (!url.match(/^https?:\/\/.*/)) {
-      url = 'https://' + url;
-    }
-    return url;
-  }
-
-  /**
-   * Parse an URI to retrieve the parameter
-   *
-   * @param uri
-   * @param parameter
-   * @param default_
-   * @private
-   */
-  public static getParameter(
-      uri: string,
-      parameter: string,
-      default_: string | null = null
-  ): string | null {
-
-    const urlParams = new URL(uri).searchParams;
-    const res = urlParams.get(parameter);
-
-    return res ?? default_
-  }
-
-  /**
-   * Extrait l'ID de l'URI passée en paramètre
-   * L'uri est supposée être de la forme `.../foo/bar/{id}`, où l'id est un identifiant numérique
-   *
-   * @param uri
-   */
-  public static extractIdFromUri (uri: string): number|null {
-    const partUri: Array<string> = uri.split('/')
-    const id:any = partUri.pop()
-
-    if(isNaN(id))
-      throw new Error('id is not a number')
-
-    return parseInt(id)
-  }
-
-  /**
-   * Extrait l'Uuid de l'URI passée en paramètre
-   * L'uri est supposée être de la forme `.../foo/bar/{uuid}`
-   *
-   * @param uri
-   */
-  public static extractUuidFromUri (uri: string): string|null {
-    const partUri: Array<string> = uri.split('/')
-    return String(partUri.pop())
-  }
-
-  /**
-   * Découpe une URI au niveau des '/'
-   * Utilisé entre autres pour le breadcrumb
-   *
-   * Ex:
-   *
-   *    foo/bar/1   =>  ['foo', 'bar', '1']
-   *    /foo/bar/1   =>  ['foo', 'bar', '1']
-   *    https://domain.com/foo/bar/1   =>  ['https:', 'domain.com', 'foo', 'bar', '1']
-   *
-   *
-   * @param uri
-   */
-  public static split(uri: string) {
-    return uri.split('/').filter((s) => s.length > 0)
-  }
-}

+ 91 - 0
services/utils/urlUtils.ts

@@ -0,0 +1,91 @@
+/**
+ * Classe permettant de construire une URL pour l'interrogation d'une API externe
+ */
+class UrlUtils {
+  /**
+   * Concatenate a base url and a tail
+   * @param base
+   * @param tails
+   * @private
+   */
+  public static join (base: string|number, ...tails: Array<string|number>): string {
+    let url = String(base)
+    tails.forEach((tail: string|number) => {
+      url = url.replace(/^|\/$/g, '') + '/' + String(tail).replace(/^\/?|$/g, '')
+    })
+    return url
+  }
+
+  /**
+   * Prepend the 'https://' part if neither 'http://' of 'https://' is present, else: does nothing
+   *
+   * @param url
+   */
+  public static prependHttps (url: string): string {
+    if (!url.match(/^https?:\/\/.*/)) {
+      url = 'https://' + url;
+    }
+    return url;
+  }
+
+  /**
+   * Parse an URI to retrieve a parameter
+   *
+   * @param uri
+   * @param parameter
+   * @param default_
+   * @private
+   */
+  public static getParameter (
+      uri: string,
+      parameter: string,
+      default_: string | number | null = null
+  ): string | number | null {
+    uri = UrlUtils.prependHttps(uri)
+
+    const urlParams = new URL(uri).searchParams;
+    let value: string | number | null = urlParams.get('page');
+
+    if (value && (default_ === null || Number.isInteger(default_)) && /^\d+$/.test(value)) {
+      // On convertit automatiquement si et seulement la valeur par défaut est elle-même un entier ou n'est pas définie
+      value = parseInt(value)
+    }
+
+    return value ?? default_
+  }
+
+  /**
+   * Extrait l'ID de l'URI passée en paramètre
+   * L'uri est supposée être de la forme `.../foo/bar/{id}`, où l'id est un identifiant numérique
+   *
+   * @param uri
+   */
+  public static extractIdFromUri (uri: string): number|null {
+    const partUri: Array<string> = uri.split('/')
+    const id:any = partUri.pop()
+
+    if(isNaN(id))
+      throw new Error('id is not a number')
+
+    return parseInt(id)
+  }
+
+  /**
+   * Découpe une URI au niveau des '/'
+   * Utilisé entre autres pour le breadcrumb
+   *
+   * Ex:
+   *
+   *    foo/bar/1   =>  ['foo', 'bar', '1']
+   *    /foo/bar/1   =>  ['foo', 'bar', '1']
+   *    https://domain.com/foo/bar/1   =>  ['https:', 'domain.com', 'foo', 'bar', '1']
+   *
+   *
+   * @param uri
+   */
+  public static split(uri: string) {
+    return uri.split('/').filter((s) => s.length > 0)
+  }
+}
+
+export default UrlUtils