Obsah
1. Validace dat s využitím knihovny spec v Clojure 1.9.0
2. Proč je validace dat důležitá a užitečná
3. Kontrola dat přímo v těle funkce
4. Kontrola dat využívající vstupní podmínky (pre-condition)
5. Deklarace validátorů pro složitější datovou strukturu (mapu)
6. První demonstrační příklad – jednoduchá validace mapy, test existence všech klíčů
7. Kontrola hodnot uložených v mapě
9. Validace jména a příjmení v mapě
10. Druhý demonstrační příklad – kontrola hodnot uložených v mapě
11. Validace parametrů předávaných funkci, validace návratových hodnot
12. Třetí demonstrační příklad – validace parametrů předávaných funkci
15. Pátý demonstrační příklad – validace mapy s volitelnými položkami
16. Validace n-tic, kolekcí a sekvencí
17. Možnosti nabízené makrem coll-of
18. Repositář s demonstračními příklady
19. Odkazy na předchozí části tohoto seriálu
1. Validace dat s využitím knihovny spec v Clojure 1.9.0
V předchozím článku jsme si popsali hlavní novinky, s nimiž se mohou vývojáři setkat v Clojure verze 1.9.0. Připomeňme si jen, že se jedná o prozatím poslední stabilní verzi tohoto jazyka, protože Clojure 1.10.0 je stále ještě v alfa stavu (nicméně se již zdá být poměrně stabilní). Jedním důležitým vylepšením, s nímž se můžeme v Clojure 1.9.0 setkat, je knihovna nazvaná spec. Tato knihovna slouží k deklaraci struktury dat a následně k jejich validaci. Složitější datové struktury jsou v jazyku Clojure většinou reprezentovány formou slovníku, který (rekurzivně) obsahuje další struktury, tj. další slovníky, seznamy, vektory či množiny. Jen málokdy se v Clojure setkáme s tím, že by byla datová struktura „zabalena“ do třídy společně s metodami.
Navíc se v Clojure, podobně jako v dalších jazycích, mnohdy velmi intenzivně pracuje se strukturovanými daty reprezentovanými v JSONu; výjimkou nejsou ani situace, kdy k těmto datům není dodáváno JSON schéma. A právě z toho důvodu, že se většinou pracuje s holými (nezabalenými) datovými strukturami, je nutné nějakým způsobem zajišťovat validitu dat. Poměrně často se pro tyto účely používala knihovna Schema (neplést se Scheme :-), ovšem knihovna spec tento problém řeší poněkud odlišným způsobem.
2. Proč je validace dat důležitá a užitečná
Ukažme si jednoduchý příklad, na němž si ukážeme, z jakého důvodu je validace dat důležitá. Představme si, že v aplikaci potřebujeme pracovat s údaji o osobách, například o zaměstnancích. Datová struktura nesoucí informace o jedné osobě se vytvoří triviálně – bude se jednat o obyčejný slovník (asociativní pole), v němž budou klíče představovány hodnotami typu keyword, a to z toho důvodu, že je u těchto hodnot zajištěna jedinečnost:
{:id 10 :name "Rich" :surname "Hickey"}
Poznámka: samozřejmě je možné použít jakoukoli hodnotu pro klíče, například i řetězce. V tomto případě se však bude jednat o zbytečné plýtvání operační pamětí, protože není zaručeno, že každý řetězec (se stejným textem) bude unikátní:
{"id" 10 "name" "Rich" "surname" "Hickey"}
Jak by mohla vypadat funkce akceptující tuto datovou strukturu? V nejjednodušším případě může taková funkce vypadat například následovně. Zde se konkrétně snažíme o uložení struktury do databázové tabulky (funkce je součástí projektu, který naleznete na adrese https://github.com/tisnik/clojure-examples/tree/master/db-store:
(defn store-user-name [db-spec user-name] (jdbc/insert! db-spec "users" user-name))
Způsob použití této funkce:
(require '[clojure.java.jdbc :as jdbc]) ; struktura reprezentující způsob připojení do vybrané databáze (def test-db {:classname "org.sqlite.JDBC" :subprotocol "sqlite" :subname "test.db" }) (store-user-name test-db {:id 1 :name "Rich" :surname "Hickey"})
Databázová tabulka byla vytvořena s následující strukturou:
create table users ( id integer primary key, name text not null, surname text not null );
Zajímavé bude zjistit, co se stane ve chvíli, kdy funkci store-user-name předáme neúplnou datovou struktur, hodnotu jiného typu popř. strukturu, která sice bude obsahovat všechny dvojice klíč-hodnota, ale hodnoty nebudou odpovídat schématu databáze.
Hodnota uložená pod klíčem :id není typu integer:
(store-user-name test-db {:id "XXXXXXXX" :name "Rich" :surname "Hickey"})
Neúplná datová struktura:
(store-user-name test-db {:id 2"})
Namísto mapy se předává odlišná hodnota:
(store-user-name test-db "foobar")
Ve všech těchto případech samozřejmě dojde k chybě, neboť není dodržena struktura vyžadovaná schématem databáze (samotný výpis zásobníkových rámců je dosti nepřehledný, nicméně chybové hlášení je pochopitelné):
Caused by: java.sql.SQLException: [SQLITE_MISMATCH] Data type mismatch (datatype mismatch) at org.sqlite.DB.newSQLException(DB.java:383) at org.sqlite.DB.newSQLException(DB.java:387) at org.sqlite.DB.execute(DB.java:342) at org.sqlite.DB.executeUpdate(DB.java:363) at org.sqlite.PrepStmt.executeUpdate(PrepStmt.java:85) at clojure.java.jdbc$db_do_prepared_return_keys$exec_and_return_keys__497.invoke(jdbc.clj:692) at clojure.java.jdbc$db_do_prepared_return_keys.invokeStatic(jdbc.clj:707) at clojure.java.jdbc$db_do_prepared_return_keys.invoke(jdbc.clj:679) at clojure.java.jdbc$multi_insert_helper$fn__556.invoke(jdbc.clj:897) at clojure.core$map$fn__5587.invoke(core.clj:2747) at clojure.lang.LazySeq.sval(LazySeq.java:40) at clojure.lang.LazySeq.seq(LazySeq.java:49) at clojure.lang.RT.seq(RT.java:528) at clojure.core$seq__5124.invokeStatic(core.clj:137) at clojure.core$dorun.invokeStatic(core.clj:3125) at clojure.core$doall.invokeStatic(core.clj:3140) at clojure.core$doall.invoke(core.clj:3140) at clojure.java.jdbc$multi_insert_helper.invokeStatic(jdbc.clj:896) at clojure.java.jdbc$multi_insert_helper.invoke(jdbc.clj:891) at clojure.java.jdbc$insert_helper$fn__559.invoke(jdbc.clj:907) at clojure.java.jdbc$db_transaction_STAR_.invokeStatic(jdbc.clj:580) at clojure.java.jdbc$db_transaction_STAR_.doInvoke(jdbc.clj:553) at clojure.lang.RestFn.invoke(RestFn.java:425) at clojure.java.jdbc$insert_helper.invokeStatic(jdbc.clj:907) at clojure.java.jdbc$insert_helper.invoke(jdbc.clj:900) at clojure.java.jdbc$insert_BANG_.invokeStatic(jdbc.clj:999) at clojure.java.jdbc$insert_BANG_.doInvoke(jdbc.clj:984) at clojure.lang.RestFn.invoke(RestFn.java:442) at db_store.core$store_user_name.invokeStatic(core.clj:14) at db_store.core$store_user_name.invoke(core.clj:12) at db_store.core$_main.invokeStatic(core.clj:20) at db_store.core$_main.doInvoke(core.clj:16) at clojure.lang.RestFn.invoke(RestFn.java:397) at clojure.lang.Var.invoke(Var.java:377) at user$eval139.invokeStatic(form-init5353435627860668704.clj:1) at user$eval139.invoke(form-init5353435627860668704.clj:1) at clojure.lang.Compiler.eval(Compiler.java:7062) at clojure.lang.Compiler.eval(Compiler.java:7052) at clojure.lang.Compiler.load(Compiler.java:7514) ... 12 more
3. Kontrola dat přímo v těle funkce
Samozřejmě je možné kontrolu dat provádět přímo ve volané funkci, což je pravděpodobně první možnost, která programátora napadne. Ukažme si to na typickém „školním“ příkladu. Bude se jednat o funkci pro výpočet faktoriálu, kterou je možné naprogramovat například tak, že při zadání záporného čísla vyhodí výjimku:
(defn factorial [n] (if (neg? n) (throw (IllegalArgumentException. "negative numbers are not supported!")) (apply * (range 1 (inc n)))))
Otestování chování je snadné:
user=> (factorial 3) 6 user=> (factorial 2) 2 user=> (factorial 1) 1 user=> (factorial 0) 1 user=> (factorial -1) IllegalArgumentException negative numbers are not supported! user/factorial (NO_SOURCE_FILE:3) user=>
Jednoduchý test však nezabrání chybám při předání hodnot odlišného typu (zde nepomůže ani type hint):
clojure9-test.core=> (factorial :x) ClassCastException clojure.lang.Keyword cannot be cast to java.lang.Number clojure.lang.Numbers.isNeg (Numbers.java:100)
clojure9-test.core=> (factorial nil) NullPointerException clojure.lang.Numbers.ops (Numbers.java:1018)
clojure9-test.core=> (factorial "42") ClassCastException java.lang.String cannot be cast to java.lang.Number clojure.lang.Numbers.isNeg (Numbers.java:100)
Další explicitně naprogramovaná kontrol vstupních parametrů vede k již dosti nečitelnému kódu (příklad je ovšem možné napsat i jinak, například s využitím nového predikátu nat-int?):
(defn factorial [n] (if (int? n) (if (neg? n) (throw (IllegalArgumentException. "negative numbers are not supported!")) (apply * (range 1 (inc n)))) (throw (IllegalArgumentException. "only integers are accepted!"))))
Otestování funkčnosti:
clojure9-test.core=> (factorial 10) 3628800
clojure9-test.core=> (factorial -1) IllegalArgumentException negative numbers are not supported! clojure9-test.core/factorial (form-init2633858370945804905.clj:4)
clojure9-test.core=> (factorial :x) IllegalArgumentException only integers are accepted! clojure9-test.core/factorial (form-init2633858370945804905.clj:6)
clojure9-test.core=> (factorial nil) IllegalArgumentException only integers are accepted! clojure9-test.core/factorial (form-init2633858370945804905.clj:6)
4. Kontrola dat využívající vstupní podmínky (pre-condition)
Výše uvedený kód sice můžeme poměrně často vidět v mnoha knihovnách, ovšem ve skutečnosti není pro programovací jazyk Clojure příliš idiomatický. Je tomu tak z toho důvodu, že jazyk Clojure umožňuje ve funkci deklarovat vstupní podmínky (pre-condition) a dokonce i podmínky výstupní (post-condition). Vstupní podmínky jsou pochopitelně kontrolovány při vstupu do funkce, podmínky výstupní pak automaticky ve všech místech, kde funkce předává návratovou hodnotu volajícímu kódu (tj. těsně před implicitně vkládanou instrukcí RETURN). Nejprve se opět podívejme na klasický „školní“ příklad použití vstupní podmínky u funkce factorial, jejíž parametr musí být přirozené číslo:
(defn factorial [n] {:pre [(pos? n)]} (apply * (range 1 (inc n))))
Chování této funkce si můžeme velmi snadno otestovat:
user=> (factorial 3) 6
user=> (factorial 2) 2
user=> (factorial 1) 1
Neočekávané hodnoty (správného typu):
user=> (factorial 0) AssertionError Assert failed: (pos? n) user/factorial (NO_SOURCE_FILE:1)
user=> (factorial -1) AssertionError Assert failed: (pos? n) user/factorial (NO_SOURCE_FILE:1)
Nejzajímavější jsou samozřejmě poslední dva případy, protože v nich je ukázáno, že se kontrola hodnoty vstupního parametru skutečně provádí.
S existencí nových predikátů přidaných do Clojure 1.9.0 navíc můžeme předchozí příklad nepatrně upravit tak, aby akceptoval i nulu (ve skutečnosti samozřejmě nemusíme nutně použít nový predikát, je to však elegantnější):
(defn factorial [n] {:pre [(nat-int? n)]} (apply * (range 1 (inc n))))
Test při předání celých čísel:
clojure9-test.core=> (factorial -1) AssertionError Assert failed: (nat-int? n) clojure9-test.core/factorial (form-init3895554752445190478.clj:1) clojure9-test.core=> (factorial 0) 1 clojure9-test.core=> (factorial 1) 1 clojure9-test.core=> (factorial 2) 2 clojure9-test.core=> (factorial 10) 3628800
Špatný typ hodnot:
clojure9-test.core=> (factorial nil) AssertionError Assert failed: (nat-int? n) clojure9-test.core/factorial (form-init8084795436103962898.clj:1)
clojure9-test.core=> (factorial "foobar") AssertionError Assert failed: (nat-int? n) clojure9-test.core/factorial (form-init8084795436103962898.clj:1)
clojure9-test.core=> (factorial :42) AssertionError Assert failed: (nat-int? n) clojure9-test.core/factorial (form-init8084795436103962898.clj:1)
5. Deklarace validátorů pro složitější datovou strukturu (mapu)
Vraťme se nyní k datové struktuře nesoucí informace o jedné osobě. Nejprve knihovnu spec načteme do interaktivní smyčky REPL:
user=> (require '[clojure.spec.alpha :as spec]) nil
Následně můžeme deklarovat kontrolu na existenci klíčů v datové struktuře. Použijeme přitom již minule popsané makro spec/def a taktéž makro spec/keys, kterému se v nejjednodušším případě předá vektor klíčů. Klíče přitom musí být buď plně kvalifikovány (ve formátu :jmenný-prostor/klíč) nebo lze použít zápis ::klíč pro klíče platné v aktuálně platném jmenném prostoru (taktéž se jedná o novinku v Clojure 1.9.0):
user=> (spec/def ::person? (spec/keys :req [::id ::name ::surname])) :user/person?
Vyzkoušejme si nyní, jak kontrola probíhá:
user=> (spec/valid? ::person? {:id 1 :name "Name" :surname "Surname"}) false user=> (spec/valid? ::person? {::id 1 ::name "Name" ::surname "Surname"}) true user=> (spec/valid? ::person? {::name "Name" ::surname "Surname"}) false
Výsledky jsou možná překvapivé, protože validní je pouze mapa obsahující klíče v aktuálním jmenném prostoru, před nimiž se zapisuje :: („čtyřtečka“). Ve skutečnosti však bude validní i mapa s plně kvalifikovanými klíči:
user=> (spec/valid? ::person? {:user/id 1 :user/name "Name" :user/surname "Surname"}) true
Pro klíče z jiného prostoru to však neplatí:
user=> (spec/valid? ::person? {:test/id 1 :test/name "Name" :test/surname "Surname"}) false
Použití plně kvalifikovaných klíčů je sice doporučováno, ovšem v praxi se mnohem častěji setkáme s datovými strukturami, v nichž jsou použity klíče „obyčejné“, tj. bez jmenného prostoru. Pokud budeme chtít validovat struktury s těmito klíči (a to většinou skutečně budeme chtít), musí se namísto klauzule :req použít klauzule :req-un. Namísto:
user=> (spec/def ::person? (spec/keys :req [::id ::name ::surname])) :user/person?
Použijeme definici:
user=> (spec/def ::person? (spec/keys :req-un [::id ::name ::surname])) :user/person?
Nyní se můžeme pokusit zvalidovat několik datových struktur:
user=> (spec/valid? ::person? {::id -10 ::name "Name" ::surname "Surname"}) false user=> (spec/valid? ::person? {::id 10 ::name "Name" ::surname "Surname"}) false user=> (spec/valid? ::person? {:id -10 :name "Name" :surname "Surname"}) true user=> (spec/valid? ::person? {:id 10 :name "Name" :surname "Surname"}) true
U map samozřejmě nezáleží na pořadí dvojic klíč-hodnota:
user=> (spec/valid? ::person? {:name "Name" :surname "Surname" :id 10}) true
6. První demonstrační příklad – jednoduchá validace mapy, test existence všech klíčů
V dnešním prvním demonstračním příkladu, jehož úplnou strukturu naleznete na adrese https://github.com/tisnik/clojure-examples/tree/master/spec-demo1, je ukázán průběh základní validace s využitím plně kvalifikovaných klíčů popř. klíčů platných pro aktuální jmenný prostor. Povšimněte si, že namísto jmenného prostoru user musíme nyní použít jmenný prostor odpovídající názvu demonstračního příkladu:
(ns spec-demo1.core) (require '[clojure.spec.alpha :as spec]) (defn -main [& args] (spec/def ::person? (spec/keys :req [::id ::name ::surname])) (println "valid?") (println "--------------------------------------------------") (println (spec/valid? ::person? {:id 1 :name "Name" :surname "Surname"})) (println (spec/valid? ::person? {::name "Name" ::surname "Surname"})) (println (spec/valid? ::person? {::id 1 ::name "Name" ::surname "Surname"})) (println (spec/valid? ::person? {:user/id 1 :user/name "Name" :user/surname "Surname"})) (println (spec/valid? ::person? {:spec-demo1.core/id 1 :spec-demo1.core/name "Name" :spec-demo1.core/surname "Surname"})) (println (spec/valid? ::person? {:other.namespace/id 1 :other.namespace/name "Name" :other.namespace/surname "Surname"})) (println "\nexplain") (println "--------------------------------------------------") (println (spec/explain ::person? {:id 1 :name "Name" :surname "Surname"})) (println) (println (spec/explain ::person? {::name "Name" ::surname "Surname"})) (println) (println (spec/explain ::person? {::id 1 ::name "Name" ::surname "Surname"})) (println) (println (spec/explain ::person? {:user/id 1 :user/name "Name" :user/surname "Surname"})) (println) (println (spec/explain ::person? {:spec-demo1.core/id 1 :spec-demo1.core/name "Name" :spec-demo1.core/surname "Surname"})) (println) (println (spec/explain ::person? {:other.namespace/id 1 :other.namespace/name "Name" :other.namespace/surname "Surname"})))
Po spuštění tohoto příkladu by se na standardním výstupu měly objevit přesně tyto řádky:
valid? -------------------------------------------------- false false true false true false explain -------------------------------------------------- val: {:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/id) val: {:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/name) val: {:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/surname) nil val: #:spec-demo1.core{:name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/id) nil Success! nil val: #:user{:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/id) val: #:user{:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/name) val: #:user{:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/surname) nil Success! nil val: #:other.namespace{:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/id) val: #:other.namespace{:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/name) val: #:other.namespace{:id 1, :name "Name", :surname "Surname"} fails spec: :spec-demo1.core/person? predicate: (contains? % :spec-demo1.core/surname) nil
7. Kontrola hodnot uložených v mapě
Kontrolu existence klíčů již máme vyřešenou, ovšem samozřejmě nám zbývá zkontrolovat i hodnoty, aby například nebylo možné pod :id uložit řetězec a pod :name prázdné jméno, číslo nebo vektor. Pro jistotu si znovu zopakujme první validační kritérium, tj. kontrolu na existenci klíčů v mapě:
user=> (spec/def ::person? (spec/keys :req [::id ::name ::surname])) :user/person?
V některých dalších knihovnách určených pro validaci dat by nyní následovala specifikace, jak má vypadat hodnota uložená pod klíčem ::id, ::name atd. V knihovně spec se však používá odlišný postup, protože specifikujeme validátor přímo pojmenovaný po klíči, tj. uvedený zcela mimo vlastní mapu. Například můžeme specifikovat validátor pro hodnotu uloženou pod klíčem ::id:
user=> (spec/def ::id pos-int?) :user/id?
Validaci lze provést pro jednotlivé hodnoty, čímž si současně ověříme, zda je test korektní:
user=> (spec/valid? ::id 10) true user=> (spec/valid? ::id -10) false
Navíc se validace začne „magicky“ provádět i pro hodnoty uložené v mapě:
user=> (spec/valid? ::person? {::id 10 ::name "Name" ::surname "Surname"}) true user=> (spec/valid? ::person? {::id -10 ::name "Name" ::surname "Surname"}) false
Samozřejmě nejsme omezeni pouze na mapy s plně kvalifikovanými klíči. Zkusme si nyní napsat pravidla pro validaci map s klíči bez uvedení jmenného prostoru. Postup je shodný, pouze se namísto klauzule :req použije nám již známá klauzule :req.un:
user=> (spec/def ::person? (spec/keys :req-un [::id ::name ::surname])) :user/person? user=> (spec/def ::id pos-int?) :user/id
Následně otestujeme validaci pro testovací mapy:
user=> (spec/valid? ::person? {:id 10 :name "Name" :surname "Surname"}) true user=> (spec/valid? ::person? {:id -10 :name "Name" :surname "Surname"}) false user=> (spec/valid? ::person? {:id 0 :name "Name" :surname "Surname"}) false user=> (spec/valid? ::person? {:id "foobar" :name "Name" :surname "Surname"}) false
8. Validace jména a příjmení
Kritéria samozřejmě můžeme rozšířit a kromě testu hodnot uložených pod klíčem :id budeme ověřovat i to, zda je zadáno korektní jméno a příjmení. Pro zápis nových predikátů se většinou kvůli sevřenějšímu zápisu používají anonymní funkce, ovšem pokud teprve jednotlivé predikáty ladíme, je lepší použít běžné (pojmenované) funkce. Příkladem může být funkce pro test, zda je zadáno jméno začínající velkým písmenem a pokračující písmeny malými. V tomto případě použijeme běžný regulární výraz:
user=> (defn name? [s] (and (string? s) (re-matches #"[A-Z][a-z]+" s))) #'user/name?
Otestování nového predikátu je snadné:
user=> (name? 42) false user=> (name? "") nil user=> (name? "hello") nil user=> (name? "Pavel") "Pavel" user=> (name? "P") nil
Pokud budete chtít být precizní a skutečně vracet jen hodnoty true nebo false (což ovšem není striktně vyžadováno), je možné predikát nepatrně upravit:
user=> (defn name? [s] (boolean (and (string? s) (re-matches #"[A-Z][a-z]+" s)))) #'user/name?
Nyní se jedná o funkci, která vždy vrátí pravdivostní hodnotu:
user=> (name? "") false user=> (name? "pavel") false user=> (name? "Pavel") true user=> (name? 42) false user=> (name? nil) false
Podobně můžeme vytvořit i funkci pro kontrolu příjmení, kde ovšem povolíme i mezery a víceslovná příjmení:
user=> (defn surname? [s] (and (string? s) (re-matches #"[A-Z][A-Za-z ]+" s))) #'user/surname?
Otestování:
user=> (surname? "Hickey") "Hickey" user=> (surname? "De Boor") "De Boor" user=> (surname? "foobar") nil user=> (surname? 42) false user=> (surname? "") nil
Úprava na striktní predikát:
user=> (defn surname? [s] (boolean (and (string? s) (re-matches #"[A-Z][A-Za-z ]+" s)))) #'user/surname?
Otestování:
user=> (surname? "Hickey") true user=> (surname? "De Boor") true user=> (surname? "foobar") false user=> (surname? 42) false user=> (surname? "") false
Použití predikátů pro validaci jména a příjmení je už jen záležitostí dvou řádků:
user=> (spec/def ::name name?) :user/name user=> (spec/def ::surname surname?) :user/surname
9. Validace jména a příjmení v mapě
Nyní již můžeme všechny kontroly provést nad různými testovacími mapami (nebo nad jinými strukturami).
Specifikace validačních kritérií:
clojure9-test.core=> (ns user) nil user=> (require '[clojure.spec.alpha :as spec]) nil user=> (spec/def ::person? (spec/keys :req-un [::id ::name ::surname])) :user/person? user=> (defn name? [s] (boolean (and (string? s) (re-matches #"[A-Z][a-z]+" s)))) #'user/name? user=> (defn surname? [s] (boolean (and (string? s) (re-matches #"[A-Z][A-Za-z ]+" s)))) #'user/surname? user=> (spec/def ::id pos-int?) :user/id user=> (spec/def ::name name?) :user/name user=> (spec/def ::surname surname?) :user/surname
Otestování, zda validace probíhá tak, jak předpokládáme:
user=> (spec/valid? ::person? nil) false user=> (spec/valid? ::person? "") false user=> (spec/valid? ::person? []) false user=> (spec/valid? ::person? {:id 10 :name "Rich" :surname "Hickey"}) true user=> (spec/valid? ::person? {:id 10 :name "Carl" :surname "De Boor"}) true user=> (spec/valid? ::person? {:id 0 :name "carl" :surname "De Boor"}) false user=> (spec/valid? ::person? {:id 10 :name "carl" :surname "De Boor"}) false
Zjištění, proč nebyla data validována:
user=> (spec/explain ::person? {:id 10 :name "Carl" :surname "De Boor"}) Success! nil user=> (spec/explain ::person? {:id -10 :name "Carl" :surname "De Boor"}) In: [:id] val: -10 fails spec: :user/id at: [:id] predicate: pos-int? nil user=> (spec/explain ::person? {:id -10 :name "Carl"}) In: [:id] val: -10 fails spec: :user/id at: [:id] predicate: pos-int? val: {:id -10, :name "Carl"} fails spec: :user/person? predicate: (contains? % :surname) nil user=> (spec/explain ::person? "") val: "" fails spec: :user/person? predicate: map? nil user=> (spec/explain ::person? []) val: [] fails spec: :user/person? predicate: map? nil user=> (spec/explain ::person? nil) val: nil fails spec: :user/person? predicate: map? nil
10. Druhý demonstrační příklad – kontrola hodnot uložených v mapě
Ve druhém demonstračním příkladu je ukázán způsob validace mapy, v níž se kontroluje jak existence jednotlivých klíčů, tak i hodnot, které jsou na klíče navázány. Identifikátor musí být celé kladné číslo, jméno musí odpovídat zadanému regulárnímu výrazu (první velké písmeno, další písmena malá) a příjmení taktéž musí odpovídat zadanému regulárnímu výrazu. Pro lepší čitelnost jsou všechny nové predikáty rozepsány do neanonymních funkcí:
(ns spec-demo2.core) (require '[clojure.spec.alpha :as spec]) (defn name? [s] (and (string? s) (re-matches #"[A-Z][a-z]+" s))) (defn surname? [s] (and (string? s) (re-matches #"[A-Z][A-Za-z ]+" s))) (defn -main [& args] (spec/def ::id pos-int?) (spec/def ::name name?) (spec/def ::surname surname?) (spec/def ::person? (spec/keys :req-un [::id ::name ::surname])) (println "valid?") (println "--------------------------------------------------") (println (spec/valid? ::person? {:id 10 :name "Rich" :surname "Hickey"})) (println (spec/valid? ::person? {:id 10 :name "rich" :surname "Hickey"})) (println (spec/valid? ::person? {:id 10 :name "Rich" :surname "hickey"})) (println (spec/valid? ::person? {:id -10 :name "Rich" :surname "Hickey"})) (println (spec/valid? ::person? {:id -10 :name "rich" :surname "hickey"})) (println "\nexplain") (println "--------------------------------------------------") (println (spec/explain ::person? {:id 10 :name "Rich" :surname "Hickey"})) (println) (println (spec/explain ::person? {:id 10 :name "rich" :surname "Hickey"})) (println) (println (spec/explain ::person? {:id 10 :name "Rich" :surname "hickey"})) (println) (println (spec/explain ::person? {:id -10 :name "Rich" :surname "Hickey"})) (println) (println (spec/explain ::person? {:id -10 :name "rich" :surname "hickey"})) (println))
valid? -------------------------------------------------- true false false false false explain -------------------------------------------------- Success! nil In: [:name] val: "rich" fails spec: :spec-demo2.core/name at: [:name] predicate: name? nil In: [:surname] val: "hickey" fails spec: :spec-demo2.core/surname at: [:surname] predicate: surname? nil In: [:id] val: -10 fails spec: :spec-demo2.core/id at: [:id] predicate: pos-int? nil In: [:id] val: -10 fails spec: :spec-demo2.core/id at: [:id] predicate: pos-int? In: [:name] val: "rich" fails spec: :spec-demo2.core/name at: [:name] predicate: name? In: [:surname] val: "hickey" fails spec: :spec-demo2.core/surname at: [:surname] predicate: surname? nil
11. Validace parametrů předávaných funkci, validace návratových hodnot
Jakmile máme v centrálním registru uložené validační kritérium ::person?, můžeme si k němu zobrazit i automaticky vygenerovanou nápovědu:
user=> (doc ::person?) ------------------------- :user/person? Spec (keys :req-un [:user/id :user/name :user/surname]) nil
Validaci je samozřejmě možné provést i u volaných funkcí. Mějme například funkci vracející jméno osoby získané z mapy (takovou funkci samozřejmě není zapotřebí programovat, takže se jedná jen o příklad):
(defn get-name [person] (:name person))
Pro validní mapy bude tato funkce vracet i očekávané výsledky:
user=> (spec/explain ::person? {:id 10 :name "Carl" :surname "De Boor"}) Success! nil user=> (get-name {:id 10 :name "Carl" :surname "De Boor"}) "Carl"
Ovšem validátor lze použít přímo ve funkci. Můžeme zvalidovat jak vstupní parametr (mapu), tak i výsledek funkce, a to například následovně:
(defn checked-get-name [person] {:pre [(spec/valid? ::person? person)] :post [(spec/valid? ::name %)]} (:name person))
Chování si opět můžeme snadno otestovat:
user=> (checked-get-name {:id 10 :name "Carl" :surname "De Boor"}) "Carl" user=> (checked-get-name {:id 10 :name "carl" :surname "De Boor"}) AssertionError Assert failed: (spec/valid? :user/person? person) user/checked-get-name (NO_SOURCE_FILE:27)
Pokud například programátor v budoucnu změní způsob generování návratové hodnoty (přidá prefix „Name: “, což je opět jednoduchý příklad; v praxi půjde nejspíše o skutečnou chybu), bude na to ihned upozorněn a bude muset buď funkci opravit nebo změnit validační kritérium:
(defn checked-get-name [person] {:pre [(spec/valid? ::person? person)] :post [(spec/valid? ::name %)]} (str "Name: " (:name person)))
user=> (checked-get-name {:id 10 :name "Carl" :surname "De Boor"}) AssertionError Assert failed: (spec/valid? :user/name %) user/checked-get-name (NO_SOURCE_FILE:33)
12. Třetí demonstrační příklad – validace parametrů předávaných funkci
Ve třetím příkladu je ukázán způsob validace vstupních parametrů funkce i návratové hodnoty funkce. Vše je založeno na příkladech z předchozích kapitol:
(ns spec-demo3.core) (require '[clojure.spec.alpha :as spec]) (defn name? [s] (and (string? s) (re-matches #"[A-Z][a-z]+" s))) (defn surname? [s] (and (string? s) (re-matches #"[A-Z][A-Za-z ]+" s))) (spec/def ::id pos-int?) (spec/def ::name name?) (spec/def ::surname surname?) (spec/def ::person? (spec/keys :req-un [::id ::name ::surname])) (defn get-name [person] (:name person)) (defn checked-get-name [person] {:pre [(spec/valid? ::person? person)] :post [(spec/valid? ::name %)]} (:name person)) (defn -main [& args] (println "get-name") (println (get-name {:id 10 :name "Rich" :surname "Hickey"})) (println (get-name {:id 10 :name "rich" :surname "Hickey"})) (println (get-name {:id 10 :name "Rich" :surname "hickey"})) (println (get-name {:id -10 :name "Rich" :surname "Hickey"})) (println (get-name {:id -10 :name "rich" :surname "hickey"})) (println) (println "checked-get-name") (println (checked-get-name {:id 10 :name "Rich" :surname "Hickey"})) (println (checked-get-name {:id 10 :name "rich" :surname "Hickey"})) (println (checked-get-name {:id 10 :name "Rich" :surname "hickey"})) (println (checked-get-name {:id -10 :name "Rich" :surname "Hickey"})) (println (checked-get-name {:id -10 :name "rich" :surname "hickey"})))
Po spuštění tohoto demonstračního příkladu by měla úspěšně proběhnout všechna volání funkce get-name, ovšem již druhé volání funkce checked-get-name skončí s chybou vlivem nevalidních vstupních dat:
get-name Rich rich Rich Rich rich checked-get-name Rich Exception in thread "main" java.lang.AssertionError: Assert failed: (spec/valid? :spec-demo3.core/person? person), compiling:(/tmp/form-init5569844852904633983.clj:1:73) at clojure.lang.Compiler.load(Compiler.java:7526) at clojure.lang.Compiler.loadFile(Compiler.java:7452) at clojure.main$load_script.invokeStatic(main.clj:278) at clojure.main$init_opt.invokeStatic(main.clj:280) at clojure.main$init_opt.invoke(main.clj:280) at clojure.main$initialize.invokeStatic(main.clj:311) at clojure.main$null_opt.invokeStatic(main.clj:345) at clojure.main$null_opt.invoke(main.clj:342) at clojure.main$main.invokeStatic(main.clj:424) at clojure.main$main.doInvoke(main.clj:387) at clojure.lang.RestFn.applyTo(RestFn.java:137) at clojure.lang.Var.applyTo(Var.java:702) at clojure.main.main(main.java:37) Caused by: java.lang.AssertionError: Assert failed: (spec/valid? :spec-demo3.core/person? person) at spec_demo3.core$checked_get_name.invokeStatic(core.clj:22) at spec_demo3.core$checked_get_name.invoke(core.clj:22) at spec_demo3.core$_main.invokeStatic(core.clj:39) at spec_demo3.core$_main.doInvoke(core.clj:28) at clojure.lang.RestFn.invoke(RestFn.java:397) at clojure.lang.Var.invoke(Var.java:377) at user$eval139.invokeStatic(form-init5569844852904633983.clj:1) at user$eval139.invoke(form-init5569844852904633983.clj:1) at clojure.lang.Compiler.eval(Compiler.java:7062) at clojure.lang.Compiler.eval(Compiler.java:7052) at clojure.lang.Compiler.load(Compiler.java:7514) ... 12 more
13. Čtvrtý demonstrační příklad – chování funkce ve chvíli, kdy není zvalidována její návratová hodnota
Předposlední příklad vznikl úpravou příkladu předchozího. Je v něm definována funkce broken-checked-get-name, která sice má podle zapsané podmínky vracet hodnotu odpovídající validačnímu kritériu ::name, ovšem ve skutečnosti programátor udělal chybu a funkce vrací řetězec, který jménu neodpovídá:
(ns spec-demo4.core) (require '[clojure.spec.alpha :as spec]) (defn name? [s] (and (string? s) (re-matches #"[A-Z][a-z]+" s))) (defn surname? [s] (and (string? s) (re-matches #"[A-Z][A-Za-z ]+" s))) (spec/def ::id pos-int?) (spec/def ::name name?) (spec/def ::surname surname?) (spec/def ::person? (spec/keys :req-un [::id ::name ::surname])) (defn get-name [person] (:name person)) (defn broken-checked-get-name [person] {:pre [(spec/valid? ::person? person)] :post [(spec/valid? ::name %)]} (str "name: " (:name person))) (defn -main [& args] (println "get-name") (println (get-name {:id 10 :name "Rich" :surname "Hickey"})) (println (get-name {:id 10 :name "rich" :surname "Hickey"})) (println (get-name {:id 10 :name "Rich" :surname "hickey"})) (println (get-name {:id -10 :name "Rich" :surname "Hickey"})) (println (get-name {:id -10 :name "rich" :surname "hickey"})) (println) (println "broken-checked-get-name") (println (broken-checked-get-name {:id 10 :name "Rich" :surname "Hickey"})))
Výsledkem je chyba při prvním návratu z funkce broken-checked-get-name:
get-name Rich rich Rich Rich rich broken-checked-get-name Exception in thread "main" java.lang.AssertionError: Assert failed: (spec/valid? :spec-demo4.core/name %), compiling:(/tmp/form-init6747488402196930090.clj:1:73) at clojure.lang.Compiler.load(Compiler.java:7526) at clojure.lang.Compiler.loadFile(Compiler.java:7452) at clojure.main$load_script.invokeStatic(main.clj:278) at clojure.main$init_opt.invokeStatic(main.clj:280) at clojure.main$init_opt.invoke(main.clj:280) at clojure.main$initialize.invokeStatic(main.clj:311) at clojure.main$null_opt.invokeStatic(main.clj:345) at clojure.main$null_opt.invoke(main.clj:342) at clojure.main$main.invokeStatic(main.clj:424) at clojure.main$main.doInvoke(main.clj:387) at clojure.lang.RestFn.applyTo(RestFn.java:137) at clojure.lang.Var.applyTo(Var.java:702) at clojure.main.main(main.java:37) Caused by: java.lang.AssertionError: Assert failed: (spec/valid? :spec-demo4.core/name %) at spec_demo4.core$broken_checked_get_name.invokeStatic(core.clj:22) at spec_demo4.core$broken_checked_get_name.invoke(core.clj:22) at spec_demo4.core$_main.invokeStatic(core.clj:38) at spec_demo4.core$_main.doInvoke(core.clj:28) at clojure.lang.RestFn.invoke(RestFn.java:397) at clojure.lang.Var.invoke(Var.java:377) at user$eval139.invokeStatic(form-init6747488402196930090.clj:1) at user$eval139.invoke(form-init6747488402196930090.clj:1) at clojure.lang.Compiler.eval(Compiler.java:7062) at clojure.lang.Compiler.eval(Compiler.java:7052) at clojure.lang.Compiler.load(Compiler.java:7514) ... 12 more
14. Volitelné položky v mapě
V mnoha případech se setkáme s daty, které obsahují volitelné položky. Když se stále budeme držet našeho příkladu s informacemi o osobách, mohou být těmito volitelnými položkami telefon a e-mailová adresa. Existuje několik způsobů specifikace volitelných položek, ovšem nejjednodušší je použití klauzule opt či opt-un. Za touto klauzulí následuje vektor s klíči, tj. formát zadání je stejný jako u klauzule req i req-un:
(spec/def ::person? (spec/keys :req-un [::id ::name ::surname] :opt-un [::phone ::e-mail]))
Samozřejmě můžete provést definici validačního kritéria i pro mapy s klíči patřícími do aktuálního jmenného prostoru:
(spec/def ::person? (spec/keys :req [::id ::name ::surname] :opt [::phone ::e-mail]))
15. Pátý demonstrační příklad – validace mapy s volitelnými položkami
V dalším demonstračním příkladu je ukázán způsob validace map, které mohou obsahovat volitelné položky. Povšimněte si, že jsou nadefinovány další dva predikáty pro telefonní číslo a e-mailovou adresu (regulární výraz pro adresu jsem si „vypůjčil“ ze Stack Overflow :). Do validační části vstupuje vektor map, což je pravděpodobně nejtypičtější případ při práci s relačními databázemi. Následuje zdrojový kód tohoto demonstračního příkladu:
(ns spec-demo5.core) (require '[clojure.spec.alpha :as spec]) (defn name? [s] (and (string? s) (re-matches #"[A-Z][a-z]+" s))) (defn surname? [s] (and (string? s) (re-matches #"[A-Z][A-Za-z ]+" s))) (defn email? [s] (let [pattern #"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"] (and (string? s) (re-matches pattern s)))) (spec/def ::id pos-int?) (spec/def ::phone pos-int?) (spec/def ::name name?) (spec/def ::surname surname?) (spec/def ::e-mail email?) (spec/def ::person? (spec/keys :req-un [::id ::name ::surname] :opt-un [::phone ::e-mail])) (defn -main "I don't do a whole lot ... yet." [& args] (let [persons [ {:id 10 :name "Rich" :surname "Hickey"} {:id 10 :name "rich" :surname "Hickey"} {:id 10 :name "Rich" :surname "hickey"} {:id -10 :name "Rich" :surname "Hickey"} {:id -10 :name "rich" :surname "hickey"} {:id 10 :name "Rich" :surname "Hickey" :e-mail "rich@somewhere.org"} {:id 10 :name "Rich" :surname "Hickey" :e-mail "wrong"} {:id 10 :name "Rich" :surname "Hickey" :phone 123456789} {:id 10 :name "Rich" :surname "Hickey" :phone nil} {:id 10 :name "Rich" :surname "Hickey" :phone 987654321 :e-mail "rich@somewhere.org"} {:id -10 :name "rich" :surname "" :phone -5 :e-mail "wrong"}]] (println "valid?") (println "-------------------------------") (doseq [person persons] (println person) (println (if (spec/valid? ::person? person) "yes\n" "no\n"))) (println "\n\n\n") (println "explain") (println "-------------------------------") (doseq [person persons] (println person) (println (spec/explain ::person? person)) (println))))
Výsledky vypsané při validaci jednotlivých záznamů:
valid? ------------------------------- {:id 10, :name Rich, :surname Hickey} yes {:id 10, :name rich, :surname Hickey} no {:id 10, :name Rich, :surname hickey} no {:id -10, :name Rich, :surname Hickey} no {:id -10, :name rich, :surname hickey} no {:id 10, :name Rich, :surname Hickey, :e-mail rich@somewhere.org} yes {:id 10, :name Rich, :surname Hickey, :e-mail wrong} no {:id 10, :name Rich, :surname Hickey, :phone 123456789} yes {:id 10, :name Rich, :surname Hickey, :phone nil} no {:id 10, :name Rich, :surname Hickey, :phone 987654321, :e-mail rich@somewhere.org} yes {:id -10, :name rich, :surname , :phone -5, :e-mail wrong} no
Výsledky vypsané při zjišťování, proč nebyl záznam zvalidován:
explain ------------------------------- {:id 10, :name Rich, :surname Hickey} Success! nil {:id 10, :name rich, :surname Hickey} In: [:name] val: "rich" fails spec: :spec-demo5.core/name at: [:name] predicate: name? nil {:id 10, :name Rich, :surname hickey} In: [:surname] val: "hickey" fails spec: :spec-demo5.core/surname at: [:surname] predicate: surname? nil {:id -10, :name Rich, :surname Hickey} In: [:id] val: -10 fails spec: :spec-demo5.core/id at: [:id] predicate: pos-int? nil {:id -10, :name rich, :surname hickey} In: [:id] val: -10 fails spec: :spec-demo5.core/id at: [:id] predicate: pos-int? In: [:name] val: "rich" fails spec: :spec-demo5.core/name at: [:name] predicate: name? In: [:surname] val: "hickey" fails spec: :spec-demo5.core/surname at: [:surname] predicate: surname? nil {:id 10, :name Rich, :surname Hickey, :e-mail rich@somewhere.org} Success! nil {:id 10, :name Rich, :surname Hickey, :e-mail wrong} In: [:e-mail] val: "wrong" fails spec: :spec-demo5.core/e-mail at: [:e-mail] predicate: email? nil {:id 10, :name Rich, :surname Hickey, :phone 123456789} Success! nil {:id 10, :name Rich, :surname Hickey, :phone nil} In: [:phone] val: nil fails spec: :spec-demo5.core/phone at: [:phone] predicate: pos-int? nil {:id 10, :name Rich, :surname Hickey, :phone 987654321, :e-mail rich@somewhere.org} Success! nil {:id -10, :name rich, :surname , :phone -5, :e-mail wrong} In: [:id] val: -10 fails spec: :spec-demo5.core/id at: [:id] predicate: pos-int? In: [:name] val: "rich" fails spec: :spec-demo5.core/name at: [:name] predicate: name? In: [:surname] val: "" fails spec: :spec-demo5.core/surname at: [:surname] predicate: surname? In: [:phone] val: -5 fails spec: :spec-demo5.core/phone at: [:phone] predicate: pos-int? In: [:e-mail] val: "wrong" fails spec: :spec-demo5.core/e-mail at: [:e-mail] predicate: email? nil
16. Validace n-tic, kolekcí a sekvencí
V závěrečné části dnešního článku se seznámíme s dvojicí maker, která jsou velmi užitečná při validaci n-tic (tuple), kolekcí i obecných sekvencí. První makro se jmenuje jednoduše tuple a najdeme ho pochopitelně ve jmenném prostoru spec:
spec-demo5.core=> (doc spec/tuple) ------------------------- clojure.spec.alpha/tuple ([& preds]) Macro takes one or more preds and returns a spec for a tuple, a vector where each element conforms to the corresponding pred. Each element will be referred to in paths using its ordinal. nil
Toto makro je možné použít k otestování typů a popř. i hodnot prvků n-tice, přičemž si pod jménem n-tice můžeme představit vektor (nikoli seznam!) s předem známým počtem prvků. Příkladem může být dvourozměrný bod reprezentovaný dvojicí čísel uložených ve vektoru.
Následuje příklad použití tohoto makra (podobný příklad naleznete v dokumentaci ke knihovně spec):
user=> (spec/def ::point (spec/tuple double? double? double?)) :user/point
Otestování, které dvojice odpovídají dvourozměrnému bodu:
user=> (spec/conform ::point [1.5 2.5 -0.5]) [1.5 2.5 -0.5] user=> (spec/valid? ::point [1.5 2.5 -0.5]) true user=> (spec/valid? ::point [1/2 2.5 -0.5]) false user=> (spec/valid? ::point [1 2 3]) false
Seznam nikdy nebude zvalidován, podobně jako n-tice s jiným počtem prvků či zcela jiný datový typ:
user=> (spec/conform ::point '(1.2 2.3 3.4)) :clojure.spec.alpha/invalid user=> (spec/conform ::point [1.2 2.3 3.4 5.6]) :clojure.spec.alpha/invalid user=> (spec/conform ::point "BOD") :clojure.spec.alpha/invalid user=> (spec/conform ::point {:x 1 :y 2}) :clojure.spec.alpha/invalid user=> (spec/conform ::point nil) :clojure.spec.alpha/invalid
Druhé makro se jmenuje coll-of a jeho použití při validaci je mnohem flexibilnější. Toto makro je možné použít ve chvíli, kdy potřebujeme otestovat delší sekvenci, vektor či seznam. Můžeme přitom specifikovat predikát aplikovaný na prvky sekvence, očekávaný počet prvků v sekvenci, minimální a maximální očekávaný počet prvků, to, zda se prvky mohou opakovat atd. Dokonce je možné specifikovat, zda se má sekvence po validaci převést na jiný typ sekvence (vektor na seznam atd.):
spec-demo5.core=> (doc spec/coll-of) ------------------------- clojure.spec.alpha/coll-of ([pred & opts]) Macro Returns a spec for a collection of items satisfying pred. Unlike 'every', coll-of will exhaustively conform every value. Same options as 'every'. conform will produce a collection corresponding to :into if supplied, else will match the input collection, avoiding rebuilding when possible. See also - every, map-of
17. Možnosti nabízené makrem coll-of
Podívejme se nyní na způsob použití makra coll-of, od těch nejjednodušších příkladů až po příklady nepatrně složitější.
Validace, zda kolekce nebo sekvence obsahují kladná čísla (a nic jiného):
user=> (spec/conform (spec/coll-of pos-int?) [1 2 3 4]) [1 2 3 4] user=> (spec/conform (spec/coll-of pos-int?) '(1 2 3 4)) (1 2 3 4) user=> (spec/conform (spec/coll-of pos-int?) '(-1 2 3 4)) :clojure.spec.alpha/invalid
Test (přesněji řečeno validace) bude provedena i pro generované sekvence:
user=> (spec/conform (spec/coll-of pos-int?) (range 10)) :clojure.spec.alpha/invalid user=> (spec/conform (spec/coll-of pos-int?) (map inc (range 10))) (1 2 3 4 5 6 7 8 9 10)
V případě potřeby je možné specifikovat minimální a maximální počet prvků v sekvenci, případně přímo počet prvků klauzulí :count:
user=> (spec/conform (spec/coll-of nat-int? :count 10) (range 10)) (0 1 2 3 4 5 6 7 8 9) user=> (spec/conform (spec/coll-of nat-int? :count 10) (range 9)) :clojure.spec.alpha/invalid user=> (spec/conform (spec/coll-of nat-int? :count 10) (range 11)) :clojure.spec.alpha/invalid user=> (spec/conform (spec/coll-of nat-int? :min-count 5 :max-count 20) (range 10)) (0 1 2 3 4 5 6 7 8 9) user=> (spec/conform (spec/coll-of nat-int? :min-count 5 :max-count 20) (range 30)) :clojure.spec.alpha/invalid user=> (spec/conform (spec/coll-of nat-int? :min-count 5 :max-count 20) (range 3)) :clojure.spec.alpha/invalid
Další možností je test, jakého typu je vstupní sekvence. Sekvence generovaná pomocí range skutečně není vektorem atd.:
user=> (spec/conform (spec/coll-of nat-int? :min-count 5 :max-count 20 :kind vector?) (range 10)) :clojure.spec.alpha/invalid user=> (spec/conform (spec/coll-of nat-int? :min-count 5 :max-count 20 :kind vector?) (into [] (range 10))) [0 1 2 3 4 5 6 7 8 9]
Taktéž je možné testovat, jestli se v sekvenci vyskytují některé prvky dvakrát či vícekrát:
user=> (spec/conform (spec/coll-of nat-int? :min-count 5 :max-count 20 :kind vector? :distinct true) [1 2 3 4 5 6]) [1 2 3 4 5 6] user=> (spec/conform (spec/coll-of nat-int? :min-count 5 :max-count 20 :kind vector? :distinct true) [1 2 3 1 5 6]) :clojure.spec.alpha/invalid
Poslední užitečná vlastnost – automatické převedení na jinou sekvenci v případě úspěšné validace:
user=> (spec/conform (spec/coll-of nat-int? :min-count 5 :max-count 20 :kind vector? :distinct true :into '()) [1 2 3 4 5 6]) (1 2 3 4 5 6)
V dalším článku si popíšeme poslední velmi užitečnou vlastnost – možnost deklarovat validační kritéria způsobem, který používá klauzule podobné „žolíkům“ z regulárních výrazů.
18. Repositář s demonstračními příklady
Všechny demonstrační příklady a projekty určené pro Clojure verze 1.9.0 byly uloženy do repositáře https://github.com/tisnik/clojure-examples. Následují odkazy na jednotlivé projekty:
Projekt | Popis | Odkaz |
---|---|---|
db-store | uložení mapy do databáze | https://github.com/tisnik/clojure-examples/tree/master/db-store |
spec-demo1 | test existence klíčů v mapě | https://github.com/tisnik/clojure-examples/tree/master/spec-demo1 |
spec-demo2 | kontrola hodnot uložených v mapě | https://github.com/tisnik/clojure-examples/tree/master/spec-demo2 |
spec-demo3 | validace parametrů funkce | https://github.com/tisnik/clojure-examples/tree/master/spec-demo3 |
spec-demo4 | validace návratových hodnot | https://github.com/tisnik/clojure-examples/tree/master/spec-demo4 |
spec-demo5 | validace návratových hodnot | https://github.com/tisnik/clojure-examples/tree/master/spec-demo5 |
Pro spuštění projektů je vyžadován nainstalovaný správce projektů Leiningen.
19. Odkazy na předchozí části tohoto seriálu
- Clojure 1: Úvod
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm/ - Clojure 2: Symboly, kolekce atd.
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-2-cast/ - Clojure 3: Funkcionální programování
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-3-cast-funkcionalni-programovani/ - Clojure 4: Kolekce, sekvence a lazy sekvence
http://www.root.cz/clanky/clojure-aneb-jazyk-umoznujici-tvorbu-bezpecnych-vicevlaknovych-aplikaci-pro-jvm-4-cast-kolekce-sekvence-a-lazy-sekvence/ - Clojure 5: Sekvence, lazy sekvence a paralelní programy
http://www.root.cz/clanky/clojure-a-bezpecne-aplikace-pro-jvm-sekvence-lazy-sekvence-a-paralelni-programy/ - Clojure 6: Podpora pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-6-futures-nejsou-jen-financni-derivaty/ - Clojure 7: Další funkce pro paralelní programování
http://www.root.cz/clanky/programovaci-jazyk-clojure-7-dalsi-podpurne-prostredky-pro-paralelni-programovani/ - Clojure 8: Identity, stavy, neměnné hodnoty a reference
http://www.root.cz/clanky/programovaci-jazyk-clojure-8-identity-stavy-nemenne-hodnoty-a-referencni-typy/ - Clojure 9: Validátory, pozorovatelé a kooperace s Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-9-validatory-pozorovatele-a-kooperace-mezi-clojure-a-javou/ - Clojure 10: Kooperace mezi Clojure a Javou
http://www.root.cz/clanky/programovaci-jazyk-clojure-10-kooperace-mezi-clojure-a-javou-pokracovani/ - Clojure 11: Generátorová notace seznamu/list comprehension
http://www.root.cz/clanky/programovaci-jazyk-clojure-11-generatorova-notace-seznamu-list-comprehension/ - Clojure 12: Překlad programů z Clojure do bajtkódu JVM I:
http://www.root.cz/clanky/programovaci-jazyk-clojure-12-preklad-programu-z-clojure-do-bajtkodu-jvm/ - Clojure 13: Překlad programů z Clojure do bajtkódu JVM II:
http://www.root.cz/clanky/programovaci-jazyk-clojure-13-preklad-programu-z-clojure-do-bajtkodu-jvm-pokracovani/ - Clojure 14: Základy práce se systémem maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-14-zaklady-prace-se-systemem-maker/ - Clojure 15: Tvorba uživatelských maker
http://www.root.cz/clanky/programovaci-jazyk-clojure-15-tvorba-uzivatelskych-maker/ - Clojure 16: Složitější uživatelská makra
http://www.root.cz/clanky/programovaci-jazyk-clojure-16-slozitejsi-uzivatelska-makra/ - Clojure 17: Využití standardních maker v praxi
http://www.root.cz/clanky/programovaci-jazyk-clojure-17-vyuziti-standardnich-maker-v-praxi/ - Clojure 18: Základní techniky optimalizace aplikací
http://www.root.cz/clanky/programovaci-jazyk-clojure-18-zakladni-techniky-optimalizace-aplikaci/ - Clojure 19: Vývojová prostředí pro Clojure
http://www.root.cz/clanky/programovaci-jazyk-clojure-19-vyvojova-prostredi-pro-clojure/ - Clojure 20: Vývojová prostředí pro Clojure (Vimu s REPL)
http://www.root.cz/clanky/programovaci-jazyk-clojure-20-vyvojova-prostredi-pro-clojure-integrace-vimu-s-repl/ - Clojure 21: ClojureScript aneb překlad Clojure do JS
http://www.root.cz/clanky/programovaci-jazyk-clojure-21-clojurescript-aneb-preklad-clojure-do-javascriptu/ - Leiningen: nástroj pro správu projektů napsaných v Clojure
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (2)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-2/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (3)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-3/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (4)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-4/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (5)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-5/ - Leiningen: nástroj pro správu projektů napsaných v Clojure (6)
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-6/ - Programovací jazyk Clojure a databáze (1.část)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-databaze-1-cast/ - Pluginy pro Leiningen
http://www.root.cz/clanky/leiningen-nastroj-pro-spravu-projektu-napsanych-v-clojure-pluginy-pro-leiningen/ - Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi/ - Programovací jazyk Clojure a knihovny pro práci s vektory a maticemi (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-knihovny-pro-praci-s-vektory-a-maticemi-2/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-2/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (2)
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-2/ - Seesaw: knihovna pro snadnou tvorbu GUI v jazyce Clojure (3)
http://www.root.cz/clanky/seesaw-knihovna-pro-snadnou-tvorbu-gui-v-jazyce-clojure-3/ - Programovací jazyk Clojure a práce s Gitem
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem/ - Programovací jazyk Clojure: syntéza procedurálních textur s využitím knihovny Clisk (dokončení)
http://www.root.cz/clanky/programovaci-jazyk-clojure-synteza-proceduralnich-textur-s-vyuzitim-knihovny-clisk-dokonceni/ - Programovací jazyk Clojure a práce s Gitem (2)
http://www.root.cz/clanky/programovaci-jazyk-clojure-a-prace-s-gitem-2/ - Programovací jazyk Clojure – triky při práci s řetězci
http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-retezci/ - Programovací jazyk Clojure – triky při práci s kolekcemi
http://www.root.cz/clanky/programovaci-jazyk-clojure-triky-pri-praci-s-kolekcemi/ - Programovací jazyk Clojure – práce s mapami a množinami
http://www.root.cz/clanky/programovaci-jazyk-clojure-prace-s-mapami-a-mnozinami/ - Programovací jazyk Clojure – základy zpracování XML
http://www.root.cz/clanky/programovaci-jazyk-clojure-zaklady-zpracovani-xml/ - Programovací jazyk Clojure – testování s využitím knihovny Expectations
http://www.root.cz/clanky/programovaci-jazyk-clojure-testovani-s-vyuzitim-knihovny-expectations/ - Programovací jazyk Clojure – některé užitečné triky použitelné (nejenom) v testech
http://www.root.cz/clanky/programovaci-jazyk-clojure-nektere-uzitecne-triky-pouzitelne-nejenom-v-testech/ - Enlive – výkonný šablonovací systém pro jazyk Clojure
http://www.root.cz/clanky/enlive-vykonny-sablonovaci-system-pro-jazyk-clojure/ - Nástroj Leiningen a programovací jazyk Clojure: tvorba vlastních knihoven pro veřejný repositář Clojars
http://www.root.cz/clanky/nastroj-leiningen-a-programovaci-jazyk-clojure-tvorba-vlastnich-knihoven-pro-verejny-repositar-clojars/ - Novinky v Clojure verze 1.8.0
http://www.root.cz/clanky/novinky-v-clojure-verze-1–8–0/ - Asynchronní programování v Clojure s využitím knihovny core.async
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async/ - Asynchronní programování v Clojure s využitím knihovny core.async (pokračování)
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async-pokracovani/ - Asynchronní programování v Clojure s využitím knihovny core.async (dokončení)
http://www.root.cz/clanky/asynchronni-programovani-v-clojure-s-vyuzitim-knihovny-core-async-dokonceni/ - Vytváříme IRC bota v programovacím jazyce Clojure
http://www.root.cz/clanky/vytvarime-irc-bota-v-programovacim-jazyce-clojure/ - Gorilla REPL: interaktivní prostředí pro programovací jazyk Clojure
https://www.root.cz/clanky/gorilla-repl-interaktivni-prostredi-pro-programovaci-jazyk-clojure/ - Multimetody v Clojure aneb polymorfismus bez použití OOP
https://www.root.cz/clanky/multimetody-v-clojure-aneb-polymorfismus-bez-pouziti-oop/ - Práce s externími Java archivy v programovacím jazyku Clojure
https://www.root.cz/clanky/prace-s-externimi-java-archivy-v-programovacim-jazyku-clojure/ - Pixie: lehký skriptovací jazyk s „kouzelnými“ schopnostmi
https://www.root.cz/clanky/pixie-lehky-skriptovaci-jazyk-s-kouzelnymi-schopnostmi/ - Programovací jazyk Pixie: funkce ze základní knihovny a použití FFI
https://www.root.cz/clanky/programovaci-jazyk-pixie-funkce-ze-zakladni-knihovny-a-pouziti-ffi/ - Novinky v Clojure verze 1.9.0
https://www.root.cz/clanky/novinky-v-clojure-verze-1–9–0/
20. Odkazy na Internetu
- 5 Differences between clojure.spec and Schema
https://lispcast.com/clojure.spec-vs-schema/ - Schema: Clojure(Script) library for declarative data description and validation
https://github.com/plumatic/schema - Zip archiv s Clojure 1.9.0
http://repo1.maven.org/maven2/org/clojure/clojure/1.9.0/clojure-1.9.0.zip - Clojure 1.9 is now available
https://clojure.org/news/2017/12/08/clojure19 - Deps and CLI Guide
https://clojure.org/guides/deps_and_cli - Changes to Clojure in Version 1.9
https://github.com/clojure/clojure/blob/master/changes.md - clojure.spec – Rationale and Overview
https://clojure.org/about/spec - Zip archiv s Clojure 1.8.0
http://repo1.maven.org/maven2/org/clojure/clojure/1.8.0/clojure-1.8.0.zip - Clojure 1.8 is now available
http://clojure.org/news/2016/01/19/clojure18 - Socket Server REPL
http://dev.clojure.org/display/design/Socket+Server+REPL - CLJ-1671: Clojure socket server
http://dev.clojure.org/jira/browse/CLJ-1671 - CLJ-1449: Add clojure.string functions for portability to ClojureScript
http://dev.clojure.org/jira/browse/CLJ-1449 - Launching a Socket Server
http://clojure.org/reference/repl_and_main#_launching_a_socket_server - API for clojure.string
http://clojure.github.io/clojure/branch-master/clojure.string-api.html - Clojars:
https://clojars.org/ - Seznam knihoven na Clojars:
https://clojars.org/projects - 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 - Expectations: příklady atd.
http://jayfields.com/expectations/ - Expectations na GitHubu
https://github.com/jaycfields/expectations - Lein-expectations na GitHubu
https://github.com/gar3thjon3s/lein-expectations - Testing Clojure With Expectations
https://semaphoreci.com/blog/2014/09/23/testing-clojure-with-expectations.html - Clojure testing TDD/BDD libraries: clojure.test vs Midje vs Expectations vs Speclj
https://www.reddit.com/r/Clojure/comments/1viilt/clojure_testing_tddbdd_libraries_clojuretest_vs/ - Testing: One assertion per test
http://blog.jayfields.com/2007/06/testing-one-assertion-per-test.html - Rewriting Your Test Suite in Clojure in 24 hours
http://blog.circleci.com/rewriting-your-test-suite-in-clojure-in-24-hours/ - Clojure doc: zipper
http://clojuredocs.org/clojure.zip/zipper - Clojure doc: parse
http://clojuredocs.org/clojure.xml/parse - Clojure doc: xml-zip
http://clojuredocs.org/clojure.zip/xml-zip - Clojure doc: xml-seq
http://clojuredocs.org/clojure.core/xml-seq - Parsing XML in Clojure
https://github.com/clojuredocs/guides - Clojure Zipper Over Nested Vector
https://vitalyper.wordpress.com/2010/11/23/clojure-zipper-over-nested-vector/ - Understanding Clojure's PersistentVector implementation
http://blog.higher-order.net/2009/02/01/understanding-clojures-persistentvector-implementation - Understanding Clojure's PersistentHashMap (deftwice…)
http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice.html - Assoc and Clojure's PersistentHashMap: part ii
http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap-part-ii.html - Ideal Hashtrees (paper)
http://lampwww.epfl.ch/papers/idealhashtrees.pdf - Clojure home page
http://clojure.org/ - Clojure (downloads)
http://clojure.org/downloads - Clojure Sequences
http://clojure.org/sequences - Clojure Data Structures
http://clojure.org/data_structures - The Structure and Interpretation of Computer Programs: 2.2.1 Representing Sequences
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-15.html#%_sec2.2.1 - The Structure and Interpretation of Computer Programs: 3.3.1 Mutable List Structure
http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-22.html#%_sec3.3.1 - Clojure – Functional Programming for the JVM
http://java.ociweb.com/mark/clojure/article.html - Clojure quick reference
http://faustus.webatu.com/clj-quick-ref.html - 4Clojure
http://www.4clojure.com/ - ClojureDoc (rozcestník s dokumentací jazyka Clojure)
http://clojuredocs.org/ - Clojure (na Wikipedia EN)
http://en.wikipedia.org/wiki/Clojure - Clojure (na Wikipedia CS)
http://cs.wikipedia.org/wiki/Clojure - SICP (The Structure and Interpretation of Computer Programs)
http://mitpress.mit.edu/sicp/ - Pure function
http://en.wikipedia.org/wiki/Pure_function - Funkcionální programování
http://cs.wikipedia.org/wiki/Funkcionální_programování - Čistě funkcionální (datové struktury, jazyky, programování)
http://cs.wikipedia.org/wiki/Čistě_funkcionální - Clojure Macro Tutorial (Part I, Getting the Compiler to Write Your Code For You)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-i-getting.html - Clojure Macro Tutorial (Part II: The Compiler Strikes Back)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-compiler.html - Clojure Macro Tutorial (Part III: Syntax Quote)
http://www.learningclojure.com/2010/09/clojure-macro-tutorial-part-ii-syntax.html - Tech behind Tech: Clojure Macros Simplified
http://techbehindtech.com/2010/09/28/clojure-macros-simplified/ - Fatvat – Exploring functional programming: Clojure Macros
http://www.fatvat.co.uk/2009/02/clojure-macros.html - Eulerovo číslo
http://cs.wikipedia.org/wiki/Eulerovo_číslo - List comprehension
http://en.wikipedia.org/wiki/List_comprehension - List Comprehensions in Clojure
http://asymmetrical-view.com/2008/11/18/list-comprehensions-in-clojure.html - Clojure Programming Concepts: List Comprehension
http://en.wikibooks.org/wiki/Clojure_Programming/Concepts#List_Comprehension - Clojure core API: for macro
http://clojure.github.com/clojure/clojure.core-api.html#clojure.core/for - cirrus machina – The Clojure for macro
http://www.cirrusmachina.com/blog/comment/the-clojure-for-macro/ - Riastradh's Lisp Style Rules
http://mumble.net/~campbell/scheme/style.txt - Dynamic Languages Strike Back
http://steve-yegge.blogspot.cz/2008/05/dynamic-languages-strike-back.html - Scripting: Higher Level Programming for the 21st Century
http://www.tcl.tk/doc/scripting.html - Java Virtual Machine Support for Non-Java Languages
http://docs.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html - Třída java.lang.String
http://docs.oracle.com/javase/7/docs/api/java/lang/String.html - Třída java.lang.StringBuffer
http://docs.oracle.com/javase/7/docs/api/java/lang/StringBuffer.html - Třída java.lang.StringBuilder
http://docs.oracle.com/javase/7/docs/api/java/lang/StringBuilder.html - StringBuffer versus String
http://www.javaworld.com/article/2076072/build-ci-sdlc/stringbuffer-versus-string.html - Threading macro (dokumentace k jazyku Clojure)
https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/-> - Understanding the Clojure → macro
http://blog.fogus.me/2009/09/04/understanding-the-clojure-macro/ - clojure.inspector
http://clojure.github.io/clojure/clojure.inspector-api.html - The Clojure Toolbox
http://www.clojure-toolbox.com/ - Unit Testing in Clojure
http://nakkaya.com/2009/11/18/unit-testing-in-clojure/ - Testing in Clojure (Part-1: Unit testing)
http://blog.knoldus.com/2014/03/22/testing-in-clojure-part-1-unit-testing/ - API for clojure.test – Clojure v1.6 (stable)
https://clojure.github.io/clojure/clojure.test-api.html - Leiningen: úvodní stránka
http://leiningen.org/ - Leiningen: Git repository
https://github.com/technomancy/leiningen - leiningen-win-installer
http://leiningen-win-installer.djpowell.net/ - Clojure.org: Vars and the Global Environment
http://clojure.org/Vars - Clojure.org: Refs and Transactions
http://clojure.org/Refs - Clojure.org: Atoms
http://clojure.org/Atoms - Clojure.org: Agents as Asynchronous Actions
http://clojure.org/agents - Transient Data Structureshttp://clojure.org/transients