Obsah
1. Formát EDN: extensible data notation (dokončení)
2. Serializace hodnot základních datových typů Go do formátu EDN
3. Převod celočíselných hodnot do formátu EDN
4. Chování systému při pokusu o převod hodnot, které nemají ve formátu EDN podporu
5. Hodnoty nil a řetězce převáděné do formátu EDN
6. Převod polí a řezů do formátu EDN
7. Struktury (záznamy) a jejich přímý převod do formátu EDN
8. Změna názvů klíčů ve výsledném souboru EDN
9. Pole záznamů a jejich převod do formátu EDN
10. Jedno z nejčastějších použití: mapy struktur (záznamů)
11. Import dat ve formátu EDN do aplikace psané v Go
12. Import jednoduché struktury (záznamu) se známým obsahem, import polí
13. Načtení předem neznámé struktury ze souboru ve formátu EDN
14. Problematika načítání EDN v jazycích se sémantikou odlišnou od Clojure
15. Konverze hodnot prováděné v Pythonu
16. Načtení složitějších datových struktur
17. Uživatelem definované štítky (tagy)
18. Práce s uživatelskými štítky v Pythonu
19. Repositář s demonstračními příklady
1. Formát EDN: extensible data notation (dokončení)
V dnešním článku, který navazuje na úterní článek, dokončíme popis použití formátu EDN neboli Extensible Data Notation. Ukážeme si především vybrané příklady použití formátu EDN v programovacím jazyce Go, některé specifické vlastnosti práce s EDN v Pythonu (což souvisí s typovým systémem tohoto jazyka) a taktéž se budeme zabývat problematikou tvorby vlastních štítků (tags). 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. Zmíníme se ovšem i o dalších specifických vlastnostech, například práci se jmennými prostory atd.
Pro začátek si připomeňme, že 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ů. A právě možnost specifikovat množiny, společně s prakticky neomezenými možnosti při použití klíčů v mapách, dělají z EDN dosti flexibilní formát s užitečnou sémentikou (to, že se přenáší množina a nikoli seznam či pole, je zřejmé již do prvního znaku atd.).
2. Serializace hodnot základních datových typů Go do formátu EDN
Pro převod libovolného typu (přesněji řečeno hodnoty libovolného typu) do formátu EDN se v programovacím jazyku Go používá funkce nazvaná Marshal, kterou nalezneme v balíčku encoding/edn (ten ovšem musí být explicitně nainstalován, neboť není součástí standardní knihovny Go):
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.
byteStream, err := edn.Marshal(valueOfAnyType) if err != nil { fmt.Println(string(byteStream)) }
3. Převod celočíselných hodnot do formátu EDN
V dnešním prvním demonstračním příkladu, jehož zdrojový kód naleznete na adrese https://github.com/tisnik/presentations/tree/master/edn/go-edn-basic-types-1 je ukázán způsob konverze celých čísel se znaménkem do formátu EDN. Připomeňme si, že v Go existuje několik celočíselných typů se znaménkem, přičemž některé typy jsou pouze jmennými aliasy:
# | Označení | Rozsah hodnot | Stručný popis |
---|---|---|---|
1 | int8 | –128 až 127 | osmibitové celé číslo se znaménkem |
2 | int16 | –32768 až 32767 | 16bitové celé číslo se znaménkem |
3 | int32 | –2147483648 až 2147483647 | 32bitové celé číslo se znaménkem |
4 | int64 | –9223372036854775808 až 9223372036854775807 | 64bitové celé číslo se znaménkem |
5 | int | různý | odpovídá buď typu int32 nebo int64 |
6 | rune | –2147483648 až 2147483647 | alias pro typ int32, používá se pro znaky |
Převod hodnot všech skupin celočíselných hodnot se znaménkem může být proveden následovně (vynechali jsme pouze typ int, který je aliasem pro int32 nebo int64, a to podle konkrétní použité architektury):
package main import ( "fmt" "olympos.io/encoding/edn" ) func main() { var a int8 = -10 var b int16 = -1000 var c int32 = -10000 var d int32 = -1000000 var r1 rune = 'a' var r2 rune = '\x40' var r3 rune = '\n' var r4 rune = '\u03BB' aEDN, err := edn.Marshal(a) fmt.Println(string(aEDN)) fmt.Println(err) bEDN, err := edn.Marshal(b) fmt.Println(string(bEDN)) fmt.Println(err) cEDN, err := edn.Marshal(c) fmt.Println(string(cEDN)) fmt.Println(err) dEDN, err := edn.Marshal(d) fmt.Println(string(dEDN)) fmt.Println(err) fmt.Println() r1EDN, err := edn.Marshal(r1) fmt.Println(string(r1EDN)) fmt.Println(err) r2EDN, err := edn.Marshal(r2) fmt.Println(string(r2EDN)) fmt.Println(err) r3EDN, err := edn.Marshal(r3) fmt.Println(string(r3EDN)) fmt.Println(err) r4EDN, err := edn.Marshal(r4) fmt.Println(string(r4EDN)) fmt.Println(err) }
Po spuštění demonstračního příkladu by se mělo zobrazit několik textových řádků, z nichž každý reprezentuje validní EDN (a pod ním hodnotu nil značící, že nedošlo k žádné chybě):
-10 <nil> -1000 <nil> -10000 <nil> -1000000 <nil> 97 <nil> 64 <nil> 10 <nil> 955 <nil>
Druhý demonstrační příklad, který najdete na adrese https://github.com/tisnik/presentations/tree/master/edn/go-edn-basic-types-2, provádí tytéž operace, ovšem nad celočíselnými hodnotami bez znaménka:
# | Označení | Rozsah hodnot | Stručný popis |
---|---|---|---|
1 | uint8 | 0 až 255 | osmibitové celé číslo bez znaménka |
2 | uint16 | 0 až 65535 | 16bitové celé číslo bez znaménka |
3 | uint32 | 0 až 4294967295 | 32bitové celé číslo bez znaménka |
4 | uint64 | 0 až 18446744073709551615 | 64bitové celé číslo bez znaménka |
5 | uint | různý | odpovídá buď typu uint32 nebo uint64 |
6 | byte | 0 až 255 | alias pro typ uint8 |
Zdrojový kód tohoto demonstračního příkladu vypadá takto:
package main import ( "fmt" "olympos.io/encoding/edn" ) func main() { var b8 byte = 0x42 var a uint8 = 10 var b uint16 = 1000 var c uint32 = 10000 var d uint32 = 1000000 b8EDN, err := edn.Marshal(b8) fmt.Println(string(b8EDN)) fmt.Println(err) aEDN, err := edn.Marshal(a) fmt.Println(string(aEDN)) fmt.Println(err) bEDN, err := edn.Marshal(b) fmt.Println(string(bEDN)) fmt.Println(err) cEDN, err := edn.Marshal(c) fmt.Println(string(cEDN)) fmt.Println(err) dEDN, err := edn.Marshal(d) fmt.Println(string(dEDN)) fmt.Println(err) }
Po spuštění tohoto demonstračního příkladu získáme deset řádků – pět korektních EDN + informaci o tom, že nedošlo k žádné chybě:
66 <nil> 10 <nil> 1000 <nil> 10000 <nil> 1000000 <nil>
4. Chování systému při pokusu o převod hodnot, které nemají ve formátu EDN podporu
Hodnoty některých datových typů programovacího jazyka Go ovšem nemají ve formátu EDN přímý ekvivalent a proto nejsou převoditelné (alespoň ne automaticky). Týká se to například i datových typů complex64 a complex128, kterými jsou v jazyku Go reprezentována komplexní čísla (dvojice hodnot typu float32, popř. float64):
var a complex64 = -1.5 + 0i var b complex64 = 1.5 + 1000i var c complex64 = 1e30 + 1e30i var d complex64 = 1i
Převod (s kontrolou průběhu převodu) je implementován v dnešním třetím demonstračním příkladu s tímto zdrojovým kódem:
package main import ( "fmt" "olympos.io/encoding/edn" ) func main() { var a complex64 = -1.5 + 0i var b complex64 = 1.5 + 1000i var c complex64 = 1e30 + 1e30i var d complex64 = 1i aEDN, err := edn.Marshal(a) fmt.Println(string(aEDN)) fmt.Println(err) bEDN, err := edn.Marshal(b) fmt.Println(string(bEDN)) fmt.Println(err) cEDN, err := edn.Marshal(c) fmt.Println(string(cEDN)) fmt.Println(err) dEDN, err := edn.Marshal(d) fmt.Println(string(dEDN)) fmt.Println(err) }
Po spuštění takto připraveného demonstračního příkladu je již jasné, že se konverze nezadařila – po každém prázdném řádku (výsledek nepovedené konverze) je totiž vypsána i chyba, ke které došlo:
edn: unsupported type: complex64 edn: unsupported type: complex64 edn: unsupported type: complex64 edn: unsupported type: complex64
5. Hodnoty nil a řetězce převáděné do formátu EDN
Podívejme se nyní na způsob uložení hodnot nil do formátu EDN. Připomeňme si, že s hodnotou nil je v jazyce Go spojen i typ této hodnoty. Podrobnosti jsme si řekli v článku Problematika nulových hodnot v Go, aneb proč nil != nil, v němž jsme si uvedli, že nil může reprezentovat „nulovou hodnotu“ pro některé datové typy:
# | Datový typ | Nulová hodnota |
---|---|---|
1 | bool | false |
2 | int (a varianty) | 0 |
3 | float (obě varianty) | 0.0 |
4 | complex (obě varianty) | 0.0+0.0i |
5 | string | "" |
6 | pointer | nil |
7 | slice | nil |
8 | map | nil |
9 | channel | nil |
10 | function | nil |
11 | interface | nil |
12 | struct | prvky s nulovými hodnotami |
Na druhou stranu ve formátu EDN je typ spojen přímo s hodnotou, přesněji řečeno literál představující hodnotu současně určuje i typ, takže nil zůstane beztypové. Po úplnost je do příkladu přidána i serializace běžného řetězce:
package main import ( "fmt" "olympos.io/encoding/edn" ) func main() { var a interface{} = nil var b map[string]string = nil c := "foo bar baz" aEDN, err := edn.Marshal(a) fmt.Println(string(aEDN)) fmt.Println(err) fmt.Println() bEDN, err := edn.Marshal(b) fmt.Println(string(bEDN)) fmt.Println(err) fmt.Println() cEDN, err := edn.Marshal(c) fmt.Println(string(cEDN)) fmt.Println(err) }
Výsledek běhu tohoto demonstračního příkladu opět ukazuje jak serializované hodnoty, tak i fakt, že při serializaci nedošlo k chybě:
nil <nil> nil <nil> "foo bar baz" <nil>
6. Převod polí a řezů do formátu EDN
S EDN obsahujícími pouze jedinou skalární hodnotu se nesetkáme příliš často, proto se nyní podívejme na způsob práce se strukturovanými daty. Začneme s poli (arrays), popř. s řezy, které se do formátu EDN provádí naprosto stejným způsobem jako pole. Vyzkoušíme si převod polí, v nichž jsou typy prvků shodné:
var a1 [10]byte var a2 [10]int32 a3 := [10]int32{1, 10, 2, 9, 3, 8, 4, 7, 5, 6} a4 := []string{"www", "root", "cz"}
Převádět samozřejmě můžeme i dvourozměrné (popř. i vícerozměrné) pole:
matice := [4][3]float32{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {0, -1, 0}, }
V případě, že je nutné pracovat s poli, v nichž mohou mít prvky rozdílné typy hodnot (což je v EDN relativně častý požadavek), můžeme si v programovacím jazyce Go pomoci poli s prvky typu „prázdné rozhraní“ (přesněji se všemi typy, které prázdné rozhraní implementují – to jsou všechny typy):
a5 := []interface{}{1, "root", 3.1415, true, []int{1, 2, 3, 4}}
Následuje úplný zdrojový kód dnešního pátého demonstračního příkladu, v němž k marshalingu polí (s prvky různých typů) dochází:
package main import ( "fmt" "olympos.io/encoding/edn" ) func main() { var a1 [10]byte var a2 [10]int32 a3 := [10]int32{1, 10, 2, 9, 3, 8, 4, 7, 5, 6} a4 := []string{"www", "root", "cz"} a5 := []interface{}{1, "root", 3.1415, true, []int{1, 2, 3, 4}} matice := [4][3]float32{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {0, -1, 0}, } a1EDN, err := edn.MarshalPPrint(a1, nil) fmt.Println(string(a1EDN)) fmt.Println(err) fmt.Println() a2EDN, err := edn.MarshalPPrint(a2, nil) fmt.Println(string(a2EDN)) fmt.Println(err) fmt.Println() a3EDN, err := edn.MarshalPPrint(a3, nil) fmt.Println(string(a3EDN)) fmt.Println(err) fmt.Println() a4EDN, err := edn.MarshalPPrint(a4, nil) fmt.Println(string(a4EDN)) fmt.Println(err) fmt.Println() a5EDN, err := edn.MarshalPPrint(a5, nil) fmt.Println(string(a5EDN)) fmt.Println(err) fmt.Println() maticeEDN, err := edn.MarshalPPrint(matice, nil) fmt.Println(string(maticeEDN)) fmt.Println(err) }
První tři pole se převedou do formátu JSON takto:
[0 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0] [1 10 2 9 3 8 4 7 5 6]
Další pole obsahuje řetězce, které mohou vypadat následovně:
["www" "root" "cz"]
Následuje pole prvků různých typů (resp. hodnot implementujících prázdné rozhraní):
[1 "root" 3.1415 true [1 2 3 4]]
A konečně posledním případem je dvourozměrné pole hodnot typu float32, které sice nejsou v EDN přímo podporovány, ale je proveden jejich převod na float64/double (což je převod, v němž nedochází ke ztrátě přesnosti ani rozsahu). Výsledek bude následující (jedná se o vektor vektorů):
[[1.0 2.0 3.0] [4.0 5.0 6.0] [7.0 8.0 9.0] [0.0 -1.0 0.0]]
7. Struktury (záznamy) a jejich přímý převod do formátu EDN
Do formátu EDN pochopitelně můžeme převádět i jednotlivé struktury. Struktura se zkonvertuje do EDN ve formě mapy (neboli asociativního pole). Ovšem musíme si dát pozor na to, že převedeny budou jen ty položky záznamu, jejichž jméno začíná velkým písmenem. Ostatně se o tom můžeme snadno přesvědčit při překladu a spuštění dalšího demonstračního příkladu:
package main import ( "fmt" "olympos.io/encoding/edn" ) // User1 je uživatelsky definovaná datová struktura s privátními atributy type User1 struct { id uint32 name string surname string } // User2 je uživatelsky definovaná datová struktura s viditelnými atributy type User2 struct { ID uint32 Name string Surname string } func main() { user1 := User1{ 1, "Pepek", "Vyskoč"} user2 := User2{ 1, "Pepek", "Vyskoč"} fmt.Println("user1") user1EDN, err := edn.Marshal(user1) fmt.Println(string(user1EDN)) fmt.Println(err) fmt.Println() user1PrettyEDN, err := edn.MarshalPPrint(user1, nil) fmt.Println(string(user1PrettyEDN)) fmt.Println(err) fmt.Println() fmt.Println("user2") user2EDN, err := edn.Marshal(user2) fmt.Println(string(user2EDN)) fmt.Println(err) fmt.Println() user2PrettyEDN, err := edn.MarshalPPrint(user2, nil) fmt.Println(string(user2PrettyEDN)) fmt.Println(err) fmt.Println() }
První struktura se převede na prázdný objekt (má položky pojmenované malými písmeny), druhá se již převede korektně:
{} {:iD 1 :name"Pepek":surname"Vyskoč"}
V příkladu je taktéž ukázán přehlednější „pretty-printovaný“ výstup:
{:iD 1, :name "Pepek", :surname "Vyskoč"}
8. Změna názvů klíčů ve výsledném souboru EDN
Velmi často se ovšem setkáme s požadavkem na to, aby měly položky v souboru EDN odlišné označení – ostatně pojmenování položek stylem CamelCase není ve světě EDN příliš běžné. Řešení tohoto problému existuje, i když není příliš elegantní – v Go jsou totiž podporovány takzvané „tagged structs“, což jsou běžné struktury/záznamy, za jejichž položkami jsou v řetězci zapsaném ve zpětných apostrofech uvedeny příslušné názvy, které se mají v EDN objevit:
type User3 struct { ID uint32 `edn:"id"` Name string `edn:"user-name"` Surname string `edn:"surname"` }
Nevýhodou tohoto způsobu deklarace je fakt, že obsah řetězců není překladačem nijak kontrolován, takže se o případných problémech (chybějící uvozovky atd.) dozvíme až v čase běhu aplikace (ideální z testů).
Chování si můžeme ověřit na tomto příkladu:
package main import ( "fmt" "olympos.io/encoding/edn" ) // User3 je uživatelsky definovaná datová struktura s viditelnými atributy type User3 struct { ID uint32 `edn:"id"` Name string `edn:"user-name"` Surname string `edn:"surname"` } func main() { user3 := User3{ 1, "Pepek", "Vyskoč"} fmt.Println("user3") user3EDN, err := edn.Marshal(user3) fmt.Println(string(user3EDN)) fmt.Println(err) fmt.Println() user3PrettyEDN, err := edn.MarshalPPrint(user3, nil) fmt.Println(string(user3PrettyEDN)) fmt.Println(err) fmt.Println() }
S následujícím výsledkem (jsou v něm zobrazeny i případné chyby, které by mohly při převodu nastat):
user3 {:id 1 :user-name"Pepek":surname"Vyskoč"} <nil> {:id 1, :user-name "Pepek", :surname "Vyskoč"} <nil>
9. Pole záznamů a jejich převod do formátu EDN
Často se taktéž setkáme s poli nebo řezy, jejichž prvky jsou struktury. Převod takové hierarchické datové struktury do formátu je stejně přímočarý, jako v předchozím příkladu:
package main import ( "fmt" "olympos.io/encoding/edn" ) // User3 je uživatelsky definovaná datová struktura s viditelnými atributy type User3 struct { ID uint32 `edn:"id"` Name string `edn:"user-name"` Surname string `edn:"surname"` } func main() { var users = [3]User3{ User3{ ID: 1, Name: "Pepek", Surname: "Vyskoč"}, User3{ ID: 2, Name: "Pepek", Surname: "Vyskoč"}, User3{ ID: 3, Name: "Josef", Surname: "Vyskočil"}, } fmt.Println("users") usersEDN, err := edn.Marshal(users) fmt.Println(string(usersEDN)) fmt.Println(err) fmt.Println() usersPrettyEDN, err := edn.MarshalPPrint(users, nil) fmt.Println(string(usersPrettyEDN)) fmt.Println(err) fmt.Println() }
Nenaformátovaný výsledek:
[{:id 1 :user-name"Pepek":surname"Vyskoč"}{:id 2 :user-name"Pepek":surname"Vyskoč"}{:id 3 :user-name"Josef":surname"Vyskočil"}]
Naformátovaný výsledek:
[{:id 1, :user-name "Pepek", :surname "Vyskoč"} {:id 2, :user-name "Pepek", :surname "Vyskoč"} {:id 3, :user-name "Josef", :surname "Vyskočil"}]
10. Jedno z nejčastějších použití: mapy struktur (záznamů)
Pravděpodobně nejčastěji se při práci s formátem EDN setkáme s mapami (resp. asociativními poli), jejichž hodnotami jsou záznamy. Mapy mají v JSON jedno omezené – klíči mohou být řetězce. Toto omezení v programovacím jazyce Go neplatí a neplatí ani pro formát EDN.
V dalším demonstračním příkladu je ukázán převod mapy se dvěma dvojicemi klíč-hodnota. Klíče jsou typu string, hodnotami jsou struktury/záznamy s předem známými prvky:
package main import ( "fmt" "olympos.io/encoding/edn" ) // User3 je uživatelsky definovaná datová struktura s viditelnými atributy type User3 struct { ID uint32 `edn:"id"` Name string `edn:"user-name"` Surname string `edn:"surname"` } func main() { m1 := make(map[string]User3) m1["user-id-1"] = User3{ ID: 1, Name: "Pepek", Surname: "Vyskoč"} m1["user-id-3"] = User3{ ID: 2, Name: "Josef", Surname: "Vyskočil"} fmt.Println("users map") usersMapEDN, _ := edn.Marshal(m1) fmt.Println(string(usersMapEDN)) fmt.Println() usersPrettyEDN, _ := edn.MarshalPPrint(m1, nil) fmt.Println(string(usersPrettyEDN)) fmt.Println() }
Výsledkem je mapa ve formátu EDN, která je v prvním případě nenaformátovaná a ve druhém naopak naformátovaná a tedy uživatelsky čitelná:
{"user-id-1"{:id 1 :user-name"Pepek":surname"Vyskoč"}"user-id-3"{:id 2 :user-name"Josef":surname"Vyskočil"}} {"user-id-1" {:id 1, :user-name "Pepek", :surname "Vyskoč"}, "user-id-3" {:id 2, :user-name "Josef", :surname "Vyskočil"}}
11. Import dat ve formátu EDN do aplikace psané v Go
V této části článku se zaměříme na popis importu dat z formátu EDN do interních datových struktur programovacího jazyka Go. Pro tuto operaci, která se nazývá unmarshaling s následující 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.
12. Import jednoduché struktury (záznamu) se známým obsahem, import polí
Nejjednodušší je situace ve chvíli, kdy přesně (tedy dopředu – již v čase vývoje aplikace) známe strukturu dat. Pokud se v EDN přenáší informace o struktuře (záznamu, objektu), lze tuto strukturu deklarovat jako datový typ, vytvořit proměnnou tohoto typu a následně zavolat výše zmíněnou funkci Unmarshal:
type User struct { ... ... ... } var user User edn.Unmarshal(bytes, &user)
Podívejme se nyní, jak by mohl vypadat celý demonstrační příklad, v němž je EDN načten ze souboru a zpracován:
package main import ( "fmt" "io/ioutil" "log" "olympos.io/encoding/edn" ) // User je uživatelsky definovaná datová struktura s viditelnými atributy type User struct { ID uint32 `edn:"id"` Name string `edn:"user-name"` Surname string `edn:"surname"` } func main() { inputEdnAsBytes, err := ioutil.ReadFile("user.edn") if err != nil { log.Fatal(err) } fmt.Println("Input (bytes):") fmt.Println(inputEdnAsBytes) fmt.Println("\nInput (string):") fmt.Println(string(inputEdnAsBytes)) var user User edn.Unmarshal(inputEdnAsBytes, &user) fmt.Println("\nOutput:") fmt.Println(user) fmt.Println("\nFields:") fmt.Printf("ID: %d\n", user.ID) fmt.Printf("Name: %s\n", user.Name) fmt.Printf("Surname: %s\n", user.Surname) }
Pro vstup:
{:id 1, :user-name "Pepek", :surname "Vyskoč"}
Dostaneme tyto výsledky:
Input (bytes): [123 58 105 100 32 49 44 10 32 58 117 115 101 114 45 110 97 109 101 32 34 80 101 112 101 107 34 44 10 32 58 115 117 114 110 97 109 101 32 34 86 121 115 107 111 196 141 34 125 10 10] Input (string): {:id 1, :user-name "Pepek", :surname "Vyskoč"} Output: {1 Pepek Vyskoč} Fields: ID: 1 Name: Pepek Surname: Vyskoč
Při importu polí využijeme skutečnosti, že v případě použití řezů je možné pole, které je řezem používáno, automaticky zvětšovat při přidávání nových prvků. Tuto operaci za nás provede přímo knihovna pro unmarshalling, takže se nemusíme zabývat (před)alokací pole:
var numbers []int edn.Unmarshal(input_json_as_bytes, &numbers)
13. Načtení předem neznámé struktury ze souboru ve formátu EDN
Nakonec se podívejme, jak se načítá obsah souboru uloženého ve formátu EDN s předem neznámou strukturou. V tomto případě použijeme mapu, jejímiž klíči musí být řetězce a hodnotami libovolný typ implementující prázdné rozhraní interface{}:
m1 := map[string]interface{}{}
Problém spočívá v další interpretaci hodnot, kdy je nutné použít obdobu reflexe. Nicméně alespoň základní práci s takto načtenou mapou lze zajistit:
package main import ( "fmt" "io/ioutil" "log" "olympos.io/encoding/edn" ) func main() { inputEdnAsBytes, err := ioutil.ReadFile("user.edn") if err != nil { log.Fatal(err) } fmt.Println("Input (bytes):") fmt.Println(inputEdnAsBytes) fmt.Println("\nInput (string):") fmt.Println(string(inputEdnAsBytes)) user := map[interface{}]interface{}{} edn.Unmarshal(inputEdnAsBytes, &user) fmt.Println("\nOutput:") fmt.Println(user) fmt.Println("\nFields:") for key, value := range user { fmt.Printf("%v\t%v\n", key, value) } }
Opět si vyzkoušejme použít tento vstupní soubor:
{:id 1, :user-name "Pepek", :surname "Vyskoč"}
Tato mapa by měla být korektně načtena a zpracována:
Input (bytes): [123 58 105 100 32 49 44 10 32 58 117 115 101 114 45 110 97 109 101 32 34 80 101 112 101 107 34 44 10 32 58 115 117 114 110 97 109 101 32 34 86 121 115 107 111 196 141 34 125 10 10] Input (string): {:id 1, :user-name "Pepek", :surname "Vyskoč"} Output: map[:id:1 :surname:Vyskoč :user-name:Pepek] Fields: :user-name Pepek :surname Vyskoč :id 1
14. Problematika načítání EDN v jazycích se sémantikou odlišnou od Clojure
V úvodním článku o formátu EDN jsme si řekli, že je tento formát do značné míry odvozený od vlastností programovacího jazyka Clojure. Tento jazyk přináší vývojářům několik zajímavých vlastností, které ovšem do značné míry určují i vlastnosti EDN – a hlavní problém spočívá v tom, že některé z těchto vlastností nejsou v jiných jazycích podporovány, takže čtečky (či konvertory) EDN musí tato omezení nějakým způsobem obcházet. Abychom byli konkrétnější, jsou v následujících bodech vypsány některé vlastnosti EDN, které mohou být v jiných programovacích jazycích při práci s EDN problematické:
- Ve formátu EDN je možné používat jak „keywords“, tak i řetězce, ovšem mnohé další jazyky „keywords“ nepodporují a v některých případech dokonce automaticky převádí jejich hodnotu na řetězce. To je pochopitelně nekorektní, protože nám to neumožní rozlišit například mezi hodnotou :foo a „foo“; tudíž nelze tyto dvě hodnoty uložit do jedné množiny, použít je jako klíče v jedné mapě atd.
- Ve formátu EDN se taktéž striktně rozlišuje mezi celočíselnými hodnotami a numerickými hodnotami s plovoucí řádovou čárkou. To například znamená, že hodnota 42 je odlišná od hodnoty 42.0 a tudíž je možné obě tyto hodnoty použít jako (rozdílné) klíče v jediné mapě či jako dva prvky v množině. Pro ty programovací jazyky, které mají pouze jeden numerický datový typ, je toto rozlišení minimálně problematické.
- EDN podporuje čtyři typy kontejnerů, a to konkrétně seznamy, vektory, mapy a množiny. Většina ostatních jazyků, které EDN podporují, dokáže pracovat se seznamy a vektory jako s poli (což je v pořádku, liší se jen způsob uložení hodnot) a s mapami (nazývanými většinou asociativní pole). Práce s množinami tedy musí být řešena tak, že se tato hodnota převede na vhodný objekt.
- Již několikrát jsme se v předchozích odstavcích zmínili o mapách. V EDN je možné použít jakékoli hodnoty ve funkci klíče nějakého prvku. To znamená, že klíčem může být například hodnota nil, pochopitelně řetězec, ale i vektor, mapa, mapa obsahující jako své prvky další mapu atd. atd. A tato vlastnost není v mnoha dalších programovacích jazycích, které s EDN pracují, korektně implementována, což znamená, že není možné mapy z EDN přímo převádět na asociativní pole v těchto programovacích jazycích.
V navazujících kapitolách si ukážeme příklady některých EDN, které nejsou či nemusí být ve všech případech správně zpracovány (což do značné míry souvisí se sémantikou daných programovacích jazyků).
15. Konverze hodnot prováděné v Pythonu
Pro otestování vlastností knihovny edn_format určené pro programovací jazyk Python, kterou jsme si popsali minule, použijeme následující jednoduchý skript, který načte soubor ve formátu EDN a zobrazí formu načtených dat:
#!/usr/bin/env python3 # Converts structured data from EDN format into JSON format. import sys import edn_format # Check if command line argument is specified (it is mandatory). if len(sys.argv) > 2: print("Usage:") print(" print_edn.py input_file.edn") print("Example:") print(" print_edn.py report.edn") sys.exit(1) # First command line argument should contain name of input EDN file. filename = sys.argv[1] # 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(payload)
Začneme mapou obsahující celočíselné hodnoty i hodnoty s plovoucí řádovou čárkou:
Vstup:
{"foo" 42 "bar" 42.0}
Výstup:
{'foo': 42, 'bar': 42.0}
V předchozím příkladu by vše v pořádku, takže zkusme použití numerických hodnot ve funkci klíčů:
Vstup:
{42 "a" 42.0 "b"}
Výstup:
{42: 'b'}
V tomto případě došlo ke konverzi a ztrátě jedné z hodnot v mapě.
Použití keywords ve funkci klíčů:
Vstup:
{:foo "a" :bar "b"}
Výstup:
{Keyword(foo): 'a', Keyword(bar): 'b'}
Vše je v pořádku – keywords jsou reprezentovány objektem.
Kombinace keyword a řetězce ve funkci klíče:
Vstup:
{:foo "a" "foo" "b"}
Výstup:
{Keyword(foo): 'a', 'foo': 'b'}
Použití hodnoty nil:
Vstup:
{nil "a" "foo" "b" "nic" nil}
Výstup:
{None: 'a', 'foo': 'b', 'nic': None}
Opět můžeme vidět, že je vše v pořádku.
16. Načtení složitějších datových struktur
Mapa se složenými klíči obsahujícími různé typy hodnot:
Vstup:
{nil "a" "foo" "b" ["a" "b"] "c" [1 2 3] 4 #{1 2 3} 4 "nic" nil}
Výstup:
{None: 'a', 'foo': 'b', ['a', 'b']: 'c', [1, 2, 3]: 4, frozenset({1, 2, 3}): 4, 'nic': None}
Vstupem nemusí být pouze mapa, ale například přímo množina:
#{nil "a" "foo" "b" ["a" "b"] "c" [1 2 3] 4 #{1 2 3} 5 "nic" :nil}
Výstup:
frozenset({'foo', 4, 5, 'a', frozenset({1, 2, 3}), 'nic', [1, 2, 3], None, ['a', 'b'], Keyword(nil), 'b', 'c'})
Často se taktéž setkáme s maticemi:
[[ 1 2 3 4] [ 5 6 7 8] [ 9 10 11 12] [13 14 15 16]]
Výstup:
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]]
Nebo se seznamy seznamů:
(( 1 2 3 4) ( 5 6 7 8) ( 9 10 11 12) (13 14 15 16))
Výstup:
((1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16))
A nakonec reálná data používaná v ekosystému programovacího jazyka Clojure:
{: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 :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 :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" }
Výstup:
{Keyword(aliases): {'downgrade': 'upgrade'}, Keyword(checkout-deps-shares): [Keyword(source-paths), Keyword(test-paths), Keyword(resource-paths), Keyword(compile-path), "#'leiningen.core.classpath/checkout-deps-paths"], Keyword(clean-targets): [Keyword(target-path)], Keyword(compile-path): '/home/ptisnovs/src/presentations/edn/edn2json/target/default/classes', Keyword(dependencies): ([Symbol(org.clojure/clojure), '1.10.1'], [Symbol(org.clojure/data.json), '2.2.0'], [Symbol(nrepl/nrepl), '0.7.0', Keyword(exclusions), ([Symbol(org.clojure/clojure)],)], [Symbol(clojure-complete/clojure-complete), '0.2.5', Keyword(exclusions), ([Symbol(org.clojure/clojure)],)], [Symbol(venantius/ultra), '0.6.0']), ... ... ...
17. Uživatelem definované štítky (tagy)
V úvodním článku jsme se zmínili o možnosti rozšiřovat formát EDN o nové uživatelem definované datové typy. Ty jsou přidávány s využitím takzvaných štítků neboli tagů. Štítek začíná znakem #, za nímž (ihned) následuje jméno štítku a taktéž data, která mohou být ke štítku přiřazena. Již v základní knihovně EDN existují dva rozšiřující štítky a tedy i dva nové 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 rozšiřujících typů je UUID, který se dnes skutečně využívá univerzálně:
#uuid "01234567-89ab-cdef-0123-456789abcdef"
Ovšem nic nám nebrání v tom vytvořit a používat vlastní štítky, například štítek uvozující nový datový typ komplexní číslo, popř. štítek specifikující uživatele (a vytvářející tak vlastně nový datový typ s hodnotou typu řetězec):
[#complex {:real 10.0 :imag 20.0} #user "root"]
Alternativně je možné u štítků používat jmenné prostory, což je u větších aplikací (a při případné snaze o standardizaci štítků) velmi výhodné:
[#cz.root/complex {:real 10.0 :imag 20.0} #cz.root/user "root"]
Podpora pro štítky ve formátu EDN existuje v knihovnách připravených pro jazyk Clojure, Go i Python. V případě jazyka Go je nutné podporu pro zpracování štítků realizovat přímo v programu, který EDN načítá. Konkrétně je nutné zaregistrovat funkci, která příslušný štítek (resp. jeho obsah) zpracuje. To se provádí následujícím způsobem (příklad pro komplexní čísla):
intoComplex := func(v [2]float64) (complex128, error) { return complex(v[0], v[1]), nil } rdr := strings.NewReader(input) dec := edn.NewDecoder(rdr) dec.AddTagFn("complex", intoComplex)
18. Práce s uživatelskými štítky v Pythonu
V případě, že se soubor uložený ve formátu EDN, jenž obsahuje „oštítkovaná data“, pokusíme načíst v našem jednoduchém skriptu, dojde k běhové chybě:
$ ./print_edn.py t1.edn Traceback (most recent call last): File "./print_edn.py", line 23, in payload = edn_format.loads(edn_input.read()) File "/home/ptisnovs/.local/lib/python3.6/site-packages/edn_format/edn_parse.py", line 244, in parse expressions = parse_all(text, **kwargs) File "/home/ptisnovs/.local/lib/python3.6/site-packages/edn_format/edn_parse.py", line 234, in parse_all expressions = p.parse(text, lexer=lex()) File "/home/ptisnovs/.local/lib/python3.6/site-packages/ply/yacc.py", line 333, in parse return self.parseopt_notrack(input, lexer, debug, tracking, tokenfunc) File "/home/ptisnovs/.local/lib/python3.6/site-packages/ply/yacc.py", line 1120, in parseopt_notrack p.callable(pslice) File "/home/ptisnovs/.local/lib/python3.6/site-packages/edn_format/edn_parse.py", line 199, in p_expression_tagged_element u"Don't know how to handle tag ImmutableDict({})".format(tag)) NotImplementedError: Don't know how to handle tag ImmutableDict(complex)
Aby skript formát EDN načetl, je nutné do něj přidat podporu pro oba uživatelsky definované štítky. To je relativně jednoduché, neboť každý štítek je realizován třídou odvozenou od třídy TaggedElement, ke které je štítek přiřazen s využitím dekorátoru:
@tag("user") class User(TaggedElement): def __init__(self, value): print("This is user:", value) self.value=value @tag("complex") class Complex(TaggedElement): def __init__(self, value): print("This is complex number:", value) self.value=value
Upravený skript může vypadat následovně:
import sys import edn_format from edn_format import tag, TaggedElement @tag("user") class User(TaggedElement): def __init__(self, value): print("This is user:", value) self.value=value @tag("complex") class Complex(TaggedElement): def __init__(self, value): print("This is complex number:", value) # Check if command line argument is specified (it is mandatory). if len(sys.argv) < 2: print("Usage:") print(" print_edn.py input_file.edn") print("Example:") print(" print_edn.py report.edn") sys.exit(1) # First command line argument should contain name of input EDN file. filename = sys.argv[1] # 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(payload)
19. Repositář s demonstračními příklady
Zdrojové kódy všech minule i 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/presentations:
# | 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/presentations/blob/master/edn/json2edn.py |
2 | edn2json.py | konverze mezi formátem EDN a JSON naprogramovaná v Pythonu | https://github.com/tisnik/presentations/blob/master/edn/edn2json.py |
3 | json2edn (adresář) | konverze mezi formátem JSON a EDN naprogramovaná v Clojure | https://github.com/tisnik/presentations/blob/master/edn/json2edn |
4 | edn2json (adresář) | konverze mezi formátem EDN a JSON naprogramovaná v Clojure | https://github.com/tisnik/presentations/blob/master/edn/edn2json |
5 | properties2edn (adresář) | konverze mezi .properties souborem a formátem EDN (Clojure) | https://github.com/tisnik/presentations/blob/master/edn/properties2edn |
6 | xml2edn (adresář) | konverze mezi XML a formátem EDN (Clojure, plná konverze) | https://github.com/tisnik/presentations/blob/master/edn/xml2edn |
7 | forest-demo (adresář) | různé možnosti konverze mezi XML a formátem EDN | https://github.com/tisnik/presentations/blob/master/edn/forest-demo |
8 | go-edn-basic-types-1 (adresář) | serializace (marshalling) celočíselných datových typů do formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-basic-types-1 |
9 | go-edn-basic-types-2 (adresář) | serializace (marshalling) celočíselných datových typů do formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-basic-types-2 |
10 | go-edn-basic-types-3 (adresář) | serializace (marshalling) komplexních čísel do formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-basic-types-3 |
11 | go-edn-basic-types-4 (adresář) | serializace (marshalling) hodnot nil a řetězců do formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-basic-types-4 |
12 | go-edn-arrays (adresář) | serializace (marshalling) polí do formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-arrays |
13 | go-edn-1 (adresář) | serializace (marshalling) datové struktury do formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-1 |
14 | go-edn-2 (adresář) | specifikace názvů klíčů v EDN formátu | https://github.com/tisnik/presentations/blob/master/edn/go-edn-2 |
15 | go-edn-3 (adresář) | uložení pole datových struktur do formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-3 |
16 | go-edn-4 (adresář) | uložení mapy datových struktur do formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-4 |
17 | go-edn-5 (adresář) | deserializace (unmarshalling) známé datové struktury z formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-5 |
18 | go-edn-6 (adresář) | deserializace (unmarshalling) neznámé datové struktury z formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/go-edn-6 |
19 | print_edn.py | skript, který vytiskne obsah vybraného souboru uloženého ve formátu EDN | https://github.com/tisnik/presentations/blob/master/edn/print_edn.py |
20. Odkazy na Internetu
- edn – extensible data notation
https://github.com/edn-format/edn - Programming with Data and EDN
https://docs.datomic.com/cloud/whatis/edn.html - Video about EDN
https://docs.datomic.com/cloud/livetutorial/edntutorial.html - (Same) video about EDN on Youtube
https://www.youtube.com/watch?v=5eKgRcvEJxU - clojure.edn
https://clojuredocs.org/clojure.edn - API for clojure.edn – Clojure v1.10.2 (stable)
https://clojure.github.io/clojure/clojure.edn-api.html - Clojure EDN Walkthrough
https://www.compoundtheory.com/clojure-edn-walkthrough/ - Články týkající se Pythonu na Rootu
https://www.root.cz/n/python/ - Články týkající se programovacího jazyka Clojure na Rootu
https://www.root.cz/n/clojure/ - Seriál Programovací jazyk Go
https://www.root.cz/serialy/programovaci-jazyk-go/ - Crux
https://opencrux.com/main/index.html - Crux Installation
https://opencrux.com/reference/21.04–1.16.0/installation.html - read
https://clojuredocs.org/clojure.edn/read - read-string
https://clojuredocs.org/clojure.edn/read-string - Tupelo 21.04.12 (dokumentace)
https://cloojure.github.io/doc/tupelo/ - tupelo – Clojure With A Spoonful of Honey
https://clojars.org/tupelo - Clojure Cookbook: Templating HTML with Enlive
https://github.com/clojure-cookbook/clojure-cookbook/blob/master/07_webapps/7–11_enlive.asciidoc - An Introduction to Enlive
https://github.com/swannodette/enlive-tutorial/ - Enlive na GitHubu
https://github.com/cgrand/enlive - data.json
https://github.com/clojure/data.json - data.json API reference
https://clojure.github.io/data.json/ - Clojure: Writing JSON to a File/Reading JSON From a File
https://dzone.com/articles/clojure-writing-json - How to pretty print JSON to a file in Clojure?
https://stackoverflow.com/questions/23307552/how-to-pretty-print-json-to-a-file-in-clojure - go-edn / edn
https://github.com/go-edn/edn - Queries (Crux)
https://opencrux.com/reference/21.04–1.16.0/queries.html - Essential EDN
https://opencrux.com/tutorials/essential-edn.html - 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/ - Introducing JSON
https://www.json.org/json-en.html - ISO 8601
https://xkcd.com/1179/ - What is the right JSON date format
https://stackoverflow.com/questions/10286204/what-is-the-right-json-date-format - ClojureScript REPL
https://clojurescript.io/