Search code examples
clojureclojure.spec

Test for a Valid Instance of java.time.LocalDate using Clojure Spec


I am trying to use Clojure Spec to define a data structure containing a java.time.LocalDate element:

(s/def :ex/first-name string?)
(s/def :ex/last-name string?)
(s/def :ex/birth-date (s/valid? inst? (java.time.LocalDate/now)))

(s/def :ex/person
  (s/keys :req [:ex/first-name
                :ex/last-name
                :ex/birth-date]))

(def p1 #:ex{:first-name "Jenny"
             :last-name  "Barnes"
             :birth-date (java.time.LocalDate/parse "1910-03-15")})

(println p1)

produces the following output

#:ex{:first-name Jenny, :last-name Barnes, :birth-date #object[java.time.LocalDate 0x4ed4f9db 1910-03-15]}

However, when I test to see if p1 conforms to the :ex/person spec, it fails:

(s/valid? :ex/person p1)
ClassCastException java.lang.Boolean cannot be cast to clojure.lang.IFn  clojure.spec.alpha/spec-impl/reify--1987 (alpha.clj:875)

Looking closer at the Clojure examples for inst?, I see:

(inst? (java.time.Instant/now))
;;=> true
(inst? (java.time.LocalDateTime/now))
;;=> false

However, I don't see an obvious reason as to why that returns false. This seems to be the root of my issue, but I have not found a solution and would like some help.


Solution

  • You're probably looking for instance?- and your example fails, because in:

    (s/def :ex/birth-date (s/valid? inst? (java.time.LocalDate/now)))
    

    this part (s/valid? inst? (java.time.LocalDate/now)) should be a function (predicate), not boolean. The full code:

    (s/def :ex/first-name string?)
    (s/def :ex/last-name string?)
    (s/def :ex/birth-date #(instance? java.time.LocalDate %))
    
    (s/def :ex/person
      (s/keys :req [:ex/first-name
                    :ex/last-name
                    :ex/birth-date]))
    
    (def p1 #:ex{:first-name "Jenny"
                 :last-name  "Barnes"
                 :birth-date (java.time.LocalDate/parse "1910-03-15")})
    
    (s/valid? :ex/person p1)
    => true
    

    inst? won't work here, because Inst is a protocol, used to extend java.util.Date and java.time.Instant:

    (defprotocol Inst
      (inst-ms* [inst]))
    
    (extend-protocol Inst
      java.util.Date
      (inst-ms* [inst] (.getTime ^java.util.Date inst)))
    
    (defn inst?
      "Return true if x satisfies Inst"
      {:added "1.9"}
      [x]
      (satisfies? Inst x))
    
    (extend-protocol clojure.core/Inst
      java.time.Instant
      (inst-ms* [inst] (.toEpochMilli ^java.time.Instant inst)))
    

    And you can use satisfies? to check whether some object satisfies given protocol:

    (satisfies? Inst (java.time.LocalDate/parse "1910-03-15"))
    => false