|
11 | 11 | (:require |
12 | 12 | [clojure.spec.alpha :as s] |
13 | 13 | [babashka.http-client.websocket :as ws] |
| 14 | + [clojure.data :as data] |
14 | 15 | [malli.core :as m] |
15 | 16 | [malli.util :as mu] |
16 | 17 | [malli.transform :as mt] |
|
631 | 632 | {:registry registry}) |
632 | 633 |
|
633 | 634 | ;; 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