-
Notifications
You must be signed in to change notification settings - Fork 215
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #983 from metosin/shared-schemas
Add Shared Schemas doc
- Loading branch information
Showing
1 changed file
with
230 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
# Reusable Schemas | ||
|
||
Malli currently has two ways for re-using schemas (instances): | ||
|
||
1. Schemas as Vars - *the [plumatic](https://github.com/plumatic/schema) way* | ||
2. Schemas via Global Registry - *the [spec](https://clojure.org/about/spec) way* | ||
3. Schemas via Local Registries | ||
|
||
## Schemas as Vars | ||
|
||
We can define Schemas using `def`: | ||
|
||
```clojure | ||
(require '[malli.core :as m]) | ||
|
||
(def UserId :uuid) | ||
|
||
(def Address | ||
[:map | ||
[:street :string] | ||
[:lonlat [:tuple :double :double]]]) | ||
|
||
(def User | ||
[:map | ||
[:id UserId] | ||
[:name :string] | ||
[:address Address]]) | ||
|
||
(def user | ||
{:id (random-uuid) | ||
:name "Tiina" | ||
:address {:street "Satakunnunkatu 10" | ||
:lonlat [61.5014816, 23.7678986]}}) | ||
|
||
(m/validate User user) | ||
; => true | ||
``` | ||
|
||
All subschemas as inlined as values: | ||
|
||
```clojure | ||
(m/schema User) | ||
;[:map | ||
; [:id :uuid] | ||
; [:name :string] | ||
; [:address [:map | ||
; [:street :string] | ||
; [:lonlat [:tuple :double :double]]]]] | ||
``` | ||
|
||
## Schemas via Global Registry | ||
|
||
To support spec-like mutable registry, we'll define the registry and a helper function to register a schema: | ||
|
||
```clojure | ||
(require '[malli.registry :as mr]) | ||
|
||
(defonce *registry (atom {})) | ||
|
||
(defn register! [type ?schema] | ||
(swap! *registry assoc type ?schema)) | ||
|
||
(mr/set-default-registry! | ||
(mr/composite-registry | ||
(m/default-schemas) | ||
(mr/mutable-registry *registry))) | ||
``` | ||
|
||
Registering Schemas: | ||
|
||
```clojure | ||
(register! ::user-id :uuid) | ||
|
||
(register! ::address [:map | ||
[:street :string] | ||
[:lonlat [:tuple :double :double]]]) | ||
|
||
(register! ::user [:map | ||
[:id ::user-id] | ||
[:name :string] | ||
[:address ::address]]) | ||
|
||
(m/validate ::user user) | ||
; => true | ||
``` | ||
|
||
By default, reference keys are used instead of values: | ||
|
||
```clojure | ||
(m/schema ::user) | ||
; :user/user | ||
``` | ||
|
||
We can recursively deref the Schema to get the values: | ||
|
||
```clojure | ||
(require '[malli.util :as mu]) | ||
|
||
(mu/deref-recursive ::user) | ||
;[:map | ||
; [:id :uuid] | ||
; [:name :string] | ||
; [:address [:map | ||
; [:street :string] | ||
; [:lonlat [:tuple :double :double]]]]] | ||
``` | ||
|
||
### Decomplect Maps, Keys and Values | ||
|
||
Clojure Spec declared [map specs should be of keysets only](https://clojure.org/about/spec#_map_specs_should_be_of_keysets_only). Malli supports this too: | ||
|
||
```clojure | ||
;; (╯°□°)╯︵ ┻━┻ | ||
(reset! *registry {}) | ||
|
||
(register! ::street :string) | ||
(register! ::latlon [:tuple :double :double]) | ||
(register! ::address [:map ::street ::latlon]) | ||
|
||
(register! ::id :uuid) | ||
(register! ::name :string) | ||
(register! ::user [:map ::id ::name ::address]) | ||
|
||
(mu/deref-recursive ::user) | ||
;[:map | ||
; [:user/id :uuid] | ||
; [:user/name :string] | ||
; [:user/address [:map | ||
; [:user/street :string] | ||
; [:user/latlon [:tuple :double :double]]]]] | ||
|
||
;; data has a different shape now | ||
(m/validate ::user {::id (random-uuid) | ||
::name "Maija" | ||
::address {::street "Kuninkaankatu 13" | ||
::latlon [61.5014816, 23.7678986]}}) | ||
; => true | ||
``` | ||
|
||
## Schemas via Local Registries | ||
|
||
Schemas can be defined as a `ref->?schema` map: | ||
|
||
```clojure | ||
(def registry | ||
{::user-id :uuid | ||
::address [:map | ||
[:street :string] | ||
[:lonlat [:tuple :double :double]]] | ||
::user [:map | ||
[:id ::user-id] | ||
[:name :string] | ||
[:address ::address]]}) | ||
``` | ||
|
||
Using registry via Schema properties: | ||
|
||
```clojure | ||
(m/schema [:schema {:registry registry} ::user]) | ||
; => :user/user | ||
``` | ||
|
||
Using registry via options: | ||
|
||
```clojure | ||
(m/schema ::user {:registry (merge (m/default-schemas) registry)}) | ||
``` | ||
|
||
Works with both: | ||
|
||
```clojure | ||
(mu/deref-recursive *1) | ||
;[:map | ||
; [:id :uuid] | ||
; [:name :string] | ||
; [:address [:map | ||
; [:street :string] | ||
; [:lonlat [:tuple :double :double]]]]] | ||
``` | ||
|
||
# Which one should I use? | ||
|
||
Here's a comparison matrix of the two different ways: | ||
|
||
| Feature | Vars | Global Registry | Local Registry | | ||
|----------------------------------|:----:|:---------------:|:--------------:| | ||
| Supported by Malli | ✅ | ✅ | ✅ | | ||
| Explicit require of Schemas | ✅ | ❌ | ✅ | | ||
| Decomplect Maps, Keys and Values | ❌ | ✅ | ✅ | | ||
|
||
You should pick the way what works best for your project. | ||
|
||
[My](https://gist.github.com/ikitommi) personal preference is the Var-style - it's simple and Plumatic proved it works well even with large codebases. | ||
|
||
# Future Work | ||
|
||
1. Could we also decomplect the Maps, Keys and Values with the Var Style? | ||
2. Clear separation of [Entities and Values](https://martinfowler.com/bliki/EvansClassification.html) | ||
3. Utilities for transforming between inlined and referenced models (why? why not!) | ||
|
||
```clojure | ||
(-flatten-refs | ||
[:schema {:registry {::user-id :uuid | ||
::address [:map | ||
[:street :string] | ||
[:lonlat [:tuple :double :double]]] | ||
::user [:map | ||
[:id ::user-id] | ||
[:name :string] | ||
[:address ::address]]}} | ||
::user]) | ||
;[:map {:id :user/user} | ||
; [:id [:uuid {:id :user/user-id}]] | ||
; [:name :string] | ||
; [:address [:map {:id :user/address} | ||
; [:street :string] | ||
; [:lonlat [:tuple :double :double]]]]] | ||
|
||
(-unflatten-refs *1) | ||
;[:schema {:registry {::user-id :uuid | ||
; ::address [:map | ||
; [:street :string] | ||
; [:lonlat [:tuple :double :double]]] | ||
; ::user [:map | ||
; [:id ::user-id] | ||
; [:name :string] | ||
; [:address ::address]]}} | ||
; ::user] | ||
``` | ||
|