Skip to content
/ sayang Public

Complects the definition of a Clojure(Script) function with its specification.

License

Notifications You must be signed in to change notification settings

lomin/sayang

Repository files navigation

sayang

sayang complects the definition of a Clojure(Script) function with its specification.

Rationale

A useful summary of any function is the combination of its name, its expectations about the input and guarantees of its output. clojure.spec provides this kind of function summary with the s/fdef macro. With clojure.spec, definition and specification of a function are separated concerns and there are good reasons for that decision. To quote Alex Miller:

"There's a lot of value in separating the specs from the functions. You can put them in different places and only use just the specs (to spec an API for example) or just the functions (for production use where you don't need the specs)."

Still, when you try to read or change a function, it is helpful to have the specification right next to the implementation. Additionally, if you need to change the number or the order of the function parameters, you only have to change it once. That is why you can optionally complect definition and specification of a function with sayang.

Syntax

While sayang values a resemblance to the syntax of prismatic/schema, it values not interfering with the functionality of Cursive more. Use the resolve macro as-Feature of Cursive to get the same tooling as function definitions with clojure.core.

Alternatives

If you do not like the syntax or any other design decisions of sayang, you might want to have a look at:

Getting Started

sayang is available from Clojars. Add the following dependency to your deps.edn or project.clj:

Current Version

Generation of specifications is turned off by default. To activate it add jvm-opts for Clojure and JVM-based ClojureScript REPLs.

Leiningen:

(defproject sayang-test "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.9.0"]
                 [orchestra  "2017.11.12-1"]
                 [org.clojure/test.check  "0.9.0"]
                 [org.clojure/spec.alpha "0.1.143"]
                 [me.lomin/sayang "0.3.0"]]
  :profiles {:dev {:jvm-opts ["-Dme.lomin.sayang.*activate*=true"]}})

Figwheel-REPL in Cursive:

Additionally, activate the toggle manually in your code:

(me.lomin.sayang/activate!)

Once activated, you need a runtime dependency to org.clojure/test.check.

Features

Specification at definition

(ns me.lomin.sayang.api-test
  (:require [clojure.test :refer [deftest is testing]]
            [clojure.spec.alpha :as spec]
            [me.lomin.sayang :as sg]
            #?(:clj [orchestra.spec.test :as orchestra]
               :cljs [orchestra-cljs.spec.test :as orchestra])))

(sg/activate!)

(sg/sdefn basic-usage {:ret    string?
                       :fn #(<= (:x (:args %))
                                (count (:ret %)))}
          [[x :- int?]]
          (str x))

(deftest basic-usage-test
  (is (= "1" (basic-usage 1)))

  (testing "Fails :fn spec"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (basic-usage 2))))

  (testing "Fails :args spec"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (basic-usage "5")))))

(orchestra/instrument `basic-usage)

(sg/sdefn int-identity {:args (spec/cat :x int?)}
          [x]
          x)

(deftest specs-from-meta-map-test
  (is (= 100 (int-identity 100)))

  (testing "Fails :args spec"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (int-identity "100")))))

(orchestra/instrument `int-identity)

Partial specification

(sg/sdefn partial-specs {:ret string?}
          [f
           [x :- int?]]
          (f x))

(deftest partial-specs-test
  (is (= "5" (partial-specs str 5)))

  (testing "Fails :args spec"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (partial-specs str "5"))))
  (testing "Fails :ret spec"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (partial-specs identity 5)))))

(orchestra/instrument `partial-specs)

Support for destructuring

(sg/sdefn sum-first-two-elements
          [[[a b] :- (spec/tuple int? int? int?)]]
          (+ a b))

(deftest support-for-destructuring-test
  (is (= 5 (sum-first-two-elements [2 3 4])))

  (testing "Fails :args spec"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (sum-first-two-elements [2 3])))))

(orchestra/instrument `sum-first-two-elements)

Specification for multiple arities

(sg/sdefn make-magic-string {:ret string?}
          ([[x :- int?]]
           (str x "?"))
          ([[x :- string?] [y :- string?]]
           (str x "?" y)))

(deftest multi-arity-test
  (is (= "2?" (make-magic-string 2)))
  (is (= "2?!" (make-magic-string "2" "!")))

  (testing "Fails :args spec for arity-2"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (make-magic-string 2 "!")))))

(orchestra/instrument `make-magic-string)

(defn result-larger-than-min-arg-value? [spec]
  (< (apply min (vals (:0 (:args spec))))
     (:ret spec)))

(sg/sdefn add-map-values {:ret    int?
                          :fn result-larger-than-min-arg-value?}
          [[{:keys [a b c]} :- map?]]
          (+ a b c))

(deftest fn-spec-for-multi-arity-test
  (is (= -1 (add-map-values {:a 1 :b 0 :c -2})))

  (testing "Fails :fn spec"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (add-map-values {:a -1 :b -2 :c -3})))))

(orchestra/instrument `add-map-values)

Reference other specs

(spec/def ::number? number?)
(sg/sdefn number-identity [[x :- ::number?]]
          x)

(deftest reference-to-speced-keywords-test
  (is (= 2 (number-identity 2)))

  (testing "Fails :args spec"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (number-identity "2")))))

(orchestra/instrument `number-identity)

(sg/sdefn call-with-7 [[f :- (sg/of make-magic-string)]]
  (f 7))

(deftest of-test

  (is (= "7?" (call-with-7 make-magic-string)))

  (testing "'identity' does not fulfill fdef of 'make-magic-string'"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (call-with-7 identity)))))

(orchestra/instrument `call-with-7)

Data DSL for homogeneous collections

(sg/sdefn speced-add {:ret number?}
          [[xs :- [number?]]]
          (apply + xs))

(deftest every-spec-data-dsl-test
  (is (= 105 (speced-add (range 15))))

  (testing "a string is not a number, therefore xs is no homogeneous collection any more"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (speced-add (cons "1" (range 15)))))))

(orchestra/instrument `speced-add)

Data DSL for tuples

(sg/sdefn sum-of-pos-pos-neg-tuple {:ret number?}
  [[tuple :- [pos? pos? neg?]]]
  (apply + tuple))

(deftest tuple-spec-data-dsl-test
  (is (= 0 (sum-of-pos-pos-neg-tuple [1 2 -3])))

  (testing "Fails tuple spec of :args"
    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo
                    :cljs :default)
                 (sum-of-pos-pos-neg-tuple [1 2 3])))))

(orchestra/instrument `sum-of-pos-pos-neg-tuple)

Acknowledgments

Thanks to Jeaye Wilkerson for figuring out how to get around CLJ-1750.

About

Sayang (사양) means specification in Korean.

License

Copyright © 2018 Steven Collins

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

About

Complects the definition of a Clojure(Script) function with its specification.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published