Fork me on GitHub
#clojure-spec
<
2022-08-21
>
Alejandro09:08:08

after reading a book called Domain Modeling Made Functional I wonder what would be a good way to attach type information to pieces of data. I'm not talking about a type system, but about reasoning of how data flows through components. I'd like have specs that claim a function only accepts data that went through some kind of processing. E.g., an email is parsed and validated on the edge of the system, but not confirmed yet by the user, and I'd like to make sure it's never used inappropriately. How would you do this? Using keywords (with inheritance, probably) or with records?

Alex Miller (Clojure team)11:08:17

Seems like a perfect use of metadata

Alejandro20:08:00

Hm. That's interesting, I'll try it, thanks

didibus21:08:10

> I'd like have specs that claim a function only accepts data that went through some kind of processing. E.g., an email is parsed and validated on the edge of the system, but not confirmed yet by the user, and I'd like to make sure it's never used inappropriately This tends to be pretty trivial in Clojure and with Spec. You can use metadata, but I'd say in general, you don't even have too, since you'll probably be working with maps only for your model anyways, you can just add more data to them. For example, on your User entity (which should be a plain Clojure map), just add a :type key where the values are the various state of a User through your system. So maybe you have a {:type :verified-user} and a {:type :registered-user}. Now in each function, where you expect a :verified-user, well just have a spec for that which will also assert that :type is :verified-user. This will probably be pretty close to the book you read, where I'm assuming they use custom ADT types to model this. Alternatively, you can enhance the user with more info, and validate the info, such as adding an :email-verified true on the User only after its been verified, then have a spec on the function that asserts that :email-verified exists and is true on the user.

❤️ 2
didibus21:08:11

You could make this metadata, but it's harder to use spec to validate meta, not super hard, but you kind of need two specs, one for the data and one for the meta. The advantage of meta is if you don't want them to participate in equality. Like if you still want two users to be equal even if one is verified and the other is not.

didibus22:08:17

(require '[com.myapp :as-alias app])
(require '[com.myapp.user :as-alias user])

(s/def ::user/type #{:registered-user :verified-user})
(s/def ::user/username string?)
(s/def ::user/email string?)
(s/def ::app/user
  (s/keys :req-un [::user/type ::user/username ::user/email]))
(s/def ::app/verified-user
  (s/and ::app/user #(= :verified-user (:type %))))
(s/def :app/registered-user
  (s/and ::app/user #(= :registered-user (:type %))))
So now on functions that expect a :verified-user you can do:
(s/fdef somefn
  :args (s/cat :verified-user ::app/verified-user)
  :ret ::app/verified-user)
And on the function that verifies a registered-user you can do:
(s/fdef verify-user
  :args (s/cat :registered-user ::app/registered-user)
  :ret ::app/verified-user)

didibus22:08:56

And if different types of users also had different set of keys, you can multi-spec the ::app/user spec.

Alejandro20:01:51

@U0K064KQV, oh, this is embarassing. I've just opened this thread five months after your answer. Thanks, looking forward to actually try this approach.

popeye14:08:36

When to use clojure spec ?