Fork me on GitHub
#clojure-spec
<
2019-12-06
>
Flo11:12:21

How can I check whether the (optional) keys are within a set? e.g. valid keys are #{:foo :bar :qux} and would (s/valid? :myspec {:foo true})

Flo11:12:17

ah! map-of is my friend

bortexz14:12:35

When using spec in the context of domain modelling, sometimes I see that some of the require I do are only for specs, as some entities would have properties from a different entity (sometimes to define relationships). This makes it very easy to end up with circular dependencies, is there any way on the namespace declaration to have only ns aliases, without requiring them to avoid circular dependencies?

Alex Miller (Clojure team)14:12:32

no, but you don't need to require them at all to use keywords with a certain namespace

Alex Miller (Clojure team)14:12:41

you just have to write the full namespace out w/o aliases

bortexz14:12:33

Ok, I thought about that, I wanted to know if there was any way to avoid having to write the full namespace

telekid18:12:51

We’ve been using this pattern at Flexport: https://ask.clojure.org/index.php/2817/lighter-weight-aliasing-for-keywords?show=8918#a8918 Not sure if it’s a good idea necessarily, but it works for us.

seancorfield18:12:56

That looks a lot more work and complexity than a simple alias/`create-ns` like I pasted in the main channel. Could you explain what your macros are trying to do, beyond just the alias/`create-ns` call I showed?

telekid20:12:49

Yeah, I think my def-synthetic-ns fn is overly complex, probably because of a combination of “my first macro” confusion and not noticing that create-ns returns the ns

vemv17:12:31

not sure if I already mentioned this one to you last time we talked, but I tend to use a namespace called kws.clj (for "keywords") which contains only specs, and zero implementation details (other than very simple predicates) since kws definitionally depend on nothing, you can always depend on a "kws" ns, and no circular deps will arise

bortexz17:12:59

That could be useful, but I prefer to have different namespaces as I want to keep ns keywords on the namespace that contains function that operate on them, for now I am using keywords under namespaces that do not correspond to the files, like :product/id instead of ::product/id , even if I define it’s spec in ::product namespace. I prefer this to writing the full namespace, for that I’ll probably wait until there is a better way of doing so

vemv18:12:04

I see! In case it wasn't clear, I actually use one kws ns per model (or 'module', depending on the chosen arch) e.g. product.clj + product/kws.clj

bortexz18:12:19

Oh ok, I thought you meant one big kws. Although this still does not solve interdependency between models where you want to reference each other through the specs, as you might need to import to define specs from another place. Image orders from a product, you might have ::product/id inside an order, and have ::orders referencing orders from product

vemv18:12:59

that design sounds problematic per se tbh: the DB design might not be sound, and in any case the generative part of spec might choke on the mutual recursion

bortexz18:12:21

But databases have relationships of this kind, by the product_id of an order you are able to get all orders from a product (::orders) . It would be the case when you want to hydrate a one-to-many relationship, the many has id of the one , and the one has an array of the many

bortexz18:12:46

I am not very familiar with the generative part of spec, but a problem I can see in generative is that it wouldn’t generate the same :product/id for all orders when generating a set of orders, aside from that the specs per se are not recursive, no spec end up calling itself, but both reference things from the other

vemv18:12:29

one-to-many is easily modeled as:

product/kws.clj # zero references to orders; since products exist independently from orders
order/kws.clj # references product specs; this is a simple (non-cyclic) dependency
and even a fancy many-to-many relationship can be modeled as:
product/kws.clj # zero references to orders
order/kws.clj # zero references to products
order_product/kws.clj # depends on orders and products

vemv18:12:48

I wouldn't let a trivial concern like ids (normally ids have the same type across all SQL tables) introduce cyclical deps IMO that's true regardless of the way you decide to use spec

bortexz18:12:31

I still think could be useful to have some product info on the order itself (without directly referencing a product). Maybe this scenario is not the ideal one for this use case, but I still think you might want to reference inside your schema specs from other ns, I don’t think it’s bad to have cyclical namespaced keywords inside specs, as it’s not a dependency itself, and allows a lot of flexibility for how you retrieve your domain entities. In my case the spec are not directly translated to database tables or columns (at least not a database I control), and sometimes properties from a parent entity are hydrated directly on the child entity, not always referencing by id

bortexz18:12:23

> I wouldn’t let a trivial concern like ids (normally ids have the same type across all SQL tables) introduce cyclical deps Namespaced keywords do not need a require, so it’s not strictly a cyclical dep, that’s only for aliasing them, and I think it is better to have the flexibility of being able to do that (or at least as it is now just using full namespace)

vemv19:12:36

Could be, I'd have to check it out carefully Still, keep in mind that specs are composable. so you can have non-cyclical, discrete product and order specs, and then completely aside, a hydrated-order spec that composes both. keeps things non-cyclical, without compromising DRY either :)

vemv19:12:56

also keep in mind that at write time, order should only have thin references to products. so a full-blown spec could be an impediment, as it could require more product keys to be present. the hypothetical hydrated-order spec keeps read/write concerns separate

bortexz19:12:29

schema/select allow to handle those contextual dependencies

👍 4
bortexz19:12:05

Basically I am going for these specs with schema like “This is everything an order might contain”, then by context I could select some properties

vemv19:12:39

yeah. it's in the radar for spec 2 as you probably know

vemv19:12:41

must be said, considering graphql-style responses, a monolithic spec can end up being problematic. there can be N "read models" of a given thing

bortexz19:12:44

Yeah, as this is a personal project I am already using it, so my concern was on that context

💯 4
bortexz19:12:42

> must be said, considering graphql-style responses, a monolithic spec can end up being problematic. there can be N “read models” of a given thing I am not familiar with graphql, what do you mean N read models?

vemv19:12:54

there are N possible schemas ("models") in which a client might want a response e.g. [[1 2] [3 4]] or {1 2 3 4} (same data, different representation) a monolithic spec cannot reasonably define all schemas that client could request

bortexz19:12:07

Why not? And what do you mean by monolithic schema?

vemv19:12:11

monolithic: a single spec that can be used for describing both reads and writes, relying on select for flexiblity

vemv19:12:48

problem with that: different clients (e.g. web, android, ios) might want different data shapes, pagination, camelCase, referenced models, etc probably a single spec defining all possible combinations of those would not be maintainable

bortexz19:12:26

What would be best in that case, one per read model and one per write?

bortexz19:12:27

I think I would have 1 spec for the read you retrieve from the DB, and then map it to whatever the client expects, probably not event having a schema for the specific data shape of the client, or if you want, then a spec for each of those too

bortexz19:12:40

It’s far from my use case though

vemv03:12:48

> What would be best in that case, one per read model and one per write? at work I advocate the following: * there's one write model (since there should be only 1 acceptable way of persisting things) * by default, that also serves as the read model (since one always can read a subset of what he wrote) * there also can be N additional read models, as your needs grow so, rule of thumb is 1:N

vemv03:12:52

> It’s far from my use case though (edited) yeah I don't even particularly like graphql, and in a personal project you likely won't have 10 different clients ;p but I like finding patterns that scale, that are universal. that way I don't have to worry later, or have different patterns per-project

bortexz08:12:15

> or have different patterns per-project I disagree here, depending on the scope of the project this could make sense

vemv08:12:59

morning :) sure thing. Personally I enjoy having a minimal amount of patterns in my head. The tradeoff being that sometimes 'scalable' solutions can certainly feel as overkill

bortexz09:12:20

Morning 👋 I think though that scalable could mean different things on different projects, to justify different patterns to achieve such scalability

👍 4
seancorfield18:12:23

You can also do this to get a local alias without needing the full namespace to actually exist as code:

(alias 'm  (create-ns 'ws.domain.member))

👍 4
bortexz19:12:23

Thanks! This looks really good and something that solves my use case 🙂

seancorfield18:12:49

Then ::m/country will expand to :ws.domain.member/country for the spec name.

seancorfield18:12:44

(in this particular case, that ns does exist, but we don't need to require it everywhere we use the specs)