{"id":21734087,"url":"https://github.com/hodur-org/hodur-engine","last_synced_at":"2025-12-12T01:30:24.327Z","repository":{"id":45106090,"uuid":"142614498","full_name":"hodur-org/hodur-engine","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":"2022-01-08T00:29:22.000Z","size":324,"stargazers_count":282,"open_issues_count":10,"forks_count":13,"subscribers_count":17,"default_branch":"main","last_synced_at":"2025-04-02T20:09:47.083Z","etag":null,"topics":["clojure","data","modeling","schema"],"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-07-27T19:08:07.000Z","updated_at":"2024-08-27T10:56:52.000Z","dependencies_parsed_at":"2022-09-02T22:40:54.895Z","dependency_job_id":null,"html_url":"https://github.com/hodur-org/hodur-engine","commit_stats":null,"previous_names":["luchiniatwork/hodur-engine"],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodur-org%2Fhodur-engine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodur-org%2Fhodur-engine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodur-org%2Fhodur-engine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hodur-org%2Fhodur-engine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hodur-org","download_url":"https://codeload.github.com/hodur-org/hodur-engine/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248119597,"owners_count":21050798,"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"],"created_at":"2024-11-26T05:07:45.056Z","updated_at":"2025-12-12T01:30:24.292Z","avatar_url":"https://github.com/hodur-org.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[ci-badge]: https://github.com/hodur-org/hodur-engine/actions/workflows/test.yml/badge.svg\n[ci-link]: https://github.com/hodur-org/hodur-engine/actions/workflows/test.yml\n[clojars-badge]: https://img.shields.io/clojars/v/hodur/engine.svg\n[clojars]: http://clojars.org/hodur/engine\n[clojure-spec]: https://clojure.org/guides/spec\n[contentful]: https://www.contentful.com/\n[datomic-cloud]: https://docs.datomic.com/cloud/index.html\n[github-issues]: https://github.com/hodur-org/hodur-engine/issues\n[graphql]: https://graphql.org/\n[graphviz]: http://www.graphviz.org/\n[hodur-contentful-schema]: https://github.com/hodur-org/hodur-contentful-schema\n[hodur-datomic-schema]: https://github.com/hodur-org/hodur-datomic-schema\n[hodur-graphviz-schema]: https://github.com/hodur-org/hodur-graphviz-schema\n[hodur-lacinia-datomic-adapter]: https://github.com/hodur-org/hodur-lacinia-datomic-adapter\n[hodur-lacinia-schema]: https://github.com/hodur-org/hodur-lacinia-schema\n[hodur-spec-schema]: https://github.com/hodur-org/hodur-spec-schema\n[hodur-visualizer-schema]: https://github.com/hodur-org/hodur-visualizer-schema\n[lacinia]: https://github.com/walmartlabs/lacinia\n[license-badge]: https://img.shields.io/badge/license-MIT-blue.svg\n[license]: ./LICENSE\n[logo]: ./docs/logo-tag-line.png\n[motivation]: ./docs/MOTIVATION.org\n[plugins]: https://github.com/hodur-org/hodur-engine#hodur-plugins\n[status-badge]: https://img.shields.io/badge/project%20status-beta-brightgreen.svg\n\n# Hodur Engine\n\n[![CI][ci-badge]][ci-link]\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\u003e This repo is the Hodur core engine that parses your model\n\u003e definitions and exposes a meta-API around it. For a list of what you\n\u003e can do once your model is in Hodur [check here][plugins].\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 (this project)\nis always required as it provides the meta-database functions and APIs\nconsumed by plugins.\n\nAdd `hodur-engine` as a dependency in your `deps.edn` file:\n\n``` clojure\n  {:deps {hodur/engine {:mvn/version \"0.1.9\"}}}\n```\n\nEither `require` Hodur as part of your `ns` definition or directly:\n\n``` clojure\n  (require '[hodur-engine.core :as hodur])\n```\n\nIn order to initialize an `atom` representing the meta-database of\nyour model call function `hodur/init-schema`:\n\n``` clojure\n  (def meta-db (hodur/init-schema\n                '[Person\n                  [^String first-name\n                   ^String last-name]]))\n```\n\nIn the above example, we are defining a `Person` entity with a\n`first-name` and a `last-name` both tagged as the scalar type\n`String`.\n\nAlternatively, Hodur can be initialized by raw EDN paths or from your\nclasspath using a `File` (i.e. `clojure.java.io/resource`):\n\n``` clojure\n  (def meta-db (-\u003e \"schemas/person.edn\"\n                   io/resource\n                   hodur/init-path))\n```\n\nHodur's usefulness can be seen when used in conjunction with several\nplugins that take care of the mechanical aspects of your\napplication. For the sake of getting started, we are also adding\n`hodur-datomic-schema`, a plugin that creates Datomic Schemas out of\nyour model to the `deps.edn` file:\n\n``` clojure\n  {:deps {hodur/engine         {:mvn/version \"0.1.5\"}\n          hodur/datomic-schema {:mvn/version \"0.1.0\"}}}\n```\n\nYou should `require` it any way you see fit:\n\n``` clojure\n  (require '[hodur-datomic-schema.core :as hodur-datomic])\n```\n\nLet's expand our `Person` model above by \"tagging\" the `Person` entity\nfor Datomic. You can read more about the concept of tagging for\nplugins in the sessions below but, in short, this is the way we, model\ndesigners, use to specify which entities we want to be exposed to\nwhich plugins.\n\n``` clojure\n  (def meta-db (hodur/init-schema\n                '[^{:datomic/tag-recursive true}\n                  Person\n                  [^String first-name\n                   ^String last-name]]))\n```\n\nThe `hodur-datomic-schema` plugin exposes a function called `schema`\nthat generates your model as a Datomic schema payload:\n\n``` clojure\n  (def datomic-schema (hodur-datomic/schema meta-db))\n```\n\nWhen you inspect `datomic-schema`, this is what you have:\n\n``` clojure\n  [{:db/ident       :person/first-name\n    :db/valueType   :db.type/string\n    :db/cardinality :db.cardinality/one}\n   {:db/ident       :person/last-name\n    :db/valueType   :db.type/string\n    :db/cardinality :db.cardinality/one}]\n```\n\nAssuming the Datomic client API is bound to `datomic`, and your\nconnection to the Database cluster is bound to `db-conn`, you can\nsimply transact your schema like this:\n\n``` clojure\n  (datomic/transact db-conn {:tx-data datomic-schema})\n```\n\nSeveral other plugins are available and you can also write your\nown. The following sections detail not only how to model your domain\nbut also these options in further detail.\n\n## Hodur Plugins\n\nFor visualization/documentation:\n\n- [hodur-graphviz-schema][hodur-graphviz-schema]: generates beautiful\n  [GraphViz][graphviz] diagrams of your domain\n- [hodur-visualizer-schema][hodur-visualizer-schema]: generates a\n  dynamically, hot-reloaded version of your domain on a web browser\n\nSchemas for persistent systems:\n\n- [hodur-datomic-schema][hodur-datomic-schema]: generates [Datomic\n  Cloud][datomic-cloud] compatible schemas\n- [hodur-contentful-schema][hodur-contentful-schema]: generates\n  [Contentful][contentful] compatible schemas\n\nSchemas for inbound interfaces:\n\n- [hodur-lacinia-schema][hodur-lacinia-schema]: generates\n  [Lacinia][lacinia] ([GraphQL][graphql]) schemas\n\nSchemas for validation/data-generation:\n\n- [hodur-spec-schema][hodur-spec-schema]: generates [Clojure\n  Spec][clojure-spec] schemas\n\nExperimental adapters:\n\n- [hodur-lacinia-datomic-adapter][hodur-lacinia-datomic-adapter]:\n  experimental utilities for bridging GraphQL queries and mutations\n  into Datomic\n\n## Model Definition\n\n### Entities, fields and parameters\n\nIn Hodur *Entities* are the highest level representation of a\nmodel. An *entity* has any number of *fields* that qualify such\nentity.\n\nFor instance, an `employee` entity may have an `employee-number`, a\n`name` and a `salary` as three distinct fields. An `entity` can have\nas many fields as you need.\n\n*Fields* can have any number of *parameters*. *Parameters* qualify the\nfield. For instance, a hypothetical `height` field could have a\nparameter specifying which `unit` to use when interpreting this\n*field* (`CENTIMETERS` or `FEET` for instance).\n\n### Basic structure\n\nHodur can be initialized by either a series of EDN files (using\nfunction `init-path`) or vectors (using function `init-schema`).\n\nA domain model is a vector of tuples of symbols and sub-vectors. The\nsymbols represent entity names and the sub-vectors represent fields.\n\nAn `Employee` entity with `name` and `salary` as fields could be\ndefined as:\n\n``` clojure\n  [Employee\n   [name\n    salary]]\n```\n\nWith this setup we are not specifying what `name` and `salary` are. It\nmight be a good idea to do something like this:\n\n``` clojure\n  [Employee\n   [^String name\n    ^Float  salary]]\n```\n\nTypes are defined using a meta payload to the symbol that represents\nthe field or the parameter. You can read more about scalar types\nbelow.\n\nTypes can also be represented by the more explicit meta object:\n\n``` clojure\n  [Employee\n   [^{:type String} name\n    ^{:type Float}  salary]]\n```\n\nEntities are also considered types therefore, if an `Employee` has a\n`supervisor` who's also an `Employee` you might write:\n\n``` clojure\n  [Employee\n   [^String   name\n    ^Float    salary\n    ^Employee supervisor]]\n```\n\nYou could want a `height` field that can return the employee's height\nin a particular unit:\n\n``` clojure\n  [Employee\n   [^String   name\n    ^Float    salary\n    ^Employee supervisor\n    ^Integer  height [^Unit unit]]\n\n   ^{:enum true}\n   Unit\n   [CENTIMETERS FEET]]\n```\n\nThere's quite a bit going on here that you can explore in detail in\nthe sections below. But here's a summary. First we've added the field\n`height` to the `Employee` entity. It returns an `Integer` and it also\nexpects a parameter called `unit` of the type `Unit`.\n\nWe've defined `Unit` separately as an enum (you can see more details\nin the sections below). `Unit` can be either `CENTIMETER` or `FEET`.\n\n### Scalar types\n\nHodur has five primitive scalar types that can be composed with your\nown entities to design your model. Four of them are quite\nself-explanatory: `String`, `Float`, `Integer` and `Boolean`.\n\nThe last two are highly opinionated and are `DateTime` and `ID`.\n\nHodur's plugins must have reasonable defaults to represent each one of\nthese scalar types. Plugins may also expose finer grained controls to\nmanage type precision (for instance 32bit integers vs 64bit integers).\n\n### Cardinalities\n\nOne employee may have a series of reportees. This kind of cardinality\nis defined with the `:cardinality` meta marker:\n\n``` clojure\n  [Employee\n   [^{:type String}       name\n    ^{:type Float}        salary\n    ^{:type Employee\n      :cardinality [0 n]} reportees]]\n```\n\nIn this example we are telling Hodur that `reportees` can be anywhere\nfrom `0` employees to `n` employees.\n\nYou can be as specific as you want. A cardinality of `[4]` means\nexactly `4` entries; `[3 5]` means `3` to `5`. If `:cardinality` is\nunspecified, it's assumed as `[1]`.\n\n### Optional fields and parameters\n\nFields and parameters are required by default. In other words, plugins\nmust implement mechanisms to avoid `null` problems if a field or\nparameter is mandatory.\n\nIf you want to make a field optional, use the `:optional` meta marker\non the field:\n\n``` clojure\n  [Employee\n   [^{:type String}    first-name\n    ^{:type String\n      :optional true}  middle-name\n    ^{:type String}    last-name]]\n```\n\nIf you want to make a parameter optional, use the `:optional` meta\nmarker on the parameter:\n\n``` clojure\n  [QueryRoot\n   [employees [^{:type String\n                 :optional true} search-term]]]\n```\n\nA common pattern is to make a parameter optional while also assigning\na default value to it with `:default`:\n\n``` clojure\n  [QueryRoot\n   [employees-by-location [^{:type String\n                             :optional true\n                             :default \"HQ\"} location]]]\n```\n\n### Special entity markers\n\n#### Interfaces and Implementations\n\nEntities can be marked as `:interface` which can be used by plugins\nthat explore such a concept. Entities that implement an interface use\nthe `:implements` marker to indicate which interface(s) they\nimplement:\n\n``` clojure\n  [^{:interface true}\n   Pet\n   [^String name]\n\n   ^{:implements Pet}\n   Dog\n   [^String bark]\n\n   ^{:implements Pet}\n   Cat\n   [^String mewow]]\n```\n\nThe `:implements` marker also accepts a vector with a series of\ninterfaces that the entity implements.\n\n#### Enums\n\nEnums are special kind of entities. They can assume one of the values\ndefined as fields. Enum fields do not support parameters.\n\nEnums are marked with `:enum`:\n\n``` clojure\n  [Employee\n   [^String   name\n    ^Float    salary\n    ^Employee supervisor\n    ^Integer  height [^Unit unit]]\n\n   ^{:enum true}\n   Unit\n   [CENTIMETERS FEET]]\n```\n\n#### Unions\n\nUnions are very similar to interfaces, but they don't get to\nspecify any common fields between the types. They are useful when\na certain field or parameter can be any one of the specified\nentities within the union.\n\nIn the following example the `search` field of the `QueryRoot` entity\nreturns a collection of `SearchItem` which are unions of `Employee`\nand `Company`:\n\n``` clojure\n  [Employee\n   [^String name\n    ^Float  salary]\n\n   Company\n   [^String address]\n\n   ^{:union true}\n   SearchItem\n   [Employee Company]\n   \n   QueryRoot\n   [^{:type SearchItem\n      :cardinality [0 n]}\n    search [^String term]]]\n```\n\n### Documentation and deprecation\n\nEntities, fields, and parameters can all be documented by using marker\n`:doc`.\n\n``` clojure\n  [^{:doc \"A representation of an Employee\"}\n   Employee\n   [^{:type String\n      :doc \"The employee's name\"}   name\n    ^{:type Float\n      :doc \"The employee's salary\"} salary]]\n```\n\nEntities, fields, and parameters can additionally be marked for\ndeprecation by using the marker `:deprecation`. Deprecation is a\nstring that describes the reasons for the deprecation as well as\npoints to alternatives.\n\n``` clojure\n  [^{:doc \"A representation of an Employee\"}\n   Employee\n   [^{:type String\n      :doc \"The employee's name\"}\n    name\n    ^{:type Float\n      :doc \"The employee's salary\"}\n    salary\n    ^{:type Float\n      :deprecation \"This field will be fully removed by December. Please use `name` instead.\"}\n    first-name]]\n```\n\n### Tagging\n\nIn general, plugins should only process entities, fields, and\nparameters that have been tagged for them. I.e. a `datomic` plugin\nwill have a particular tagging marker such as `:datomic/tag` that\nneeds to be added to each symbol you want the plugin to process.\n\nThe following example tags `Employee` and its fields `first-name` and\n`last-name` for the `datomic` plugin.\n\n``` clojure\n  [^{:datomic/tag true}\n   Employee\n   [^{:type String\n      :datomic/tag true} first-name\n    ^{:type String\n      :datomic/tag} last-name]\n\n   Project\n   [^{:type String} name]]\n```\n\n#### Recursive tagging\n\nTagging can be very repetitive so Hodur provides features for tagging\nin a recursive fashion. The example above could be rewritten with:\n\n``` clojure\n  [^{:datomic/tag-recursive true}\n   Employee\n   [^{:type String} first-name\n    ^{:type String} last-name]\n\n   Project\n   [^{:type String} name]]\n```\n\nThis kind of scenario is ideal for entities that have several fields\nand/or parameters.\n\nThe marker `:\u003cplugin\u003e/tag-recursive` can also have filters such as\n`:only` and `:except`.\n\nThe following example will only tag the `Employee` entity and the\nfields `first-name` and `last-name`:\n\n``` clojure\n  [^{:datomic/tag-recursive {:only [Employee first-name last-name]}}\n   Employee\n   [^{:type String} first-name\n    ^{:type String} middle-name\n    ^{:type String} last-name]]\n```\n\nThe following example would achieve the same result as above but by\ntagging everything but `middle-name`:\n\n``` clojure\n  [^{:datomic/tag-recursive {:except [middle-name]}}\n   Employee\n   [^{:type String} first-name\n    ^{:type String} middle-name\n    ^{:type String} last-name]]\n```\n\n#### Default tagging\n\nSome times you just want to tag everything you are sending as part of\na group of entities. In these scenarios you need to first name the\nvery first symbol of your group `default` and then mark it. Hodur will\napply whatever you mark on `default` to all items in the group.\n\nIn the following example, Hodur will tag everything for the `datomic`\nplugin:\n\n``` clojure\n  [^{:datomic/tag true}\n   default\n   \n   Employee\n   [^{:type String} first-name\n    ^{:type String} last-name]\n\n   Project\n   [^{:type String} name]]\n```\n\nThe special `default` symbol can also be used to carry other markers\ndown into the group's items but the general usage is for tagging.\n\n### Naming conventions\n\nHodur does not care about naming conventions. However, it does\ndelegate naming choices fully to plugins. The way Hodur achieves this\nis by internally converting whatever naming convention was used in the\nsymbols into several options. This is done by leveraging\n[[https://github.com/qerub/camel-snake-kebab][camel-snake-kebab]].\n\n## Meta API\n\nOnce your model gets parsed, Hodur will retain an in-memory\nmeta-database that can be queried by either plugins or your\nimplementation proper.\n\nThe API is exposed as a DataScript API atom and DataScript proper is a\ndependency of Hodur. Therefore, you can require DataScript and use its\nquery directly.\n\nThe example below uses both `pull` and a Datalog query to return all\nthe items which are marked with a `:datomic/tag`.\n\n``` clojure\n  (require '[datascript.core :as d])\n\n  (d/q '[:find [(pull ?e [*]) ...]\n         :where\n         [?e :datomic/tag true]]\n       @c)\n```\n\nAttributes are named with qualified keywords in four different\ncategories:\n\n1. `:type/...`: all entities (AKA types)\n2. `:field/...`: all fields\n3. `:param/...`: all parameters\n4. `\u003cplugin\u003e/...`: plugin names should qualify keywords (see\n   `:datomic/tag` above)\n\n### Naming\n\nFor entities, fields, and parameters the provided name in the model is\nexposed as either `:type/name`, `:field/name`, and\n`:param/name`. Additionally, Hodur generates indexes with:\n\n- `/kebab-case-name`\n- `/PascalCaseName`\n- `/camelCaseName`\n- `/snake_case_name`\n\n### Entity Markers API\n\nEntities have Boolean attributes for interfaces, enums and unions:\n`:type/interface`, `:type/enum`, and `:type/union` respectively.\n\n### Field Markers API\n\nTBD: `:field/type` and `:field/parent` (`:field/_parent`) `:field/cardinality`\n\n### Param Markers API\n\nTBD: `:param/type` and `:param/parent` (`:param/_parent`) `:param/cardinality`\n\n### Authoring Plugins\n\nTBD: choose naming convention, use d/q, filter by \u003cplugin\u003e/tag, do your thing\n\n### Bugs\n\nIf you find a bug, submit a\n[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 © 2018 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-engine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhodur-org%2Fhodur-engine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhodur-org%2Fhodur-engine/lists"}