{"id":21734088,"url":"https://github.com/hodur-org/hodur-spec-schema","last_synced_at":"2025-12-12T01:34:39.605Z","repository":{"id":62432902,"uuid":"158615955","full_name":"hodur-org/hodur-spec-schema","owner":"hodur-org","description":"Hodur is a domain modeling approach and collection of libraries to Clojure. By using Hodur you can define your domain model as data, parse and validate it, and then either consume your model via an API or use one of the many plugins to help you achieve mechanical results faster and in a purely functional manner.","archived":false,"fork":false,"pushed_at":"2019-11-19T15:03:02.000Z","size":84,"stargazers_count":18,"open_issues_count":6,"forks_count":9,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-13T01:30:02.216Z","etag":null,"topics":["clojure","data","modeling","schema","spec","types","validation"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hodur-org.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-21T23:01:24.000Z","updated_at":"2022-01-09T19:18:29.000Z","dependencies_parsed_at":"2022-11-01T21:01:35.027Z","dependency_job_id":null,"html_url":"https://github.com/hodur-org/hodur-spec-schema","commit_stats":null,"previous_names":["luchiniatwork/hodur-spec-schema"],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodur-org%2Fhodur-spec-schema","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodur-org%2Fhodur-spec-schema/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodur-org%2Fhodur-spec-schema/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodur-org%2Fhodur-spec-schema/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hodur-org","download_url":"https://codeload.github.com/hodur-org/hodur-spec-schema/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248654045,"owners_count":21140236,"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","data","modeling","schema","spec","types","validation"],"created_at":"2024-11-26T05:07:45.502Z","updated_at":"2025-12-12T01:34:39.549Z","avatar_url":"https://github.com/hodur-org.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[circleci-badge]: https://circleci.com/gh/hodur-org/hodur-spec-schema.svg?style=shield\u0026circle-token=4efa55e1e836d3613c886708b4246b488090b263\n[circleci]: https://circleci.com/gh/hodur-org/hodur-spec-schema\n[clojars-badge]: https://img.shields.io/clojars/v/hodur/spec-schema.svg\n[clojars]: http://clojars.org/hodur/spec-schema\n[github-issues]: https://github.com/hodur-org/hodur-spec-schema/issues\n[graphviz-colors]: https://www.graphviz.org/doc/info/colors.html\n[graphviz]: https://www.graphviz.org/\n[hodur-engine-clojars-badge]: https://img.shields.io/clojars/v/hodur/engine.svg\n[hodur-engine-clojars]: http://clojars.org/hodur/engine\n[hodur-engine-definition]: https://github.com/hodur-org/hodur-engine#model-definition\n[hodur-engine-started]: https://github.com/hodur-org/hodur-engine#getting-started\n[hodur-engine]: https://github.com/hodur-org/hodur-engine\n[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg\n[license]: ./LICENSE\n[logo]: ./docs/logo-tag-line.png\n[motivation]: https://github.com/hodur-org/hodur-engine/blob/master/docs/MOTIVATION.org\n[plugins]: https://github.com/hodur-org/hodur-engine#hodur-plugins\n[spec]: https://clojure.org/guides/spec\n[status-badge]: https://img.shields.io/badge/project%20status-beta-brightgreen.svg\n\n# Hodur Spec Schema\n\n[![CircleCI][circleci-badge]][circleci]\n[![Clojars][hodur-engine-clojars-badge]][hodur-engine-clojars]\n[![Clojars][clojars-badge]][clojars]\n[![License][license-badge]][license]\n![Status][status-badge]\n\n![Logo][logo]\n\nHodur is a descriptive domain modeling approach and related collection\nof libraries for Clojure.\n\nBy using Hodur you can define your domain model as data, parse and\nvalidate it, and then either consume your model via an API making your\napps respond to the defined model or use one of the many plugins to\nhelp you achieve mechanical, repetitive results faster and in a purely\nfunctional manner.\n\n\n\u003e This Hodur plugin provides the ability to generate [Clojure\n\u003e Spec][spec] schemas out of your Hodur model. You can then validate\n\u003e your data structures, generate random payloads, extend yours\n\u003e tests... you name it.\n\n## Motivation\n\nFor a deeper insight into the motivations behind Hodur, check the\n[motivation doc][motivation].\n\n## Getting Started\n\nHodur has a highly modular architecture. [Hodur Engine][hodur-engine]\nis always required as it provides the meta-database functions and APIs\nconsumed by plugins.\n\nTherefore, refer the [Hodur Engine's Getting\nStarted][hodur-engine-started] first and then return here for\nDatomic-specific setup.\n\nAfter having set up `hodur-engine` as described above, we also need to\nadd `hodur/spec-schema`, a plugin that creates Lacinia Schemas out\nof your model to the `deps.edn` file:\n\n``` clojure\n  {:deps {hodur/engine      {:mvn/version \"0.1.6\"}\n          hodur/spec-schema {:mvn/version \"0.1.5\"}}}\n```\n\nYou should `require` it any way you see fit:\n\n``` clojure\n  (require '[hodur-engine.core :as hodur])\n  (require '[hodur-spec-schema.core :as hodur-spec])\n```\n\nLet's expand our `Person` model from the original getting started by\n\"tagging\" the `Person` entity for Spec. You can read more about the\nconcept of tagging for plugins in the sessions below but, in short,\nthis is the way we, model designers, use to specify which entities we\nwant to be exposed to which plugins.\n\n``` clojure\n  (def meta-db (hodur/init-schema\n                '[^{:spec/tag-recursive true}\n                  Person\n                  [^String first-name\n                   ^String last-name]]))\n```\n\nThe `hodur-spec-schema` plugin exposes a function called `schema` that\nreturns a vector with all the spec definitions your model needs:\n\n``` clojure\n  (def spec-schema (hodur-spec/schema meta-db))\n```\n\nWhen you inspect `spec-schema`, this is what you have:\n\n``` clojure\n  [(clojure.spec.alpha/def\n     :my-app.core.person/last-name\n     clojure.core/string?)\n   (clojure.spec.alpha/def\n     :my-app.core.person/first-name\n     clojure.core/string?)\n   (clojure.spec.alpha/def\n     :my-app.core/person\n     (clojure.spec.alpha/keys\n      :req-un\n      [:my-app.core.person/first-name\n       :my-app.core.person/last-name]\n      :opt-un\n      []))]\n```\n\nAs a convenience, `hodur-spec-schema` also provides a macro called\n`defspecs` that already defines all your specs onto your registry:\n\n``` clojure\n  (hodur-spec/defspecs meta-db)\n```\n\nOnce `defspecs` is run, you'll have three specs to use:\n\n- `:my-app.core.person/last-name`\n- `:my-app.core.person/first-name`\n- `:my-app.core/person`\n\nTherefore, we can use spec normally like:\n\n``` clojure\n  (require '[clojure.spec.alpha :as s])\n\n  (s/valid? :my-app.core/person {:first-name \"Jane\"\n                                 :last-name \"Janet\"}) ;; =\u003e true\n\n  (s/valid? :my-app.core/person {:firs-name \"Jane\"\n                                 :last-name \"Janet\"}) ;; =\u003e false\n```\n\n## Model Definition\n\nAll Hodur plugins follow the [Model\nDefinition][hodur-engine-definition] as described on Hodur [Engine's\ndocumentation][hodur-engine].\n\n## Naming Conventions\n\nFor the sake of composability each of your entities, fields, and\nparameters will have their own bespoke specs defined.\n\nThe convention is that each spec will have a fully-qualified name in\nthe namespace where `defspecs` is called pretty much as if a `::` was\nused. Example:\n\n``` clojure\n  (ns my-app.core\n    (:require [clojure.spec.alpha :as s]\n              [hodur-engine.core :as hodur]\n              [hodur-spec-schema.core :as hodur-spec]))\n\n  (def meta-db (engine/init-schema\n                '[^{:spec/tag-recursive true}\n                  Person\n                  [^String first-name\n                   ^String last-name]]))\n\n  (hodur-spec/defspecs meta-db)\n  ;; =\u003e [:my-app.core.person/last-name\n  ;;     :my-app.core.person/first-name\n  ;;     :my-app.core/person]\n\n  (s/valid? :my-app.core/person {:first-name \"Jane\"\n                                 :last-name \"Janet\"}) ;; =\u003e true\n\n  (s/valid? :my-app.core/person {:firs-name \"Jane\"\n                                 :last-name \"Janet\"});; =\u003e false\n\n  (s/explain :my-app.core/person {:firs-name \"Jane\"\n                                  :last-name \"Janet\"})\n  ;; prints out:\n  ;; val: {:firs-name \"Jane\", :last-name \"Janet\"} fails spec: :hodur-spec-schema.core/person predicate: (contains? % :first-name)\n```\n\n## Influencing Names with Aliases and Prefix\n\nSometimes the default behavior of the naming convention above might\nnot suit you. There are two ways to affect the names.\n\nThe first one is to use `:prefix` on `defspecs`. It will override\nthe default namespace altogether. Example:\n\n``` clojure\n  (ns my-app.core\n    (:require [clojure.spec.alpha :as s]\n              [hodur-engine.core :as hodur]\n              [hodur-spec-schema.core :as hodur-spec]))\n\n  (def meta-db (engine/init-schema\n                '[^{:spec/tag-recursive true}\n                  Person\n                  [^String first-name\n                   ^String last-name]]))\n\n  (hodur-spec/defspecs meta-db {:prefix :app})\n  ;; =\u003e [:app.person/last-name\n  ;;     :app.person/first-name\n  ;;     :app/person]\n\n  (s/valid? :app/person {:first-name \"Jane\"\n                         :last-name \"Janet\"}) ;; =\u003e true\n```\n\nThe second method is to use the marker `:spec/alias` or\n`:spec/aliases` when defining entities, fields or parameters. Example:\n\n``` clojure\n  (ns my-app.core\n    (:require [clojure.spec.alpha :as s]\n              [hodur-engine.core :as hodur]\n              [hodur-spec-schema.core :as hodur-spec]))\n\n  (def meta-db (engine/init-schema\n                '[^{:spec/tag-recursive true\n                    :spec/alias :la/persona}\n                  Person\n                  [^{:spec/aliases [:a-persons/first-name\n                                    :el/primo]}\n                   ^String first-name\n                   ^{:spec/aliases [:el/secondo]}\n                   ^String last-name]]))\n\n  (hodur-spec/defspecs meta-db)\n  ;; =\u003e [:my-app.core.person/last-name\n  ;;     :my-app.core.person/first-name\n  ;;     :my-app.core/person\n  ;;     :la/persona\n  ;;     :a-persons/first-name\n  ;;     :el/primo\n  ;;     :el/secondo]\n\n  (s/valid? :la/persona {:first-name \"Jane\"\n                         :last-name \"Janet\"}) ;; =\u003e true\n\n  (s/valid? :el/secondo \"Janet\") ;; =\u003e true\n```\n\n## Primitive Types\n\nAll Hodur primitive types have natural specs as described below:\n\n| Hodur Type | Equivalent Spec |\n|------------|-----------------|\n| `String`   | `string?`       |\n| `ID`       | `string?`       |\n| `Integer`  | `integer?`      |\n| `Boolean`  | `boolean?`      |\n| `Float`    | `float?`        |\n| `DateTime` | `inst?`         |\n\nOther specs can be specified by using the `:spec/override` or\n`:spec/extend` features described in more detail in the respective\nsection below.\n\n## Cardinality\n\nMultiple cardinalities are dealt with as expected. The following table\nshows some examples:\n\n| Hodur Cardinality      | Equivalent Spec                              |\n|------------------------|----------------------------------------------|\n| `nil` (none specified) | a single `\u003cspec\u003e`                            |\n| `[0 n]`                | `s/coll-of \u003cspec\u003e :min-count 0`              |\n| `[4 n]`                | `s/coll-of \u003cspec\u003e :min-count 4`              |\n| `3`                    | `s/coll-of \u003cspec\u003e :count 3`                  |\n| `[5 9]`                | `s/coll-of \u003cspec\u003e :min-count 5 :max-count 9` |\n| `[n 7]`                | `s/coll-of \u003cspec\u003e :max-count 7`              |\n\n## Interfaces\n\nHodur interfaces are supported. The approach taken is that the\nresulting spec for the child entity is an `s/and` of itself and all of\nits interfaces.\n\nTake the following example:\n\n``` clojure\n  '[^:interface\n    Animal\n    [^String race]\n\n    ^{:implements Animal}\n    Person\n    [^String first-name\n     ^String last-name]]\n```\n\nThe resulting high level specs would be `:app/animal` and\n`:app/person` where `:app/person` needs to validate the keys in the\n`Person` entity and also the keys on `Animal`.\n\n## Enums and Unions\n\nHodur enums are spec'd as exact keywords or strings. Therefore the\nhodur model below:\n\n``` clojure\n  '[^:enum\n    Gender\n    [FEMALE MALE]]\n```\n\nWill create two specs where one of them would be along the lines of\n`:app.core.gender/female` where `#(= \"FEMALE\" (name %))` (one for\nfemale and one for male).\n\nThe enum per se is an `s/or` between all of the enum's options.\n\nIf you need a different behavior, you can use `:spec/override`\ndescribed in the section below.\n\nHodur unions work similarly but the `s/or` is between the entities the\nunion refers to.\n\n## Overriding and Extending\n\nSpecs can get very elaborate and Hodur models do not capture - nor\neven try to capture - all the possibilities. Instead there are two\nconcepts in place: you can either override the spec that Hodur would\nuse or extend it.\n\nOverriding is as simple as providing a marker `:spec/override` that\npoints to the function you want to use:\n\n``` clojure\n  '[MyEntity\n    [{:spec/override keyword?}\n     a-keyword-field]]\n```\n\nIn the example above the spec for `a-keyword-field` will be simply\n`keyword?`. You can also specify your own validation functions. Simply\nmake them fully qualified and make sure they have been required in the\ncorrect context:\n\n``` clojure\n  '[User\n    [{:spec/override my-app.user/email?}\n     email]]\n```\n\nThen, just make sure you have something along these lines for your\nemail validation (or any other in fact):\n\n``` clojure\n  (ns my-app.user\n    (:require [clojure.test.check.generators :as gen]))\n\n  (defn email? [s]\n    (let [email-regex #\"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63}$\"]\n      (re-matches email-regex s)))\n```\n\nSometimes you are happy with the default spec used by Hodur but want\nto extend it a bit. For instance, in the email example above you might\nwant to still make it a `string?` but also an email. By using the\nmarker `:spec/extend` you can automatically wrap the basic spec with\nan `s/and`:\n\n``` clojure\n  '[User\n    [{:type String\n      :spec/extend my-app.user/email?}\n     email]]\n```\n\nThe resulting spec will be a `string?` `s/and` a\n`my-app.validations/email?`.\n\n## Custom Generators\n\nCustom generators can be provided with the marker\n`:spec/gen`. Example:\n\n``` clojure\n  '[User\n    [{:type String\n      :spec/extend my-app.user/email?\n      :spec/gen my-app.user/gen-email}\n     email]]\n```\n\nThen the hypothetical code below could validate and genarate out of a\nset of possible emails:\n\n``` clojure\n  (ns my-app.user\n    (:require [clojure.test.check.generators :as gen]))\n\n  (defn email? [s]\n    (let [email-regex #\"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63}$\"]\n      (re-matches email-regex s)))\n\n  (defn email-gen []\n    (gen/elements #{\"asd@qwe.com\" \"qwe@asd.com\" \"foo@bar.edu\" \"bar@edu.br\"}))\n```\n\nOnce you have these in place, you can easily generate users like:\n\n``` clojure\n  (require '[clojure.spec.gen.alpha :as gen])\n  (require '[clojure.spec.alpha :as s])\n\n  (gen/sample (s/gen :app/user))\n  ;; =\u003e [{:email \"qwe@asd.com\"}\n  ;;     {:email \"foo@bar.edu\"}\n  ;;     {:email \"qwe@asd.com\"}\n  ;;     {:email \"asd@qwe.com\"}\n  ;;     {:email \"bar@edu.br\"}]\n\n  (s/valid? :app/user (gen/generate (s/gen :app/user))) ;; =\u003e true\n```\n\n## Parameters and Parameter Groups' Specs\n\nHodur parameters are each individually spec'd so that you are able to\nrun validations against specific entries in your functions.\n\nIn some situations though, it is also possible that you want to\nvalidate the whole set of parameters as a group. This is particularly\nuseful if your parameters are set as a kind of argument map or ordered\ntuple/vector.\n\nHodur's spec plugin will always create two specs for the parameter\ngroup, one as a map and one as a tuple. What this means in practice is\nthat in the following example the specs `:app.core.user/avatar-url%`\nand `:app.core.user/avatar-url-ordered%` are created.\n\n`:app.core.user/avatar-url%` will represent a map that will include\nthe required entries `:max-width` and `:max-height`.\n\n`:app.core.user/avatar-url-ordered%` will represent a tuple of two\nintegers (the first representing `:max-width` and the second\nrepresenting `:max-height`). As you can see, in this spec entry the\nnames of the parameters get lost. Another feature to notice is that\noptional parameters are not supported in such case. This is as per\ntuple spec.\n\n``` clojure\n  '[User\n    [^String email\n     ^String avatar-url [^Integer max-width\n                         ^Integer max-height]]]\n```\n\nSpecial attention must be given to the naming convention here. A `%`\nis added as a postfix to the name of the field the parameters\nrefer. For the ordered spec, a `-ordered%` is added. In the above\nexample, `:app.core.user/avatar-url` is the spec to the `avatar-url`\nfield (which happens to be a String - or `string?`),\n`:app.core.user/avatar-url%` refers to the parameter group as a map,\nand `:app.core.user/avatar-url-ordered%` refers to the parameter group\nas a tuple..\n\nYou can also choose a different postfix when calling the `defspecs`\nmacro if `%` doesn't work for you. In the following example, instead\nof `%`, `-params` will be used (for ordered specs, this will mean\n`-ordered-params` will be used).\n\n``` clojure\n  (defspecs meta-db {:params-postfix \"-params\"})\n```\n\n## Bugs\n\nIf you find a bug, submit a [GitHub issue][github-issues].\n\n## Help!\n\nThis project is looking for team members who can help this project\nsucceed! If you are interested in becoming a team member please open\nan issue.\n\n## License\n\nCopyright © 2019 Tiago Luchini\n\nDistributed under the MIT License (see [LICENSE][license]).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhodur-org%2Fhodur-spec-schema","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhodur-org%2Fhodur-spec-schema","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhodur-org%2Fhodur-spec-schema/lists"}