diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd565b5e..43614722 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,8 @@ jobs: uses: DeLaGuardo/setup-clojure@master with: lein: latest + - name: Check kondo hooks + run: cd examples/clj-kondo-hooks && ./script/test - name: Run tests run: lein do clean, all midje, all check deploy: diff --git a/README.md b/README.md index 914f95b3..f6f9f2af 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,25 @@ lein new compojure-api my-api +clojure-test ## License +Copied code from tools.macro has license: + +``` +Copyright (c) Rich Hickey. All rights reserved. +The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (https://opensource.org/license/epl-1-0/) +which can be found in the file epl-v10.html at the root of this distribution. By using this software in any fashion, you are agreeing to +be bound bythe terms of this license. You must not remove this notice, or any other, from this software. +``` + +Copied code from compojure has license: + +``` +Copyright © 2024 James Reeves + +Distributed under the Eclipse Public License, the same as Clojure. +``` + +All other code: + Copyright © 2014-2016 [Metosin Oy](https://www.metosin.fi) Distributed under the Eclipse Public License, the same as Clojure. diff --git a/deps.edn b/deps.edn new file mode 100644 index 00000000..86955aa2 --- /dev/null +++ b/deps.edn @@ -0,0 +1,11 @@ +{:paths ["src" "resources"] + :deps {prismatic/plumbing {:mvn/version "0.6.0"} + cheshire/cheshire {:mvn/version "5.13.0"} + compojure/compojure {:mvn/version "1.6.1"} + prismatic/schema {:mvn/version "1.1.12"} + org.tobereplaced/lettercase {:mvn/version "1.0.0"} + frankiesardo/linked {:mvn/version "1.3.0"} + ring-middleware-format/ring-middleware-format {:mvn/version "0.7.4"} + metosin/ring-http-response {:mvn/version "0.9.1"} + metosin/ring-swagger {:mvn/version "1.0.0"} + metosin/ring-swagger-ui {:mvn/version "2.2.10"}}} diff --git a/dev/compojure_api_kondo_hooks/compojure/core.clj b/dev/compojure_api_kondo_hooks/compojure/core.clj new file mode 100644 index 00000000..7b95c17b --- /dev/null +++ b/dev/compojure_api_kondo_hooks/compojure/core.clj @@ -0,0 +1,60 @@ +;; Copyright © 2024 James Reeves +;; +;; Distributed under the Eclipse Public License, the same as Clojure. +(ns compojure-api-kondo-hooks.compojure.core) + +(defn- and-binding [req binds] + `(dissoc (:params ~req) ~@(map keyword (keys binds)) ~@(map str (keys binds)))) + +(defn- symbol-binding [req sym] + `(get-in ~req [:params ~(keyword sym)] (get-in ~req [:params ~(str sym)]))) + +(defn- application-binding [req sym func] + `(~func ~(symbol-binding req sym))) + +(defn- vector-bindings [args req] + (loop [args args, binds {}] + (if (seq args) + (let [[x y z] args] + (cond + (= '& x) + (recur (nnext args) (assoc binds y (and-binding req binds))) + (= :as x) + (recur (nnext args) (assoc binds y req)) + (and (symbol? x) (= :<< y) (nnext args)) + (recur (drop 3 args) (assoc binds x (application-binding req x z))) + (symbol? x) + (recur (next args) (assoc binds x (symbol-binding req x))) + :else + (throw (Exception. (str "Unexpected binding: " x))))) + (mapcat identity binds)))) + +(defn- warn-on-*-bindings! [bindings] + (when (and (vector? bindings) (contains? (set bindings) '*)) + (binding [*out* *err*] + (println "WARNING: * should not be used as a route binding.")))) + +(defn- application-symbols [args] + (loop [args args, syms '()] + (if (seq args) + (let [[x y] args] + (if (and (symbol? x) (= :<< y)) + (recur (drop 3 args) (conj syms x)) + (recur (next args) syms))) + (seq syms)))) + +(defmacro ^:no-doc let-request [[bindings request] & body] + (if (vector? bindings) + `(let [~@(vector-bindings bindings request)] + ~(if-let [syms (application-symbols bindings)] + `(if (and ~@(for [s syms] `(not (nil? ~s)))) (do ~@body)) + `(do ~@body))) + `(let [~bindings ~request] ~@body))) + +(defn compile-route + "Compile a route in the form `(method path bindings & body)` into a function. + Used to create custom route macros." + [method path bindings body] + (let [greq (gensym "greq")] + `(fn [~greq] + ~(macroexpand-1 `(let-request [~bindings ~greq] ~@body))))) diff --git a/dev/compojure_api_kondo_hooks/plumbing/core.clj b/dev/compojure_api_kondo_hooks/plumbing/core.clj new file mode 100644 index 00000000..5d0189a7 --- /dev/null +++ b/dev/compojure_api_kondo_hooks/plumbing/core.clj @@ -0,0 +1,42 @@ +(ns compojure-api-kondo-hooks.plumbing.core + "Utility belt for Clojure in the wild" + (:refer-clojure :exclude [update]) + (:require + [compojure-api-kondo-hooks.schema.macros :as schema-macros] + [compojure-api-kondo-hooks.plumbing.fnk.impl :as fnk-impl])) + +(defmacro letk + "Keyword let. Accepts an interleaved sequence of binding forms and map forms like: + (letk [[a {b 2} [:f g h] c d {e 4} :as m & more] a-map ...] & body) + a, c, d, and f are required keywords, and letk will barf if not in a-map. + b and e are optional, and will be bound to default values if not present. + g and h are required keys in the map found under :f. + m will be bound to the entire map (a-map). + more will be bound to all the unbound keys (ie (dissoc a-map :a :b :c :d :e)). + :as and & are both optional, but must be at the end in the specified order if present. + The same symbol cannot be bound multiple times within the same destructing level. + + Optional values can reference symbols bound earlier within the same binding, i.e., + (= [2 2] (let [a 1] (letk [[a {b a}] {:a 2}] [a b]))) but + (= [2 1] (let [a 1] (letk [[{b a} a] {:a 2}] [a b]))) + + If present, :as and :& symbols are bound before other symbols within the binding. + + Namespaced keys are supported by specifying fully-qualified key in binding form. The bound + symbol uses the _name_ portion of the namespaced key, i.e, + (= 1 (letk [[a/b] {:a/b 1}] b)). + + Map destructuring bindings can be mixed with ordinary symbol bindings." + [bindings & body] + (reduce + (fn [cur-body-form [bind-form value-form]] + (if (symbol? bind-form) + `(let [~bind-form ~value-form] ~cur-body-form) + (let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form + &env + bind-form ;(fnk-impl/ensure-schema-metadata &env bind-form) + [] + cur-body-form)] + `(let [~map-sym ~value-form] ~body-form)))) + `(do ~@body) + (reverse (partition 2 bindings)))) diff --git a/dev/compojure_api_kondo_hooks/plumbing/fnk/impl.clj b/dev/compojure_api_kondo_hooks/plumbing/fnk/impl.clj new file mode 100644 index 00000000..5e1ad75f --- /dev/null +++ b/dev/compojure_api_kondo_hooks/plumbing/fnk/impl.clj @@ -0,0 +1,116 @@ +(ns compojure-api-kondo-hooks.plumbing.fnk.impl + (:require + [clojure.set :as set] + [schema.core :as-alias s] + [compojure-api-kondo-hooks.schema.macros :as schema-macros])) + +;;;;; Helpers + +(defn name-sym + "Returns symbol of x's name. + Converts a keyword/string to symbol, or removes namespace (if any) of symbol" + [x] + (with-meta (symbol (name x)) (meta x))) + +;;; Parsing new fnk binding style + +(declare letk-input-schema-and-body-form) + +(defn- process-schematized-map + "Take an optional binding map like {a 2} or {a :- Number 2} and convert the schema + information to canonical metadata, if present." + [env binding] + (case (count binding) + 1 (let [[sym v] (first binding)] + {sym v}) + + 2 (let [[[[sym _]] [[schema v]]] ((juxt filter remove) #(= (val %) :-) binding)] + {sym v}))) + +;; TODO: unify this with positional version. +(defn letk-arg-bind-sym-and-body-form + "Given a single element of a single letk binding form and a current body form, return + a map {:schema-entry :body-form} where schema-entry is a tuple + [bound-key schema external-schema?], and body-form wraps body with destructuring + for this binding as necessary." + [env map-sym binding key-path body-form] + (cond (symbol? binding) + {:schema-entry [] + :body-form `(let [~(name-sym binding) (get ~map-sym ~(keyword binding) ~key-path)] + ~body-form)} + + (map? binding) + (let [schema-fixed-binding (process-schematized-map env binding) + [bound-sym opt-val-expr] (first schema-fixed-binding) + bound-key (keyword bound-sym)] + {:schema-entry [] + :body-form `(let [~(name-sym bound-sym) (get ~map-sym ~bound-key ~opt-val-expr)] + ~body-form)}) + + (vector? binding) + (let [[bound-key & more] binding + {inner-input-schema :input-schema + inner-external-input-schema :external-input-schema + inner-map-sym :map-sym + inner-body-form :body-form} (letk-input-schema-and-body-form + env + (with-meta (vec more) (meta binding)) + (conj key-path bound-key) + body-form)] + {:schema-entry [] + :body-form `(let [~inner-map-sym (get ~map-sym ~bound-key ~key-path)] + ~inner-body-form)}) + + :else (throw (ex-info (format "bad binding: %s" binding) {})))) + +(defn- extract-special-args + "Extract trailing & sym and :as sym, possibly with schema metadata. Returns + [more-bindings special-args-map] where special-args-map is a map from each + special symbol found to the symbol that was found." + [env special-arg-signifier-set binding-form] + {:pre [(set? special-arg-signifier-set)]} + (let [[more-bindings special-bindings] (split-with (complement special-arg-signifier-set) binding-form)] + (loop [special-args-map {} + special-arg-set special-arg-signifier-set + [arg-signifier & other-bindings :as special-bindings] special-bindings] + (if-not (seq special-bindings) + [more-bindings special-args-map] + (do + (let [[sym remaining-bindings] (schema-macros/extract-arrow-schematized-element env other-bindings)] + (recur (assoc special-args-map arg-signifier sym) + (disj special-arg-set arg-signifier) + remaining-bindings))))))) + +(defn letk-input-schema-and-body-form + "Given a single letk binding form, value form, key path, and body + form, return a map {:input-schema :external-input-schema :map-sym :body-form} + where input-schema is the schema imposed by binding-form, external-input-schema + is like input-schema but includes user overrides for binding vectors, + map-sym is the symbol which it expects the bound value to be bound to, + and body-form wraps body in the bindings from binding-form from map-sym." + [env binding-form key-path body-form] + (let [[bindings {more-sym '& as-sym :as}] (extract-special-args env #{'& :as} binding-form) + as-sym (or as-sym (gensym "map")) + [input-schema-elts + external-input-schema-elts + bound-body-form] (reduce + (fn [[input-schema-elts external-input-schema-elts cur-body] binding] + (let [{:keys [schema-entry body-form]} + (letk-arg-bind-sym-and-body-form + env as-sym binding key-path cur-body) + [bound-key input-schema external-input-schema] schema-entry] + [(conj input-schema-elts [bound-key input-schema]) + (conj external-input-schema-elts + [bound-key (or external-input-schema input-schema)]) + body-form])) + [[] [] body-form] + (reverse + (schema-macros/process-arrow-schematized-args + env bindings))) + explicit-schema-keys [] + final-body-form (if more-sym + `(let [~more-sym (dissoc ~as-sym ~@explicit-schema-keys)] + ~bound-body-form) + bound-body-form)] + {:map-sym as-sym + :body-form final-body-form})) diff --git a/dev/compojure_api_kondo_hooks/plumbing/fnk/schema.clj b/dev/compojure_api_kondo_hooks/plumbing/fnk/schema.clj new file mode 100644 index 00000000..b69379e0 --- /dev/null +++ b/dev/compojure_api_kondo_hooks/plumbing/fnk/schema.clj @@ -0,0 +1,16 @@ + +(s/defn unwrap-schema-form-key :- (s/maybe (s/pair s/Keyword "k" s/Bool "optional?")) + "Given a possibly-unevaluated schema map key form, unpack an explicit keyword + and optional? flag, or return nil for a non-explicit key" + [k] + (cond (s/specific-key? k) + [(s/explicit-schema-key k) (s/required-key? k)] + + ;; Deal with `(s/optional-key k) form from impl + (and (sequential? k) (not (vector? k)) (= (count k) 2) + (= (first k) 'schema.core/optional-key)) + [(second k) false] + + ;; Deal with `(with-meta ...) form from impl + (and (sequential? k) (not (vector? k)) (= (first k) `with-meta)) + (unwrap-schema-form-key (second k)))) diff --git a/dev/compojure_api_kondo_hooks/schema/macros.clj b/dev/compojure_api_kondo_hooks/schema/macros.clj new file mode 100644 index 00000000..aefe1448 --- /dev/null +++ b/dev/compojure_api_kondo_hooks/schema/macros.clj @@ -0,0 +1,24 @@ +(ns compojure-api-kondo-hooks.schema.macros) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Helpers for processing and normalizing element/argument schemas in s/defrecord and s/(de)fn + +(defn extract-arrow-schematized-element + "Take a nonempty seq, which may start like [a ...] or [a :- schema ...], and return + a list of [first-element-with-schema-attached rest-elements]" + [env s] + (assert (seq s)) + (let [[f & more] s] + (if (= :- (first more)) + [f (drop 2 more)] + [f more]))) + +(defn process-arrow-schematized-args + "Take an arg vector, in which each argument is followed by an optional :- schema, + and transform into an ordinary arg vector where the schemas are metadata on the args." + [env args] + (loop [in args out []] + (if (empty? in) + out + (let [[arg more] (extract-arrow-schematized-element env in)] + (recur more (conj out arg)))))) diff --git a/epl-v10.html b/epl-v10.html new file mode 100644 index 00000000..813c07d8 --- /dev/null +++ b/epl-v10.html @@ -0,0 +1,261 @@ + + + + + + +Eclipse Public License - Version 1.0 + + + + + + +

Eclipse Public License - v 1.0

+ +

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE +PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR +DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS +AGREEMENT.

+ +

1. DEFINITIONS

+ +

"Contribution" means:

+ +

a) in the case of the initial Contributor, the initial +code and documentation distributed under this Agreement, and

+

b) in the case of each subsequent Contributor:

+

i) changes to the Program, and

+

ii) additions to the Program;

+

where such changes and/or additions to the Program +originate from and are distributed by that particular Contributor. A +Contribution 'originates' from a Contributor if it was added to the +Program by such Contributor itself or anyone acting on such +Contributor's behalf. Contributions do not include additions to the +Program which: (i) are separate modules of software distributed in +conjunction with the Program under their own license agreement, and (ii) +are not derivative works of the Program.

+ +

"Contributor" means any person or entity that distributes +the Program.

+ +

"Licensed Patents" mean patent claims licensable by a +Contributor which are necessarily infringed by the use or sale of its +Contribution alone or when combined with the Program.

+ +

"Program" means the Contributions distributed in accordance +with this Agreement.

+ +

"Recipient" means anyone who receives the Program under +this Agreement, including all Contributors.

+ +

2. GRANT OF RIGHTS

+ +

a) Subject to the terms of this Agreement, each +Contributor hereby grants Recipient a non-exclusive, worldwide, +royalty-free copyright license to reproduce, prepare derivative works +of, publicly display, publicly perform, distribute and sublicense the +Contribution of such Contributor, if any, and such derivative works, in +source code and object code form.

+ +

b) Subject to the terms of this Agreement, each +Contributor hereby grants Recipient a non-exclusive, worldwide, +royalty-free patent license under Licensed Patents to make, use, sell, +offer to sell, import and otherwise transfer the Contribution of such +Contributor, if any, in source code and object code form. This patent +license shall apply to the combination of the Contribution and the +Program if, at the time the Contribution is added by the Contributor, +such addition of the Contribution causes such combination to be covered +by the Licensed Patents. The patent license shall not apply to any other +combinations which include the Contribution. No hardware per se is +licensed hereunder.

+ +

c) Recipient understands that although each Contributor +grants the licenses to its Contributions set forth herein, no assurances +are provided by any Contributor that the Program does not infringe the +patent or other intellectual property rights of any other entity. Each +Contributor disclaims any liability to Recipient for claims brought by +any other entity based on infringement of intellectual property rights +or otherwise. As a condition to exercising the rights and licenses +granted hereunder, each Recipient hereby assumes sole responsibility to +secure any other intellectual property rights needed, if any. For +example, if a third party patent license is required to allow Recipient +to distribute the Program, it is Recipient's responsibility to acquire +that license before distributing the Program.

+ +

d) Each Contributor represents that to its knowledge it +has sufficient copyright rights in its Contribution, if any, to grant +the copyright license set forth in this Agreement.

+ +

3. REQUIREMENTS

+ +

A Contributor may choose to distribute the Program in object code +form under its own license agreement, provided that:

+ +

a) it complies with the terms and conditions of this +Agreement; and

+ +

b) its license agreement:

+ +

i) effectively disclaims on behalf of all Contributors +all warranties and conditions, express and implied, including warranties +or conditions of title and non-infringement, and implied warranties or +conditions of merchantability and fitness for a particular purpose;

+ +

ii) effectively excludes on behalf of all Contributors +all liability for damages, including direct, indirect, special, +incidental and consequential damages, such as lost profits;

+ +

iii) states that any provisions which differ from this +Agreement are offered by that Contributor alone and not by any other +party; and

+ +

iv) states that source code for the Program is available +from such Contributor, and informs licensees how to obtain it in a +reasonable manner on or through a medium customarily used for software +exchange.

+ +

When the Program is made available in source code form:

+ +

a) it must be made available under this Agreement; and

+ +

b) a copy of this Agreement must be included with each +copy of the Program.

+ +

Contributors may not remove or alter any copyright notices contained +within the Program.

+ +

Each Contributor must identify itself as the originator of its +Contribution, if any, in a manner that reasonably allows subsequent +Recipients to identify the originator of the Contribution.

+ +

4. COMMERCIAL DISTRIBUTION

+ +

Commercial distributors of software may accept certain +responsibilities with respect to end users, business partners and the +like. While this license is intended to facilitate the commercial use of +the Program, the Contributor who includes the Program in a commercial +product offering should do so in a manner which does not create +potential liability for other Contributors. Therefore, if a Contributor +includes the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and +indemnify every other Contributor ("Indemnified Contributor") +against any losses, damages and costs (collectively "Losses") +arising from claims, lawsuits and other legal actions brought by a third +party against the Indemnified Contributor to the extent caused by the +acts or omissions of such Commercial Contributor in connection with its +distribution of the Program in a commercial product offering. The +obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property infringement. In +order to qualify, an Indemnified Contributor must: a) promptly notify +the Commercial Contributor in writing of such claim, and b) allow the +Commercial Contributor to control, and cooperate with the Commercial +Contributor in, the defense and any related settlement negotiations. The +Indemnified Contributor may participate in any such claim at its own +expense.

+ +

For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those +performance claims and warranties, and if a court requires any other +Contributor to pay any damages as a result, the Commercial Contributor +must pay those damages.

+ +

5. NO WARRANTY

+ +

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS +PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, +ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY +OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely +responsible for determining the appropriateness of using and +distributing the Program and assumes all risks associated with its +exercise of rights under this Agreement , including but not limited to +the risks and costs of program errors, compliance with applicable laws, +damage to or loss of data, programs or equipment, and unavailability or +interruption of operations.

+ +

6. DISCLAIMER OF LIABILITY

+ +

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT +NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR +DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED +HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

+ +

7. GENERAL

+ +

If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further action +by the parties hereto, such provision shall be reformed to the minimum +extent necessary to make such provision valid and enforceable.

+ +

If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other +software or hardware) infringes such Recipient's patent(s), then such +Recipient's rights granted under Section 2(b) shall terminate as of the +date such litigation is filed.

+ +

All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of time +after becoming aware of such noncompliance. If all Recipient's rights +under this Agreement terminate, Recipient agrees to cease use and +distribution of the Program as soon as reasonably practicable. However, +Recipient's obligations under this Agreement and any licenses granted by +Recipient relating to the Program shall continue and survive.

+ +

Everyone is permitted to copy and distribute copies of this +Agreement, but in order to avoid inconsistency the Agreement is +copyrighted and may only be modified in the following manner. The +Agreement Steward reserves the right to publish new versions (including +revisions) of this Agreement from time to time. No one other than the +Agreement Steward has the right to modify this Agreement. The Eclipse +Foundation is the initial Agreement Steward. The Eclipse Foundation may +assign the responsibility to serve as the Agreement Steward to a +suitable separate entity. Each new version of the Agreement will be +given a distinguishing version number. The Program (including +Contributions) may always be distributed subject to the version of the +Agreement under which it was received. In addition, after a new version +of the Agreement is published, Contributor may elect to distribute the +Program (including its Contributions) under the new version. Except as +expressly stated in Sections 2(a) and 2(b) above, Recipient receives no +rights or licenses to the intellectual property of any Contributor under +this Agreement, whether expressly, by implication, estoppel or +otherwise. All rights in the Program not expressly granted under this +Agreement are reserved.

+ +

This Agreement is governed by the laws of the State of New York and +the intellectual property laws of the United States of America. No party +to this Agreement will bring a legal action under this Agreement more +than one year after the cause of action arose. Each party waives its +rights to a jury trial in any resulting litigation.

+ + + + diff --git a/examples/clj-kondo-hooks/.clj-kondo/.keep b/examples/clj-kondo-hooks/.clj-kondo/.keep new file mode 100644 index 00000000..e69de29b diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/common.clj b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/common.clj new file mode 100644 index 00000000..2a0dc7d5 --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/common.clj @@ -0,0 +1,84 @@ +(ns compojure.api.common + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [(:require [linked.core :as linked])])) + +(defn plain-map? + "checks whether input is a map, but not a record" + [x] (and (map? x) (not (record? x)))) + +(defn extract-parameters + "Extract parameters from head of the list. Parameters can be: + + 1. a map (if followed by any form) `[{:a 1 :b 2} :body]` => `{:a 1 :b 2}` + 2. list of keywords & values `[:a 1 :b 2 :body]` => `{:a 1 :b 2}` + 3. else => `{}` + + Returns a tuple with parameters and body without the parameters" + [c expect-body] + (cond + (and (plain-map? (first c)) (or (not expect-body) (seq (rest c)))) + [(first c) (seq (rest c))] + + (keyword? (first c)) + (let [parameters (->> c + (partition 2) + (take-while (comp keyword? first)) + (mapcat identity) + (apply array-map)) + form (drop (* 2 (count parameters)) c)] + [parameters (seq form)]) + + :else + [{} (seq c)])) + +(defn group-with + "Groups a sequence with predicate returning a tuple of sequences." + [pred coll] + [(seq (filter pred coll)) + (seq (remove pred coll))]) + +(defn merge-vector + "Merges vector elements, optimized for 1 arity (x10 faster than merge)." + [v] + (if (get v 1) + (apply merge v) + (get v 0))) + +(defn fast-map-merge + [x y] + (reduce-kv + (fn [m k v] + (assoc m k v)) + x + y)) + +#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default +(defn fifo-memoize [f size] + "Returns a memoized version of a referentially transparent f. The + memoized version of the function keeps a cache of the mapping from arguments + to results and, when calls with the same arguments are repeated often, has + higher performance at the expense of higher memory use. FIFO with size entries." + (let [cache (atom (linked/map))] + (fn [& xs] + (or (@cache xs) + (let [value (apply f xs)] + (swap! cache (fn [mem] + (let [mem (assoc mem xs value)] + (if (>= (count mem) size) + (dissoc mem (-> mem first first)) + mem)))) + value))))) +) + +;; NB: when-ns eats all exceptions inside the body, including those about +;; unresolvable symbols. Keep this in mind when debugging the definitions below. + +(defmacro when-ns [ns & body] + `(try + (eval + '(do + (require ~ns) + ~@body)) + (catch Exception ~'_))) + diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/core.clj b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/core.clj new file mode 100644 index 00000000..ffb711ea --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/core.clj @@ -0,0 +1,91 @@ +(ns compojure.api.core + (:require [compojure.api.meta :as meta] + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [[compojure.api.async] + [compojure.core :as compojure]]) + [compojure.api.routes #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) routes] + [compojure.api.middleware #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) mw])) + +(defn ring-handler + "Creates vanilla ring-handler from any invokable thing (e.g. compojure-api route)" + [handler] + (fn + ([request] (handler request)) + ([request respond raise] (handler request respond raise)))) + +(defn routes + "Create a Ring handler by combining several handlers into one." + [& handlers] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" (throw (ex-info "Not supported in bb")) + :default (let [handlers (seq (keep identity (flatten handlers)))] + (routes/map->Route + {:childs (vec handlers) + :handler (meta/routing handlers)})))) + +(defmacro defroutes + "Define a Ring handler function from a sequence of routes. + The name may optionally be followed by a doc-string and metadata map." + {:style/indent 1} + [name & routes] + (let [[name routes] (meta/name-with-attributes name routes)] + `(def ~name (routes ~@routes)))) + +(defmacro let-routes + "Takes a vector of bindings and a body of routes. + + Equivalent to: `(let [...] (routes ...))`" + {:style/indent 1} + [bindings & body] + `(let ~bindings (routes ~@body))) + +(defn undocumented + "Routes without route-documentation. Can be used to wrap routes, + not satisfying compojure.api.routes/Routing -protocol." + [& handlers] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" (throw (ex-info "Not supported in bb")) + :default (let [handlers (keep identity handlers)] + (routes/map->Route {:handler (meta/routing handlers)})))) + +(defmacro middleware + "Wraps routes with given middlewares using thread-first macro. + + Note that middlewares will be executed even if routes in body + do not match the request uri. Be careful with middleware that + has side-effects." + {:style/indent 1 + :deprecated "1.1.14" + :superseded-by "route-middleware"} + [middleware & body] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default (when (not= "true" (System/getProperty "compojure.api.core.suppress-middleware-warning")) + (println (str "compojure.api.core.middleware is deprecated because of security issues. " + "Please use route-middleware instead. middleware will be disabled in a future release." + "Set -dcompojure.api.core.suppress-middleware-warning=true to suppress this warning.")))) + `(let [body# (routes ~@body) + wrap-mw# (mw/compose-middleware ~middleware)] + (routes/create nil nil {} [body#] (wrap-mw# body#)))) + +(defn route-middleware + "Wraps routes with given middleware using thread-first macro." + {:style/indent 1 + :supercedes "middleware"} + [middleware & body] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" (throw (ex-info "Not supported in bb")) + :default + (let [handler (apply routes body) + x-handler (compojure/wrap-routes handler (mw/compose-middleware middleware))] + ;; use original handler for docs and wrapped handler for implementation + (routes/map->Route + {:childs [handler] + :handler x-handler})))) + +(defmacro context {:style/indent 2} [& args] (meta/restructure nil args {:context? true :&form &form :&env &env :kondo-rule? #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" true :default false)})) + +(defmacro GET {:style/indent 2} [& args] (meta/restructure :get args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro ANY {:style/indent 2} [& args] (meta/restructure nil args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro HEAD {:style/indent 2} [& args] (meta/restructure :head args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro PATCH {:style/indent 2} [& args] (meta/restructure :patch args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro DELETE {:style/indent 2} [& args] (meta/restructure :delete args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro OPTIONS {:style/indent 2} [& args] (meta/restructure :options args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro POST {:style/indent 2} [& args] (meta/restructure :post args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro PUT {:style/indent 2} [& args] (meta/restructure :put args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/meta.clj b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/meta.clj new file mode 100644 index 00000000..ab1ead60 --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure/api/meta.clj @@ -0,0 +1,432 @@ +(ns compojure.api.meta + (:require [clojure.walk :as walk] + [compojure.api.common :as common :refer [extract-parameters]] + [compojure.api.middleware #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) mw] + [compojure.api.routes #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) routes] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [compojure-api-kondo-hooks.plumbing.core :as p] + :default [plumbing.core :as p]) + [plumbing.fnk.impl #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) fnk-impl] + [ring.swagger.common #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) rsc] + [ring.swagger.json-schema #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) js] + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [[schema.core :as s] + [schema-tools.core :as st]]) + [compojure.api.coerce #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) coerce] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [compojure-api-kondo-hooks.compojure.core :as comp-core] + :default [compojure.core :as comp-core]))) + +(defmacro ^:private system-property-check + [& body] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default `(do ~@body))) + +(def +compojure-api-request+ + "lexically bound ring-request for handlers." + '+compojure-api-request+) + +;; https://github.com/clojure/tools.macro/blob/415512648bb51153f380823c41323cda2c13f47f/src/main/clojure/clojure/tools/macro.clj +;; Copyright (c) Rich Hickey. All rights reserved. +;; The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (https://opensource.org/license/epl-1-0/) +;; which can be found in the file epl-v10.html at the root of this distribution. By using this software in any fashion, you are agreeing to +;; be bound bythe terms of this license. You must not remove this notice, or any other, from this software. +(defn name-with-attributes + "To be used in macro definitions. + Handles optional docstrings and attribute maps for a name to be defined + in a list of macro arguments. If the first macro argument is a string, + it is added as a docstring to name and removed from the macro argument + list. If afterwards the first macro argument is a map, its entries are + added to the name's metadata map and the map is removed from the + macro argument list. The return value is a vector containing the name + with its extended metadata map and the list of unprocessed macro + arguments." + [name macro-args] + (let [[docstring macro-args] (if (string? (first macro-args)) + [(first macro-args) (next macro-args)] + [nil macro-args]) + [attr macro-args] (if (map? (first macro-args)) + [(first macro-args) (next macro-args)] + [{} macro-args]) + attr (if docstring + (assoc attr :doc docstring) + attr) + attr (if (meta name) + (conj (meta name) attr) + attr)] + [(with-meta name attr) macro-args])) + +;; +;; Schema +;; + +(defn strict [schema] + (dissoc schema 'schema.core/Keyword)) + +(defn fnk-schema [bind] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {} + :default (->> + (:input-schema + (fnk-impl/letk-input-schema-and-body-form + nil (with-meta bind {:schema s/Any}) [] nil)) + reverse + (into {})))) + +(defn src-coerce! + "Return source code for coerce! for a schema with coercion type, + extracted from a key in a ring request." + [schema, key, type #_#_:- mw/CoercionType] + (assert (not (#{:query :json} type)) (str type " is DEPRECATED since 0.22.0. Use :body or :string instead.")) + (assert (#{:body :string :response} type)) + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" `(do ~schema ~key ~type ~+compojure-api-request+) + :default `(coerce/coerce! ~schema ~key ~type ~+compojure-api-request+))) + +(defn- convert-return [schema] + {200 {:schema schema + :description (or #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default (js/json-schema-meta schema)) + "")}}) + +;; +;; Extension point +;; + +(defmulti restructure-param + "Restructures a key value pair in smart routes. By default the key + is consumed form the :parameters map in acc. k = given key, v = value." + (fn [k v acc] k)) + +;; +;; Pass-through swagger metadata +;; + +(defmethod restructure-param :summary [k v acc] + (update-in acc [:swagger] assoc k v)) + +(defmethod restructure-param :description [k v acc] + (update-in acc [:swagger] assoc k v)) + +(defmethod restructure-param :operationId [k v acc] + (update-in acc [:swagger] assoc k v)) + +(defmethod restructure-param :consumes [k v acc] + (update-in acc [:swagger] assoc k v)) + +(defmethod restructure-param :produces [k v acc] + (update-in acc [:swagger] assoc k v)) + +;; +;; Smart restructurings +;; + +; Boolean to discard the route out from api documentation +; Example: +; :no-doc true +(defmethod restructure-param :no-doc [_ v acc] + (update-in acc [:swagger] assoc :x-no-doc v)) + +; publishes the data as swagger-parameters without any side-effects / coercion. +; Examples: +; :swagger {:responses {200 {:schema User} +; 404 {:schema Error +; :description "Not Found"} } +; :parameters {:query {:q s/Str} +; :body NewUser}}} +(defmethod restructure-param :swagger [_ swagger acc] + (assoc-in acc [:swagger :swagger] swagger)) + +; Route name, used with path-for +; Example: +; :name :user-route +(defmethod restructure-param :name [_ v acc] + (update-in acc [:swagger] assoc :x-name v)) + +; Tags for api categorization. Ignores duplicates. +; Examples: +; :tags [:admin] +(defmethod restructure-param :tags [_ tags acc] + (update-in acc [:swagger :tags] (comp set into) tags)) + +; Defines a return type and coerces the return value of a body against it. +; Examples: +; :return MySchema +; :return {:value String} +; :return #{{:key (s/maybe Long)}} +(defmethod restructure-param :return [_ schema acc] + (let [response (convert-return schema)] + (-> acc + (update-in [:swagger :responses] (fnil conj []) response) + (update-in [:responses] (fnil conj []) response)))) + +; value is a map of http-response-code -> Schema. Translates to both swagger +; parameters and return schema coercion. Schemas can be decorated with meta-data. +; Examples: +; :responses {403 nil} +; :responses {403 {:schema ErrorEnvelope}} +; :responses {403 {:schema ErrorEnvelope, :description \"Underflow\"}} +(defmethod restructure-param :responses [_ responses acc] + (-> acc + (update-in [:swagger :responses] (fnil conj []) responses) + (update-in [:responses] (fnil conj []) responses))) + +; reads body-params into a enhanced let. First parameter is the let symbol, +; second is the Schema to be coerced! against. +; Examples: +; :body [user User] +(defmethod restructure-param :body [_ [value schema :as bv] acc] + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-body")) + (assert (= 2 (count bv)) + (str ":body should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-body=true")))) + (-> acc + (update-in [:lets] into [value (src-coerce! schema :body-params :body)]) + (assoc-in [:swagger :parameters :body] schema))) + +; reads query-params into a enhanced let. First parameter is the let symbol, +; second is the Schema to be coerced! against. +; Examples: +; :query [user User] +(defmethod restructure-param :query [_ [value schema :as bv] acc] + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-query")) + (assert (= 2 (count bv)) + (str ":query should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-query=true")))) + (-> acc + (update-in [:lets] into [value (src-coerce! schema :query-params :string)]) + (assoc-in [:swagger :parameters :query] schema))) + +; reads header-params into a enhanced let. First parameter is the let symbol, +; second is the Schema to be coerced! against. +; Examples: +; :headers [headers Headers] +(defmethod restructure-param :headers [_ [value schema :as bv] acc] + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-headers")) + (assert (= 2 (count bv)) + (str ":headers should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-headers=true")))) + + (-> acc + (update-in [:lets] into [value (src-coerce! schema :headers :string)]) + (assoc-in [:swagger :parameters :header] schema))) + +; restructures body-params with plumbing letk notation. Example: +; :body-params [id :- Long name :- String] +(defmethod restructure-param :body-params [_ body-params acc] + (let [schema (strict (fnk-schema body-params))] + (-> acc + (update-in [:letks] into [body-params (src-coerce! schema :body-params :body)]) + (assoc-in [:swagger :parameters :body] schema)))) + +; restructures form-params with plumbing letk notation. Example: +; :form-params [id :- Long name :- String] +(defmethod restructure-param :form-params [_ form-params acc] + (let [schema (strict (fnk-schema form-params))] + (-> acc + (update-in [:letks] into [form-params (src-coerce! schema :form-params :string)]) + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [(update-in [:swagger :parameters :formData] st/merge schema)]) + (assoc-in [:swagger :consumes] ["application/x-www-form-urlencoded"])))) + +; restructures multipart-params with plumbing letk notation and consumes "multipart/form-data" +; :multipart-params [file :- compojure.api.upload/TempFileUpload] +(defmethod restructure-param :multipart-params [_ params acc] + (let [schema (strict (fnk-schema params))] + (-> acc + (update-in [:letks] into [params (src-coerce! schema :multipart-params :string)]) + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [(update-in [:swagger :parameters :formData] st/merge schema)]) + (assoc-in [:swagger :consumes] ["multipart/form-data"])))) + +; restructures header-params with plumbing letk notation. Example: +; :header-params [id :- Long name :- String] +(defmethod restructure-param :header-params [_ header-params acc] + (let [schema (fnk-schema header-params)] + (-> acc + (update-in [:letks] into [header-params (src-coerce! schema :headers :string)]) + (assoc-in [:swagger :parameters :header] schema)))) + +; restructures query-params with plumbing letk notation. Example: +; :query-params [id :- Long name :- String] +(defmethod restructure-param :query-params [_ query-params acc] + (let [schema (fnk-schema query-params)] + (-> acc + (update-in [:letks] into [query-params (src-coerce! schema :query-params :string)]) + (assoc-in [:swagger :parameters :query] schema)))) + +; restructures path-params by plumbing letk notation. Example: +; :path-params [id :- Long name :- String] +(defmethod restructure-param :path-params [_ path-params acc] + (let [schema (fnk-schema path-params)] + (-> acc + (update-in [:letks] into [path-params (src-coerce! schema :route-params :string)]) + (assoc-in [:swagger :parameters :path] schema)))) + +; Applies the given vector of middlewares to the route +(defmethod restructure-param :middleware [_ middleware acc] + (update-in acc [:middleware] into middleware)) + +; Bind to stuff in request components using letk syntax +(defmethod restructure-param :components [_ components acc] + (update-in acc [:letks] into [components `(mw/get-components ~+compojure-api-request+)])) + +; route-specific override for coercers +(defmethod restructure-param :coercion [_ coercion acc] + (update-in acc [:middleware] conj [#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" `mw/wrap-coercion + ;;FIXME why not quoted? + :default mw/wrap-coercion) + coercion])) + +;; +;; Impl +;; + +#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default +(defmacro dummy-let + "Dummy let-macro used in resolving route-docs. not part of normal invocation chain." + [bindings & body] + (let [bind-form (vec (apply concat (for [n (take-nth 2 bindings)] [n nil])))] + `(let ~bind-form ~@body))) +) + +#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default +(defmacro dummy-letk + "Dummy letk-macro used in resolving route-docs. not part of normal invocation chain." + [bindings & body] + (reduce + (fn [cur-body-form [bind-form]] + (if (symbol? bind-form) + `(let [~bind-form nil] ~cur-body-form) + (let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form ;;TODO clj-kondo + &env + (fnk-impl/ensure-schema-metadata &env bind-form) + [] + cur-body-form) + body-form (walk/prewalk-replace {'plumbing.fnk.schema/safe-get 'clojure.core/get} body-form)] + `(let [~map-sym nil] ~body-form)))) + `(do ~@body) + (reverse (partition 2 bindings)))) +) + +#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default +(defn routing [handlers] + (if-let [handlers (seq (keep identity (flatten handlers)))] + (apply comp-core/routes handlers) + (fn ([_] nil) ([_ respond _] (respond nil))))) +) + +;; +;; Api +;; + +(defn- destructure-compojure-api-request + "Returns a vector of four elements: + - pruned path string + - new lets list + - bindings form for compojure route + - symbol to which request will be bound" + [path arg] + (let [path-string (if (vector? path) (first path) path)] + (cond + ;; GET "/route" [] + (vector? arg) [path-string [] (into arg [:as +compojure-api-request+]) +compojure-api-request+] + ;; GET "/route" {:as req} + (map? arg) (if-let [as (:as arg)] + [path-string [+compojure-api-request+ as] arg as] + [path-string [] (merge arg [:as +compojure-api-request+]) +compojure-api-request+]) + ;; GET "/route" req + (symbol? arg) [path-string [+compojure-api-request+ arg] arg arg] + :else (throw + (ex-info + (str "unknown compojure destruction syntax: " arg) + {}))))) + +(defn merge-parameters + "Merge parameters at runtime to allow usage of runtime-parameters with route-macros." + [{:keys [responses swagger] :as parameters}] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" parameters + :default (cond-> parameters + (seq responses) (assoc :responses (common/merge-vector responses)) + swagger (-> (dissoc :swagger) (rsc/deep-merge swagger))))) + +(defn restructure [method [path arg & args] {:keys [context? kondo-rule?]}] + (let [[options body] (extract-parameters args true) + [path-string lets arg-with-request arg] (destructure-compojure-api-request path arg) + + {:keys [lets + letks + responses + middleware + middlewares + swagger + parameters + body]} (reduce + (fn [acc [k v]] + (restructure-param k v (update-in acc [:parameters] dissoc k))) + {:lets lets + :letks [] + :responses nil + :middleware [] + :swagger {} + :body body + :kondo-rule? kondo-rule?} + options) + + ;; migration helpers + _ (assert (not middlewares) ":middlewares is deprecated with 1.0.0, use :middleware instead.") + _ (assert (not parameters) ":parameters is deprecated with 1.0.0, use :swagger instead.") + + ;; response coercion middleware, why not just code? + middleware (if (seq responses) (conj middleware `[coerce/body-coercer-middleware (common/merge-vector ~responses)]) middleware)] + + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" (do (assert kondo-rule?) + (if context? + ;; context + (let [form `(do ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form `(comp-core/context ~path ~arg-with-request ~form)] + (prn "context" form) + form) + + ;; endpoints + (let [form `(do ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form (comp-core/compile-route method path arg-with-request (list form)) + form `(fn [~'+compojure-api-request+] + ~'+compojure-api-request+ ;;always used + ~form)] + (prn "endpoint" form) + form))) + :default ;; JVM + (if context? + ;; context + (let [form `(comp-core/routes ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form (if (seq middleware) `((mw/compose-middleware ~middleware) ~form) form) + form `(comp-core/context ~path ~arg-with-request ~form) + + ;; create and apply a separate lookup-function to find the inner routes + childs (let [form (vec body) + form (if (seq letks) `(dummy-letk ~letks ~form) form) + form (if (seq lets) `(dummy-let ~lets ~form) form) + form `(comp-core/let-request [~arg-with-request ~'+compojure-api-request+] ~form) + form `(fn [~'+compojure-api-request+] ~form) + form `(~form {})] + form)] + + `(routes/create ~path-string ~method (merge-parameters ~swagger) ~childs ~form)) + + ;; endpoints + (let [form `(do ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form (comp-core/compile-route method path arg-with-request (list form)) + form (if (seq middleware) `(comp-core/wrap-routes ~form (mw/compose-middleware ~middleware)) form)] + + `(routes/create ~path-string ~method (merge-parameters ~swagger) nil ~form)))))) diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/compojure/core.clj b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/compojure/core.clj new file mode 100644 index 00000000..7b95c17b --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/compojure/core.clj @@ -0,0 +1,60 @@ +;; Copyright © 2024 James Reeves +;; +;; Distributed under the Eclipse Public License, the same as Clojure. +(ns compojure-api-kondo-hooks.compojure.core) + +(defn- and-binding [req binds] + `(dissoc (:params ~req) ~@(map keyword (keys binds)) ~@(map str (keys binds)))) + +(defn- symbol-binding [req sym] + `(get-in ~req [:params ~(keyword sym)] (get-in ~req [:params ~(str sym)]))) + +(defn- application-binding [req sym func] + `(~func ~(symbol-binding req sym))) + +(defn- vector-bindings [args req] + (loop [args args, binds {}] + (if (seq args) + (let [[x y z] args] + (cond + (= '& x) + (recur (nnext args) (assoc binds y (and-binding req binds))) + (= :as x) + (recur (nnext args) (assoc binds y req)) + (and (symbol? x) (= :<< y) (nnext args)) + (recur (drop 3 args) (assoc binds x (application-binding req x z))) + (symbol? x) + (recur (next args) (assoc binds x (symbol-binding req x))) + :else + (throw (Exception. (str "Unexpected binding: " x))))) + (mapcat identity binds)))) + +(defn- warn-on-*-bindings! [bindings] + (when (and (vector? bindings) (contains? (set bindings) '*)) + (binding [*out* *err*] + (println "WARNING: * should not be used as a route binding.")))) + +(defn- application-symbols [args] + (loop [args args, syms '()] + (if (seq args) + (let [[x y] args] + (if (and (symbol? x) (= :<< y)) + (recur (drop 3 args) (conj syms x)) + (recur (next args) syms))) + (seq syms)))) + +(defmacro ^:no-doc let-request [[bindings request] & body] + (if (vector? bindings) + `(let [~@(vector-bindings bindings request)] + ~(if-let [syms (application-symbols bindings)] + `(if (and ~@(for [s syms] `(not (nil? ~s)))) (do ~@body)) + `(do ~@body))) + `(let [~bindings ~request] ~@body))) + +(defn compile-route + "Compile a route in the form `(method path bindings & body)` into a function. + Used to create custom route macros." + [method path bindings body] + (let [greq (gensym "greq")] + `(fn [~greq] + ~(macroexpand-1 `(let-request [~bindings ~greq] ~@body))))) diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/core.clj b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/core.clj new file mode 100644 index 00000000..5d0189a7 --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/core.clj @@ -0,0 +1,42 @@ +(ns compojure-api-kondo-hooks.plumbing.core + "Utility belt for Clojure in the wild" + (:refer-clojure :exclude [update]) + (:require + [compojure-api-kondo-hooks.schema.macros :as schema-macros] + [compojure-api-kondo-hooks.plumbing.fnk.impl :as fnk-impl])) + +(defmacro letk + "Keyword let. Accepts an interleaved sequence of binding forms and map forms like: + (letk [[a {b 2} [:f g h] c d {e 4} :as m & more] a-map ...] & body) + a, c, d, and f are required keywords, and letk will barf if not in a-map. + b and e are optional, and will be bound to default values if not present. + g and h are required keys in the map found under :f. + m will be bound to the entire map (a-map). + more will be bound to all the unbound keys (ie (dissoc a-map :a :b :c :d :e)). + :as and & are both optional, but must be at the end in the specified order if present. + The same symbol cannot be bound multiple times within the same destructing level. + + Optional values can reference symbols bound earlier within the same binding, i.e., + (= [2 2] (let [a 1] (letk [[a {b a}] {:a 2}] [a b]))) but + (= [2 1] (let [a 1] (letk [[{b a} a] {:a 2}] [a b]))) + + If present, :as and :& symbols are bound before other symbols within the binding. + + Namespaced keys are supported by specifying fully-qualified key in binding form. The bound + symbol uses the _name_ portion of the namespaced key, i.e, + (= 1 (letk [[a/b] {:a/b 1}] b)). + + Map destructuring bindings can be mixed with ordinary symbol bindings." + [bindings & body] + (reduce + (fn [cur-body-form [bind-form value-form]] + (if (symbol? bind-form) + `(let [~bind-form ~value-form] ~cur-body-form) + (let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form + &env + bind-form ;(fnk-impl/ensure-schema-metadata &env bind-form) + [] + cur-body-form)] + `(let [~map-sym ~value-form] ~body-form)))) + `(do ~@body) + (reverse (partition 2 bindings)))) diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk/impl.clj b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk/impl.clj new file mode 100644 index 00000000..5e1ad75f --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk/impl.clj @@ -0,0 +1,116 @@ +(ns compojure-api-kondo-hooks.plumbing.fnk.impl + (:require + [clojure.set :as set] + [schema.core :as-alias s] + [compojure-api-kondo-hooks.schema.macros :as schema-macros])) + +;;;;; Helpers + +(defn name-sym + "Returns symbol of x's name. + Converts a keyword/string to symbol, or removes namespace (if any) of symbol" + [x] + (with-meta (symbol (name x)) (meta x))) + +;;; Parsing new fnk binding style + +(declare letk-input-schema-and-body-form) + +(defn- process-schematized-map + "Take an optional binding map like {a 2} or {a :- Number 2} and convert the schema + information to canonical metadata, if present." + [env binding] + (case (count binding) + 1 (let [[sym v] (first binding)] + {sym v}) + + 2 (let [[[[sym _]] [[schema v]]] ((juxt filter remove) #(= (val %) :-) binding)] + {sym v}))) + +;; TODO: unify this with positional version. +(defn letk-arg-bind-sym-and-body-form + "Given a single element of a single letk binding form and a current body form, return + a map {:schema-entry :body-form} where schema-entry is a tuple + [bound-key schema external-schema?], and body-form wraps body with destructuring + for this binding as necessary." + [env map-sym binding key-path body-form] + (cond (symbol? binding) + {:schema-entry [] + :body-form `(let [~(name-sym binding) (get ~map-sym ~(keyword binding) ~key-path)] + ~body-form)} + + (map? binding) + (let [schema-fixed-binding (process-schematized-map env binding) + [bound-sym opt-val-expr] (first schema-fixed-binding) + bound-key (keyword bound-sym)] + {:schema-entry [] + :body-form `(let [~(name-sym bound-sym) (get ~map-sym ~bound-key ~opt-val-expr)] + ~body-form)}) + + (vector? binding) + (let [[bound-key & more] binding + {inner-input-schema :input-schema + inner-external-input-schema :external-input-schema + inner-map-sym :map-sym + inner-body-form :body-form} (letk-input-schema-and-body-form + env + (with-meta (vec more) (meta binding)) + (conj key-path bound-key) + body-form)] + {:schema-entry [] + :body-form `(let [~inner-map-sym (get ~map-sym ~bound-key ~key-path)] + ~inner-body-form)}) + + :else (throw (ex-info (format "bad binding: %s" binding) {})))) + +(defn- extract-special-args + "Extract trailing & sym and :as sym, possibly with schema metadata. Returns + [more-bindings special-args-map] where special-args-map is a map from each + special symbol found to the symbol that was found." + [env special-arg-signifier-set binding-form] + {:pre [(set? special-arg-signifier-set)]} + (let [[more-bindings special-bindings] (split-with (complement special-arg-signifier-set) binding-form)] + (loop [special-args-map {} + special-arg-set special-arg-signifier-set + [arg-signifier & other-bindings :as special-bindings] special-bindings] + (if-not (seq special-bindings) + [more-bindings special-args-map] + (do + (let [[sym remaining-bindings] (schema-macros/extract-arrow-schematized-element env other-bindings)] + (recur (assoc special-args-map arg-signifier sym) + (disj special-arg-set arg-signifier) + remaining-bindings))))))) + +(defn letk-input-schema-and-body-form + "Given a single letk binding form, value form, key path, and body + form, return a map {:input-schema :external-input-schema :map-sym :body-form} + where input-schema is the schema imposed by binding-form, external-input-schema + is like input-schema but includes user overrides for binding vectors, + map-sym is the symbol which it expects the bound value to be bound to, + and body-form wraps body in the bindings from binding-form from map-sym." + [env binding-form key-path body-form] + (let [[bindings {more-sym '& as-sym :as}] (extract-special-args env #{'& :as} binding-form) + as-sym (or as-sym (gensym "map")) + [input-schema-elts + external-input-schema-elts + bound-body-form] (reduce + (fn [[input-schema-elts external-input-schema-elts cur-body] binding] + (let [{:keys [schema-entry body-form]} + (letk-arg-bind-sym-and-body-form + env as-sym binding key-path cur-body) + [bound-key input-schema external-input-schema] schema-entry] + [(conj input-schema-elts [bound-key input-schema]) + (conj external-input-schema-elts + [bound-key (or external-input-schema input-schema)]) + body-form])) + [[] [] body-form] + (reverse + (schema-macros/process-arrow-schematized-args + env bindings))) + explicit-schema-keys [] + final-body-form (if more-sym + `(let [~more-sym (dissoc ~as-sym ~@explicit-schema-keys)] + ~bound-body-form) + bound-body-form)] + {:map-sym as-sym + :body-form final-body-form})) diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/schema/macros.clj b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/schema/macros.clj new file mode 100644 index 00000000..aefe1448 --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/compojure_api_kondo_hooks/schema/macros.clj @@ -0,0 +1,24 @@ +(ns compojure-api-kondo-hooks.schema.macros) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Helpers for processing and normalizing element/argument schemas in s/defrecord and s/(de)fn + +(defn extract-arrow-schematized-element + "Take a nonempty seq, which may start like [a ...] or [a :- schema ...], and return + a list of [first-element-with-schema-attached rest-elements]" + [env s] + (assert (seq s)) + (let [[f & more] s] + (if (= :- (first more)) + [f (drop 2 more)] + [f more]))) + +(defn process-arrow-schematized-args + "Take an arg vector, in which each argument is followed by an optional :- schema, + and transform into an ordinary arg vector where the schemas are metadata on the args." + [env args] + (loop [in args out []] + (if (empty? in) + out + (let [[arg more] (extract-arrow-schematized-element env in)] + (recur more (conj out arg)))))) diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/config.edn b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/config.edn new file mode 100644 index 00000000..d0cb4934 --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/compojure-api/config.edn @@ -0,0 +1 @@ +{:hooks {:macroexpand {compojure.api.sweet/PATCH compojure.api.core/PATCH, compojure.api.sweet/ANY compojure.api.core/ANY, compojure.api.sweet/context compojure.api.core/context, compojure.api.core/POST compojure.api.core/POST, compojure.api.core/GET compojure.api.core/GET, compojure.api.sweet/DELETE compojure.api.core/DELETE, compojure.api.core/ANY compojure.api.core/ANY, compojure.api.sweet/POST compojure.api.core/POST, compojure.api.sweet/GET compojure.api.core/GET, compojure.api.sweet/HEAD compojure.api.core/HEAD, compojure.api.core/PUT compojure.api.core/PUT, compojure.api.core/DELETE compojure.api.core/DELETE, compojure.api.sweet/OPTIONS compojure.api.core/OPTIONS, compojure.api.sweet/PUT compojure.api.core/PUT, compojure.api.core/context compojure.api.core/context, compojure.api.core/HEAD compojure.api.core/HEAD, compojure.api.core/PATCH compojure.api.core/PATCH, compojure.api.core/OPTIONS compojure.api.core/OPTIONS}}} \ No newline at end of file diff --git a/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/config.edn b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/config.edn new file mode 100644 index 00000000..45b2fddd --- /dev/null +++ b/examples/clj-kondo-hooks/.clj-kondo/imports/metosin/config.edn @@ -0,0 +1,5 @@ +{:hooks + {:macroexpand + {compojure.api.sweet/GET compojure.api.core/GET + compojure.api.core/GET compojure.api.core/GET + compojure.api.core/defroutes compojure.api.core/defroutes}}} diff --git a/examples/clj-kondo-hooks/.gitignore b/examples/clj-kondo-hooks/.gitignore new file mode 100644 index 00000000..179043b0 --- /dev/null +++ b/examples/clj-kondo-hooks/.gitignore @@ -0,0 +1,3 @@ +.cpcache +.clj-kondo/.cache +output/actual-output diff --git a/examples/clj-kondo-hooks/deps.edn b/examples/clj-kondo-hooks/deps.edn new file mode 100644 index 00000000..f1d06aa0 --- /dev/null +++ b/examples/clj-kondo-hooks/deps.edn @@ -0,0 +1,19 @@ +{:deps {org.clojure/clojure {:mvn/version "1.11.1"} + metosin/compojure-api {:local/root "../.."}} + :aliases {:dev {:extra-paths ["test"]} + :clj-kondo + {:replace-deps {clj-kondo/clj-kondo {:mvn/version "RELEASE"}} + :main-opts ["-m" "clj-kondo.main"]} + :test {:extra-deps {io.github.cognitect-labs/test-runner + {:git/tag "v0.5.0" :git/sha "b3fd0d2"}} + :main-opts ["-m" "cognitect.test-runner"] + :exec-fn cognitect.test-runner.api/test} + :nREPL + {:extra-deps + {cider/cider-nrepl {:mvn/version "0.28.2"}, + cider/piggieback {:mvn/version "0.5.3"}, + net.cgrand/parsley {:mvn/version "0.9.3"}, + nrepl/nrepl {:mvn/version "0.8.3"}, + reply/reply {:mvn/version "0.5.1"}}, + :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] + :main-opts ["-m" "nrepl.cmdline" "--interactive"]}}} diff --git a/examples/clj-kondo-hooks/output/expected-output b/examples/clj-kondo-hooks/output/expected-output new file mode 100644 index 00000000..cf0b4345 --- /dev/null +++ b/examples/clj-kondo-hooks/output/expected-output @@ -0,0 +1,6 @@ +src/compojure_api_example/clj_kondo_hooks.clj:26:20: error: keyword :ok is called with 0 args but expects 1 or 2 +src/compojure_api_example/clj_kondo_hooks.clj:30:21: error: keyword :ok is called with 0 args but expects 1 or 2 +src/compojure_api_example/clj_kondo_hooks.clj:33:17: error: Unresolved symbol: req +src/compojure_api_example/clj_kondo_hooks.clj:35:25: error: Unresolved symbol: body +src/compojure_api_example/clj_kondo_hooks.clj:38:17: error: Unresolved symbol: _ +src/compojure_api_example/clj_kondo_hooks.clj:46:29: error: Unresolved symbol: qparam diff --git a/examples/clj-kondo-hooks/script/clj-kondo b/examples/clj-kondo-hooks/script/clj-kondo new file mode 100755 index 00000000..00ce0795 --- /dev/null +++ b/examples/clj-kondo-hooks/script/clj-kondo @@ -0,0 +1,7 @@ +#!/bin/bash + +if command -v clj-kondo &> /dev/null; then + clj-kondo "$@" +else + clojure -M:clj-kondo "$@" +fi diff --git a/examples/clj-kondo-hooks/script/lint b/examples/clj-kondo-hooks/script/lint new file mode 100755 index 00000000..f3629a0e --- /dev/null +++ b/examples/clj-kondo-hooks/script/lint @@ -0,0 +1,3 @@ +#!/bin/bash + +./script/clj-kondo --lint src --config '{:output {:format :text :summary false}}' --debug diff --git a/examples/clj-kondo-hooks/script/prep-lint b/examples/clj-kondo-hooks/script/prep-lint new file mode 100755 index 00000000..214e3464 --- /dev/null +++ b/examples/clj-kondo-hooks/script/prep-lint @@ -0,0 +1,4 @@ +#!/bin/bash + +rm -fr .clj-kondo/metosin +./script/clj-kondo --lint "$(clojure -Spath)" --copy-configs --skip-lint diff --git a/examples/clj-kondo-hooks/script/regen-expected-output b/examples/clj-kondo-hooks/script/regen-expected-output new file mode 100755 index 00000000..cdb94912 --- /dev/null +++ b/examples/clj-kondo-hooks/script/regen-expected-output @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +./script/prep-lint +./script/lint > output/expected-output diff --git a/examples/clj-kondo-hooks/script/repl b/examples/clj-kondo-hooks/script/repl new file mode 100755 index 00000000..b81b9f41 --- /dev/null +++ b/examples/clj-kondo-hooks/script/repl @@ -0,0 +1,3 @@ +#!/bin/bash + +clojure -M:dev:nREPL diff --git a/examples/clj-kondo-hooks/script/test b/examples/clj-kondo-hooks/script/test new file mode 100755 index 00000000..e5541ae9 --- /dev/null +++ b/examples/clj-kondo-hooks/script/test @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +./script/prep-lint +set +e +./script/lint > output/actual-output +set -e +diff output/expected-output output/actual-output diff --git a/examples/clj-kondo-hooks/src/compojure_api_example/clj_kondo_hooks.clj b/examples/clj-kondo-hooks/src/compojure_api_example/clj_kondo_hooks.clj new file mode 100644 index 00000000..1e6b62d0 --- /dev/null +++ b/examples/clj-kondo-hooks/src/compojure_api_example/clj_kondo_hooks.clj @@ -0,0 +1,49 @@ +(ns compojure-api-example.clj-kondo-hooks + (:require [compojure.api.sweet :as sweet] + [compojure.api.core :as core] + [ring.util.http-response :as resp] + [schema.core :as s] + ;; intentionally blank + ;; intentionally blank + ;; intentionally blank + ;; intentionally blank + ;; intentionally blank + ;; intentionally blank + ;; intentionally blank + )) +;; intentionally blank +;; intentionally blank +;; intentionally blank +;; intentionally blank +;; intentionally blank +;; intentionally blank +;; intentionally blank +;; intentionally blank +;; intentionally blank +;; intentionally blank + +(core/GET "/30" [] (resp/ok {:result 30})) +(core/GET "/30" [] (:ok)) ;; src/compojure_api_example/clj_kondo_hooks.clj:26:20: error: keyword :ok is called with 0 args but expects 1 or 2 +(core/GET "/30" [] '(:ok)) + +(sweet/GET "/30" [] (resp/ok {:result 30})) +(sweet/GET "/30" [] (:ok)) ;; src/compojure_api_example/clj_kondo_hooks.clj:30:21: error: keyword :ok is called with 0 args but expects 1 or 2 +(sweet/GET "/30" [] '(:ok)) + +(core/GET "/30" req (resp/ok {:result (:body req)})) +(core/GET "/30" [:as req] (resp/ok {:result (:body req)})) +(core/GET "/30" {:keys [body]} (resp/ok {:result body})) +(core/GET "/30" {:as req} (resp/ok {:result (:body req)})) +(core/GET "/30" {:as req} (resp/ok {:result (:body req)})) +(core/GET "/30" _ (resp/ok {:result (:body req)})) ;; src/compojure_api_example/clj_kondo_hooks.clj:38:44: error: Unresolved symbol: req + +(core/PUT "/30" req (resp/ok {:result (:body req)})) + +(core/routes + (core/PUT "/" [] + :responses {200 {:schema s/Any}} + :summary "summary" + :query-params [{qparam :- s/Int nil}] + :body [body (resp/describe s/Any "description")] + :description (str "foo" "bar") + (resp/ok (str qparam body)))) diff --git a/project.clj b/project.clj index ac12927b..0d13ff10 100644 --- a/project.clj +++ b/project.clj @@ -47,7 +47,9 @@ [criterium "0.4.5"]] :ring {:handler examples.thingie/app :reload-paths ["src" "examples/thingie/src"]} - :source-paths ["examples/thingie/src" "examples/thingie/dev-src"] + :source-paths ["examples/thingie/src" + "examples/thingie/dev-src" + "dev"] :main examples.server} :perf {:jvm-opts ^:replace ["-server" "-Xmx4096m" diff --git a/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/common.clj b/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/common.clj new file mode 100644 index 00000000..2a0dc7d5 --- /dev/null +++ b/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/common.clj @@ -0,0 +1,84 @@ +(ns compojure.api.common + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [(:require [linked.core :as linked])])) + +(defn plain-map? + "checks whether input is a map, but not a record" + [x] (and (map? x) (not (record? x)))) + +(defn extract-parameters + "Extract parameters from head of the list. Parameters can be: + + 1. a map (if followed by any form) `[{:a 1 :b 2} :body]` => `{:a 1 :b 2}` + 2. list of keywords & values `[:a 1 :b 2 :body]` => `{:a 1 :b 2}` + 3. else => `{}` + + Returns a tuple with parameters and body without the parameters" + [c expect-body] + (cond + (and (plain-map? (first c)) (or (not expect-body) (seq (rest c)))) + [(first c) (seq (rest c))] + + (keyword? (first c)) + (let [parameters (->> c + (partition 2) + (take-while (comp keyword? first)) + (mapcat identity) + (apply array-map)) + form (drop (* 2 (count parameters)) c)] + [parameters (seq form)]) + + :else + [{} (seq c)])) + +(defn group-with + "Groups a sequence with predicate returning a tuple of sequences." + [pred coll] + [(seq (filter pred coll)) + (seq (remove pred coll))]) + +(defn merge-vector + "Merges vector elements, optimized for 1 arity (x10 faster than merge)." + [v] + (if (get v 1) + (apply merge v) + (get v 0))) + +(defn fast-map-merge + [x y] + (reduce-kv + (fn [m k v] + (assoc m k v)) + x + y)) + +#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default +(defn fifo-memoize [f size] + "Returns a memoized version of a referentially transparent f. The + memoized version of the function keeps a cache of the mapping from arguments + to results and, when calls with the same arguments are repeated often, has + higher performance at the expense of higher memory use. FIFO with size entries." + (let [cache (atom (linked/map))] + (fn [& xs] + (or (@cache xs) + (let [value (apply f xs)] + (swap! cache (fn [mem] + (let [mem (assoc mem xs value)] + (if (>= (count mem) size) + (dissoc mem (-> mem first first)) + mem)))) + value))))) +) + +;; NB: when-ns eats all exceptions inside the body, including those about +;; unresolvable symbols. Keep this in mind when debugging the definitions below. + +(defmacro when-ns [ns & body] + `(try + (eval + '(do + (require ~ns) + ~@body)) + (catch Exception ~'_))) + diff --git a/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/core.clj b/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/core.clj new file mode 100644 index 00000000..ffb711ea --- /dev/null +++ b/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/core.clj @@ -0,0 +1,91 @@ +(ns compojure.api.core + (:require [compojure.api.meta :as meta] + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [[compojure.api.async] + [compojure.core :as compojure]]) + [compojure.api.routes #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) routes] + [compojure.api.middleware #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) mw])) + +(defn ring-handler + "Creates vanilla ring-handler from any invokable thing (e.g. compojure-api route)" + [handler] + (fn + ([request] (handler request)) + ([request respond raise] (handler request respond raise)))) + +(defn routes + "Create a Ring handler by combining several handlers into one." + [& handlers] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" (throw (ex-info "Not supported in bb")) + :default (let [handlers (seq (keep identity (flatten handlers)))] + (routes/map->Route + {:childs (vec handlers) + :handler (meta/routing handlers)})))) + +(defmacro defroutes + "Define a Ring handler function from a sequence of routes. + The name may optionally be followed by a doc-string and metadata map." + {:style/indent 1} + [name & routes] + (let [[name routes] (meta/name-with-attributes name routes)] + `(def ~name (routes ~@routes)))) + +(defmacro let-routes + "Takes a vector of bindings and a body of routes. + + Equivalent to: `(let [...] (routes ...))`" + {:style/indent 1} + [bindings & body] + `(let ~bindings (routes ~@body))) + +(defn undocumented + "Routes without route-documentation. Can be used to wrap routes, + not satisfying compojure.api.routes/Routing -protocol." + [& handlers] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" (throw (ex-info "Not supported in bb")) + :default (let [handlers (keep identity handlers)] + (routes/map->Route {:handler (meta/routing handlers)})))) + +(defmacro middleware + "Wraps routes with given middlewares using thread-first macro. + + Note that middlewares will be executed even if routes in body + do not match the request uri. Be careful with middleware that + has side-effects." + {:style/indent 1 + :deprecated "1.1.14" + :superseded-by "route-middleware"} + [middleware & body] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default (when (not= "true" (System/getProperty "compojure.api.core.suppress-middleware-warning")) + (println (str "compojure.api.core.middleware is deprecated because of security issues. " + "Please use route-middleware instead. middleware will be disabled in a future release." + "Set -dcompojure.api.core.suppress-middleware-warning=true to suppress this warning.")))) + `(let [body# (routes ~@body) + wrap-mw# (mw/compose-middleware ~middleware)] + (routes/create nil nil {} [body#] (wrap-mw# body#)))) + +(defn route-middleware + "Wraps routes with given middleware using thread-first macro." + {:style/indent 1 + :supercedes "middleware"} + [middleware & body] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" (throw (ex-info "Not supported in bb")) + :default + (let [handler (apply routes body) + x-handler (compojure/wrap-routes handler (mw/compose-middleware middleware))] + ;; use original handler for docs and wrapped handler for implementation + (routes/map->Route + {:childs [handler] + :handler x-handler})))) + +(defmacro context {:style/indent 2} [& args] (meta/restructure nil args {:context? true :&form &form :&env &env :kondo-rule? #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" true :default false)})) + +(defmacro GET {:style/indent 2} [& args] (meta/restructure :get args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro ANY {:style/indent 2} [& args] (meta/restructure nil args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro HEAD {:style/indent 2} [& args] (meta/restructure :head args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro PATCH {:style/indent 2} [& args] (meta/restructure :patch args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro DELETE {:style/indent 2} [& args] (meta/restructure :delete args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro OPTIONS {:style/indent 2} [& args] (meta/restructure :options args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro POST {:style/indent 2} [& args] (meta/restructure :post args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) +(defmacro PUT {:style/indent 2} [& args] (meta/restructure :put args #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {:kondo-rule? true} :default nil))) diff --git a/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/meta.clj b/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/meta.clj new file mode 100644 index 00000000..ab1ead60 --- /dev/null +++ b/resources/clj-kondo.exports/metosin/compojure-api/compojure/api/meta.clj @@ -0,0 +1,432 @@ +(ns compojure.api.meta + (:require [clojure.walk :as walk] + [compojure.api.common :as common :refer [extract-parameters]] + [compojure.api.middleware #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) mw] + [compojure.api.routes #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) routes] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [compojure-api-kondo-hooks.plumbing.core :as p] + :default [plumbing.core :as p]) + [plumbing.fnk.impl #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) fnk-impl] + [ring.swagger.common #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) rsc] + [ring.swagger.json-schema #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) js] + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [[schema.core :as s] + [schema-tools.core :as st]]) + [compojure.api.coerce #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" :as-alias :default :as) coerce] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [compojure-api-kondo-hooks.compojure.core :as comp-core] + :default [compojure.core :as comp-core]))) + +(defmacro ^:private system-property-check + [& body] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default `(do ~@body))) + +(def +compojure-api-request+ + "lexically bound ring-request for handlers." + '+compojure-api-request+) + +;; https://github.com/clojure/tools.macro/blob/415512648bb51153f380823c41323cda2c13f47f/src/main/clojure/clojure/tools/macro.clj +;; Copyright (c) Rich Hickey. All rights reserved. +;; The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (https://opensource.org/license/epl-1-0/) +;; which can be found in the file epl-v10.html at the root of this distribution. By using this software in any fashion, you are agreeing to +;; be bound bythe terms of this license. You must not remove this notice, or any other, from this software. +(defn name-with-attributes + "To be used in macro definitions. + Handles optional docstrings and attribute maps for a name to be defined + in a list of macro arguments. If the first macro argument is a string, + it is added as a docstring to name and removed from the macro argument + list. If afterwards the first macro argument is a map, its entries are + added to the name's metadata map and the map is removed from the + macro argument list. The return value is a vector containing the name + with its extended metadata map and the list of unprocessed macro + arguments." + [name macro-args] + (let [[docstring macro-args] (if (string? (first macro-args)) + [(first macro-args) (next macro-args)] + [nil macro-args]) + [attr macro-args] (if (map? (first macro-args)) + [(first macro-args) (next macro-args)] + [{} macro-args]) + attr (if docstring + (assoc attr :doc docstring) + attr) + attr (if (meta name) + (conj (meta name) attr) + attr)] + [(with-meta name attr) macro-args])) + +;; +;; Schema +;; + +(defn strict [schema] + (dissoc schema 'schema.core/Keyword)) + +(defn fnk-schema [bind] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" {} + :default (->> + (:input-schema + (fnk-impl/letk-input-schema-and-body-form + nil (with-meta bind {:schema s/Any}) [] nil)) + reverse + (into {})))) + +(defn src-coerce! + "Return source code for coerce! for a schema with coercion type, + extracted from a key in a ring request." + [schema, key, type #_#_:- mw/CoercionType] + (assert (not (#{:query :json} type)) (str type " is DEPRECATED since 0.22.0. Use :body or :string instead.")) + (assert (#{:body :string :response} type)) + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" `(do ~schema ~key ~type ~+compojure-api-request+) + :default `(coerce/coerce! ~schema ~key ~type ~+compojure-api-request+))) + +(defn- convert-return [schema] + {200 {:schema schema + :description (or #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default (js/json-schema-meta schema)) + "")}}) + +;; +;; Extension point +;; + +(defmulti restructure-param + "Restructures a key value pair in smart routes. By default the key + is consumed form the :parameters map in acc. k = given key, v = value." + (fn [k v acc] k)) + +;; +;; Pass-through swagger metadata +;; + +(defmethod restructure-param :summary [k v acc] + (update-in acc [:swagger] assoc k v)) + +(defmethod restructure-param :description [k v acc] + (update-in acc [:swagger] assoc k v)) + +(defmethod restructure-param :operationId [k v acc] + (update-in acc [:swagger] assoc k v)) + +(defmethod restructure-param :consumes [k v acc] + (update-in acc [:swagger] assoc k v)) + +(defmethod restructure-param :produces [k v acc] + (update-in acc [:swagger] assoc k v)) + +;; +;; Smart restructurings +;; + +; Boolean to discard the route out from api documentation +; Example: +; :no-doc true +(defmethod restructure-param :no-doc [_ v acc] + (update-in acc [:swagger] assoc :x-no-doc v)) + +; publishes the data as swagger-parameters without any side-effects / coercion. +; Examples: +; :swagger {:responses {200 {:schema User} +; 404 {:schema Error +; :description "Not Found"} } +; :parameters {:query {:q s/Str} +; :body NewUser}}} +(defmethod restructure-param :swagger [_ swagger acc] + (assoc-in acc [:swagger :swagger] swagger)) + +; Route name, used with path-for +; Example: +; :name :user-route +(defmethod restructure-param :name [_ v acc] + (update-in acc [:swagger] assoc :x-name v)) + +; Tags for api categorization. Ignores duplicates. +; Examples: +; :tags [:admin] +(defmethod restructure-param :tags [_ tags acc] + (update-in acc [:swagger :tags] (comp set into) tags)) + +; Defines a return type and coerces the return value of a body against it. +; Examples: +; :return MySchema +; :return {:value String} +; :return #{{:key (s/maybe Long)}} +(defmethod restructure-param :return [_ schema acc] + (let [response (convert-return schema)] + (-> acc + (update-in [:swagger :responses] (fnil conj []) response) + (update-in [:responses] (fnil conj []) response)))) + +; value is a map of http-response-code -> Schema. Translates to both swagger +; parameters and return schema coercion. Schemas can be decorated with meta-data. +; Examples: +; :responses {403 nil} +; :responses {403 {:schema ErrorEnvelope}} +; :responses {403 {:schema ErrorEnvelope, :description \"Underflow\"}} +(defmethod restructure-param :responses [_ responses acc] + (-> acc + (update-in [:swagger :responses] (fnil conj []) responses) + (update-in [:responses] (fnil conj []) responses))) + +; reads body-params into a enhanced let. First parameter is the let symbol, +; second is the Schema to be coerced! against. +; Examples: +; :body [user User] +(defmethod restructure-param :body [_ [value schema :as bv] acc] + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-body")) + (assert (= 2 (count bv)) + (str ":body should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-body=true")))) + (-> acc + (update-in [:lets] into [value (src-coerce! schema :body-params :body)]) + (assoc-in [:swagger :parameters :body] schema))) + +; reads query-params into a enhanced let. First parameter is the let symbol, +; second is the Schema to be coerced! against. +; Examples: +; :query [user User] +(defmethod restructure-param :query [_ [value schema :as bv] acc] + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-query")) + (assert (= 2 (count bv)) + (str ":query should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-query=true")))) + (-> acc + (update-in [:lets] into [value (src-coerce! schema :query-params :string)]) + (assoc-in [:swagger :parameters :query] schema))) + +; reads header-params into a enhanced let. First parameter is the let symbol, +; second is the Schema to be coerced! against. +; Examples: +; :headers [headers Headers] +(defmethod restructure-param :headers [_ [value schema :as bv] acc] + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-headers")) + (assert (= 2 (count bv)) + (str ":headers should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-headers=true")))) + + (-> acc + (update-in [:lets] into [value (src-coerce! schema :headers :string)]) + (assoc-in [:swagger :parameters :header] schema))) + +; restructures body-params with plumbing letk notation. Example: +; :body-params [id :- Long name :- String] +(defmethod restructure-param :body-params [_ body-params acc] + (let [schema (strict (fnk-schema body-params))] + (-> acc + (update-in [:letks] into [body-params (src-coerce! schema :body-params :body)]) + (assoc-in [:swagger :parameters :body] schema)))) + +; restructures form-params with plumbing letk notation. Example: +; :form-params [id :- Long name :- String] +(defmethod restructure-param :form-params [_ form-params acc] + (let [schema (strict (fnk-schema form-params))] + (-> acc + (update-in [:letks] into [form-params (src-coerce! schema :form-params :string)]) + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [(update-in [:swagger :parameters :formData] st/merge schema)]) + (assoc-in [:swagger :consumes] ["application/x-www-form-urlencoded"])))) + +; restructures multipart-params with plumbing letk notation and consumes "multipart/form-data" +; :multipart-params [file :- compojure.api.upload/TempFileUpload] +(defmethod restructure-param :multipart-params [_ params acc] + (let [schema (strict (fnk-schema params))] + (-> acc + (update-in [:letks] into [params (src-coerce! schema :multipart-params :string)]) + #?@(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" [] + :default [(update-in [:swagger :parameters :formData] st/merge schema)]) + (assoc-in [:swagger :consumes] ["multipart/form-data"])))) + +; restructures header-params with plumbing letk notation. Example: +; :header-params [id :- Long name :- String] +(defmethod restructure-param :header-params [_ header-params acc] + (let [schema (fnk-schema header-params)] + (-> acc + (update-in [:letks] into [header-params (src-coerce! schema :headers :string)]) + (assoc-in [:swagger :parameters :header] schema)))) + +; restructures query-params with plumbing letk notation. Example: +; :query-params [id :- Long name :- String] +(defmethod restructure-param :query-params [_ query-params acc] + (let [schema (fnk-schema query-params)] + (-> acc + (update-in [:letks] into [query-params (src-coerce! schema :query-params :string)]) + (assoc-in [:swagger :parameters :query] schema)))) + +; restructures path-params by plumbing letk notation. Example: +; :path-params [id :- Long name :- String] +(defmethod restructure-param :path-params [_ path-params acc] + (let [schema (fnk-schema path-params)] + (-> acc + (update-in [:letks] into [path-params (src-coerce! schema :route-params :string)]) + (assoc-in [:swagger :parameters :path] schema)))) + +; Applies the given vector of middlewares to the route +(defmethod restructure-param :middleware [_ middleware acc] + (update-in acc [:middleware] into middleware)) + +; Bind to stuff in request components using letk syntax +(defmethod restructure-param :components [_ components acc] + (update-in acc [:letks] into [components `(mw/get-components ~+compojure-api-request+)])) + +; route-specific override for coercers +(defmethod restructure-param :coercion [_ coercion acc] + (update-in acc [:middleware] conj [#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" `mw/wrap-coercion + ;;FIXME why not quoted? + :default mw/wrap-coercion) + coercion])) + +;; +;; Impl +;; + +#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default +(defmacro dummy-let + "Dummy let-macro used in resolving route-docs. not part of normal invocation chain." + [bindings & body] + (let [bind-form (vec (apply concat (for [n (take-nth 2 bindings)] [n nil])))] + `(let ~bind-form ~@body))) +) + +#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default +(defmacro dummy-letk + "Dummy letk-macro used in resolving route-docs. not part of normal invocation chain." + [bindings & body] + (reduce + (fn [cur-body-form [bind-form]] + (if (symbol? bind-form) + `(let [~bind-form nil] ~cur-body-form) + (let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form ;;TODO clj-kondo + &env + (fnk-impl/ensure-schema-metadata &env bind-form) + [] + cur-body-form) + body-form (walk/prewalk-replace {'plumbing.fnk.schema/safe-get 'clojure.core/get} body-form)] + `(let [~map-sym nil] ~body-form)))) + `(do ~@body) + (reverse (partition 2 bindings)))) +) + +#?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" nil + :default +(defn routing [handlers] + (if-let [handlers (seq (keep identity (flatten handlers)))] + (apply comp-core/routes handlers) + (fn ([_] nil) ([_ respond _] (respond nil))))) +) + +;; +;; Api +;; + +(defn- destructure-compojure-api-request + "Returns a vector of four elements: + - pruned path string + - new lets list + - bindings form for compojure route + - symbol to which request will be bound" + [path arg] + (let [path-string (if (vector? path) (first path) path)] + (cond + ;; GET "/route" [] + (vector? arg) [path-string [] (into arg [:as +compojure-api-request+]) +compojure-api-request+] + ;; GET "/route" {:as req} + (map? arg) (if-let [as (:as arg)] + [path-string [+compojure-api-request+ as] arg as] + [path-string [] (merge arg [:as +compojure-api-request+]) +compojure-api-request+]) + ;; GET "/route" req + (symbol? arg) [path-string [+compojure-api-request+ arg] arg arg] + :else (throw + (ex-info + (str "unknown compojure destruction syntax: " arg) + {}))))) + +(defn merge-parameters + "Merge parameters at runtime to allow usage of runtime-parameters with route-macros." + [{:keys [responses swagger] :as parameters}] + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" parameters + :default (cond-> parameters + (seq responses) (assoc :responses (common/merge-vector responses)) + swagger (-> (dissoc :swagger) (rsc/deep-merge swagger))))) + +(defn restructure [method [path arg & args] {:keys [context? kondo-rule?]}] + (let [[options body] (extract-parameters args true) + [path-string lets arg-with-request arg] (destructure-compojure-api-request path arg) + + {:keys [lets + letks + responses + middleware + middlewares + swagger + parameters + body]} (reduce + (fn [acc [k v]] + (restructure-param k v (update-in acc [:parameters] dissoc k))) + {:lets lets + :letks [] + :responses nil + :middleware [] + :swagger {} + :body body + :kondo-rule? kondo-rule?} + options) + + ;; migration helpers + _ (assert (not middlewares) ":middlewares is deprecated with 1.0.0, use :middleware instead.") + _ (assert (not parameters) ":parameters is deprecated with 1.0.0, use :swagger instead.") + + ;; response coercion middleware, why not just code? + middleware (if (seq responses) (conj middleware `[coerce/body-coercer-middleware (common/merge-vector ~responses)]) middleware)] + + #?(:default #_"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj" (do (assert kondo-rule?) + (if context? + ;; context + (let [form `(do ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form `(comp-core/context ~path ~arg-with-request ~form)] + (prn "context" form) + form) + + ;; endpoints + (let [form `(do ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form (comp-core/compile-route method path arg-with-request (list form)) + form `(fn [~'+compojure-api-request+] + ~'+compojure-api-request+ ;;always used + ~form)] + (prn "endpoint" form) + form))) + :default ;; JVM + (if context? + ;; context + (let [form `(comp-core/routes ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form (if (seq middleware) `((mw/compose-middleware ~middleware) ~form) form) + form `(comp-core/context ~path ~arg-with-request ~form) + + ;; create and apply a separate lookup-function to find the inner routes + childs (let [form (vec body) + form (if (seq letks) `(dummy-letk ~letks ~form) form) + form (if (seq lets) `(dummy-let ~lets ~form) form) + form `(comp-core/let-request [~arg-with-request ~'+compojure-api-request+] ~form) + form `(fn [~'+compojure-api-request+] ~form) + form `(~form {})] + form)] + + `(routes/create ~path-string ~method (merge-parameters ~swagger) ~childs ~form)) + + ;; endpoints + (let [form `(do ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form (comp-core/compile-route method path arg-with-request (list form)) + form (if (seq middleware) `(comp-core/wrap-routes ~form (mw/compose-middleware ~middleware)) form)] + + `(routes/create ~path-string ~method (merge-parameters ~swagger) nil ~form)))))) diff --git a/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/compojure/core.clj b/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/compojure/core.clj new file mode 100644 index 00000000..7b95c17b --- /dev/null +++ b/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/compojure/core.clj @@ -0,0 +1,60 @@ +;; Copyright © 2024 James Reeves +;; +;; Distributed under the Eclipse Public License, the same as Clojure. +(ns compojure-api-kondo-hooks.compojure.core) + +(defn- and-binding [req binds] + `(dissoc (:params ~req) ~@(map keyword (keys binds)) ~@(map str (keys binds)))) + +(defn- symbol-binding [req sym] + `(get-in ~req [:params ~(keyword sym)] (get-in ~req [:params ~(str sym)]))) + +(defn- application-binding [req sym func] + `(~func ~(symbol-binding req sym))) + +(defn- vector-bindings [args req] + (loop [args args, binds {}] + (if (seq args) + (let [[x y z] args] + (cond + (= '& x) + (recur (nnext args) (assoc binds y (and-binding req binds))) + (= :as x) + (recur (nnext args) (assoc binds y req)) + (and (symbol? x) (= :<< y) (nnext args)) + (recur (drop 3 args) (assoc binds x (application-binding req x z))) + (symbol? x) + (recur (next args) (assoc binds x (symbol-binding req x))) + :else + (throw (Exception. (str "Unexpected binding: " x))))) + (mapcat identity binds)))) + +(defn- warn-on-*-bindings! [bindings] + (when (and (vector? bindings) (contains? (set bindings) '*)) + (binding [*out* *err*] + (println "WARNING: * should not be used as a route binding.")))) + +(defn- application-symbols [args] + (loop [args args, syms '()] + (if (seq args) + (let [[x y] args] + (if (and (symbol? x) (= :<< y)) + (recur (drop 3 args) (conj syms x)) + (recur (next args) syms))) + (seq syms)))) + +(defmacro ^:no-doc let-request [[bindings request] & body] + (if (vector? bindings) + `(let [~@(vector-bindings bindings request)] + ~(if-let [syms (application-symbols bindings)] + `(if (and ~@(for [s syms] `(not (nil? ~s)))) (do ~@body)) + `(do ~@body))) + `(let [~bindings ~request] ~@body))) + +(defn compile-route + "Compile a route in the form `(method path bindings & body)` into a function. + Used to create custom route macros." + [method path bindings body] + (let [greq (gensym "greq")] + `(fn [~greq] + ~(macroexpand-1 `(let-request [~bindings ~greq] ~@body))))) diff --git a/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/core.clj b/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/core.clj new file mode 100644 index 00000000..5d0189a7 --- /dev/null +++ b/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/core.clj @@ -0,0 +1,42 @@ +(ns compojure-api-kondo-hooks.plumbing.core + "Utility belt for Clojure in the wild" + (:refer-clojure :exclude [update]) + (:require + [compojure-api-kondo-hooks.schema.macros :as schema-macros] + [compojure-api-kondo-hooks.plumbing.fnk.impl :as fnk-impl])) + +(defmacro letk + "Keyword let. Accepts an interleaved sequence of binding forms and map forms like: + (letk [[a {b 2} [:f g h] c d {e 4} :as m & more] a-map ...] & body) + a, c, d, and f are required keywords, and letk will barf if not in a-map. + b and e are optional, and will be bound to default values if not present. + g and h are required keys in the map found under :f. + m will be bound to the entire map (a-map). + more will be bound to all the unbound keys (ie (dissoc a-map :a :b :c :d :e)). + :as and & are both optional, but must be at the end in the specified order if present. + The same symbol cannot be bound multiple times within the same destructing level. + + Optional values can reference symbols bound earlier within the same binding, i.e., + (= [2 2] (let [a 1] (letk [[a {b a}] {:a 2}] [a b]))) but + (= [2 1] (let [a 1] (letk [[{b a} a] {:a 2}] [a b]))) + + If present, :as and :& symbols are bound before other symbols within the binding. + + Namespaced keys are supported by specifying fully-qualified key in binding form. The bound + symbol uses the _name_ portion of the namespaced key, i.e, + (= 1 (letk [[a/b] {:a/b 1}] b)). + + Map destructuring bindings can be mixed with ordinary symbol bindings." + [bindings & body] + (reduce + (fn [cur-body-form [bind-form value-form]] + (if (symbol? bind-form) + `(let [~bind-form ~value-form] ~cur-body-form) + (let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form + &env + bind-form ;(fnk-impl/ensure-schema-metadata &env bind-form) + [] + cur-body-form)] + `(let [~map-sym ~value-form] ~body-form)))) + `(do ~@body) + (reverse (partition 2 bindings)))) diff --git a/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk/impl.clj b/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk/impl.clj new file mode 100644 index 00000000..5e1ad75f --- /dev/null +++ b/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk/impl.clj @@ -0,0 +1,116 @@ +(ns compojure-api-kondo-hooks.plumbing.fnk.impl + (:require + [clojure.set :as set] + [schema.core :as-alias s] + [compojure-api-kondo-hooks.schema.macros :as schema-macros])) + +;;;;; Helpers + +(defn name-sym + "Returns symbol of x's name. + Converts a keyword/string to symbol, or removes namespace (if any) of symbol" + [x] + (with-meta (symbol (name x)) (meta x))) + +;;; Parsing new fnk binding style + +(declare letk-input-schema-and-body-form) + +(defn- process-schematized-map + "Take an optional binding map like {a 2} or {a :- Number 2} and convert the schema + information to canonical metadata, if present." + [env binding] + (case (count binding) + 1 (let [[sym v] (first binding)] + {sym v}) + + 2 (let [[[[sym _]] [[schema v]]] ((juxt filter remove) #(= (val %) :-) binding)] + {sym v}))) + +;; TODO: unify this with positional version. +(defn letk-arg-bind-sym-and-body-form + "Given a single element of a single letk binding form and a current body form, return + a map {:schema-entry :body-form} where schema-entry is a tuple + [bound-key schema external-schema?], and body-form wraps body with destructuring + for this binding as necessary." + [env map-sym binding key-path body-form] + (cond (symbol? binding) + {:schema-entry [] + :body-form `(let [~(name-sym binding) (get ~map-sym ~(keyword binding) ~key-path)] + ~body-form)} + + (map? binding) + (let [schema-fixed-binding (process-schematized-map env binding) + [bound-sym opt-val-expr] (first schema-fixed-binding) + bound-key (keyword bound-sym)] + {:schema-entry [] + :body-form `(let [~(name-sym bound-sym) (get ~map-sym ~bound-key ~opt-val-expr)] + ~body-form)}) + + (vector? binding) + (let [[bound-key & more] binding + {inner-input-schema :input-schema + inner-external-input-schema :external-input-schema + inner-map-sym :map-sym + inner-body-form :body-form} (letk-input-schema-and-body-form + env + (with-meta (vec more) (meta binding)) + (conj key-path bound-key) + body-form)] + {:schema-entry [] + :body-form `(let [~inner-map-sym (get ~map-sym ~bound-key ~key-path)] + ~inner-body-form)}) + + :else (throw (ex-info (format "bad binding: %s" binding) {})))) + +(defn- extract-special-args + "Extract trailing & sym and :as sym, possibly with schema metadata. Returns + [more-bindings special-args-map] where special-args-map is a map from each + special symbol found to the symbol that was found." + [env special-arg-signifier-set binding-form] + {:pre [(set? special-arg-signifier-set)]} + (let [[more-bindings special-bindings] (split-with (complement special-arg-signifier-set) binding-form)] + (loop [special-args-map {} + special-arg-set special-arg-signifier-set + [arg-signifier & other-bindings :as special-bindings] special-bindings] + (if-not (seq special-bindings) + [more-bindings special-args-map] + (do + (let [[sym remaining-bindings] (schema-macros/extract-arrow-schematized-element env other-bindings)] + (recur (assoc special-args-map arg-signifier sym) + (disj special-arg-set arg-signifier) + remaining-bindings))))))) + +(defn letk-input-schema-and-body-form + "Given a single letk binding form, value form, key path, and body + form, return a map {:input-schema :external-input-schema :map-sym :body-form} + where input-schema is the schema imposed by binding-form, external-input-schema + is like input-schema but includes user overrides for binding vectors, + map-sym is the symbol which it expects the bound value to be bound to, + and body-form wraps body in the bindings from binding-form from map-sym." + [env binding-form key-path body-form] + (let [[bindings {more-sym '& as-sym :as}] (extract-special-args env #{'& :as} binding-form) + as-sym (or as-sym (gensym "map")) + [input-schema-elts + external-input-schema-elts + bound-body-form] (reduce + (fn [[input-schema-elts external-input-schema-elts cur-body] binding] + (let [{:keys [schema-entry body-form]} + (letk-arg-bind-sym-and-body-form + env as-sym binding key-path cur-body) + [bound-key input-schema external-input-schema] schema-entry] + [(conj input-schema-elts [bound-key input-schema]) + (conj external-input-schema-elts + [bound-key (or external-input-schema input-schema)]) + body-form])) + [[] [] body-form] + (reverse + (schema-macros/process-arrow-schematized-args + env bindings))) + explicit-schema-keys [] + final-body-form (if more-sym + `(let [~more-sym (dissoc ~as-sym ~@explicit-schema-keys)] + ~bound-body-form) + bound-body-form)] + {:map-sym as-sym + :body-form final-body-form})) diff --git a/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/schema/macros.clj b/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/schema/macros.clj new file mode 100644 index 00000000..aefe1448 --- /dev/null +++ b/resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/schema/macros.clj @@ -0,0 +1,24 @@ +(ns compojure-api-kondo-hooks.schema.macros) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Helpers for processing and normalizing element/argument schemas in s/defrecord and s/(de)fn + +(defn extract-arrow-schematized-element + "Take a nonempty seq, which may start like [a ...] or [a :- schema ...], and return + a list of [first-element-with-schema-attached rest-elements]" + [env s] + (assert (seq s)) + (let [[f & more] s] + (if (= :- (first more)) + [f (drop 2 more)] + [f more]))) + +(defn process-arrow-schematized-args + "Take an arg vector, in which each argument is followed by an optional :- schema, + and transform into an ordinary arg vector where the schemas are metadata on the args." + [env args] + (loop [in args out []] + (if (empty? in) + out + (let [[arg more] (extract-arrow-schematized-element env in)] + (recur more (conj out arg)))))) diff --git a/resources/clj-kondo.exports/metosin/compojure-api/config.edn b/resources/clj-kondo.exports/metosin/compojure-api/config.edn new file mode 100644 index 00000000..d0cb4934 --- /dev/null +++ b/resources/clj-kondo.exports/metosin/compojure-api/config.edn @@ -0,0 +1 @@ +{:hooks {:macroexpand {compojure.api.sweet/PATCH compojure.api.core/PATCH, compojure.api.sweet/ANY compojure.api.core/ANY, compojure.api.sweet/context compojure.api.core/context, compojure.api.core/POST compojure.api.core/POST, compojure.api.core/GET compojure.api.core/GET, compojure.api.sweet/DELETE compojure.api.core/DELETE, compojure.api.core/ANY compojure.api.core/ANY, compojure.api.sweet/POST compojure.api.core/POST, compojure.api.sweet/GET compojure.api.core/GET, compojure.api.sweet/HEAD compojure.api.core/HEAD, compojure.api.core/PUT compojure.api.core/PUT, compojure.api.core/DELETE compojure.api.core/DELETE, compojure.api.sweet/OPTIONS compojure.api.core/OPTIONS, compojure.api.sweet/PUT compojure.api.core/PUT, compojure.api.core/context compojure.api.core/context, compojure.api.core/HEAD compojure.api.core/HEAD, compojure.api.core/PATCH compojure.api.core/PATCH, compojure.api.core/OPTIONS compojure.api.core/OPTIONS}}} \ No newline at end of file diff --git a/scripts/regen-kondo.clj b/scripts/regen-kondo.clj new file mode 100755 index 00000000..a5300986 --- /dev/null +++ b/scripts/regen-kondo.clj @@ -0,0 +1,11 @@ +#!/bin/bash + +set -ex + +rm -r resources/clj-kondo.exports +mkdir -p resources/clj-kondo.exports/metosin/compojure-api/compojure/api +mkdir -p resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/compojure +mkdir -p resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk +mkdir -p resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/schema + +bb -f ./scripts/regen_kondo_config.clj diff --git a/scripts/regen_kondo_config.clj b/scripts/regen_kondo_config.clj new file mode 100755 index 00000000..65bb0477 --- /dev/null +++ b/scripts/regen_kondo_config.clj @@ -0,0 +1,36 @@ +#!/usr/bin/env bb + +(ns regen-kondo-config + (:require [clojure.string :as str])) + +(def renames + ;; rename to .clj + {"src/compojure/api/common.cljc" "resources/clj-kondo.exports/metosin/compojure-api/compojure/api/common.clj" + "src/compojure/api/core.cljc" "resources/clj-kondo.exports/metosin/compojure-api/compojure/api/core.clj" + "src/compojure/api/meta.cljc" "resources/clj-kondo.exports/metosin/compojure-api/compojure/api/meta.clj" + "dev/compojure_api_kondo_hooks/compojure/core.clj" "resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/compojure/core.clj" + "dev/compojure_api_kondo_hooks/plumbing/core.clj" "resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/core.clj" + "dev/compojure_api_kondo_hooks/schema/macros.clj" "resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/schema/macros.clj" + "dev/compojure_api_kondo_hooks/plumbing/fnk/impl.clj" "resources/clj-kondo.exports/metosin/compojure-api/compojure_api_kondo_hooks/plumbing/fnk/impl.clj" + }) + +(def restructured-macro-names + '#{context GET ANY HEAD PATCH DELETE OPTIONS POST PUT}) + +(defn -main [& args] + (doseq [[from to] renames] + (spit to + (str/replace (slurp from) ":clj-kondo" ":default #_\"the redundant :default is intentional, see ./scripts/regen_kondo_config.clj\""))) + (spit "resources/clj-kondo.exports/metosin/compojure-api/config.edn" + {:hooks + {:macroexpand + (reduce + (fn [m n] + (let [core-macro (symbol "compojure.api.core" (name n)) + sweet-macro (symbol "compojure.api.sweet" (name n))] + (-> m + (assoc core-macro core-macro + sweet-macro core-macro)))) + {} restructured-macro-names)}})) + +(when (= *file* (System/getProperty "babashka.file")) (-main)) diff --git a/src/compojure/api/common.clj b/src/compojure/api/common.cljc similarity index 95% rename from src/compojure/api/common.clj rename to src/compojure/api/common.cljc index 73f64f94..0e57af50 100644 --- a/src/compojure/api/common.clj +++ b/src/compojure/api/common.cljc @@ -1,5 +1,6 @@ (ns compojure.api.common - (:require [linked.core :as linked])) + #?@(:clj-kondo [] + :default [(:require [linked.core :as linked])])) (defn plain-map? "checks whether input is a map, but not a record" @@ -51,6 +52,8 @@ x y)) +#?(:clj-kondo nil + :default (defn fifo-memoize [f size] "Returns a memoized version of a referentially transparent f. The memoized version of the function keeps a cache of the mapping from arguments @@ -66,6 +69,7 @@ (dissoc mem (-> mem first first)) mem)))) value))))) +) ;; NB: when-ns eats all exceptions inside the body, including those about ;; unresolvable symbols. Keep this in mind when debugging the definitions below. diff --git a/src/compojure/api/core.cljc b/src/compojure/api/core.cljc index 07b22a14..6cfcc324 100644 --- a/src/compojure/api/core.cljc +++ b/src/compojure/api/core.cljc @@ -1,10 +1,10 @@ (ns compojure.api.core (:require [compojure.api.meta :as meta] - [compojure.api.async] - [compojure.api.routes :as routes] - [compojure.api.middleware :as mw] - [compojure.core :as compojure] - [clojure.tools.macro :as macro])) + #?@(:clj-kondo [] + :default [[compojure.api.async] + [compojure.core :as compojure]]) + [compojure.api.routes #?(:clj-kondo :as-alias :default :as) routes] + [compojure.api.middleware #?(:clj-kondo :as-alias :default :as) mw])) (defn ring-handler "Creates vanilla ring-handler from any invokable thing (e.g. compojure-api route)" @@ -16,17 +16,18 @@ (defn routes "Create a Ring handler by combining several handlers into one." [& handlers] - (let [handlers (seq (keep identity (flatten handlers)))] - (routes/map->Route - {:childs (vec handlers) - :handler (meta/routing handlers)}))) + #?(:clj-kondo (throw (ex-info "Not supported in bb")) + :default (let [handlers (seq (keep identity (flatten handlers)))] + (routes/map->Route + {:childs (vec handlers) + :handler (meta/routing handlers)})))) (defmacro defroutes "Define a Ring handler function from a sequence of routes. The name may optionally be followed by a doc-string and metadata map." {:style/indent 1} [name & routes] - (let [[name routes] (macro/name-with-attributes name routes)] + (let [[name routes] (meta/name-with-attributes name routes)] `(def ~name (routes ~@routes)))) (defmacro let-routes @@ -41,8 +42,9 @@ "Routes without route-documentation. Can be used to wrap routes, not satisfying compojure.api.routes/Routing -protocol." [& handlers] - (let [handlers (keep identity handlers)] - (routes/map->Route {:handler (meta/routing handlers)}))) + #?(:clj-kondo (throw (ex-info "Not supported in bb")) + :default (let [handlers (keep identity handlers)] + (routes/map->Route {:handler (meta/routing handlers)})))) (defmacro middleware "Wraps routes with given middlewares using thread-first macro. @@ -54,10 +56,11 @@ :deprecated "1.1.14" :superseded-by "route-middleware"} [middleware & body] - (when (not= "true" (System/getProperty "compojure.api.core.suppress-middleware-warning")) - (println (str "compojure.api.core.middleware is deprecated because of security issues. " - "Please use route-middleware instead. middleware will be disabled in a future release." - "Set -dcompojure.api.core.suppress-middleware-warning=true to suppress this warning."))) + #?(:clj-kondo nil + :default (when (not= "true" (System/getProperty "compojure.api.core.suppress-middleware-warning")) + (println (str "compojure.api.core.middleware is deprecated because of security issues. " + "Please use route-middleware instead. middleware will be disabled in a future release." + "Set -dcompojure.api.core.suppress-middleware-warning=true to suppress this warning.")))) `(let [body# (routes ~@body) wrap-mw# (mw/compose-middleware ~middleware)] (routes/create nil nil {} [body#] (wrap-mw# body#)))) @@ -67,20 +70,22 @@ {:style/indent 1 :supercedes "middleware"} [middleware & body] - (let [handler (apply routes body) - x-handler (compojure/wrap-routes handler (mw/compose-middleware middleware))] - ;; use original handler for docs and wrapped handler for implementation - (routes/map->Route - {:childs [handler] - :handler x-handler}))) + #?(:clj-kondo (throw (ex-info "Not supported in bb")) + :default + (let [handler (apply routes body) + x-handler (compojure/wrap-routes handler (mw/compose-middleware middleware))] + ;; use original handler for docs and wrapped handler for implementation + (routes/map->Route + {:childs [handler] + :handler x-handler})))) -(defmacro context {:style/indent 2} [& args] (meta/restructure nil args {:context? true :&form &form :&env &env})) +(defmacro context {:style/indent 2} [& args] (meta/restructure nil args {:context? true :&form &form :&env &env :kondo-rule? #?(:clj-kondo true :default false)})) -(defmacro GET {:style/indent 2} [& args] (meta/restructure :get args nil)) -(defmacro ANY {:style/indent 2} [& args] (meta/restructure nil args nil)) -(defmacro HEAD {:style/indent 2} [& args] (meta/restructure :head args nil)) -(defmacro PATCH {:style/indent 2} [& args] (meta/restructure :patch args nil)) -(defmacro DELETE {:style/indent 2} [& args] (meta/restructure :delete args nil)) -(defmacro OPTIONS {:style/indent 2} [& args] (meta/restructure :options args nil)) -(defmacro POST {:style/indent 2} [& args] (meta/restructure :post args nil)) -(defmacro PUT {:style/indent 2} [& args] (meta/restructure :put args nil)) +(defmacro GET {:style/indent 2} [& args] (meta/restructure :get args #?(:clj-kondo {:kondo-rule? true} :default nil))) +(defmacro ANY {:style/indent 2} [& args] (meta/restructure nil args #?(:clj-kondo {:kondo-rule? true} :default nil))) +(defmacro HEAD {:style/indent 2} [& args] (meta/restructure :head args #?(:clj-kondo {:kondo-rule? true} :default nil))) +(defmacro PATCH {:style/indent 2} [& args] (meta/restructure :patch args #?(:clj-kondo {:kondo-rule? true} :default nil))) +(defmacro DELETE {:style/indent 2} [& args] (meta/restructure :delete args #?(:clj-kondo {:kondo-rule? true} :default nil))) +(defmacro OPTIONS {:style/indent 2} [& args] (meta/restructure :options args #?(:clj-kondo {:kondo-rule? true} :default nil))) +(defmacro POST {:style/indent 2} [& args] (meta/restructure :post args #?(:clj-kondo {:kondo-rule? true} :default nil))) +(defmacro PUT {:style/indent 2} [& args] (meta/restructure :put args #?(:clj-kondo {:kondo-rule? true} :default nil))) diff --git a/src/compojure/api/meta.cljc b/src/compojure/api/meta.cljc index 0702121d..712a576f 100644 --- a/src/compojure/api/meta.cljc +++ b/src/compojure/api/meta.cljc @@ -1,21 +1,59 @@ (ns compojure.api.meta (:require [clojure.walk :as walk] [compojure.api.common :as common :refer [extract-parameters]] - [compojure.api.middleware :as mw] - [compojure.api.routes :as routes] - [plumbing.core :as p] - [plumbing.fnk.impl :as fnk-impl] - [ring.swagger.common :as rsc] - [ring.swagger.json-schema :as js] - [schema.core :as s] - [schema-tools.core :as st] - [compojure.api.coerce :as coerce] - compojure.core)) + [compojure.api.middleware #?(:clj-kondo :as-alias :default :as) mw] + [compojure.api.routes #?(:clj-kondo :as-alias :default :as) routes] + #?(:clj-kondo [compojure-api-kondo-hooks.plumbing.core :as p] + :default [plumbing.core :as p]) + [plumbing.fnk.impl #?(:clj-kondo :as-alias :default :as) fnk-impl] + [ring.swagger.common #?(:clj-kondo :as-alias :default :as) rsc] + [ring.swagger.json-schema #?(:clj-kondo :as-alias :default :as) js] + #?@(:clj-kondo [] + :default [[schema.core :as s] + [schema-tools.core :as st]]) + [compojure.api.coerce #?(:clj-kondo :as-alias :default :as) coerce] + #?(:clj-kondo [compojure-api-kondo-hooks.compojure.core :as comp-core] + :default [compojure.core :as comp-core]))) + +(defmacro ^:private system-property-check + [& body] + #?(:clj-kondo nil + :default `(do ~@body))) (def +compojure-api-request+ "lexically bound ring-request for handlers." '+compojure-api-request+) +;; https://github.com/clojure/tools.macro/blob/415512648bb51153f380823c41323cda2c13f47f/src/main/clojure/clojure/tools/macro.clj +;; Copyright (c) Rich Hickey. All rights reserved. +;; The use and distribution terms for this software are covered by the Eclipse Public License 1.0 (https://opensource.org/license/epl-1-0/) +;; which can be found in the file epl-v10.html at the root of this distribution. By using this software in any fashion, you are agreeing to +;; be bound bythe terms of this license. You must not remove this notice, or any other, from this software. +(defn name-with-attributes + "To be used in macro definitions. + Handles optional docstrings and attribute maps for a name to be defined + in a list of macro arguments. If the first macro argument is a string, + it is added as a docstring to name and removed from the macro argument + list. If afterwards the first macro argument is a map, its entries are + added to the name's metadata map and the map is removed from the + macro argument list. The return value is a vector containing the name + with its extended metadata map and the list of unprocessed macro + arguments." + [name macro-args] + (let [[docstring macro-args] (if (string? (first macro-args)) + [(first macro-args) (next macro-args)] + [nil macro-args]) + [attr macro-args] (if (map? (first macro-args)) + [(first macro-args) (next macro-args)] + [{} macro-args]) + attr (if docstring + (assoc attr :doc docstring) + attr) + attr (if (meta name) + (conj (meta name) attr) + attr)] + [(with-meta name attr) macro-args])) + ;; ;; Schema ;; @@ -24,23 +62,28 @@ (dissoc schema 'schema.core/Keyword)) (defn fnk-schema [bind] - (->> - (:input-schema - (fnk-impl/letk-input-schema-and-body-form - nil (with-meta bind {:schema s/Any}) [] nil)) - reverse - (into {}))) - -(s/defn src-coerce! + #?(:clj-kondo {} + :default (->> + (:input-schema + (fnk-impl/letk-input-schema-and-body-form + nil (with-meta bind {:schema s/Any}) [] nil)) + reverse + (into {})))) + +(defn src-coerce! "Return source code for coerce! for a schema with coercion type, extracted from a key in a ring request." - [schema, key, type :- mw/CoercionType] + [schema, key, type #_#_:- mw/CoercionType] (assert (not (#{:query :json} type)) (str type " is DEPRECATED since 0.22.0. Use :body or :string instead.")) - `(coerce/coerce! ~schema ~key ~type ~+compojure-api-request+)) + (assert (#{:body :string :response} type)) + #?(:clj-kondo `(do ~schema ~key ~type ~+compojure-api-request+) + :default `(coerce/coerce! ~schema ~key ~type ~+compojure-api-request+))) (defn- convert-return [schema] {200 {:schema schema - :description (or (js/json-schema-meta schema) "")}}) + :description (or #?(:clj-kondo nil + :default (js/json-schema-meta schema)) + "")}}) ;; ;; Extension point @@ -129,10 +172,11 @@ ; Examples: ; :body [user User] (defmethod restructure-param :body [_ [value schema :as bv] acc] - (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-body")) - (assert (= 2 (count bv)) - (str ":body should be [sym schema], provided: " bv - "\nDisable this check with -Dcompojure.api.meta.allow-bad-body=true"))) + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-body")) + (assert (= 2 (count bv)) + (str ":body should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-body=true")))) (-> acc (update-in [:lets] into [value (src-coerce! schema :body-params :body)]) (assoc-in [:swagger :parameters :body] schema))) @@ -142,10 +186,11 @@ ; Examples: ; :query [user User] (defmethod restructure-param :query [_ [value schema :as bv] acc] - (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-query")) - (assert (= 2 (count bv)) - (str ":query should be [sym schema], provided: " bv - "\nDisable this check with -Dcompojure.api.meta.allow-bad-query=true"))) + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-query")) + (assert (= 2 (count bv)) + (str ":query should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-query=true")))) (-> acc (update-in [:lets] into [value (src-coerce! schema :query-params :string)]) (assoc-in [:swagger :parameters :query] schema))) @@ -155,10 +200,11 @@ ; Examples: ; :headers [headers Headers] (defmethod restructure-param :headers [_ [value schema :as bv] acc] - (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-headers")) - (assert (= 2 (count bv)) - (str ":headers should be [sym schema], provided: " bv - "\nDisable this check with -Dcompojure.api.meta.allow-bad-headers=true"))) + (system-property-check + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-headers")) + (assert (= 2 (count bv)) + (str ":headers should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-headers=true")))) (-> acc (update-in [:lets] into [value (src-coerce! schema :headers :string)]) @@ -178,7 +224,8 @@ (let [schema (strict (fnk-schema form-params))] (-> acc (update-in [:letks] into [form-params (src-coerce! schema :form-params :string)]) - (update-in [:swagger :parameters :formData] st/merge schema) + #?@(:clj-kondo [] + :default [(update-in [:swagger :parameters :formData] st/merge schema)]) (assoc-in [:swagger :consumes] ["application/x-www-form-urlencoded"])))) ; restructures multipart-params with plumbing letk notation and consumes "multipart/form-data" @@ -187,7 +234,8 @@ (let [schema (strict (fnk-schema params))] (-> acc (update-in [:letks] into [params (src-coerce! schema :multipart-params :string)]) - (update-in [:swagger :parameters :formData] st/merge schema) + #?@(:clj-kondo [] + :default [(update-in [:swagger :parameters :formData] st/merge schema)]) (assoc-in [:swagger :consumes] ["multipart/form-data"])))) ; restructures header-params with plumbing letk notation. Example: @@ -224,26 +272,34 @@ ; route-specific override for coercers (defmethod restructure-param :coercion [_ coercion acc] - (update-in acc [:middleware] conj [mw/wrap-coercion coercion])) + (update-in acc [:middleware] conj [#?(:clj-kondo `mw/wrap-coercion + ;;FIXME why not quoted? + :default mw/wrap-coercion) + coercion])) ;; ;; Impl ;; +#?(:clj-kondo nil + :default (defmacro dummy-let - "Dummy let-macro used in resolving route-docs. not part of normal invokation chain." + "Dummy let-macro used in resolving route-docs. not part of normal invocation chain." [bindings & body] (let [bind-form (vec (apply concat (for [n (take-nth 2 bindings)] [n nil])))] `(let ~bind-form ~@body))) +) +#?(:clj-kondo nil + :default (defmacro dummy-letk - "Dummy letk-macro used in resolving route-docs. not part of normal invokation chain." + "Dummy letk-macro used in resolving route-docs. not part of normal invocation chain." [bindings & body] (reduce (fn [cur-body-form [bind-form]] (if (symbol? bind-form) `(let [~bind-form nil] ~cur-body-form) - (let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form + (let [{:keys [map-sym body-form]} (fnk-impl/letk-input-schema-and-body-form ;;TODO clj-kondo &env (fnk-impl/ensure-schema-metadata &env bind-form) [] @@ -252,11 +308,15 @@ `(let [~map-sym nil] ~body-form)))) `(do ~@body) (reverse (partition 2 bindings)))) +) +#?(:clj-kondo nil + :default (defn routing [handlers] (if-let [handlers (seq (keep identity (flatten handlers)))] - (apply compojure.core/routes handlers) + (apply comp-core/routes handlers) (fn ([_] nil) ([_ respond _] (respond nil))))) +) ;; ;; Api @@ -280,17 +340,19 @@ ;; GET "/route" req (symbol? arg) [path-string [+compojure-api-request+ arg] arg arg] :else (throw - (RuntimeException. - (str "unknown compojure destruction syntax: " arg)))))) + (ex-info + (str "unknown compojure destruction syntax: " arg) + {}))))) (defn merge-parameters - "Merge parameters at runtime to allow usage of runtime-paramers with route-macros." + "Merge parameters at runtime to allow usage of runtime-parameters with route-macros." [{:keys [responses swagger] :as parameters}] - (cond-> parameters - (seq responses) (assoc :responses (common/merge-vector responses)) - swagger (-> (dissoc :swagger) (rsc/deep-merge swagger)))) + #?(:clj-kondo parameters + :default (cond-> parameters + (seq responses) (assoc :responses (common/merge-vector responses)) + swagger (-> (dissoc :swagger) (rsc/deep-merge swagger))))) -(defn restructure [method [path arg & args] {:keys [context?]}] +(defn restructure [method [path arg & args] {:keys [context? kondo-rule?]}] (let [[options body] (extract-parameters args true) [path-string lets arg-with-request arg] (destructure-compojure-api-request path arg) @@ -309,7 +371,8 @@ :responses nil :middleware [] :swagger {} - :body body} + :body body + :kondo-rule? kondo-rule?} options) ;; migration helpers @@ -319,20 +382,40 @@ ;; response coercion middleware, why not just code? middleware (if (seq responses) (conj middleware `[coerce/body-coercer-middleware (common/merge-vector ~responses)]) middleware)] + #?(:clj-kondo (do (assert kondo-rule?) + (if context? + ;; context + (let [form `(do ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form `(comp-core/context ~path ~arg-with-request ~form)] + (prn "context" form) + form) + + ;; endpoints + (let [form `(do ~@body) + form (if (seq letks) `(p/letk ~letks ~form) form) + form (if (seq lets) `(let ~lets ~form) form) + form (comp-core/compile-route method path arg-with-request (list form)) + form `(fn [~'+compojure-api-request+] + ~'+compojure-api-request+ ;;always used + ~form)] + (prn "endpoint" form) + form))) + :default ;; JVM (if context? - ;; context - (let [form `(compojure.core/routes ~@body) + (let [form `(comp-core/routes ~@body) form (if (seq letks) `(p/letk ~letks ~form) form) form (if (seq lets) `(let ~lets ~form) form) form (if (seq middleware) `((mw/compose-middleware ~middleware) ~form) form) - form `(compojure.core/context ~path ~arg-with-request ~form) + form `(comp-core/context ~path ~arg-with-request ~form) ;; create and apply a separate lookup-function to find the inner routes childs (let [form (vec body) form (if (seq letks) `(dummy-letk ~letks ~form) form) form (if (seq lets) `(dummy-let ~lets ~form) form) - form `(compojure.core/let-request [~arg-with-request ~'+compojure-api-request+] ~form) + form `(comp-core/let-request [~arg-with-request ~'+compojure-api-request+] ~form) form `(fn [~'+compojure-api-request+] ~form) form `(~form {})] form)] @@ -343,7 +426,7 @@ (let [form `(do ~@body) form (if (seq letks) `(p/letk ~letks ~form) form) form (if (seq lets) `(let ~lets ~form) form) - form (compojure.core/compile-route method path arg-with-request (list form)) - form (if (seq middleware) `(compojure.core/wrap-routes ~form (mw/compose-middleware ~middleware)) form)] + form (comp-core/compile-route method path arg-with-request (list form)) + form (if (seq middleware) `(comp-core/wrap-routes ~form (mw/compose-middleware ~middleware)) form)] - `(routes/create ~path-string ~method (merge-parameters ~swagger) nil ~form))))) + `(routes/create ~path-string ~method (merge-parameters ~swagger) nil ~form))))))