cClaude.rocks ☕ Le blog

[Nouvelles technologies, sciences et coups de gueule…]

Menu

Afin de réduire la taille de certains fichiers JSON, il peut être intéressant d’utiliser la notion de valeur par défaut.

On va voir comment utiliser un filtre jq pour faire cela à moindres frais.



Présentation de la problématique

Imaginons le fichier JSON suivant : (fichier1.json)

{
  "context1": {
    "config": {
      "cle1": "valeur-context-1",
      "cle2": "valeur-commune-2",
      "cle3": "valeur-commune-3",
      "cle4": "valeur-commune-4",
      "cle5": "valeur-commune-5"
    }
  },
  "context2": {
    "config": {
      "cle1": "valeur-commune-1",
      "cle2": "valeur-context-2",
      "cle3": "valeur-commune-3",
      "cle4": "valeur-commune-4"
    }
  },
  "context3": {
    "config": {
      "cle1": "valeur-commune-1",
      "cle2": "valeur-commune-2",
      "cle3": "valeur-commune-3",
      "cle4": "valeur-commune-4"
    }
  }
}

On constate qu’il est possible d’avoir la même information comme suit : (fichier2.json)

{
  "config": {
    "cle1": "valeur-commune-1",
    "cle2": "valeur-commune-2",
    "cle3": "valeur-commune-3",
    "cle4": "valeur-commune-4"
  },
  "context1": {
    "config": {
      "cle1": "valeur-context-1",
      "cle5": "valeur-commune-5"
    }
  },
  "context2": {
    "config": {
      "cle2": "valeur-context-2"
    }
  },
  "context3": {
    "config": {}
  }
}

Cette version est plus courte, mais également plus claire, puisqu’elle met en évidence les valeurs communes. On dira qu’on a factorisé la configuration.

Dans le premier cas, pour obtenir la configuration d’un « contexte », on écrira : 🕹jq-play

jq '.context1.config' fichier1.json
{
  "cle1": "valeur-context-1",
  "cle2": "valeur-commune-2",
  "cle3": "valeur-commune-3",
  "cle4": "valeur-commune-4"
}

Comment obtenir le même résultat à partir du second fichier ? 🕹jq-play

jq '.config + .context1.config' fichier2.json

On peut reconstruire la structure initiale, comme suit : 🕹jq-play

jq '.config as $D | { context1: { config: ($D + .context1.config) } }' fichier2.json
{
  "context1": {
    "config": {
      "cle1": "valeur-context-1",
      "cle2": "valeur-commune-2",
      "cle3": "valeur-commune-3",
      "cle4": "valeur-commune-4"
    }
  }
}

Pour reconstruire tout le fichier : 🕹jq-play

jq '.config as $D |
{
  context1: { config: ($D + .context1.config) },
  context2: { config: ($D + .context2.config) },
  context3: { config: ($D + .context3.config) }
}' fichier2.json
  • Explications

    Détaillons le filtre jq de ce dernier cas qui utilise le mot clé as :

    .config as $D |
    {
      context1: { config: ($D + .context1.config) },
      context2: { config: ($D + .context2.config) },
      context3: { config: ($D + .context3.config) }
    }
    
    • .config as $D : On applique le filtre .config et on le met dans la variable $D (du coup, le flux courant reste inchangé)
    • `|' : Le flux courant restant inchangé, on continue avec fichier initial.
    • { … } : On définit un nouvel objet
    • context1: { config: … }, … : On reconstruit la structure
    • ($D + .context1.config) : pour la valeur de chaque clé .config on va prendre le contenu de $D et on ajoute ou modifie + avec l’objet .context.config du contexte courant.


Généralisons au cas où il y a d’autres valeurs dans le fichier

Si vous écrivez un nouveau code, peut-être le plus simple sera de prendre en compte cette notion de valeur par défaut, mais dans un traitement existant, il faut mieux revenir au format initial.

Pour l’exemple, on ajoutera uniquement une clé name avec sa valeur, mais cela peut être quelque chose de beaucoup plus compliqué.

Les données complètes : (fichier11.json)

{
  "context1": {
    "config": {
      "cle1": "valeur-context-1",
      "cle2": "valeur-commune-2",
      "cle3": "valeur-commune-3",
      "cle4": "valeur-commune-4"
    },
    "name": "context-1"
  },
  "context2": {
    "config": {
      "cle1": "valeur-commune-1",
      "cle2": "valeur-context-2",
      "cle3": "valeur-commune-3",
      "cle4": "valeur-commune-4"
    },
    "name": "context-2"
  },
  "context3": {
    "config": {
      "cle1": "valeur-commune-1",
      "cle2": "valeur-commune-2",
      "cle3": "valeur-commune-3",
      "cle4": "valeur-commune-4"
    }
  },
  "name": "context-3"
}

Les données misent en facteur : (fichier12.json)

{
  "config": {
    "cle1": "valeur-commune-1",
    "cle2": "valeur-commune-2",
    "cle3": "valeur-commune-3",
    "cle4": "valeur-commune-4"
  },
  "context1": {
    "config": {
      "cle1": "valeur-context-1"
    },
    "name": "context-1"
  },
  "context2": {
    "config": {
      "cle2": "valeur-context-2"
    },
    "name": "context-2"
  },
  "context3": {
    "config": {},
    "name": "context-3"
  }
}

Pour reconstruire le premier fichier à partir du second, on utilisera : 🕹jq-play

jq '.config as $D |
  del( .config ) |
  .context1.config = ($D + .context1.config) |
  .context2.config = ($D + .context2.config) |
  .context3.config = ($D + .context3.config)
' fichier12.json
  • Explications

    Détaillons le filtre jq :

    .config as $D |
      del( .config ) |
      .context1.config = ($D + .context1.config) |
      .context2.config = ($D + .context2.config) |
      .context3.config = ($D + .context3.config)
    
    • .config as $D : On applique le filtre .config et on le met dans la variable $D (du coup, le flux courant reste inchangé)
    • `|' : Le flux courant restant inchangé, on continue avec fichier initial.
    • .context1.config = … : On applique une nouvelle valeur à l’objet .context1.config.
    • ($D + .context1.config) : pour la valeur de chaque clé .config on va prendre le contenu de $D et on ajoute ou modifie + avec l’objet .context.config du contexte courant.
    • et on reproduit pour les 2 autres objets .context2 et .context3.


Généralisation aux tableaux

Dans les exemples précédant, le choix de l’utilisation de .context1, .context2, et .context3 n’est probablement pas une bonne idée.
On va donc regarder cela pour le cas où l’on souhaite factoriser pour un tableau.

Considérons ce fichier : (fichier22.json)

{
  "config": {
    "cle1": "valeur-commune-1",
    "cle2": "valeur-commune-2",
    "cle3": "valeur-commune-3",
    "cle4": "valeur-commune-4"
  },
  "contexts": [
    {
      "config": {
        "cle1": "valeur-context-1"
      },
      "name": "context-1"
    },
    {
      "config": {
        "cle2": "valeur-context-2"
      },
      "name": "context-2"
    },
    {
      "config": {},
      "name": "context-3"
    }
  ]
}

Dans ce cas, on a un tableau de contextes, on peut obtenir toutes les configurations avec un code assez compactes, mais en perdant les autres valeurs (cependant, cela peut être suffisant pour votre cas d’usage). 🕹jq-play

jq '.config as $D | [ $D + .contexts[].config ]' fichier22.json

Mais on peut facilement reconstruire le fichier complet à l’aide de : 🕹jq-play

jq '.config as $D |
  del( .config ) | .contexts[] | [ .config = ($D + .config) ]
' fichier22.json


Factorisation sur plusieurs niveaux

Imaginons que l’on ait la structure suivante : (fichier32.json)

{
  "config": {
    "cle1": "valeur-globale-1",
    "cle2": "valeur-globale-2",
    "cle3": null
  },
  "context1": {
    "config": {
      "cle1": "valeur-context-1",
      "cle2": null,
      "cle4": "valeur-context-4"
    },
    "name": "context-1",
    "sous-contexts": [
      {
        "config": {
          "cle1": "sous-valeur-context-1"
        },
        "name": "sous-context-1"
      },
      {
        "config": {
          "cle3": "sous-valeur-context-3"
        },
        "name": "sous-context-2"
      },
      {
        "config": {},
        "name": "sous-context-3"
      }
    ]
  },
  "context2": {
    "config": {
      "cle2": "valeur-context-2"
    },
    "name": "context-2",
    "sous-contexts": [
      {
        "config": {},
        "name": "sous-context-1"
      },
      {
        "config": {
          "cle2": "valeur-sous-context-2"
        },
        "name": "sous-context-2"
      }
    ]
  }
}

Les configurations du second contexte s’obtiennent comme suit : 🕹jq-play

jq '(.context2.config + .config ) as $config | [ .context2."sous-contexts"[] | $config + .config ]' fichier32.json

Avec un peu de bash, on peut avoir toutes les configurations :

for C in context1 context2 ; do
  jq --arg context "$C" '(.[$context].config + .config ) as $config | [ .[$context]."sous-contexts"[] | $config + .config ]' fichier32.json
done

Pour obtenir une configuration donnée : 🕹jq-play

jq '(.context2.config + .config ) as $config | .context2."sous-contexts"[] | select( .name =="sous-context-2" ) | $config + .config' fichier32.json

On peut généraliser :

function show_config {
  local -r objet_context="$1"
  local -r nom_sous_context="$2"

  jq --arg context "${objet_context}" \
     --arg name "${nom_sous_context}" '
(.context2.config + .config ) as $config | .context2."sous-contexts"[] | select( .name == $name ) | $config + .config
' fichier32.json
}

show_config 'context2' 'sous-context-2'

Ce qui dans notre cas donnera :

{
  "cle2": "valeur-sous-context-2",
  "cle1": "valeur-globale-1",
  "cle3": null
}

On peut éventuellement améliorer le filtre pour supprimer les valeurs à null en ajoutant :

… | [to_entries[] | select( .value != null )] | from_entries
😵

Je me suis cassé les dents sur la création du fichier « dé-factorisé » avec du code jq pur, si vous avez une piste élégante, je vous invite à laisser un commentaire.



Liens

ᦿ


ℹ 2006 - 2023 | 🏠 Accueil du domaine | 🏡 Accueil du blog