{"id":18550137,"url":"https://github.com/lambdaisland/facai","last_synced_at":"2025-06-20T12:03:13.114Z","repository":{"id":38379969,"uuid":"471374195","full_name":"lambdaisland/facai","owner":"lambdaisland","description":"Factories for fun and profit. 恭喜發財！","archived":false,"fork":false,"pushed_at":"2023-02-28T08:57:45.000Z","size":121,"stargazers_count":47,"open_issues_count":2,"forks_count":0,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-06-08T21:35:35.962Z","etag":null,"topics":["clojure","testing"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lambdaisland.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2022-03-18T13:14:52.000Z","updated_at":"2025-04-30T04:12:32.000Z","dependencies_parsed_at":"2024-04-23T08:49:34.894Z","dependency_job_id":"a258f28b-e4a7-4a6e-aca7-912b14faacdc","html_url":"https://github.com/lambdaisland/facai","commit_stats":{"total_commits":70,"total_committers":5,"mean_commits":14.0,"dds":0.05714285714285716,"last_synced_commit":"3fbd1d6a1942f28f7d2a9c2127b04757f0d1db8c"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/lambdaisland/facai","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ffacai","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ffacai/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ffacai/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ffacai/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lambdaisland","download_url":"https://codeload.github.com/lambdaisland/facai/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lambdaisland%2Ffacai/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260158901,"owners_count":22967306,"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","testing"],"created_at":"2024-11-06T21:03:28.530Z","updated_at":"2025-06-20T12:03:08.051Z","avatar_url":"https://github.com/lambdaisland.png","language":"Clojure","funding_links":["http://opencollective.com/lambda-island"],"categories":[],"sub_categories":[],"readme":"# facai\n\n\u003c!-- badges --\u003e\n[![cljdoc badge](https://cljdoc.org/badge/com.lambdaisland/facai)](https://cljdoc.org/d/com.lambdaisland/facai) [![Clojars Project](https://img.shields.io/clojars/v/com.lambdaisland/facai.svg)](https://clojars.org/com.lambdaisland/facai)\n\u003c!-- /badges --\u003e\n\nFactories for fun and profit! Gongxi facai!\n\nWe are working on the next iteration of this idea called\n[Harvest](https://github.com/lambdaisland/harvest). Rather than introducing some\nmajor breaking changes here, we are going to keep things stable here, while\nworking on the new API under `lambdaisland.harvest`.\n\nNew users should consider using Harvest once it has an official release out.\nExisting users can decide if they want to stick to Facai, or make the effort to\nswitch.\n\nWe will still accept bugfixes on this repo, but no new features, and we will no\nlonger actively maintain the code here beyond shepherding incoming issues and\nPRs.\n\n## Getting Started\n\nAn example speaks a thousand words. How often have you looked at a function\ndefinition and said to yourself, \"if only I could see a real-world example of\nthe arguments that this function is called with\".\n\nIt's a recurring complaint with Clojure, it's often not obvious from the code\nwhat the shape of the data is. And yet we constantly are in need of snippets of\ndata, to drive our REPL sessions, to use in our tests, to populate databases so\nwe can test the UI, or to display in Devcards or notebooks.\n\nThere are a few solutions to this.\n\n- simply write out example data as EDN literals, whether it be in tests, or in\n  comments interspersed or at the bottom of the file\n- provide test.check style generators so you can generate values on the fly\n- provide schema/spec-like type information, as a form of documentation, runtime\n  validation, and possibly to drive test.check-style generators\n\nThese approaches all have value, but we think there's room for something that\nsits in the middle of literal data and generators, and that can complement these\napproaches.\n\nTest.check is great at finding obscure bugs and testing the narrowest edge\ncases, it does this by deliberately generating \"weird\" data. Unreadable\ncharacters, huge data structures, nils. It's great for validating code\nproperties, but it's not meant for human consumption. The data is hard to scan.\nAs an example of what kind of data flows around in your system it's worthless.\nIt's the epitome of untypical data.\n\nEven for testing test.check is not always the best option. Writing good\ngenerators (even with the help of Spec or Malli) and finding suitable properties\nto test is not trivial and takes practice. Many code bases would be well served\nwith more and better example based tests before going that route.\n\nWriting data simply as literals, for instance in unit tests, can be totally\nadequate for that, but it gets repetitive. And much of that data may be defaults\nthat have little bearing on what your test is asserting, thus adding noise. It\nalso becomes a maintenance issue if you ever decide to change your data\nrepresentation, because of the amount of duplication.\n\nFinally we think that coming up with good example data is valuable work that\ndeserves a larger payoff. The same shape of data you need for your tests can\nserve you well when testing UI, or when documenting internal APIs in repl\nsessions or notebooks.\n\nSo this is what Facai factories are about. A standardized way to define\n\"factories\" for the various types of information that flow through your app. It\nhas a bit of smarts, it knows about associations between different types of\nentities, and can integrate with the database (be it relational or datalog), to\nquickly create and insert the data you need.\n\nThat concludes the TED talk. Let's have a quick runthrough of what all of this\nlooks like.\n\n```clojure\n(ns my-app.factories\n  \"When starting out simply create a single `factories` namespace, you can split\n  it up later if it gets too big.\n\n  I'll alias `lambdaisland.facai` to `f` for brevity, you can alias to `facai`\n  if you prefer to be a bit more explicit.\n  \"\n  (:require [lambdaisland.facai :as f]))\n\n;; Let's define our first factory. You pass it a template of what your data\n;; looks like. In this case we simply put a literal map.\n(f/defactory user\n  {:user/name   \"Lilliam Predovic\"\n   :user/handle \"lilli42\"\n   :user/email  \"lilli42@example.com\"})\n\n;; We can generate data from this template by calling the factory as a function,\n;; which is an alias for calling `f/build-val`\n\n(user) ; or (f/build-val user)\n;; =\u003e #:user{:name \"Lilliam Predovic\",\n;;           :handle \"lilli42\",\n;;           :email \"lilli42@example.com\"}\n\n;; It can take a few options, use `:with` to override or supply additional\n;; values.\n\n(user {:with {:user/name \"Mellissa Schimmel\"}})\n;; =\u003e #:user{:name \"Mellissa Schimmel\",\n;;           :handle \"lilli42\",\n;;           :email \"lilli42@example.com\"}\n\n;; Functions in the template will be called, and the result in turn is treated\n;; as a factory template.\n\n(f/defactory user\n  {:user/name   \"Lilliam Predovic\"\n   :user/handle \"lilli42\"\n   :user/email  #(str \"lilli\" (rand-int 100) \"@example.com\")})\n\n(user)\n;; =\u003e #:user{:name \"Lilliam Predovic\",\n;;           :handle \"lilli42\",\n;;           :email \"lilli92@example.com\"}\n\n;; This means you can decide if you want your factories to be fairly static, or\n;; more dynamic and random. Maybe you want all usernames to be unique, the\n;; numbered helper can help with that.\n\n(require '[lambdaisland.facai.helpers :as fh])\n\n(f/defactory user\n  {:user/name   \"Lilliam Predovic\"\n   :user/handle (fh/numbered #(str \"lilli\" %))\n   :user/email  #(str \"lilli\" (rand-int 100) \"@example.com\")})\n\n;; The Faker library can also be useful, especially if you want to for instance\n;; populate UIs to take screenshots\n\n(require '[lambdaisland.faker :refer [fake]])\n\n(f/defactory user\n  {:user/name   #(fake [:name :name])\n   :user/handle #(fake [:internet :username])\n   :user/email  #(fake [:internet :email])})\n\n;; Factories can have traits, which are merged into the main template on demand.\n\n(f/defactory article\n  {:article/title \"7 Tip-top Things To Try\"\n   :article/status :draft}\n\n  :traits\n  {:with\n   {:published {:article/status \"published\"}\n    :unpublished {:article/status \"unpublished\"}\n    :in-the-future {:article/published-at #(fh/days-from-now 2)}\n    :in-the-past {:article/published-at #(fh/days-ago 2)}}})\n\n(article {:traits [:published :in-the-future]})\n;; =\u003e #:article{:title \"7 Tip-top Things To Try\",\n;;              :status \"published\",\n;;              :published-at\n;;              #object[java.time.ZonedDateTime 0x345be9a1 \"2022-03-30T10:09:17.584903790Z[UTC]\"]}\n\n;; If you have associations between data, then you simply use the factory as the value.\n;; Or if you want to set some specifics with `:with` or `:traits` you can call the factories.\n\n(f/defactory article\n  {:article/title \"7 Tip-top Things To Try\"\n   :article/submitter user\n   :article/author (user {:with {:user/roles #{\"author\"}}})})\n\n(article)\n;; =\u003e #:article{:title \"7 Tip-top Things To Try\",\n;;              :submitter\n;;              #:user{:name \"Mr. Reinaldo Hartmann\",\n;;                     :handle \"garth\",\n;;                     :email \"ligia@grantgroup.co\"},\n;;              :author\n;;              #:user{:name \"Doria Wisoky Sr.\",\n;;                     :handle \"romaine\",\n;;                     :email \"darrin@schummandsons.co\",\n;;                     :roles #{\"author\"}}}\n\n\n;; Note that the actual expansion gets deferred in this case, the `(user)` call\n;; will only get expanded when building the `article`, so you get a different\n;; username each time.\n\n(article)\n;; =\u003e #:article{:title \"7 Tip-top Things To Try\",\n;;              :submitter\n;;              #:user{:name \"Hobert Fadel\",\n;;                     :handle \"kerry91\",\n;;                     :email \"delana57@bednarbednarand.com\"},\n;;              :author\n;;              #:user{:name \"Rosemary Reynolds\",\n;;                     :handle \"spencer.bruen\",\n;;                     :email \"laurine@lockmanlockmana.info\",\n;;                     :roles #{\"author\"}}}\n\n\n;; Factories can inherit from other factories. Often it's preferrable to use\n;; traits, but this can be a useful feature.\n\n(f/defactory blog-post\n  :inherit article\n  {:post/uri-slug \"/post\"})\n\n(blog-post)\n;; =\u003e {:article/title \"7 Tip-top Things To Try\",\n;;     :article/submitter\n;;     #:user{:name \"Rima Wintheiser\",\n;;            :handle \"numbers\",\n;;            :email \"leandrowelch@welchwelchandwe.co\"},\n;;     :article/author\n;;     #:user{:name \"Jeffrey Bruen DO\",\n;;            :handle \"margy81\",\n;;            :email \"williams.rippin@rippingroup.biz\",\n;;            :roles #{\"author\"}},\n;;     :post/uri-slug \"/post\"}\n```\n\n## Paths, Selectors, Rules, and Unification\n\nDuring the build process Facai keeps track of the \"path\" it is currently at.\nWhen it handles a map entry the map key is added to the path. When it handles a\nnested factory then the factory id (a fully qualified symbol) is added onto the\npath. When handling other collections the sequential index is added to the path.\n\nSo for the `(article)` example above you get these successive paths:\n\n```clj\n[my-app.factories/article]\n[my-app.factories/article :article/title]\n[my-app.factories/article :article/submitter my-app.factories/user]\n[my-app.factories/article :article/submitter my-app.factories/user :user/name]\n[my-app.factories/article :article/submitter my-app.factories/user :user/name]\n[my-app.factories/article :article/submitter my-app.factories/user :user/handle]\n[my-app.factories/article :article/submitter my-app.factories/user :user/handle]\n[my-app.factories/article :article/submitter my-app.factories/user :user/email]\n[my-app.factories/article :article/submitter my-app.factories/user :user/email]\n[my-app.factories/article :article/author my-app.factories/user]\n[my-app.factories/article :article/author my-app.factories/user :user/name]\n[my-app.factories/article :article/author my-app.factories/user :user/name]\n[my-app.factories/article :article/author my-app.factories/user :user/handle]\n[my-app.factories/article :article/author my-app.factories/user :user/handle]\n[my-app.factories/article :article/author my-app.factories/user :user/email]\n[my-app.factories/article :article/author my-app.factories/user :user/email]\n[my-app.factories/article :article/author my-app.factories/user :user/roles]\n[my-app.factories/article :article/author my-app.factories/user :user/roles 0]\n```\n\nThese paths can be matched with selectors, these function conceptually a lot like CSS selectors:\n\n- Selectors are vectors of keywords, symbols/factories, or numbers (indices)\n- A non-vector path `:xxx` is the same as `[:xxx]`\n- `[:foo]` matches any path that ends with `:foo`\n- `[:foo :bar]` matches any path that ends with `:bar`, and that has `:foo` somewhere before `:bar` in the path\n- `[:foo :\u003e :bar]` matches any path that ends `:foo :bar`\n- `[#{:foo :bar}]` matches any path that ends with `:foo` or `:bar`\n\n### Selecting Associations\n\nWhen creating an `(article)` Facai also created two users, one of them the\nauthor, and one of them the submitter. You can use selectors to conveniently\npull out these associated entities. For this you use `facai/build`, which\nreturns a map with `:facai.result/value` (in this case the author), as well as\n`:facai.result/linked`, a map with any \"linked\" entities. With this result map\nin hand you can select linked values.\n\n```clj\n(let [result    (f/build article)\n      submitter (f/sel1 result :article/submitter)\n      author    (f/sel1 result :article/author)]\n  {:article   (f/value result)\n   :submitter submitter\n   :author    author})\n```\n\nThis is quite convenient in tests, you can let a single factory generate a bunch\nof linked entities, and then you just reference the ones you need.\n\nWe could also get all users:\n\n```clj\n(let [result (f/build article)]\n  (f/sel result user))\n```\n\n### Rules\n\nSelectors can also be used to pass in rules when calling a factory. This\nprovides a convenient way to set deeply nested values. Rules are provided as a map from selector to value.\n\n```clj\n(article {:rules {[:article/submitter :user/handle] \"editosaurus\"}})\n;;=\u003e\n{:article/title \"7 Tip-top Things To Try\",\n :article/submitter\n {:user/name \"Elisa Ferry\",\n  :user/handle \"editosaurus\",\n  :user/email \"ty41@ankundingankund.net\"},\n :article/author\n {:user/name \"Amalia Sporer\",\n  :user/handle \"bobby\",\n  :user/email \"aimee24@brakusbrakusand.org\",\n  :user/roles #{\"author\"}}}\n```\n\n### Unification\n\nAs a rule value you can pass the special value `(f/unify)`. The first time such\na rule matches, it will generate factory data as usual, any subsequent matches\nwill then reuse that data. This has some really useful applications. Say we have\na data model where many types of entities have a link back to an `organization`.\n\n```clj\n(f/defactory user\n  {:username \"jonny\"\n   :org organization})\n\n(f/defactory department\n  {:name \"sales\"\n   :org organization\n   :head user})\n\n(f/defactory meeting-room\n  {:org organization\n   :room-number \"123\"\n   :department department})\n\n(f/defactory booking\n  {:start-time #(java.util.Date.)\n   :booked-by user\n   :room meeting-room})\n```\n\nWhen asking for a `(booking)` we'll get no fewer than three different\norganizations. That doesn't really make sense, bookings are only made within a\nsingle organization. We can unify these as follows:\n\n```clj\n(booking {:rules {organization (f/unify)}})\n```\n\nYou can pass a value to `(f/unify)` to unify across multiple rules:\n\n```clj\n(booking {:rules {:org-id (f/unify :org) :organization-id (f/unify :org)}})\n```\n\nAll rules that have the same unify value will end up linking to the same value.\n\n### Hooks\n\nFactories can contain an `after-build` hook, this is function which gets called\nafter we've constructed a value for that factory. It gets passed a \"context\"\nmap, which contains among other things the `:facai.result/value`. A common use\ncase is to update this value. You can use the `f/update-result` helper for that,\nwhich is a shorthand for `(update ctx :facai.result/value ...)`\n\n```clj\n(f/defactory product\n  {:sku \"123\"\n   :price 12.99})\n\n(f/defactory product-line-item\n  {:product product\n   :quantity 1}\n\n  :after-build\n  (fn [ctx]\n    (f/update-result\n     ctx\n     (fn [{:as res :keys [product quantity]}]\n       (assoc res :total (* (:price product) quantity))))))\n\n(product-line-item);; =\u003e {:product {:sku \"123\", :price 12.99}, :quantity 1, :total 12.99}\n(product-line-item {:with {:quantity 2}});; =\u003e {:product {:sku \"123\", :price 12.99}, :quantity 2, :total 25.98}\n(product-line-item {:rules {:price 69 :quantity 2}});; =\u003e {:product {:sku \"123\", :price 69}, :quantity 2, :total 138}\n```\n\nNotice how the result always has the right total price.\n\n## Database Integration\n\nOne of the big selling points of Facai is that it makes it trivial to create\ntest data and insert it into the database in one go. This can dramatically clean\nup test setup. Currently we ship initial support for next-jdbc, and for Datomic\npeer. These integrations will indubitably have to be improved, but they provide\na good starting off point. In practice people will likely want to tailor these\nto their specific persistence conventions, so they can serve as inspiration.\n\n### Next.jdbc\n\nIn this case you first use `create-fn`, passing in a datasource, and any\nspecific configuration regarding the conventions of how you map Clojure data to\ntables.\n\nFactories themselves can take some options as well, like setting an explici\n`facai.next-jdbc/table-name`, if the name can't be inferred from the factory\nname.\n\n```clojure\n(in-ns 'my-app.factories)\n(require '[clojure.string :as str]\n         '[lambdaisland.facai.next-jdbc :as fnj]\n         '[next.jdbc :as nj]\n         '[next.jdbc.quoted :as quoted])\n\n;; Some setup to quickly get an in-memory database. Supposedly you'll have this\n;; kind of stuff in your app somewhere already.\n\n(defn create-table-sql [{:keys [table columns]}]\n  (str \"CREATE TABLE \" (quoted/ansi table)\n       \" (\" (str/join\n             \",\"\n             (for [[k v] columns] (str (quoted/ansi k) \" \" v)))\n       \")\"))\n\n(def table-defs\n  [{:table \"users\"\n    :columns {\"id\" \"INT AUTO_INCREMENT PRIMARY KEY\"\n              \"name\" \"VARCHAR(255)\"}}\n   {:table \"posts\"\n    :columns {\"id\" \"INT AUTO_INCREMENT PRIMARY KEY\"\n              \"title\" \"VARCHAR(255)\"\n              \"author_id\" \"INT\"\n              \"created_at\" \"TIMESTAMP DEFAULT CURRENT_TIMESTAMP\"}}])\n\n(f/defactory user\n  {:name #(fake [:name :name])})\n\n(f/defactory post\n  {:title #(fake [:dc-comics :title])\n   :author user})\n\n(f/defactory article\n  {:title \"Article\"\n   :author user}\n  :facai.jdbc/table \"posts\")\n\n\n(def ds (nj/get-datasource (str \"jdbc:h2:/tmp/h2-db-\" (rand-int 1e8))))\n(run! #(nj/execute! ds [(create-table-sql %)]) table-defs)\n(def create!\n  (fnj/create-fn\n   {:facai.next-jdbc/ds ds\n    :facai.next-jdbc/fk-col-fn #(keyword (str (name %) \"-id\"))}))\n\n(:facai.result/value (create! post))\n;; =\u003e {:title \"Gotham Central\",\n;;     :author-id 2,\n;;     :id 2,\n;;     :created-at #inst \"2022-03-28T10:44:05.447624000-00:00\"}\n\n;; So what happened here? Facai generated a user, persisted it to the database,\n;; the database assigned a unique id, and we then used that for the association\n;; when generating the article. The database also generated the value for\n;; created-at.\n\n;; If you look at the full result of `create!` you see that it also includes these\n;; linked entities.\n\n(create! post)\n;; =\u003e {:facai.result/value\n;;     {:title \"Crisis On Infinite Earths\",\n;;      :author-id 3,\n;;      :id 3,\n;;      :created-at #inst \"2022-03-28T10:45:03.367515000-00:00\"},\n;;     :facai.result/linked\n;;     {[my-app.factories/post :author] {:name \"Richard Quigley\", :id 3}},\n;;     :facai.factory/id my-app.factories/post}\n\n;; These are in a map with the keys being the \"path\". This includes map keys\n;; whenever we recurse into a map, and it includes the names of factories. When\n;; recursing into a sequential collection the indexes are added to the path.\n\n(create! (repeat 3 post))\n;; =\u003e #:facai.result{:value (6 5 4),\n;;                   :linked\n;;                   {[0 my-app.factories/post :author]\n;;                    {:name \"Ettie Weissnat DDS\", :id 4},\n;;                    [0]\n;;                    {:title \"Swamp Thing: The Anatomy Lesson\",\n;;                     :author-id 4,\n;;                     :id 4,\n;;                     :created-at #inst \"2022-03-28T10:46:39.842557000-00:00\"},\n;;                    [1 my-app.factories/post :author]\n;;                    {:name \"Kermit Hartmann\", :id 5},\n;;                    [1]\n;;                    {:title \"Detective Comics\",\n;;                     :author-id 5,\n;;                     :id 5,\n;;                     :created-at #inst \"2022-03-28T10:46:39.898035000-00:00\"},\n;;                    [2 my-app.factories/post :author]\n;;                    {:name \"Rolanda Torphy\", :id 6},\n;;                    [2]\n;;                    {:title \"All Star Superman\",\n;;                     :author-id 6,\n;;                     :id 6,\n;;                     :created-at #inst \"2022-03-28T10:46:39.947156000-00:00\"}}}\n\n;; You can use the `sel` and `sel1` helpers to fetch these objects. The matching\n;; works similar to CSS selectors.\n\n;; Say we want to create three posts in the database, and then care about the\n;; authors of the posts.\n\n(f/sel (create! (repeat 3 post)) [:author])\n;; =\u003e ({:name \"The Hon. Bradley Leuschke\", :id 7}\n;;     {:name \"Violet Koelpin\", :id 8}\n;;     {:name \"Annita Hauck\", :id 9})\n\n(let [res (create! (repeat 3 post))\n      first-author (f/sel1 res [0 :author])]\n  first-author)\n;; =\u003e {:name \"Fr. Denisha Wyman\", :id 28}\n```\n\n### Datomic\n\nThe datomic integration has a `create!` function which takes a datomic\nconnection and a factory/template. It will handle tempids.\n\n```clojure\n(require '[datomic.api :as d]\n         '[lambdaisland.facai :as f]\n         '[lambdaisland.facai.datomic :as fd])\n\n(d/create-database \"datomic:mem://foo\")\n(def conn (d/connect \"datomic:mem://foo\"))\n\n(f/defactory line-item\n  {:line-item/description \"Widgets\"\n   :line-item/quantity 5\n   :line-item/price 1.0})\n\n(f/defactory cart\n  {:cart/created-at #(java.util.Date.)\n   :cart/line-items [line-item line-item]})\n\n(def schema\n  [{:db/ident       :line-item/description,\n    :db/valueType   :db.type/string,\n    :db/cardinality :db.cardinality/one}\n   {:db/ident       :line-item/quantity,\n    :db/valueType   :db.type/long,\n    :db/cardinality :db.cardinality/one}\n   {:db/ident       :line-item/price,\n    :db/valueType   :db.type/double,\n    :db/cardinality :db.cardinality/one}\n   {:db/ident       :cart/created-at,\n    :db/valueType   :db.type/instant,\n    :db/cardinality :db.cardinality/one}\n   {:db/ident       :cart/line-items,\n    :db/valueType   :db.type/ref,\n    :db/cardinality :db.cardinality/many}])\n\n(let [url (doto (str \"datomic:mem://db\" (rand-int 1e8))\n            d/create-database)\n      conn (d/connect url)]\n  @(d/transact conn schema)\n  (fd/create! conn cart))\n\n;; =\u003e {:facai.result/value\n;;     {:cart/created-at #inst \"2022-03-28T11:02:19.667-00:00\",\n;;      :cart/line-items [17592186045419 17592186045420],\n;;      :db/id 17592186045418},\n;;     :facai.result/linked\n;;     {[user/cart :cart/line-items 0]\n;;      {:line-item/description \"Widgets\",\n;;       :line-item/quantity 5,\n;;       :line-item/price 1.0,\n;;       :db/id 17592186045419},\n;;      [user/cart :cart/line-items 1]\n;;      {:line-item/description \"Widgets\",\n;;       :line-item/quantity 5,\n;;       :line-item/price 1.0,\n;;       :db/id 17592186045420}},\n;;     :facai.factory/id user/cart,\n;;     :db-after datomic.db.Db@9e510866}\n\n```\n\n\n## Introduction\n\nMost application code deals with some form of \"entities\", often stored in a\ndatabase, be it relational or something else. In Clojure code these are\ntypically represented and passed around as maps.\n\nWhen calling functions from the REPL, or writing tests, or rendering demo UI\n(e.g. with devcards), you will need data to feed into these things.\n\n```clj\n(deftest user-login-test\n  (let [user {:user/handle \"plexus\", :user/full-name \"Arne Brasseur\", :user/pwd-hsh \"ff2f92842fa0428\"}\n        user (db/create! user)]\n    (is (user/auth-ok? user \"sekrit\"))))\n```\n\nThis is fine, but it has some drawbacks. This test does not care about\n`:user/handle` or `:user/full-name`, but there's a good chance we have to\ninclude them to have a valid user representation, so it adds noise and reduces\nclarity. And if we add an extra mandatory attribute to a user then we might have\nto update a lot of tests.\n\nThis is also a trivial example, maybe to have a valid `user` you first have to\ncreate a `profile`. Now every test that touches users needs to also create a\nprofile and link them, even if the test does not actually care about profiles.\n\n## Same data, different viewpoint\n\nTo continue our example of users and profiles. When inserting these into a\ndatalog database they might look like this:\n\n```clj\n[{:db/id \"profile\"\n  :profile/avatar \"avatar.jpg\"}\n {:db/id \"user\"\n  :user/handle \"plexus\"\n  :user/profile \"profile\"}]\n```\n\nWhen getting them back from the database they may look like this:\n\n```clj\n[{:db/id 16904842\n  :profile/avatar \"avatar.jpg\"}\n {:db/id 16904913\n  :user/handle \"plexus\"\n  :user/profile 16904842}]\n```\n\nBut when requesting them through the `entity` API (as provided e.g. by Datomic)\nthey'll look like this:\n\n```clj\n{:db/id 16904913\n :user/handle \"plexus\"\n :user/profile {:db/id 16904842\n                :profile/avatar \"avatar.jpg\"}}\n```\n\nWhen using relational databases you may add an `_id` to a foreign key column,\nand so forth. It's all the same data, but depending on the part of the system\nthere may be different convenients around nesting vs flat, normalized or\ndenormalized, representation of associations/links, etc.\n\nThe idea with factories is that you define the factories for your data once, and\nthen set up helpers to get data in the shape you need, and to deal with any\npersistence concerns. This way you get maximal reuse between backend, frontend,\nClerk notebooks, demo UI, and so forth.\n\n## Getting started\n\n\n\n## Design goals\n\n- simple, convenient, example-based factories a la factory-bot\n- for use in tests, devcards, clerk...\n- handle groups of related objects, associations, etc.\n- easy to pick up, more intuitive than Specmonstah\n- can generate data suitable for relational (flat, explicit foreign keys), or datalog (tx-style or entity style), or whatever you need. FK handling is pluggable.\n- optionally integrate with spec and/or malli, for validation and/or generation\n- provide a smooth on-ramp towards generative testing\n- comes with ready-made database integration for JDBC, Datomic, Crux, etc.\n- Handle inserting and getting default values from the db (e.g. `auto_increment` keys, `created_by`, etc)\n- for datalog dbs: deal with tempids\n- separate factory definitions from db/app particulars -\u003e encourage reusability in multiple contexts in the same app\n- manipulate factories through code\n- inheritance, composition, specialization, nestable\n- arbitrarily shaped data. Maps, collections, whatever.\n- Clojure + ClojureScript\n\n## Potential design goals\n\n- Allow for factory serializability, store in db, travel over the wire\n- Faker integration\n\nNote that this is not a replacement but rather a complement to test.check-style generators.\n\n\u003c!-- installation --\u003e\n\u003c!-- /installation --\u003e\n\n\u003c!-- opencollective --\u003e\n## Lambda Island Open Source\n\n\u003cimg align=\"left\" src=\"https://github.com/lambdaisland/open-source/raw/master/artwork/lighthouse_readme.png\"\u003e\n\n\u0026nbsp;\n\nfacai is part of a growing collection of quality Clojure libraries created and maintained\nby the fine folks at [Gaiwan](https://gaiwan.co).\n\nPay it forward by [becoming a backer on our Open Collective](http://opencollective.com/lambda-island),\nso that we may continue to enjoy a thriving Clojure ecosystem.\n\nYou can find an overview of our projects at [lambdaisland/open-source](https://github.com/lambdaisland/open-source).\n\n\u0026nbsp;\n\n\u0026nbsp;\n\u003c!-- /opencollective --\u003e\n\n\u003c!-- contributing --\u003e\n## Contributing\n\nEveryone has a right to submit patches to facai, and thus become a contributor.\n\nContributors MUST\n\n- adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide)\n- write patches that solve a problem. Start by stating the problem, then supply a minimal solution. `*`\n- agree to license their contributions as MPL 2.0.\n- not break the contract with downstream consumers. `**`\n- not break the tests.\n\nContributors SHOULD\n\n- update the CHANGELOG and README.\n- add tests for new functionality.\n\nIf you submit a pull request that adheres to these rules, then it will almost\ncertainly be merged immediately. However some things may require more\nconsideration. If you add new dependencies, or significantly increase the API\nsurface, then we need to decide if these changes are in line with the project's\ngoals. In this case you can start by [writing a pitch](https://nextjournal.com/lambdaisland/pitch-template),\nand collecting feedback on it.\n\n`*` This goes for features too, a feature needs to solve a problem. State the problem it solves, then supply a minimal solution.\n\n`**` As long as this project has not seen a public release (i.e. is not on Clojars)\nwe may still consider making breaking changes, if there is consensus that the\nchanges are justified.\n\u003c!-- /contributing --\u003e\n\n\u003c!-- license --\u003e\n## License\n\nCopyright \u0026copy; 2022 Arne Brasseur and Contributors\n\nLicensed under the term of the Mozilla Public License 2.0, see LICENSE.\n\u003c!-- /license --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdaisland%2Ffacai","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flambdaisland%2Ffacai","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flambdaisland%2Ffacai/lists"}