{"id":19293951,"url":"https://github.com/igrishaev/spec-dict","last_synced_at":"2025-04-22T07:32:34.835Z","repository":{"id":62434684,"uuid":"267532811","full_name":"igrishaev/spec-dict","owner":"igrishaev","description":"Better map specs","archived":false,"fork":false,"pushed_at":"2020-09-28T12:05:07.000Z","size":43,"stargazers_count":19,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-01T20:51:24.422Z","etag":null,"topics":["clojure","spec"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igrishaev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-28T08:16:45.000Z","updated_at":"2025-01-09T11:54:52.000Z","dependencies_parsed_at":"2022-11-01T21:15:47.874Z","dependency_job_id":null,"html_url":"https://github.com/igrishaev/spec-dict","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fspec-dict","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fspec-dict/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fspec-dict/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igrishaev%2Fspec-dict/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igrishaev","download_url":"https://codeload.github.com/igrishaev/spec-dict/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250195054,"owners_count":21390230,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","spec"],"created_at":"2024-11-09T22:36:40.057Z","updated_at":"2025-04-22T07:32:34.565Z","avatar_url":"https://github.com/igrishaev.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dictionary-like Specs\n\nMaps are quite common in Clojure, and thus `s/keys` specs too. Here is a common\nexample:\n\n```clojure\n(s/def :profile/url    string?)\n(s/def :profile/rating int?)\n(s/def ::profile\n  (s/keys :req-un [:profile/url\n                   :profile/rating]))\n\n(s/def :user/name    string?)\n(s/def :user/age     int?)\n(s/def :user/profile ::profile)\n(s/def ::user\n  (s/keys :req-un [:user/name\n                   :user/age\n                   :user/profile]))\n```\n\nWhat's wrong with it? Namely:\n\n- each key requires its own spec, which is verbose;\n- keys without a namespace still need it to declare a spec;\n- for the top level map you use the current namespace, but for children you have\n  to specify it manually, which leads to spec overriding (not only you have\n  declared `:user/name`);\n- keys are only keywords which is fine in 99%, but still;\n- there is no a strict version of `s/keys` which fails when extra keys were\n  passed. Doing it manually looks messy.\n\nNow imagine if it would have been like this:\n\n```clojure\n(s/def ::user\n  {:name string?\n   :age int?\n   :profile {:url? string?\n             :rating int?}})\n```\n\nor this (full keys):\n\n```clojure\n(s/def ::user\n  #:user{:name string?\n         :age int?\n         :profile #:profile{:url? string?\n                            :rating int?}})\n```\n\nThis library is it to fix everything said above. Add it:\n\n```clojure\n;; deps\n[spec-dict \"0.2.1\"]\n\n(require '[spec-dict :refer [dict dict*]])\n```\n\nA simple dictionary spec:\n\n```clojure\n(s/def ::user-simple\n  (dict {:name string? :age int?}))\n\n(s/valid? ::user-simple {:name \"Ivan\" :age 34})\n```\n\nBy default, extra keys are OK:\n\n```clojure\n(s/valid? ::user-simple {:name \"Ivan\" :age 34 :extra 1})\n```\n\nKeys of different types:\n\n```clojure\n(s/def ::user-types\n  (dict {\"name\" string?\n         :age int?\n         'active boolean?}))\n\n(s/valid? ::user-types {\"name\" \"Ivan\" :age 34 'active true})\n```\n\nThe dicts can be nested:\n\n```clojure\n(s/def ::post-nested\n  (dict {:title string?\n         :author (dict {:name string?\n                        :email string?})}))\n\n(s/valid? ::post-nested\n          {:title \"Hello\"\n           :author {:name \"Ivan\"\n                    :email \"test@test.com\"}})\n```\n\nA dict may reference another dict:\n\n```clojure\n(s/def ::post-author\n  (dict {:name string?\n         :email string?}))\n\n\n(s/def ::post-ref\n  (dict {:title string?\n         :author ::post-author}))\n```\n\nor be a part of a collection as well:\n\n```clojure\n(s/def ::post-coll-of\n  (dict {:title string?\n         :authors (s/coll-of ::post-author)}))\n```\n\nThe inner map can be prefixed to get full keys:\n\n```clojure\n;; spec\n(dict #:user{:extra/test boolean?\n             :name string?\n             :age int?})\n\n;; data\n{:extra/test false\n :user/name \"Ivan\"\n :user/age 34}\n```\n\nThe dict consumes multiple maps on creation, the final keys get merged in the\nsame order:\n\n```clojure\n;; spec\n(dict {:name string?} {:age int?})\n\n;; data\n{:name \"Ivan\" :age 34}\n```\n\nYou can override types if you need:\n\n```clojure\n;; spec\n(dict {:name string?}\n      {:age int?}\n      {:name int?})\n\n;; data\n{:name 42 :age 34}\n```\n\nBy default, all the keys are required. To mark keys as optional, put the `^:opt`\nmetadata flag:\n\n```clojure\n;; spec\n(dict {:name string?}\n      ^:opt {:age int?})\n\n;; data OK\n{:name \"Ivan\" :age 34}\n{:name \"Ivan\"}\n\n;; data ERR\n{:name \"Ivan\" :age nil}\n```\n\nBut if you pass optional keys as a variable, wrap it with a function:\n\n```clojure\n(dict {:name string?}\n      (-\u003eopt some-other-mapping))\n```\n\nA dict can reference any spec:\n\n```clojure\n(dict ::user-simple\n      {:active :fields/boolean})\n```\n\nConforming:\n\n```clojure\n(s/def ::-\u003eint\n  (s/conformer (fn [x]\n                 (try\n                   (Integer/parseInt x)\n                   (catch Exception e\n                     ::s/invalid)))))\n\n;; spec\n(dict {:value ::-\u003eint})\n\n(s/conform spec {:value \"123\"})\n{:value 123}\n```\n\nUnforming:\n\n```clojure\n(s/def ::-\u003eint2\n  (s/conformer (fn [x]\n                 (try\n                   (Integer/parseInt x)\n                   (catch Exception e\n                     ::s/invalid)))\n               (fn [x]\n                 (str x))))\n\n;; spec\n(dict {:value ::-\u003eint2})\n\n(s/unform spec (s/conform spec {:value \"123\"}))\n{:value \"123\"}\n```\n\nStrict version of a dict which fails when extra keys were passed:\n\n\n```clojure\n;; spec\n(dict* {:name string?\n        :age int?}\n       ^:opt {:active boolean?})\n\n;; data OK\n{:name \"test\" :age 34}\n{:name \"test\" :age 34 :active true}\n\n;; data ERR\n{:name \"test\" :age 34 :extra \"aa\"}\n{:name \"test\" :age 34 :active true :extra \"aa\"}\n```\n\nGenerators:\n\n```clojure\n\n;; spec\n(dict {:name #{\"Ivan\" \"Juan\" \"Iogann\"}\n       :age int?})\n\n\n(gen/generate (s/gen spec))\n{:name \"Iogann\" :age -2}\n```\n\nExplain:\n\n```clojure\n;; spec\n(dict {:name ::some-name\n       :age int?})\n\n;; not a map\n(s/explain-data spec 123)\n\n;; problem\n{:reason \"not a map\"\n :path []\n :pred clojure.core/map?\n :val 123\n :via []\n :in []}\n\n;; missing key\n(s/explain-data spec {:age 34})\n\n;; problem\n{:reason \"missing key\"\n :val nil\n :pred (clojure.core/contains? #{:age :name} :name)\n :path [:name]\n :via [:spec-dict-test/some-name]\n :in [:name]}\n\n;; wrong value\n(s/explain-data spec {:name 123 :age 43})\n\n;; problem\n{:reason \"spec failure\"\n :val 123\n :pred clojure.core/string?\n :path [:name]\n :via [:spec-dict-test/some-name]\n :in [:name]}\n```\n\nExplain for a strict version:\n\n```clojure\n;; spec\n(dict* {:name string?\n        :age int?})\n\n;; extra key in a strict dict\n(s/explain-data spec {:name \"Ivan\" :age 34 :extra true})\n\n;; problem\n\n{:reason \"extra keys\"\n :path []\n :pred (clojure.set/subset? #{:age :name :extra} #{:age :name})\n :val {:name \"Ivan\" :age 34 :extra true}\n :via []\n :in []}\n```\n\nA dictionary spec supports `s/keys`. A `s/keys` one gets converted into a\ndictionary keeping in mind all type of keys: `req`, `req-opt`, `opt`, and\n`opt-un`:\n\n```clojure\n(s/def :profile/url    string?)\n(s/def :profile/rating int?)\n(s/def ::profile\n  (s/keys :req-un [:profile/url\n                   :profile/rating]))\n\n(s/def :user/name    string?)\n(s/def :user/age     int?)\n(s/def :user/profile ::profile)\n(s/def ::user\n  (s/keys :req-un [:user/name\n                   :user/age\n                   :user/profile]))\n\n\n;; profile spec\n(dict ::profile)\n\n;; data\n{:url \"http://test.com\"\n :rating 99.99}\n```\n\nHaving a dict spec makes it easier to merge other keys:\n\n```clojure\n(let [spec-p (dict ::profile {:paid boolean?})\n      spec-u (dict ::user {:profile spec-p\n                           :active? boolean?})]\n  ...)\n\n\n;; data for spec-u\n{:name \"test\"\n :age 42\n :active? true\n :profile {:url \"http://test.com\"\n           :rating 99\n           :paid true}}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Fspec-dict","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figrishaev%2Fspec-dict","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figrishaev%2Fspec-dict/lists"}