{"id":24811643,"url":"https://github.com/lomin/sayang","last_synced_at":"2025-10-13T13:31:37.470Z","repository":{"id":57713846,"uuid":"109834706","full_name":"lomin/sayang","owner":"lomin","description":"Complects the definition of a Clojure(Script) function with its specification.","archived":false,"fork":false,"pushed_at":"2019-04-02T04:09:02.000Z","size":102,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-09-12T17:33:46.479Z","etag":null,"topics":["clojure","clojure-spec","clojurescript","cursive","schema"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lomin.png","metadata":{"files":{"readme":"README.md","changelog":"changes.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-11-07T12:46:14.000Z","updated_at":"2019-12-08T22:36:38.000Z","dependencies_parsed_at":"2022-08-25T12:51:40.007Z","dependency_job_id":null,"html_url":"https://github.com/lomin/sayang","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/lomin/sayang","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lomin%2Fsayang","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lomin%2Fsayang/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lomin%2Fsayang/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lomin%2Fsayang/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lomin","download_url":"https://codeload.github.com/lomin/sayang/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lomin%2Fsayang/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279015312,"owners_count":26085684,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojure-spec","clojurescript","cursive","schema"],"created_at":"2025-01-30T13:15:39.226Z","updated_at":"2025-10-13T13:31:37.131Z","avatar_url":"https://github.com/lomin.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sayang\n\n*sayang* complects the definition of a Clojure(Script) function with its specification.\n\n## Rationale\n\nA useful summary of any function is the combination of its name, its expectations about the input and guarantees of its output.\n[clojure.spec](https://clojure.org/about/spec) provides this kind of function summary with the [s/fdef](https://clojure.org/guides/spec#_spec_ing_functions) macro.\nWith *clojure.spec*, definition and specification of a function are separated concerns and there are good reasons for that decision. To quote [Alex Miller](https://groups.google.com/forum/m/#!topic/clojure/0wqvG2sef8I):\n \u003e \"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).\"\n\nStill, when you try to read or change a function, it is helpful to have the specification right next to the implementation.\nAdditionally, if you need to change the number or the order of the function parameters, you only have to change it once.\nThat is why you can optionally complect definition and specification of a function with *sayang*.\n\n### Syntax\n\nWhile *sayang* values a resemblance to the syntax of [prismatic/schema](https://github.com/plumatic/schema), it values not interfering with the functionality of [Cursive](https://cursive-ide.com/) more.\nUse the *resolve macro as*-Feature of *Cursive* to get the same tooling as function definitions with *clojure.core*.\n\n### Alternatives\n\nIf you do not like the syntax or any other design decisions of *sayang*, you might want to have a look at:\n\n* the `defn-spec` macro of [Orchestra](https://github.com/jeaye/orchestra)\n* [Ghostwheel](https://github.com/gnl/ghostwheel)\n* [Speck](https://github.com/j-cr/speck)\n* [Provisdom/defn-spec](https://github.com/Provisdom/defn-spec)\n* [danielcompton/defn-spec](https://github.com/danielcompton/defn-spec)\n\n## Getting Started\n\n*sayang* is available from Clojars. Add the following dependency to your *deps.edn* or *project.clj*:\n\n[![Current Version](https://clojars.org/me.lomin/sayang/latest-version.svg)](https://clojars.org/me.lomin/sayang)\n\nGeneration of specifications is turned off by default. To activate it add jvm-opts for Clojure and JVM-based ClojureScript REPLs.\n\nLeiningen:\n\n```clojure\n(defproject sayang-test \"0.1.0-SNAPSHOT\"\n  :dependencies [[org.clojure/clojure \"1.9.0\"]\n                 [orchestra  \"2017.11.12-1\"]\n                 [org.clojure/test.check  \"0.9.0\"]\n                 [org.clojure/spec.alpha \"0.1.143\"]\n                 [me.lomin/sayang \"0.3.0\"]]\n  :profiles {:dev {:jvm-opts [\"-Dme.lomin.sayang.*activate*=true\"]}})\n```\n\nFigwheel-REPL in Cursive:\n\n\u003cimg src=\"/images/Readme/figwheel-repl.png?raw=true\"\u003e\n\nAdditionally, activate the toggle manually in your code:\n\n```clojure\n(me.lomin.sayang/activate!)\n```\n\nOnce activated, you need a runtime dependency to org.clojure/test.check.\n\n## Features\n\n* [in place function specification at definition](#specification-at-definition)\n* [partial specification](#partial-specification)\n* [support for destructuring](#support-for-destructuring)\n* [auto generate specs for multiple arities](#specification-for-multiple-arities)\n* [reference other specs](#reference-other-specs)\n* [data DSL for homogeneous collections](#data-dsl-for-homogeneous-collections)\n* [data DSL for tuples](#data-dsl-for-tuples)\n* [global switch to toggle specification](#getting-started)\n\n### Specification at definition\n```clojure\n(ns me.lomin.sayang.api-test\n  (:require [clojure.test :refer [deftest is testing]]\n            [clojure.spec.alpha :as spec]\n            [me.lomin.sayang :as sg]\n            #?(:clj [orchestra.spec.test :as orchestra]\n               :cljs [orchestra-cljs.spec.test :as orchestra])))\n\n(sg/activate!)\n\n(sg/sdefn basic-usage {:ret    string?\n                       :fn #(\u003c= (:x (:args %))\n                                (count (:ret %)))}\n          [[x :- int?]]\n          (str x))\n\n(deftest basic-usage-test\n  (is (= \"1\" (basic-usage 1)))\n\n  (testing \"Fails :fn spec\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (basic-usage 2))))\n\n  (testing \"Fails :args spec\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (basic-usage \"5\")))))\n\n(orchestra/instrument `basic-usage)\n\n(sg/sdefn int-identity {:args (spec/cat :x int?)}\n          [x]\n          x)\n\n(deftest specs-from-meta-map-test\n  (is (= 100 (int-identity 100)))\n\n  (testing \"Fails :args spec\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (int-identity \"100\")))))\n\n(orchestra/instrument `int-identity)\n```\n### Partial specification\n```clojure\n(sg/sdefn partial-specs {:ret string?}\n          [f\n           [x :- int?]]\n          (f x))\n\n(deftest partial-specs-test\n  (is (= \"5\" (partial-specs str 5)))\n\n  (testing \"Fails :args spec\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (partial-specs str \"5\"))))\n  (testing \"Fails :ret spec\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (partial-specs identity 5)))))\n\n(orchestra/instrument `partial-specs)\n```\n### Support for destructuring\n```clojure\n(sg/sdefn sum-first-two-elements\n          [[[a b] :- (spec/tuple int? int? int?)]]\n          (+ a b))\n\n(deftest support-for-destructuring-test\n  (is (= 5 (sum-first-two-elements [2 3 4])))\n\n  (testing \"Fails :args spec\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (sum-first-two-elements [2 3])))))\n\n(orchestra/instrument `sum-first-two-elements)\n```\n### Specification for multiple arities\n```clojure\n(sg/sdefn make-magic-string {:ret string?}\n          ([[x :- int?]]\n           (str x \"?\"))\n          ([[x :- string?] [y :- string?]]\n           (str x \"?\" y)))\n\n(deftest multi-arity-test\n  (is (= \"2?\" (make-magic-string 2)))\n  (is (= \"2?!\" (make-magic-string \"2\" \"!\")))\n\n  (testing \"Fails :args spec for arity-2\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (make-magic-string 2 \"!\")))))\n\n(orchestra/instrument `make-magic-string)\n\n(defn result-larger-than-min-arg-value? [spec]\n  (\u003c (apply min (vals (:0 (:args spec))))\n     (:ret spec)))\n\n(sg/sdefn add-map-values {:ret    int?\n                          :fn result-larger-than-min-arg-value?}\n          [[{:keys [a b c]} :- map?]]\n          (+ a b c))\n\n(deftest fn-spec-for-multi-arity-test\n  (is (= -1 (add-map-values {:a 1 :b 0 :c -2})))\n\n  (testing \"Fails :fn spec\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (add-map-values {:a -1 :b -2 :c -3})))))\n\n(orchestra/instrument `add-map-values)\n```\n### Reference other specs\n```clojure\n(spec/def ::number? number?)\n(sg/sdefn number-identity [[x :- ::number?]]\n          x)\n\n(deftest reference-to-speced-keywords-test\n  (is (= 2 (number-identity 2)))\n\n  (testing \"Fails :args spec\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (number-identity \"2\")))))\n\n(orchestra/instrument `number-identity)\n\n(sg/sdefn call-with-7 [[f :- (sg/of make-magic-string)]]\n  (f 7))\n\n(deftest of-test\n\n  (is (= \"7?\" (call-with-7 make-magic-string)))\n\n  (testing \"'identity' does not fulfill fdef of 'make-magic-string'\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (call-with-7 identity)))))\n\n(orchestra/instrument `call-with-7)\n```\n### Data DSL for homogeneous collections\n```clojure\n(sg/sdefn speced-add {:ret number?}\n          [[xs :- [number?]]]\n          (apply + xs))\n\n(deftest every-spec-data-dsl-test\n  (is (= 105 (speced-add (range 15))))\n\n  (testing \"a string is not a number, therefore xs is no homogeneous collection any more\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (speced-add (cons \"1\" (range 15)))))))\n\n(orchestra/instrument `speced-add)\n```\n### Data DSL for tuples\n```clojure\n(sg/sdefn sum-of-pos-pos-neg-tuple {:ret number?}\n  [[tuple :- [pos? pos? neg?]]]\n  (apply + tuple))\n\n(deftest tuple-spec-data-dsl-test\n  (is (= 0 (sum-of-pos-pos-neg-tuple [1 2 -3])))\n\n  (testing \"Fails tuple spec of :args\"\n    (is (thrown? #?(:clj  clojure.lang.ExceptionInfo\n                    :cljs :default)\n                 (sum-of-pos-pos-neg-tuple [1 2 3])))))\n\n(orchestra/instrument `sum-of-pos-pos-neg-tuple)\n```\n\n## Acknowledgments\n\nThanks to [Jeaye Wilkerson](https://github.com/jeaye) for figuring out how to get around [CLJ-1750](https://dev.clojure.org/jira/browse/CLJ-1750).\n\n## About\n\nSayang (사양) means specification in Korean.\n\n## License\n\nCopyright © 2018 Steven Collins\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flomin%2Fsayang","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flomin%2Fsayang","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flomin%2Fsayang/lists"}