Skip to content

Commit 4685e7f

Browse files
committed
Add best practices
1 parent 85fb463 commit 4685e7f

1 file changed

Lines changed: 185 additions & 1 deletion

File tree

src/malli/elements_of_malli.clj

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
(:require
1212
[clojure.spec.alpha :as s]
1313
[babashka.http-client.websocket :as ws]
14+
[clojure.data :as data]
1415
[malli.core :as m]
1516
[malli.util :as mu]
1617
[malli.transform :as mt]
@@ -631,4 +632,187 @@
631632
{:registry registry})
632633

633634
;; If the value passes muster, we get a coerced value back, otherwise
634-
;; coercion will fail in a configurable fashion.
635+
;; coercion will fail in a configurable fashion and explode by default.
636+
637+
;; The opposite of decoding is encoding. Some values must be converted to other types before getting sent over the wire. Big decimals to strings, java Instants to string or UNIX timestamps, etc.
638+
639+
(let [[only-in-decoded only-in-encoded in-both] (data/diff decoded (m/encode BinanceUserEvent3 decoded {:registry registry} mt/string-transformer))]
640+
(def only-in-encoded only-in-encoded)
641+
(def only-in-decoded only-in-decoded)
642+
(def in-both in-both))
643+
644+
only-in-decoded
645+
only-in-encoded
646+
647+
;; Integers got encoded as strings, decimals didn't
648+
649+
;; This could potentially be an issue for malli
650+
651+
;; Transformers are actually interceptors and can be composed and
652+
;; chained in a variety of interesting ways. Interesting in how
653+
;; confusing they are, so if you can avoid the non trivial use cases,
654+
;; please do.
655+
656+
;; # Custom Schemas
657+
658+
;; # Recommendations and Best Practices (The Meta)
659+
660+
;; ## Names
661+
662+
;; Choosing names is hard. Choose names that map to domain entities and attributes:
663+
664+
;; ```clojure
665+
;; IntRange100 => CampaignID
666+
;; User
667+
;; Version
668+
;; CampaignName
669+
;; CharacterAlignment
670+
;; ```
671+
672+
;; - Why?
673+
;; - Domain Modeling
674+
;; - What do schemas *mean*?
675+
;; - They model our domain with types and predicates
676+
;; - Similar to a type system
677+
;; - Types reflect the domain entities and data
678+
;; - We don't have `IntegerInRange100` entity or property
679+
;; - Reuse
680+
;; - `MySchema` can be used in more than one place.
681+
;; - By naming the domain entity we make it possible to be reused correctly
682+
;; - One change propagates across the code base.
683+
;; - No need to change every occurrence of `:campaign-id`
684+
685+
;; How?
686+
687+
(def CampaignID [:int {:max 100 :min 0}])
688+
(def Message [:map [:campaign-id CampaignID]])
689+
690+
;; IF for some esoteric reason it appears more than once, feel free to define that range
691+
692+
(def IntRange0To100 [:int {:max 100 :min 0}])
693+
(def CampaignID IntRange0To100)
694+
(def Foo IntRange0To100)
695+
(def Message [:map [:campaign-id CampaignID] [:foo Foo]])
696+
697+
;; - Use good case
698+
;; - Use `PascalCase` for `def` forms
699+
;; - `camelCase` is not `PascalCase`
700+
;; - Acronyms have a consistent case
701+
;; - `url` `:url` `URL`, not `Url`
702+
;; - Therefor `UserID` not `UserId`
703+
;; - Use `::snake-case` or `"PascalCase"` for schema references
704+
;; - Example
705+
;; ```clojure
706+
;; QueryParams
707+
;; ::query-params
708+
;; ```
709+
;; - Don't
710+
;; - `SHOUT-CASE`
711+
;; - Have some style
712+
;; - This is Clojure, everything in `def` is constant anyway
713+
;; - `snake-case`
714+
;; - It is good to provide a way to visually distinguish domain model definitions from any values
715+
;; - Rationale
716+
;; - Need a way to visually distinguish schemas
717+
;; - The convention across the programming world is naming types and domain entities in `PascalCase`
718+
719+
;; Schema references as strings or keywords for custom schema types require a registry, see the cons example above.
720+
721+
;; - Avoid noisy names
722+
;; - No need to add a `-schema` suffix to everything
723+
;; - A good and consistent naming convention can make it clear
724+
;; - Works well with using a good case
725+
;; - Compare `User` vs. `USER-SCHEMA`
726+
;; - This advice is doubly relevant if all the schemas are in a `*.schema` namespace. DRY.
727+
728+
;; - Prefer the narrowest schema
729+
;; - Always ask - does this make sense
730+
;; ```clojure
731+
;; [:map
732+
;; [:messages-recieved int?]]
733+
;; ```
734+
;; - Example
735+
;; - Is it possible to have received a negative number of messages?
736+
;; - Is it legal in our domain model to have received 0?
737+
;; - `nat-int?` and `pos-int?` are a fit for those cases
738+
739+
;; ## [Make Illegal States Impossible](https://www.youtube.com/watch?v=IcgmSRJHu_8)
740+
;; - If you're into that
741+
;; - Example - Versions
742+
;; ```clojure
743+
;; (def Version [:enum 1 2 3])
744+
;; (def MessageV1 [:map [:version Version]])
745+
;; ,,,
746+
;; [:multi {:dispatch :version}
747+
;; [1 MessageV1]
748+
;; [2 MessageV2]
749+
;; ,,,]
750+
;; ```
751+
;; - Where's the hole?
752+
;; - Is this a valid version 1 message?
753+
;; ```clojure
754+
;; {:version 3}
755+
;; ```
756+
;; - Example - optional keys
757+
;; - Ingest messages from two sources
758+
;; ```clojure
759+
;; [:map
760+
;; ,,, ;; common
761+
;; :from-a {:optional true} schema-a
762+
;; :from-b {:optional true} schema-b]
763+
;; ```
764+
;; - Pat ourselves on the back because we cover two options with one schema
765+
;; - Wrong! This schema covers 4 options, 2 of them are incorrect!
766+
;; - We can find this out when we generate values
767+
;; - Split
768+
;; ```clojure
769+
;; (def Common [:map ,,,])
770+
;; (def FromA (mu/merge Common [:map [:from-a schema-a]]))
771+
;; (def FromB (mu/merge Common [:map [:from-b schema-b]]))
772+
;; (def Message [:or FromA FromB])
773+
;; ```
774+
775+
;; This also gives better generators
776+
777+
;; ## Prefer built ins to ad-hoc schemas
778+
779+
;; #### numbers
780+
781+
;; Type schemas are flexible
782+
783+
;; Good:
784+
785+
[:int {:min 0 :max 100}]
786+
787+
;; meh
788+
[:and int? [:>= 0] [:<= 100]]
789+
790+
;; Bad:
791+
[:and
792+
int?
793+
[:fn
794+
{:error/message "should be in range 1-100"}
795+
#(and (<= 0 %) (<= % 100))]]
796+
797+
;; Sometimes predicates are sufficient
798+
[:int {:min 0}]
799+
pos-int?
800+
801+
;; #### Strings
802+
803+
;; Good
804+
805+
[:string {:max 256}]
806+
807+
;; Bad
808+
809+
[:and
810+
string?
811+
[:fn
812+
{:error/message "should be < 256 characters"}
813+
#(>= 256 (count %))]]
814+
815+
;; - Prefer defining new schemas over functions
816+
;; - Prefer decoding over string validation - add a decoder
817+
818+

0 commit comments

Comments
 (0)