-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Since I don't have time to implement it all right now, and since this will be interesting for reference and discussion anyway, I'll do a write-up of how I want Harvest to be different from Facai. Facai has been an interesting experiment, and I'm happy with a lot of what's in there, but the API and general mental model have some rough edges, and recently the vision has been crystallizing on how to do a much improved iteration of these ideas.
- lean into factories as values
- overrides, selected traits, rules, etc. are part of the factory
- selections are part of a factory
- provide APIs for manipulating factory values, rather than passing a bunch of different keyword options
- get rid of
*deferred*, everything is (implicitly) deferred - more flexible selector syntax, allow arbitrary function calls
- rules should be a vector, not a map, take cue from garden for the syntax
- persistence might be part of the factory, still undecided on that one
- possibly use
derefas sugar forbuild - decouple "is an entity" from "is a factory"
lean into factories as values
To be clear factories are already values, basically a factory is a map
{:harvest.factory/id `my-factory
:harvest.factory/template {:name "hello"}}But currently we require a ^{:type :harvest/factory} metadata, I'd drop that in favor of looking for certain "membership attributes", in particular :harvest.factory/template, to distinguish a factory from a plain map/template.
overrides, selected traits, rules, etc. are part of the factory
Currently when building a new value through a factory, you can specify a bunch of parameters.
(build my-factory {:with {:name "John"}, :traits [:cool]})And we have sugar for this, calling the factory as a function
(build my-factory {:with {:name "John"}, :traits [:cool]})In either case, if this call is made within a factory definition, then we defer the build, we basically return a special object with the factory and the options, so we can do the build at some later time, at which point it can participate in a larger build process.
A recent PR on Facai made that we always defer when called as a function, and you have to call build explicitly to get a value. On Facai that was reverted, here in Harvest that is currently still the case.
Instead I'd like to get rid of this deferred concept, and make these parameters part of the factory.
{:harvest.factory/id `my-factory
:harvest.factory/template {:name "hello"}
:harvest.factory/overrides {:name "john"}
:harvest.factory/selected-traits [:cool]}Now you can imagine assoc'ing these things in manually, more likely we'd provide APIs for this, so you start getting stuff like
(-> user
(h/with :name "John")
(h/with-traits :cool)
(h/apply-rules ....))We would probably still keep the Factory type, which most of these API calls would return. You can mostly treat it as a map. We probably would keep function call semantics for passing in top level value overrides, just cause it's nice syntax, especially within other factories.
(defactory user ,,,)
(defactory post
{:author (user :name "John Authorman")})These API calls probably also wouldn't actually assoc directly onto the map, but make a new factory with :harvest.factory/inherit the original factory. The main reason being that top-level defactory factories contain a reference to the var they are stored in, so that we can do late binding on them (imagine in a REPL flow, you have a factory B with a reference to factory A, and then you redefine factory A, we want to see those new values).
(so factories have a :harvest.factory/var #'the-factory-var, and if it's present the build algorithm will deref that and use that value to continue)
selections are part of a factory
Currently we have a build and build-val, the latter returns the plain value that the factory generates, e.g. a user or a post, the former returns a "result set" which contains a lot of metadata and stuff that's part of the construction process, in particular it includes all the built sub-entities, so you can select them using selectors.
(let [result (build factories/user)
user (sel1 result [factories/user])
company (sel1 result [factories/company])]
,,,)Instead the selection can also be part of the factory, you're just telling it to construct a derived or composite value. I'm imagining you would typically make it either a vector or a map, and then destructure.
(let [{:keys [user company]}
@(-> user
(h/sel {:user [factories/user first]
:company [factories/company first]}))]
,,,)
(let [[user company]
@(-> user
(h/sel [factories/user first]
[factories/company first]))]
,,,)more flexible selector syntax, allow arbitrary function calls
You also see here I'm using first in the selector instead of sel1.
Still a bunch of things to be figured out on what exactly the API for selections/selectors will/would be.
rules should be a vector, not a map, take cue from garden for the syntax
Currently rules are a map from selector to value, this looks kind of weird (vectors as map keys), I think it's more intuitive to do a garden-ish thing of having the last element in the vector be the value.
:rules
[[factories/user :name "Arne]
[factories/user factories/company :name "Acme"]]possibly use deref as sugar for build
I already demonstrated that in the earlier example, you build up your (derived) factory with API calls, then deref it. Or just deref it directly to get a plain value.
@user
@(user :name "Arne)
@(-> user (with-traits :admin))This is perhaps a slighlty unorthodox use for deref, but I think it makes sense intuitive. The result here is a static, pure value. Whereas the factory can continue randomness, database calls, etc.
persistence might be part of the factory, still undecided on that one
Currently the API has build/build-val, and then for persistence you call a specific create! function. This is (IIRC) largely analoguous with factory_bot. But if we go the deref route then there wouldn't necessarily be a build API, and so perhaps there shouldn't also be a create API, instead you do similar things as before
@(-> user harvest.jdbc/persist)
@(-> user (with-traits :admin) harvest.datomic/persist)