Hlavní navigace

Formát EDN: extensible data notation

20. 4. 2021
Doba čtení: 32 minut

Sdílet

 Autor: Clojure
V dnešním článku se seznámíme se základními vlastnostmi datového formátu pojmenovaného EDN, což je zkratka celého názvu Extensible Data Notation. Jedná se o formát určený pro reprezentaci a přenosy strukturovaných dat.

Obsah

1. Formát EDN: extensible data notation

2. Sémantické rozdíly mezi formáty EDN, JSON a XML

3. Základní datové typy podporované formátem EDN

4. Řetězce versus keywords

5. Kolekce, které lze v EDN použít

6. Klíče

7. Štítky jako prostředek pro rozšíření formátu EDN

8. Podpora formátu EDN v různých programovacích jazycích

9. Clojure

10. Python

11. Go

12. Praktická část

13. Převod mezi formátem JSON a EDN v Pythonu a Clojure

14. Převod mezi formátem EDN a JSON v Pythonu a Clojure

15. Převod mezi souborem .properties a formátem EDN v Clojure

16. Převod mezi XML a EDN (plná varianta zachovávající vlastnosti XML)

17. Převod XML do různých forem formátu EDN s využitím knihovny Tupelo.forest

18. Práce s formátem EDN v jazyce Go, vlastní štítky rozšiřující možnosti EDN

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Formát EDN: extensible data notation

V dnešním článku se seznámíme se základními vlastnostmi datového formátu pojmenovaného EDN, což je zkratka celého názvu Extensible Data Notation. Tento formát, který je primárně určený pro přenos strukturovaných dat mezi různými systémy a službami, vychází ze syntaxe a sémantiky programovacího jazyka Clojure, je tedy založen na původních S-výrazech rozšířených o možnost zápisu map (slovníků), množin a vektorů. Popis tohoto formátu (a tím pádem i popis syntaxe Clojure, resp. jeho podmnožiny) naleznete na stránce https://github.com/edn-format/edn, nás však v dnešním článku bude zajímat i porovnání EDN s dalšími často používanými datovými formáty JSON a XML a taktéž to, jak lze EDN využít i v dalších programovacích jazycích; nejenom v samotném Clojure. Popíšeme si tudíž podporu EDN v Pythonu a v jazyce Go; ve skutečnosti je však EDN podporován i v Rustu či v Javě.

Obrázek 1: Logo programovacího jazyka Clojure.

Poznámka:  formátem EDN se setkáme například v databázovém systému Crux, kde EDN slouží mj. i pro tvorbu dotazů (Crux je velmi zajímavou technologií, ke které se taktéž na stránkách Roota vrátíme):
(crux/q
         (crux/db node)
         '{:find [p1]
           :where [[p1 :name n]
                   [p1 :last-name n]
                   [p1 :name name]]
           :in [name]}
         "Pavel")
(crux/q
         (crux/db node)
         '{:find [(sum ?heads)
                  (min ?heads)
                  (max ?heads)
                  (count ?heads)
                  (count-distinct ?heads)]
           :where [[(identity [["Cerberus" 3]
                               ["Medusa" 1]
                               ["Cyclops" 1]
                               ["Chimera" 1]])
                    [[?monster ?heads]]]]})

2. Sémantické rozdíly mezi formáty EDN, JSON a XML

V současnosti se pro přenos strukturovaných informací (ale popř. i tabulek) používá relativně velké množství různých datových formátů. Pravděpodobně nejznámějšími formáty jsou XML a JSON, ovšem vznikají a postupně jsou adaptovány i další formáty, ať již textové (YAML, edn) či binární (BSON, B-JSON, Smile, Protocol-Buffers). Je velmi pravděpodobné, že se JSON i XML bude i nadále poměrně masivně využívat, na druhou stranu je dobré znát i některá omezení těchto formátů, popř. vztah JSONu a XML k formátu EDN. Nejedná se nám přitom o syntaktické rozdíly – ty jsou viditelné na první pohled a každý dovede tyto rozdíly rozeznat (i když například absence čárek v EDN je krokem kupředu). Důležitější jsou rozdíly sémantické a pravě zde můžeme nalézt celou řadu různých omezení.

Nejvíce podobností nalezneme mezi formáty EDN a JSONem. JSON má ovšem několik omezení, a to jak v podporovaných základních datových typech (chybí například celá čísla), tak i v podpoře kolekcí (jen pole a mapa) a v rozšiřitelnosti (výhodné by například bylo rozšíření o typ pro jednoznačnou reprezentaci časových razítek). Pravděpodobně nejdůležitějším rozdílem je chybějící sémantika pro uložení množin a taktéž omezení klíčů v mapách – musí se totiž jednat o řetězce, zatímco ve formátu EDN může být klíč jakoukoli hodnotou – tedy například mapou, vektorem (relativně často používáno) nebo dokonce lze použít nil.

V předchozím textu jsem několikrát použil sousloví „formát XML“, i když se mnohdy setkáme spíše s označením „jazyk XML“ (které je ovšem nepřesné). XML, i když se jedná o značkovací jazyk (markup language) se totiž začal používat i pro přenosy strukturovaných netextových dat, což se může při zpětném pohledu znát poněkud zvláštní, neboť pro tento účel není příliš uzpůsoben (ale už fakt, že se XML podařilo „ohnout“ něco říká o jeho flexibilitě). Sémantika XML se v mnoha ohledech odlišuje jak od JSONu, tak i od EDN, protože je v něm nutné datové struktury reprezentovat neexplicitně – celý XML dokument tvoří stromovou strukturu a pouze z popisu jednotlivých značek, popř. jejich atributů vyplyne, jaká konkrétní struktura je vlastně v XML dokumentu uložena, tj. zda se jedná o seznam, asociativní pole či o množinu (navíc i typ jednotlivých prvků je nutné popsat externě). Ovšem například v případě množiny a asociativního pole se musí (externě) kontrolovat integrita datové struktury.

Poznámka: nutnost použít další XML s popisem schématu je dvousečná zbraň; skvělá pro enterprise řešení, výhodná pro některé nasazení v oblasti SOA, ovšem na druhou stranu je dosti neflexibilní. I z tohoto důvodu se stále častěji v oblasti (nejenom) webových služeb setkáme s formátem JSON.

3. Základní datové typy podporované formátem EDN

V datech reprezentovaných ve formátu EDN lze využít hned několik datových typů, z nichž mnohé jsou známé i z dalších podobně koncipovaných formátů (textové JSON, popř. binární formáty). Jedná se o následující osmici základních datových typů:

# Typ Popis Příklad
1 nil symbolizuje neexistující hodnotu nil
2 boolean pravdivostní true, false
3 keyword jednoznačný symbol (ovšem ne řetězec) :test
4 string běžný řetězec v Unicode „hello\n“
5 character jediný znak \newline, \c
6 symbol použití pro identifikátory, včetně specifikace jmenného prostoru foo/bar
7 integer celá čísla (64 bitů se znaménkem) 42
8 floating point numbers hodnoty s plovoucí řádovou čárkou (double) 1.3e-10
Poznámka: povšimněte si, že jsou jednoznačně definovány celočíselné hodnoty i s jejich rozsahem, což je v praxi velmi důležité, neboť ne všechny hodnoty typu int64 lze beze ztráty přesnosti reprezentovat typem double

4. Řetězce versus keywords

Na chvíli se zastavme u datového typu keyword. Překlad tohoto slova je poněkud problematický kvůli jeho dvojímu významu (alespoň v programování), takže se pokusím používat sousloví „klíčová hesla“, protože termín „keywords“ v jazyce Clojure i ve formátu EDN neznamená, že by se jednalo o rezervovaná klíčová slova jazyka. Klíčová hesla, která reprezentují samy sebe (svoje označení) jsou na použití jednodušší než symboly či řetězce, protože jim nemůže být přiřazena žádná hodnota. Na co se tedy vlastně v praxi tento typ formy hodí? Jedním z důvodů zavedení tohoto typu formy do programovacího jazyka Clojure i do formátu EDN byla podpora pro datového typu (kolekce) mapa (asociativní pole), v němž je možné uchovávat dvojice klíč:hodnota. A jako klíče jsou s výhodou používána právě klíčová hesla, protože jejich hodnotu nelze měnit a navíc se jejich hešovací hodnota (zkráceně heš) může vypočítat pouze jedenkrát.

Příklad použití tohoto typu:

{
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {...}
  :dependencies [...]
  :plugins [...]
  :project-edn {...}
  :target-path "target/%s"
  :profiles {...}
}
Poznámka: vidíme tedy, že by bylo možné klíčová hesla nahradit řetězci. To je pochopitelně dovoleno, i když se nejedná o příliš idiomatický EDN:
{
  "description" "FIXME: write description"
  "url" "http://example.com/FIXME"
  "license" {...}
  "dependencies" [...]
  "plugins" [...]
  "project-edn" {...}
  "target-path" "target/%s"
  "profiles" {...}
}

5. Kolekce, které lze v EDN použít

Největší sémantický rozdíl mezi EDN a dalšími formáty spočívá v tom, jaké kolekce jsou podporovány. Přitom zde pod pojmem kolekce myslíme datovou strukturu, která je používána jako kontejner pro další hodnoty – pochopitelně je dovoleno rekurzivní využití kolekcí v jiné kolekci (naprosto stejně, jako je tomu v JSONu, ovšem s tím rozdílem, že JSON podporuje jen mapy a pole/vektory). Ve formátu EDN se používají čtyři typy kolekcí: seznamy (list), vektory (vector), množiny (set) a mapy map neboli asociativní pole. Všechny čtyři typy složených forem, ať již se jedná o seznam, vektor, množinu či mapu, jsou z obou stran uvozeny závorkami, přičemž musí být zachována párovost závorek (ke každé otevírací závorce přísluší jedna závorka uzavírací), která je kontrolována při načítání dat uložených v EDN.

Prvky seznamů se již tradičně (více než padesát let!) zapisují do kulatých závorek. Pro zápis vektorů se používají hranaté závorky, mapy využívají závorky složené a množiny taktéž závorky složené, ovšem před otevírací závorkou se musí napsat křížek (hash, #). Jednotlivé prvky jsou od sebe odděleny bílým znakem. V následující tabulce jsou vypsány všechny čtyři typy složených forem:

Typ kolekce Zápis
Seznam (prvky)
Vektor [prvky]
Mapa {dvojice klíč-hodnota}
Množina #{unikátní prvky}

Nyní by tedy měl být následující datový soubor dobře čitelný. Jedná se o mapu s několika prvky, jejichž hodnoty jsou buď reprezentovány řetězci, vektory nebo dalšími mapami:

{
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [tupelo "21.04.13"]]
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})
}

6. Klíče

Již ve druhé kapitole jsme si řekli, že ve formátu EDN lze v mapách použít jako klíče naprosto libovolné hodnoty, což si můžeme velmi snadno demonstrovat v interaktivní smyčce REPL jazyka Clojure. Dokonce si ani nemusíte jazyk Clojure instalovat, protože postačuje příklady otestovat na stránce https://clojurescript.io/:

user=> {:foo "bar"}
{:foo "bar"}
 
user=> {42 "answer"}
{42 "answer"}
 
user=> {[1 2 3] "a vector"}
{[1 2 3] "a vector"}
 
user=> {{:type :map} "map is a key"}
{{:type :map} "map is a key"}
 
user=> {{:another-key [1 2 3]} "strange key"}
{{:another-key [1 2 3]} "strange key"}
 
user=> {nil "nil as a key"}
{nil "nil as a key"}
Poznámka: liché prvky jsou klíče, sudé hodnotami. Použít lze i čárky pro oddělení jednotlivých dvojic klíč-hodnota:
user=> {{:another-key [1 2 3]}, "strange key"}
{{:another-key [1 2 3]} "strange key"}

7. Štítky jako prostředek pro rozšíření formátu EDN

Důvodem, proč se v názvu formátu (a to hned na jeho začátku) objevuje slovo „Extensible“ je fakt, že se skutečně jedná o rozšiřitelný formát. Není sice možné rozšiřovat množství podporovaných kolekcí (ovšem na druhou stranu – pravděpodobně žádná známá kolekce nechybí, až na obecný graf), ale zato lze rozšiřovat počet datových typů. Pro tento účel se používají takzvané štítky (tags), které začínají znakem # (hash). Již v základní variantě EDN existují dva takové rozšiřující datové typy.

Prvním z těchto typů je časové razítko podle RFC-3339:

#inst "1985-04-12T23:20:50.52Z"

To je velmi užitečné, ostatně právě v oblasti dat a časových razítek existují zmatky.

Druhým z těchto typů je UUID, který se dnes skutečně využívá univerzálně:

#uuid "01234567-89ab-cdef-0123-456789abcdef"
Poznámka: pochopitelně by bylo možné použít běžné řetězce, ovšem poté by bylo nutné typ předepsat ve schématu, což není jednoduché a mnohdy ani možné.

Použitím štítků se budeme podrobněji zabývat ve druhém pokračování dnešního článku – jedná se totiž o relativně rozsáhlé a přitom užitečné téma.

8. Podpora formátu EDN v různých programovacích jazycích

Formát EDN sice není rozšířený do takové míry, jako formáty XML a JSON, ovšem i přesto ho lze použít v relativně velkém množství programovacích jazyků. Jedná se v první řadě o jazyk Clojure, což ovšem není nijak překvapivé, neboť EDN je prakticky celý odvozen od Clojure (až na štítky). Kromě Clojure je možné EDN použít v Pythonu, Javě, JavaScriptu, Rustu, jazyku Go a s velkou pravděpodobností i v dalších jazycích (ovšem již zmíněné jazyky pokrývají velké množství aplikačních použití). Podporu pro formát EDN nalezneme i v projektu Babashka, s nímž jsme se již na stránkách Roota ve stručnosti seznámili.

Knihovny pro práci s EDN pro jednotlivé jazyky:

# Programovací jazyk Knihovna
1 Clojure clojure.edn (standardní)
2 Python edn_format
3 Java edn-java
4 JavaScript jsedn
5 Rust edn-rs
6 Go go-edn/edn
7 Babashka clojure.edn (standardní)

9. Clojure

Podpora formátu EDN v programovacím jazyku Clojure je „nativní“, což znamená, že všechny potřebné funkce jsou přímo součástí standardní knihovny tohoto jazyka. Konkrétně se jedná o balíček (nebo možná přesněji řečeno jmenný prostor) clojure.edn, ve kterém nalezneme pouhé dvě funkce:

# Funkce Stručný popis funkce
1 read načtení dat ve formátu EDN z libovolného vstupního proudu
2 read-string načtení dat ve formátu EDN z řetězce (získaného libovolným postupem)

Jak je v ekosystému programovacího jazyka Clojure zvykem, lze nápovědu k těmto funkcím získat přímo z interaktivního prostředí vybaveném smyčkou REPL. Nejdříve je ovšem nutné příslušnou knihovnu načíst:

user=> (require '[clojure.edn :as edn])

Nápověda k funkci read:

user=> (doc edn/read)
-------------------------
clojure.edn/read
([] [stream] [opts stream])
  Reads the next object from stream, which must be an instance of
  java.io.PushbackReader or some derivee.  stream defaults to the
  current value of *in*.
 
  Reads data in the edn format (subset of Clojure data):
  http://edn-format.org
 
  opts is a map that can include the following keys:
  :eof - value to return on end-of-file. When not supplied, eof throws an exception.
  :readers  - a map of tag symbols to data-reader functions to be considered before default-data-readers.
              When not supplied, only the default-data-readers will be used.
  :default - A function of two args, that will, if present and no reader is found for a tag,
             be called with the tag and the value.

Nápověda k funkci read-string:

user=> (doc edn/read-string)
-------------------------
clojure.edn/read-string
([s] [opts s])
  Reads one object from the string s. Returns nil when s is nil or empty.
 
  Reads data in the edn format (subset of Clojure data):
  http://edn-format.org
 
  opts is a map as per clojure.edn/read

Možná se nyní ptáte, jakým způsobem se vlastně zapisují data ve formátu EDN. Odpověď je jednoduchá – výpis jakékoli datové struktury jazyka Clojure či jakékoli hodnoty do čitelného formátu již odpovídá formátu EDN. To znamená, že pro tyto účely lze použít například standardní funkci prn-str, jejíž výsledek se uloží do souboru funkcí spit atd.:

user=> (doc prn-str)
-------------------------
clojure.core/prn-str
([& xs])
  prn to a string, returning it
Poznámka: s formátem EDN se v ekosystému jazyka Clojure setkáme doslova na každém kroku. Ostatně samotné projektové soubory project.clj i přes odlišnou zkratku jsou reprezentovány jako EDN:
(defproject edn2json "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [org.clojure/data.json "2.2.0"]]
  :plugins [[lein-project-edn "0.3.0"]]
  :project-edn {:output-file "details.edn"}
  :main ^:skip-aot edn2json.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})

Podobně soubory deps.edn podporované od Clojure 1.9 mají tento formát:

{:deps
    {net.mikera/core.matrix {:mvn/version "0.62.0"}}}

Do souborů ve formátu EDN lze vyexportovat i detailní informace o projektu, což zajišťuje plugin lein-project-edn:

{:aliases {"downgrade" "upgrade"},
 :checkout-deps-shares
 [:source-paths
  :test-paths
  :resource-paths
  :compile-path
  "#'leiningen.core.classpath/checkout-deps-paths"],
 :clean-targets [:target-path],
 :compile-path
 "/home/ptisnovs/src/presentations/edn/edn2json/target/default/classes",
 :dependencies
 ([org.clojure/clojure "1.10.1"]
  [org.clojure/data.json "2.2.0"]
  [nrepl/nrepl "0.7.0" :exclusions ([org.clojure/clojure])]
  [clojure-complete/clojure-complete
   "0.2.5"
   :exclusions
   ([org.clojure/clojure])]
  [venantius/ultra "0.6.0"]),
 :deploy-repositories
 [["clojars"
   {:url "https://repo.clojars.org/",
    :password :gpg,
    :username :gpg}]],
 :description "FIXME: write description",
 :eval-in :subprocess,
 :global-vars {},
 :group "edn2json",
 :jar-exclusions ["#\"^\\.\"" "#\"\\Q/.\\E\""],
 :jvm-opts
 ["-XX:-OmitStackTraceInFastThrow"
  "-XX:+TieredCompilation"
  "-XX:TieredStopAtLevel=1"],
 :license
 {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0",
  :url "https://www.eclipse.org/legal/epl-2.0/"},
 :main edn2json.core,
 :monkeypatch-clojure-test false,
 :name "edn2json",
 :native-path
 "/home/ptisnovs/src/presentations/edn/edn2json/target/default/native",
 :offline? false,
 :pedantic? ranges,
 :plugin-repositories
 [["central"
   {:url "https://repo1.maven.org/maven2/", :snapshots false}]
  ["clojars" {:url "https://repo.clojars.org/"}]],
 :plugins
 ([lein-project-edn/lein-project-edn "0.3.0"]
  [venantius/ultra "0.6.0"]
  [lein-kibit/lein-kibit "0.1.8"]),
 :prep-tasks ["javac" "compile"],
 :profiles
 {:uberjar
  {:aot [:all], :jvm-opts ["-Dclojure.compiler.direct-linking=true"]},
  :whidbey/repl
  {:dependencies [[mvxcvi/whidbey "RELEASE"]],
   :repl-options
   {:init
    (do
     nil
     (clojure.core/require 'whidbey.repl)
     (whidbey.repl/init! nil)),
    :custom-init (do nil (whidbey.repl/update-print-fn!)),
    :nrepl-context
    {:interactive-eval {:printer whidbey.repl/render-str}}}}},
 :project-edn {:output-file "details.clj"},
 :release-tasks
 [["vcs" "assert-committed"]
  ["change" "version" "leiningen.release/bump-version" "release"]
  ["vcs" "commit"]
  ["vcs" "tag"]
  ["deploy"]
  ["change" "version" "leiningen.release/bump-version"]
  ["vcs" "commit"]
  ["vcs" "push"]],
 :repl-options
 {:init
  (do
   (do
    (clojure.core/require 'ultra.hardcore)
    (clojure.core/require 'whidbey.repl)
    (whidbey.repl/init! nil)
    (ultra.hardcore/configure!
     {:repl
      {:print-meta false,
       :map-delimiter "",
       :print-fallback :print,
       :sort-keys true}})))},
 :repositories
 [["central"
   {:url "https://repo1.maven.org/maven2/", :snapshots false}]
  ["clojars" {:url "https://repo.clojars.org/"}]],
 :resource-paths
 ("/home/ptisnovs/src/presentations/edn/edn2json/dev-resources"
  "/home/ptisnovs/src/presentations/edn/edn2json/resources"),
 :root
 "/home/ptisnovs/src/presentations/edn/edn2json",
 :source-paths
 ("/home/ptisnovs/src/presentations/edn/edn2json/src"),
 :target-path
 "/home/ptisnovs/src/presentations/edn/edn2json/target/default",
 :test-paths
 ("/home/ptisnovs/src/presentations/edn/edn2json/test"),
 :test-selectors {:default (constantly true)},
 :uberjar-exclusions ["#\"(?i)^META-INF/[^/]*\\.(SF|RSA|DSA)$\""],
 :url "http://example.com/FIXME",
 :version "0.1.0-SNAPSHOT"}

10. Python

V programovacím jazyku Python je formát EDN dostupný přes balíček nazvaný přímočaře edn_format. Tento balíček nainstalujete jednoduše přes pip:

$ pip install --user edn_format

Dostupné jsou tyto funkce:

# Funkce Stručný popis funkce
1 loads načtení objektu z formátu EDN
2 loads_all načtení všech objektů z formátu EDN (výsledkem může být prázdný seznam)
3 dumps opak předchozích funkcí – export dat do formátu EDN

Nápovědu k jednotlivým funkcím opět – podobně jako v případě jazyka Clojure – získáme přímo přes interaktivní smyčku REPL.

Import příslušného balíčku:

>>> import edn_format

Zobrazení nápovědy k celému balíčku:

>>> help(edn_format)
 
Help on package edn_format:
 
NAME
    edn_format - # -*- coding: utf-8 -*-
 
PACKAGE CONTENTS
    edn_dump
    edn_lex
    edn_parse
    exceptions
    immutable_dict
    immutable_list
    parsetab

Nápověda k vybrané funkci, například k funkci loads_all:

>>> help(edn_format.loads_all)
 
...
...
...

11. Go

Podporu formátu EDN nalezneme i v programovacím jazyce Go, pro nějž vznikla knihovna nazvaná encoding/edn, což je v ekosystému tohoto jazyka zcela konzistentní jméno, protože na „encoding/“ začínají i jména mnohých dalších knihoven zajištujících kódování či dekódování dat, popř. jejich marshaling a unmarshaling. Připomeňme si především knihovnu encoding/json.

Balíček pro práci s EDN není součástí standardní knihovny jazyka Go a proto je nutné balíček explicitně doinstalovat. To se provede následujícím příkazem:

$ go get olympos.io/encoding/edn

Po instalaci si lze prohlédnou dokumentaci s využitím go doc, popř. použít stránku s již vygenerovanou dokumentací: https://pkg.go.dev/olympos­.io/encoding/edn?utm_source=go­doc.

Pro převod libovolného typu (přesněji řečeno hodnoty libovolného typu) do formátu EDN se používá funkce nazvaná Marshal, kterou nalezneme v balíčku encoding/edn nainstalovaném výše zmíněným příkazem:

func Marshal(v interface{}) ([]byte, error)

Povšimněte si, že tato funkce skutečně akceptuje hodnotu libovolného typu, protože prázdné rozhraní implementuje (zcela automaticky!) každý datový typ (s tímto zajímavým konceptem „univerzálního datového typu“ se ještě několikrát setkáme, zejména v rozhraních mezi Go a dalšími systémy). Návratovou hodnotou je sekvence bajtů (nikoli řetězec!) a popř. i struktura reprezentující chybový stav, pokud k chybě skutečně došlo. V opačném případě se ve druhé návratové hodnotě funkce Marshal vrací nil, jak jsme ostatně zvyklí ze všech podobně koncipovaných funkcí, které mohou za určitých okolností skončit s chybou.

V typických zdrojových kódech se tedy setkáme s tímto idiomatickým zápisem:

edn_bytes, err := json.Marshal(a)
 
if err != nil {
        log.Fatal(err)
}
...
...
...
Poznámka: to, že funkci Marshal můžeme zavolat a předat jí libovolnou hodnotu ovšem v žádném případě neznamená, že převod bude skutečně proveden. Některé typy hodnot totiž nemají v EDN svoji obdobu (je to například funkce nebo ukazatel). Podrobnosti o podporovaných a nepodporovaných typech budou vysvětleny v navazujících kapitolách při popisu demonstračních příkladů.

Jméno funkce odpovídá prováděné operaci, takzvanému marshalingu. Opačná operace, tj. převod dat z EDN do datových struktur Go, se nazývá unmarshalling. Jedná se o funkci s touto hlavičkou:

func Unmarshal(data []byte, v interface{}) error

Vstupem je v tomto případě pole (řez) bajtů, výstup je vrácen přes ukazatel předaný ve druhém parametru (což znamená, že se musíme sami postarat o případnou alokaci paměti pro strukturu či pro mapu). Samozřejmě, že při unmarshalingu může dojít k nějaké chybě, která je vrácena volající funkci. Pokud k chybě nedošlo, je návratová hodnota rovna nil, opět přesně podle zvyklostí programovacího jazyka Go.

12. Praktická část

Druhá část dnešního článku bude zaměřena spíše prakticky. Ukážeme si totiž některé možnosti práce s formátem EDN v programovacích jazycích Clojure, Python i Go. Uvidíme – což je ovšem očekávatelné – že se jednotlivé jazyky od sebe velmi odlišují při operacích typu serializace a deserializace datových struktur (též se setkáme s termíny marshaling a unmarshaling, které sice striktně řečeno nemají úplně shodný význam, ale mnohdy se mezi sebou zaměňují).

13. Převod mezi formátem JSON a EDN v Pythonu a Clojure

Nejprve se podívejme na způsob realizace převodů mezi formáty JSON a EDN, což může být dosti častý případ využitelný v praxi. Ukážeme si, jak se tyto převody provedou v jazyce Python a taktéž v programovacím jazyku Clojure. Začneme příkladem vypracovaným v Pythonu, který provede převod mezi souborem s daty uloženými ve formátu JSON a formátem EDN. Realizace tohoto příkladu je poměrně přímočará – nejprve se s využitím funkce load z balíčku json načte vstupní soubor a posléze se funkcí dumps z balíčku end_format data převedou do formátu EDN. Typicky se budou převádět seznamy, popř. mapy (asociativní pole):

#!/usr/bin/env python3
 
# Converts structured data from JSON format into EDN format.
 
import sys
import json
import edn_format
 
# Check if command line argument is specified (it is mandatory).
if len(sys.argv) < 2:
    print("Usage:")
    print("  json2edn.py input_file.csv")
    print("Example:")
    print("  json2edn.py report.csv")
    sys.exit(1)
 
# First command line argument should contain name of input JSON file.
input_json = sys.argv[1]
 
# Try to open the JSON file specified.
with open(input_json) as json_input:
    # open the JSON file and parse it
    payload = json.load(json_input)
    # dump the parsed data structure into EDN format
    print(edn_format.dumps(payload))

Pro porovnání se podívejme na realizaci stejného převodníku, nyní ovšem vypracovaného v jazyku Clojure. Zde se použije funkce read-str z balíčku clojure.data.json pro vstup a de facto standardní funkce pprint z balíčku clojure.pprint pro výstup dat, protože ten bude kompatibilní s formátem EDN:

(ns json2edn.core)
 
(require '[clojure.data.json :as json])
(require '[clojure.edn :as edn])
 
 
(defn json->edn
  "Convert JSON format into EDN format."
  [json-file-name edn-file-name]
  (let [payload (-> json-file-name slurp (json/read-str :key-fn keyword))
        fout    (clojure.java.io/writer edn-file-name)]
    (clojure.pprint/pprint payload)
    (clojure.pprint/pprint payload fout)))
 
 
(defn -main
  [& args]
  (json->edn "details.json" "details.edn"))

Následuje příklad použití:

Vstup (reálná data):

{
    "Messages": [
        {
            "MessageId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
            "ReceiptHandle": "123",
            "MD5OfBody": "cafe0000cafe0000cafe0000cafe0000",
            "Body": "{\"Records\": [{\"s3\": {\"object\": {\"key\": \"7307752/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/20200609125740-0123456789abcdef0123456789abcdef\",\"size\": 11805,\"eTag\": \"beef0000beef0000beef0000beef0000\",\"sequencer\": \"005EDF87444DDF7525\"}}}]}",
            "Attributes": {
                "SenderId": "321123",
                "ApproximateFirstReceiveTimestamp": "1591707477622",
                "ApproximateReceiveCount": "1",
                "SentTimestamp": "1591707468964"
            }
        }
    ],
    "ResponseMetadata": {
        "RequestId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
        "HTTPStatusCode": 200,
        "HTTPHeaders": {
            "x-amzn-requestid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
            "date": "Tue, 09 Jun 2020 12:57:57 GMT",
            "content-type": "text/xml",
            "content-length": "22437"
        },
        "RetryAttempts": 0
    }
}

Převod:

$ lein run

Výstup:

{:Messages
 [{:MessageId "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
   :ReceiptHandle "123",
   :MD5OfBody "cafe0000cafe0000cafe0000cafe0000",
   :Body
   "{\"Records\": [{\"s3\": {\"object\": {\"key\": \"7307752/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/20200609125740-0123456789abcdef0123456789abcdef\",\"size\": 11805,\"eTag\": \"beef0000beef0000beef0000beef0000\",\"sequencer\": \"005EDF87444DDF7525\"}}}]}",
   :Attributes
   {:SenderId "321123",
    :ApproximateFirstReceiveTimestamp "1591707477622",
    :ApproximateReceiveCount "1",
    :SentTimestamp "1591707468964"}}],
 :ResponseMetadata
 {:RequestId "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  :HTTPStatusCode 200,
  :HTTPHeaders
  {:x-amzn-requestid "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
   :date "Tue, 09 Jun 2020 12:57:57 GMT",
   :content-type "text/xml",
   :content-length "22437"},
  :RetryAttempts 0}}

14. Převod mezi formátem EDN a JSON v Pythonu a Clojure

Opačný převod mezi formátem EDN a JSON v Pythonu není tak přímočarý, jak by se mohlo na první pohled zdát. Je tomu tak z toho důvodu, že seznamy a mapy (asociativní pole) v EDN jsou realizovány formou neměnitelné (immutable) datové struktury typu ImmutableList a ImmutableDict, kterou nelze přímo do JSONu serializovat. Je tedy nutné tyto struktury převést na běžný (měnitelný) seznam, popř. slovník, což v dalším kódu zajišťuje rekurzivně volaná funkce edn_to_map, která byla převzata z https://github.com/swaro­opch/edn_format/issues/76#is­suecomment-749618312:

# Taken from https://github.com/swaroopch/edn_format/issues/76#issuecomment-749618312
def edn_to_map(x):
    if isinstance(x, edn_format.ImmutableDict):
        return {edn_to_map(k): edn_to_map(v) for k, v in x.items()}
    elif isinstance(x, edn_format.ImmutableList):
        return [edn_to_map(v) for v in x]
    elif isinstance(x, edn_format.Keyword):
        return x.name
    else:
        return x

Úplný kód převodníku může vypadat takto:

#!/usr/bin/env python3
 
# Converts structured data from EDN format into JSON format.
 
import sys
import json
import edn_format
 
# Check if command line argument is specified (it is mandatory).
if len(sys.argv) < 2:
    print("Usage:")
    print("  edn2json.py input_file.edn")
    print("Example:")
    print("  edn2json.py report.edn")
    sys.exit(1)

# First command line argument should contain name of input EDN file.
filename = sys.argv[1]
 
 
# Taken from https://github.com/swaroopch/edn_format/issues/76#issuecomment-749618312
def edn_to_map(x):
    if isinstance(x, edn_format.ImmutableDict):
        return {edn_to_map(k): edn_to_map(v) for k, v in x.items()}
    elif isinstance(x, edn_format.ImmutableList):
        return [edn_to_map(v) for v in x]
    elif isinstance(x, edn_format.Keyword):
        return x.name
    else:
        return x
 
 
# Try to open the EDN file specified.
with open(filename, "r") as edn_input:
    # open the EDN file and parse it
    payload = edn_format.loads(edn_input.read())
    print(json.dumps(edn_to_map(payload), indent=2))

Přepis do jazyka Clojure je ovšem přímočarý a struktura tohoto příkladu odpovídá příkladu pro převod opačný (viz předchozí kapitolu):

(ns edn2json.core)
 
(require '[clojure.data.json :as json])
(require '[clojure.edn :as edn])
 
 
(defn edn->json
  "Convert EDN format into JSON format."
  [edn-file-name json-file-name]
  (let [payload (-> edn-file-name slurp edn/read-string)
        output (with-out-str (json/pprint payload))]
    (spit json-file-name output)))
 
 
(defn -main
  [& args]
  (edn->json "details.edn" "details.json"))

Opět následuje příklad použití:

Vstup (detailní informace o projektu reprezentované v EDN):

{:aliases {"downgrade" "upgrade"},
 :checkout-deps-shares
 [:source-paths
  :test-paths
  :resource-paths
  :compile-path
  "#'leiningen.core.classpath/checkout-deps-paths"],
 :clean-targets [:target-path],
 :compile-path
 "/home/ptisnovs/src/presentations/edn/edn2json/target/default/classes",
 :dependencies
 ([org.clojure/clojure "1.10.1"]
  [org.clojure/data.json "2.2.0"]
  [nrepl/nrepl "0.7.0" :exclusions ([org.clojure/clojure])]
  [clojure-complete/clojure-complete
   "0.2.5"
   :exclusions
   ([org.clojure/clojure])]
  [venantius/ultra "0.6.0"]),
 :deploy-repositories
 [["clojars"
   {:url "https://repo.clojars.org/",
    :password :gpg,
    :username :gpg}]],
 :description "FIXME: write description",
 :eval-in :subprocess,
 :global-vars {},
 :group "edn2json",
 :jar-exclusions ["#\"^\\.\"" "#\"\\Q/.\\E\""],
 :jvm-opts
 ["-XX:-OmitStackTraceInFastThrow"
  "-XX:+TieredCompilation"
  "-XX:TieredStopAtLevel=1"],
 :license
 {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0",
  :url "https://www.eclipse.org/legal/epl-2.0/"},
 :main edn2json.core,
 :monkeypatch-clojure-test false,
 :name "edn2json",
 :native-path
 "/home/ptisnovs/src/presentations/edn/edn2json/target/default/native",
 :offline? false,
 :pedantic? ranges,
 :plugin-repositories
 [["central"
   {:url "https://repo1.maven.org/maven2/", :snapshots false}]
  ["clojars" {:url "https://repo.clojars.org/"}]],
 :plugins
 ([lein-project-edn/lein-project-edn "0.3.0"]
  [venantius/ultra "0.6.0"]
  [lein-kibit/lein-kibit "0.1.8"]),
 :prep-tasks ["javac" "compile"],
 :profiles
 {:uberjar
  {:aot [:all], :jvm-opts ["-Dclojure.compiler.direct-linking=true"]},
  :whidbey/repl
  {:dependencies [[mvxcvi/whidbey "RELEASE"]],
   :repl-options
   {:init
    (do
     nil
     (clojure.core/require 'whidbey.repl)
     (whidbey.repl/init! nil)),
    :custom-init (do nil (whidbey.repl/update-print-fn!)),
    :nrepl-context
    {:interactive-eval {:printer whidbey.repl/render-str}}}}},
 :project-edn {:output-file "details.clj"},
 :release-tasks
 [["vcs" "assert-committed"]
  ["change" "version" "leiningen.release/bump-version" "release"]
  ["vcs" "commit"]
  ["vcs" "tag"]
  ["deploy"]
  ["change" "version" "leiningen.release/bump-version"]
  ["vcs" "commit"]
  ["vcs" "push"]],
 :repl-options
 {:init
  (do
   (do
    (clojure.core/require 'ultra.hardcore)
    (clojure.core/require 'whidbey.repl)
    (whidbey.repl/init! nil)
    (ultra.hardcore/configure!
     {:repl
      {:print-meta false,
       :map-delimiter "",
       :print-fallback :print,
       :sort-keys true}})))},
 :repositories
 [["central"
   {:url "https://repo1.maven.org/maven2/", :snapshots false}]
  ["clojars" {:url "https://repo.clojars.org/"}]],
 :resource-paths
 ("/home/ptisnovs/src/presentations/edn/edn2json/dev-resources"
  "/home/ptisnovs/src/presentations/edn/edn2json/resources"),
 :root
 "/home/ptisnovs/src/presentations/edn/edn2json",
 :source-paths
 ("/home/ptisnovs/src/presentations/edn/edn2json/src"),
 :target-path
 "/home/ptisnovs/src/presentations/edn/edn2json/target/default",
 :test-paths
 ("/home/ptisnovs/src/presentations/edn/edn2json/test"),
 :test-selectors {:default (constantly true)},
 :uberjar-exclusions ["#\"(?i)^META-INF/[^/]*\\.(SF|RSA|DSA)$\""],
 :url "http://example.com/FIXME",
 :version "0.1.0-SNAPSHOT"
}

Převod:

$ lein run

Výstup:

{"description":"FIXME: write description",
 "compile-path":
 "\/home\/ptisnovs\/src\/presentations\/edn\/edn2json\/target\/default\/classes",
 "deploy-repositories":
 [["clojars",
   {"url":"https:\/\/repo.clojars.org\/",
    "password":"gpg",
    "username":"gpg"}]],
 "group":"edn2json",
 "license":
 {"name":"EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0",
  "url":"https:\/\/www.eclipse.org\/legal\/epl-2.0\/"},
 "project-edn":{"output-file":"details.clj"},
 "resource-paths":
 ["\/home\/ptisnovs\/src\/presentations\/edn\/edn2json\/dev-resources",
  "\/home\/ptisnovs\/src\/presentations\/edn\/edn2json\/resources"],
 "name":"edn2json",
 "checkout-deps-shares":
 ["source-paths", "test-paths", "resource-paths", "compile-path",
  "#'leiningen.core.classpath\/checkout-deps-paths"],
 "source-paths":
 ["\/home\/ptisnovs\/src\/presentations\/edn\/edn2json\/src"],
 "eval-in":"subprocess",
 "repositories":
 [["central",
   {"url":"https:\/\/repo1.maven.org\/maven2\/", "snapshots":false}],
  ["clojars", {"url":"https:\/\/repo.clojars.org\/"}]],
 "test-paths":
 ["\/home\/ptisnovs\/src\/presentations\/edn\/edn2json\/test"],
 "target-path":
 "\/home\/ptisnovs\/src\/presentations\/edn\/edn2json\/target\/default",
 "prep-tasks":["javac", "compile"],
 "native-path":
 "\/home\/ptisnovs\/src\/presentations\/edn\/edn2json\/target\/default\/native",
 "offline?":false,
 "root":"\/home\/ptisnovs\/src\/presentations\/edn\/edn2json",
 "pedantic?":"ranges",
 "clean-targets":["target-path"],
 "plugins":
 [["lein-project-edn", "0.3.0"], ["ultra", "0.6.0"],
  ["lein-kibit", "0.1.8"]],
 "url":"http:\/\/example.com\/FIXME",
 "profiles":
 {"uberjar":
  {"aot":["all"],
   "jvm-opts":["-Dclojure.compiler.direct-linking=true"]},
  "repl":
  {"dependencies":[["whidbey", "RELEASE"]],
   "repl-options":
   {"init":
    ["do", null, ["require", "'whidbey.repl"], ["init!", null]],
    "custom-init":["do", null, ["update-print-fn!"]],
    "nrepl-context":{"interactive-eval":{"printer":"render-str"}}}}},
 "plugin-repositories":
 [["central",
   {"url":"https:\/\/repo1.maven.org\/maven2\/", "snapshots":false}],
  ["clojars", {"url":"https:\/\/repo.clojars.org\/"}]],
 "aliases":{"downgrade":"upgrade"},
 "version":"0.1.0-SNAPSHOT",
 "jar-exclusions":["#\"^\\.\"", "#\"\\Q\/.\\E\""],
 "main":"edn2json.core",
 "global-vars":{},
 "uberjar-exclusions":["#\"(?i)^META-INF\/[^\/]*\\.(SF|RSA|DSA)$\""],
 "jvm-opts":
 ["-XX:-OmitStackTraceInFastThrow", "-XX:+TieredCompilation",
  "-XX:TieredStopAtLevel=1"],
 "dependencies":
 [["clojure", "1.10.1"], ["data.json", "2.2.0"],
  ["nrepl", "0.7.0", "exclusions", [["clojure"]]],
  ["clojure-complete", "0.2.5", "exclusions", [["clojure"]]],
  ["ultra", "0.6.0"]],
 "release-tasks":
 [["vcs", "assert-committed"],
  ["change", "version", "leiningen.release\/bump-version", "release"],
  ["vcs", "commit"], ["vcs", "tag"], ["deploy"],
  ["change", "version", "leiningen.release\/bump-version"],
  ["vcs", "commit"], ["vcs", "push"]],
 "repl-options":
 {"init":
  ["do",
   ["do", ["require", "'ultra.hardcore"], ["require", "'whidbey.repl"],
    ["init!", null],
    ["configure!",
     {"repl":
      {"print-meta":false,
       "map-delimiter":"",
       "print-fallback":"print",
       "sort-keys":true}}]]]},
 "test-selectors":{"default":["constantly", true]},
 "monkeypatch-clojure-test":false}

15. Převod mezi souborem .properties a formátem EDN v Clojure

Relativně často se v aplikacích vytvářených v jazyku Clojure setkáme se soubory s koncovkou .properties, které jsou ostatně v ekosystému programovacího jazyka Java běžné. Převod takových souborů do EDN je snadný, protože můžeme využít možnosti nabízené třídou java.util.Properties pro načtení hodnot s jejich následnou konverzí funkcí properties->map:

(ns properties2edn.core)
 
 
(defn properties->map
  "Convert properties entries into a map. Keys are converted into proper keywords."
  [properties]
  (into {}
        (for [[k v] properties]
              [(keyword k) v])))
 
 
(defn load-properties-file
  "Load configuration from the provided properties file."
  [file-name]
  (with-open [reader (clojure.java.io/reader file-name)]
    (let [properties (java.util.Properties.)]
      (.load properties reader)
      (properties->map properties))))
 
 
(defn properties->edn
  "Convert properties file into EDN format."
  [properties-file-name edn-file-name]
  (let [payload (load-properties-file properties-file-name)
        fout    (clojure.java.io/writer edn-file-name)]
    (clojure.pprint/pprint payload fout)))
 
 
(defn -main
  [& args]
  (properties->edn "test.properties" "test.edn"))

Příklad převodu:

Vstup (soubor s reálnými daty):

# This file is shared currently between this Gradle build and the
# Ant builds for fd303 and JavaScript. Keep the dotted notation for
# the properties to minimize the changes in the dependencies.
thrift.version=0.14.0
thrift.groupid=org.apache.thrift
release=false
 
# Local Install paths
install.path=/usr/local/lib
install.javadoc.path=/usr/local/lib
 
# Test execution properties
testPort=9090
 
# Test with Clover Code coverage (disabled by default)
cloverEnabled=false
 
# Maven dependency download locations
mvn.repo=https://repo1.maven.org/maven2
apache.repo=https://repository.apache.org/content/repositories/releases
 
# Apache Maven publish
license=https://www.apache.org/licenses/LICENSE-2.0.txt
maven-repository-url=https://repository.apache.org/service/local/staging/deploy/maven2
maven-repository-id=apache.releases.https
 
# Dependency versions
httpclient.version=4.5.10
httpcore.version=4.4.12
slf4j.version=1.7.28
#servlet.version=2.5
#It contains servlet3
tomcat.embed.version=8.5.46
junit.version=4.12
mockito.version=1.10.19
javax.annotation.version=1.3.2

Převod:

$ lein run

Výstup:

{:tomcat.embed.version "8.5.46",
 :thrift.version "0.14.0",
 :httpclient.version "4.5.10",
 :mockito.version "1.10.19",
 :javax.annotation.version "1.3.2",
 :thrift.groupid "org.apache.thrift",
 :license "https://www.apache.org/licenses/LICENSE-2.0.txt",
 :install.javadoc.path "/usr/local/lib",
 :slf4j.version "1.7.28",
 :httpcore.version "4.4.12",
 :release "false",
 :mvn.repo "https://repo1.maven.org/maven2",
 :maven-repository-url "https://repository.apache.org/service/local/staging/deploy/maven2",
 :maven-repository-id "apache.releases.https",
 :testPort "9090",
 :apache.repo "https://repository.apache.org/content/repositories/releases",
 :junit.version "4.12",
 :cloverEnabled "false",
 :install.path "/usr/local/lib"}

16. Převod mezi XML a EDN (plná varianta zachovávající vlastnosti XML)

V základní knihovně programovacího jazyka Clojure nalezneme i balíček clojure.xml s funkcemi určenými pro načítání souborů XML. Výsledkem načtení (parsingu) je mapa, jejíž prvky mají jednotný formát. Samotný převodník z formátu XML přes mapu do formátu EDN je přímočarý (další možnosti jsou ukázány v navazující kapitole):

(ns xml2edn.core)
 
(use '[clojure.xml])
 
 
(defn xml->edn
  "Convert XML file into EDN format."
  [xml-file-name edn-file-name]
  (let [payload (clojure.xml/parse "nested.xml")
        fout    (clojure.java.io/writer edn-file-name)]
    (clojure.pprint/pprint payload fout)))
 
 
(defn -main
  [& args]
  (xml->edn "nested.xml" "nested.edn"))

Podívejme se nyní, jakým způsobem je zkonvertován následující (na první pohled velmi jednoduchý) soubor:

<?xml version="1.0"?>
<first>
  <second value="A">
    <third>
      <fourth>Hello A</fourth>
    </third>
  </second>
  <second value="B">
    <third>
      <fourth>Hello B</fourth>
    </third>
  </second>
</first>

Výsledkem převodu bude relativně složitá rekurzivní mapa, která ovšem přesně odpovídá vstupnímu XML, ovšem s tím rozdílem, že jsou explicitně vypsány i hodnoty atributů:

{:tag :first,
 :attrs nil,
 :content
 [{:tag :second,
   :attrs {:value "A"},
   :content
   [{:tag :third,
     :attrs nil,
     :content [{:tag :fourth, :attrs nil, :content ["Hello A"]}]}]}
  {:tag :second,
   :attrs {:value "B"},
   :content
   [{:tag :third,
     :attrs nil,
     :content [{:tag :fourth, :attrs nil, :content ["Hello B"]}]}]}]}

17. Převod XML do různých forem formátu EDN s využitím knihovny Tupelo.forest

Do XML je možné provést převod hodnot (tedy vektorů, map atd.) několika různými způsoby. Většina z nich není podporována v základních knihovnách jazyka Clojure, takže si budeme muset vypomoci externí knihovnou, konkrétně knihovnou nazvanou Tupelo.forest. Do projektového souboru se tato závislost přidá snadno:

(defproject forest-demo "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [tupelo "21.04.13"]]
  :main ^:skip-aot forest-demo.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})

V dalším demonstračním příkladu použijeme několik způsobů převodu a serializace hodnot z XML do EDN. Jednotlivé varianty EDN jsou nazvány „enlive“ „hiccup“, „bush“ a „tree“ (jsou pro to různé důvody, například první název vychází ze jména šablonovacího systému Enlive):

(ns forest-demo.core)
 
(require '[clojure.xml :as xml])
(require '[tupelo.forest :as tf])
 
 
(defn pprint-to-file
  [filename payload]
  (clojure.pprint/pprint payload (clojure.java.io/writer filename)))
 
 
(defn -main
  [& args]
  (let [payload        (xml/parse "nested.xml")
        hiccup-format  (tf/enlive->hiccup payload)
        bush-format    (tf/enlive->bush payload)
        tree-format    (tf/enlive->tree payload)]
    (pprint-to-file "nested-enlive.edn" payload)
    (pprint-to-file "nested-hiccup.edn" hiccup-format)
    (pprint-to-file "nested-bush.edn" bush-format)
    (pprint-to-file "nested-tree.edn" tree-format)))

Vstupem do příkladu je následující XML soubor:

<?xml version="1.0"?>
<first>
  <second value="A">
    <third>
      <fourth>Hello A</fourth>
    </third>
  </second>
  <second value="B">
    <third>
      <fourth>Hello B</fourth>
    </third>
  </second>
</first>

Export do formátu „enlive“:

{:tag :first,
 :attrs nil,
 :content
 [{:tag :second,
   :attrs {:value "A"},
   :content
   [{:tag :third,
     :attrs nil,
     :content [{:tag :fourth, :attrs nil, :content ["Hello A"]}]}]}
  {:tag :second,
   :attrs {:value "B"},
   :content
   [{:tag :third,
     :attrs nil,
     :content [{:tag :fourth, :attrs nil, :content ["Hello B"]}]}]}]}

Export do formátu „hiccup“ je nejstručnější:

[:first
 [:second {:value "A"} [:third [:fourth "Hello A"]]]
 [:second {:value "B"} [:third [:fourth "Hello B"]]]]

Formát nazvaný „bush“:

[{:tag :first}
 [{:value "A", :tag :second}
  [{:tag :third} [{:tag :fourth, :value "Hello A"}]]]
 [{:value "B", :tag :second}
  [{:tag :third} [{:tag :fourth, :value "Hello B"}]]]]

A konečně formát nazvaný „tree“, v němž jsou synovské uzly reprezentovány formou vloženého vektoru:

{:tag :first,
 :tupelo.forest/kids
 [{:value "A",
   :tag :second,
   :tupelo.forest/kids
   [{:tag :third,
     :tupelo.forest/kids
     [{:tag :fourth, :value "Hello A", :tupelo.forest/kids []}]}]}
  {:value "B",
   :tag :second,
   :tupelo.forest/kids
   [{:tag :third,
     :tupelo.forest/kids
     [{:tag :fourth, :value "Hello B", :tupelo.forest/kids []}]}]}]}
Poznámka: je zřejmé, že se každý formát hodí k jiným účelům.

Ve skutečnosti jsou možnosti knihovny Tupelo.forest ještě mnohem větší a pravděpodobně se s nimi setkáme v některém dalším článku o programovacím jazyku Clojure.

Root_skoleni

18. Práce s formátem EDN v jazyce Go, vlastní štítky rozšiřující možnosti EDN

V navazujícím článku téma formátu EDN dokončíme. Ukážeme si především použití formátu EDN v programovacím jazyce Go (ostatně některé demonstrační příklady jsou již uloženy do repositáře) a taktéž se budeme zabývat problematikou tvorby vlastních štítků. Jedná se o užitečný koncept, protože právě štítky umožňují rozšiřování EDN o další datové typy, což je vlastnost, kterou v dalších podobně koncipovaných formátech většinou nenalezneme.

19. Repositář s demonstračními příklady

Zdrojové kódy všech dnes popsaných demonstračních příkladů vyvinutých v programovacích jazycích Clojure, Python i Go, byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/pre­sentations:

# Zdrojový kód Stručný popis souboru Cesta
1 json2edn.py konverze mezi formátem JSON a EDN naprogramovaná v Pythonu https://github.com/tisnik/pre­sentations/blob/master/ed­n/json2edn.py
2 edn2json.py konverze mezi formátem EDN a JSON naprogramovaná v Pythonu https://github.com/tisnik/pre­sentations/blob/master/ed­n/edn2json.py
       
3 json2edn (adresář) konverze mezi formátem JSON a EDN naprogramovaná v Clojure https://github.com/tisnik/pre­sentations/blob/master/ed­n/json2edn
4 edn2json (adresář) konverze mezi formátem EDN a JSON naprogramovaná v Clojure https://github.com/tisnik/pre­sentations/blob/master/ed­n/edn2json
       
3 properties2edn (adresář) konverze mezi .properties souborem a formátem EDN (Clojure) https://github.com/tisnik/pre­sentations/blob/master/ed­n/properties2edn
4 xml2edn (adresář) konverze mezi XML a formátem EDN (Clojure, plná konverze) https://github.com/tisnik/pre­sentations/blob/master/ed­n/xml2edn
5 forest-demo (adresář) různé možnosti konverze mezi XML a formátem EDN https://github.com/tisnik/pre­sentations/blob/master/ed­n/forest-demo
       
6 go-edn-1 (adresář) serializace (marshalling) datové struktury do formátu EDN https://github.com/tisnik/pre­sentations/blob/master/ed­n/go-edn-1
7 go-edn-2 (adresář) specifikace názvů klíčů v EDN formátu https://github.com/tisnik/pre­sentations/blob/master/ed­n/go-edn-2
8 go-edn-3 (adresář) uložení pole datových struktur do formátu EDN https://github.com/tisnik/pre­sentations/blob/master/ed­n/go-edn-3
9 go-edn-4 (adresář) uložení mapy datových struktur do formátu EDN https://github.com/tisnik/pre­sentations/blob/master/ed­n/go-edn-4
       
10 go-edn-5 (adresář) deserializace (unmarshalling) datové struktury z formátu EDN https://github.com/tisnik/pre­sentations/blob/master/ed­n/go-edn-5

20. Odkazy na Internetu

  1. edn – extensible data notation
    https://github.com/edn-format/edn
  2. Programming with Data and EDN
    https://docs.datomic.com/clou­d/whatis/edn.html
  3. Video about EDN
    https://docs.datomic.com/clou­d/livetutorial/edntutorial­.html
  4. (Same) video about EDN on Youtube
    https://www.youtube.com/wat­ch?v=5eKgRcvEJxU
  5. clojure.edn
    https://clojuredocs.org/clojure.edn
  6. API for clojure.edn – Clojure v1.10.2 (stable)
    https://clojure.github.io/clo­jure/clojure.edn-api.html
  7. Clojure EDN Walkthrough
    https://www.compoundtheory.com/clojure-edn-walkthrough/
  8. Články týkající se Pythonu na Rootu
    https://www.root.cz/n/python/
  9. Články týkající se programovacího jazyka Clojure na Rootu
    https://www.root.cz/n/clojure/
  10. Seriál Programovací jazyk Go
    https://www.root.cz/seria­ly/programovaci-jazyk-go/
  11. Crux
    https://opencrux.com/main/index.html
  12. Crux Installation
    https://opencrux.com/reference/21.04–1.16.0/installation.html
  13. read
    https://clojuredocs.org/clo­jure.edn/read
  14. read-string
    https://clojuredocs.org/clo­jure.edn/read-string
  15. Tupelo 21.04.12 (dokumentace)
    https://cloojure.github.i­o/doc/tupelo/
  16. tupelo – Clojure With A Spoonful of Honey
    https://clojars.org/tupelo
  17. Clojure Cookbook: Templating HTML with Enlive
    https://github.com/clojure-cookbook/clojure-cookbook/blob/master/07_webapps/7–11_enlive.asciidoc
  18. An Introduction to Enlive
    https://github.com/swannodette/enlive-tutorial/
  19. Enlive na GitHubu
    https://github.com/cgrand/enlive
  20. data.json
    https://github.com/clojure/data.json
  21. data.json API reference
    https://clojure.github.io/data.json/
  22. Clojure: Writing JSON to a File/Reading JSON From a File
    https://dzone.com/articles/clojure-writing-json
  23. How to pretty print JSON to a file in Clojure?
    https://stackoverflow.com/qu­estions/23307552/how-to-pretty-print-json-to-a-file-in-clojure
  24. go-edn / edn
    https://github.com/go-edn/edn
  25. Queries (Crux)
    https://opencrux.com/reference/21.04–1.16.0/queries.html
  26. Essential EDN
    https://opencrux.com/tuto­rials/essential-edn.html
  27. Babashka: interpret Clojure určený pro rychlé spouštění utilit z příkazového řádku
    https://www.root.cz/clanky/babashka-interpret-clojure-urceny-pro-rychle-spousteni-utilit-z-prikazoveho-radku/
  28. Introducing JSON
    https://www.json.org/json-en.html
  29. ISO 8601
    https://xkcd.com/1179/
  30. What is the right JSON date format
    https://stackoverflow.com/qu­estions/10286204/what-is-the-right-json-date-format
  31. ClojureScript REPL
    https://clojurescript.io/

Autor článku

Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.