├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── deps.edn ├── project.clj ├── src └── hicgql │ └── core.cljc └── test └── hicgql └── core_test.cljc /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Setup Clojure 15 | uses: DeLaGuardo/setup-clojure@5.1 16 | with: 17 | cli: 1.10.3.929 18 | lein: 2.9.1 19 | - name: Run ClojureScript tests 20 | run: clojure -Mtest-cljs 21 | - name: Run Clojure tests 22 | run: clojure -Mtest-clj 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 5 | CONSTITUTES RECIPIENT’S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and documentation 12 | distributed under this Agreement, and 13 | b) in the case of each subsequent Contributor: 14 | 15 | i) changes to the Program, and 16 | 17 | ii) additions to the Program; 18 | 19 | where such changes and/or additions to the Program originate from and are 20 | distributed by that particular Contributor. A Contribution 'originates' from a 21 | Contributor if it was added to the Program by such Contributor itself or anyone 22 | acting on such Contributor’s behalf. Contributions do not include additions to 23 | the Program which: (i) are separate modules of software distributed in 24 | conjunction with the Program under their own license agreement, and (ii) are not 25 | derivative works of the Program. 26 | 27 | "Contributor" means any person or entity that distributes the Program. 28 | 29 | "Licensed Patents " mean patent claims licensable by a Contributor which are 30 | necessarily infringed by the use or sale of its Contribution alone or when 31 | combined with the Program. 32 | 33 | "Program" means the Contributions distributed in accordance with this Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and such 44 | derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed 48 | Patents to make, use, sell, offer to sell, import and otherwise transfer the 49 | Contribution of such Contributor, if any, in source code and object code form. 50 | This patent license shall apply to the combination of the Contribution and the 51 | Program if, at the time the Contribution is added by the Contributor, such 52 | addition of the Contribution causes such combination to be covered by the 53 | Licensed Patents. The patent license shall not apply to any other combinations 54 | which include the Contribution. No hardware per se is licensed hereunder. 55 | 56 | c) Recipient understands that although each Contributor grants the licenses to 57 | its Contributions set forth herein, no assurances are provided by any 58 | Contributor that the Program does not infringe the patent or other intellectual 59 | property rights of any other entity. Each Contributor disclaims any liability to 60 | Recipient for claims brought by any other entity based on infringement of 61 | intellectual property rights or otherwise. As a condition to exercising the 62 | rights and licenses granted hereunder, each Recipient hereby assumes sole 63 | responsibility to secure any other intellectual property rights needed, if any. 64 | For example, if a third party patent license is required to allow Recipient to 65 | distribute the Program, it is Recipient’s responsibility to acquire that license 66 | before distributing the Program. 67 | 68 | d) Each Contributor represents that to its knowledge it has sufficient copyright 69 | rights in its Contribution, if any, to grant the copyright license set forth in 70 | this Agreement. 71 | 72 | 3. REQUIREMENTS 73 | 74 | A Contributor may choose to distribute the Program in object code form under its 75 | own license agreement, provided that: 76 | 77 | a) it complies with the terms and conditions of this Agreement; and 78 | 79 | b) its license agreement: 80 | 81 | i) effectively disclaims on behalf of all Contributors all warranties and 82 | conditions, express and implied, including warranties or conditions of title and 83 | non-infringement, and implied warranties or conditions of merchantability and 84 | fitness for a particular purpose; 85 | 86 | ii) effectively excludes on behalf of all Contributors all liability for 87 | damages, including direct, indirect, special, incidental and consequential 88 | damages, such as lost profits; 89 | 90 | iii) states that any provisions which differ from this Agreement are offered by 91 | that Contributor alone and not by any other party; and 92 | 93 | iv) states that source code for the Program is available from such Contributor, 94 | and informs licensees how to obtain it in a reasonable manner on or through a 95 | medium customarily used for software exchange. 96 | 97 | When the Program is made available in source code form: 98 | 99 | a) it must be made available under this Agreement; and 100 | b) a copy of this Agreement must be included with each copy of the Program. 101 | 102 | Contributors may not remove or alter any copyright notices contained within the 103 | Program. 104 | 105 | Each Contributor must identify itself as the originator of its Contribution, if 106 | any, in a manner that reasonably allows subsequent Recipients to identify the 107 | originator of the Contribution. 108 | 109 | 4. COMMERCIAL DISTRIBUTION 110 | 111 | Commercial distributors of software may accept certain responsibilities with 112 | respect to end users, business partners and the like. While this license is 113 | intended to facilitate the commercial use of the Program, the Contributor who 114 | includes the Program in a commercial product offering should do so in a manner 115 | which does not create potential liability for other Contributors. Therefore, if 116 | a Contributor includes the Program in a commercial product offering, such 117 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify 118 | every other Contributor ("Indemnified Contributor") against any losses, damages 119 | and costs (collectively "Losses") arising from claims, lawsuits and other legal 120 | actions brought by a third party against the Indemnified Contributor to the 121 | extent caused by the acts or omissions of such Commercial Contributor in 122 | connection with its distribution of the Program in a commercial product 123 | offering. The obligations in this section do not apply to any claims or Losses 124 | relating to any actual or alleged intellectual property infringement. In order 125 | to qualify, an Indemnified Contributor must: a) promptly notify the Commercial 126 | Contributor in writing of such claim, and b) allow the Commercial Contributor to 127 | control, and cooperate with the Commercial Contributor in, the defense and any 128 | related settlement negotiations. The Indemnified Contributor may participate in 129 | any such claim at its own expense. 130 | 131 | For example, a Contributor might include the Program in a commercial product 132 | offering, Product X. That Contributor is then a Commercial Contributor. If that 133 | Commercial Contributor then makes performance claims, or offers warranties 134 | related to Product X, those performance claims and warranties are such 135 | Commercial Contributor’s responsibility alone. Under this section, the 136 | Commercial Contributor would have to defend claims against the other 137 | Contributors related to those performance claims and warranties, and if a court 138 | requires any other Contributor to pay any damages as a result, the Commercial 139 | Contributor must pay those damages. 140 | 141 | 5. NO WARRANTY 142 | 143 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN 144 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 145 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, 146 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each 147 | Recipient is solely responsible for determining the appropriateness of using and 148 | distributing the Program and assumes all risks associated with its exercise of 149 | rights under this Agreement , including but not limited to the risks and costs 150 | of program errors, compliance with applicable laws, damage to or loss of data, 151 | programs or equipment, and unavailability or interruption of operations. 152 | 153 | 6. DISCLAIMER OF LIABILITY 154 | 155 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 156 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 157 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 158 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 159 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 160 | OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS 161 | GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 162 | 163 | 7. GENERAL 164 | 165 | If any provision of this Agreement is invalid or unenforceable under applicable 166 | law, it shall not affect the validity or enforceability of the remainder of the 167 | terms of this Agreement, and without further action by the parties hereto, such 168 | provision shall be reformed to the minimum extent necessary to make such 169 | provision valid and enforceable. 170 | 171 | If Recipient institutes patent litigation against any entity (including a 172 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 173 | (excluding combinations of the Program with other software or hardware) 174 | infringes such Recipient’s patent(s), then such Recipient’s rights granted under 175 | Section 2(b) shall terminate as of the date such litigation is filed. 176 | 177 | All Recipient’s rights under this Agreement shall terminate if it fails to 178 | comply with any of the material terms or conditions of this Agreement and does 179 | not cure such failure in a reasonable period of time after becoming aware of 180 | such noncompliance. If all Recipient’s rights under this Agreement terminate, 181 | Recipient agrees to cease use and distribution of the Program as soon as 182 | reasonably practicable. However, Recipient’s obligations under this Agreement 183 | and any licenses granted by Recipient relating to the Program shall continue and 184 | survive. 185 | 186 | Everyone is permitted to copy and distribute copies of this Agreement, but in 187 | order to avoid inconsistency the Agreement is copyrighted and may only be 188 | modified in the following manner. The Agreement Steward reserves the right to 189 | publish new versions (including revisions) of this Agreement from time to time. 190 | No one other than the Agreement Steward has the right to modify this Agreement. 191 | The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation 192 | may assign the responsibility to serve as the Agreement Steward to a suitable 193 | separate entity. Each new version of the Agreement will be given a 194 | distinguishing version number. The Program (including Contributions) may always 195 | be distributed subject to the version of the Agreement under which it was 196 | received. In addition, after a new version of the Agreement is published, 197 | Contributor may elect to distribute the Program (including its Contributions) 198 | under the new version. Except as expressly stated in Sections 2(a) and 2(b) 199 | above, Recipient receives no rights or licenses to the intellectual property of 200 | any Contributor under this Agreement, whether expressly, by implication, 201 | estoppel or otherwise. All rights in the Program not expressly granted under 202 | this Agreement are reserved. 203 | 204 | This Agreement is governed by the laws of the State of New York and the 205 | intellectual property laws of the United States of America. No party to this 206 | Agreement will bring a legal action under this Agreement more than one year 207 | after the cause of action arose. Each party waives its rights to a jury trial in 208 | any resulting litigation. 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hicgql 2 | GraphQL queries as Clojure data structures, [Hiccup](https://github.com/weavejester/hiccup) style. 3 | 4 | 1. [Installation](#installation) 5 | 2. [Examples](#examples) 6 | 1. [Operations](#operations) 7 | 2. [Fields, Fragments, and Selections](#fields-fragments-and-selections) 8 | 1. [Arguments and Values](#arguments-and-values) 9 | 2. [Directives](#directives) 10 | 3. [Aliases](#aliases) 11 | 4. [Inline Fragments](#inline-fragments) 12 | 5. [Fragment Definitions](#fragment-definitions) 13 | 6. [Clojure Sequences](#clojure-sequences) 14 | 3. [Usage with `re-graph`](#usage-with-re-graph) 15 | 4. [References](#references) 16 | 5. [License](#license) 17 | 18 | ___ 19 | 20 | ## Installation 21 | [![Clojars Project](https://img.shields.io/clojars/v/io.github.timrichardt/hicgql.svg)](https://clojars.org/io.github.timrichardt/hicgql) 22 | 23 | `hicgql` is in Clojars. Just add it. 24 | ```clojure 25 | [io.github.timrichardt/hicgql "0.3.1"] 26 | ```` 27 | ```clojure 28 | io.github.timrichardt/hicgql {:mvn/version "0.3.1"} 29 | ``` 30 | 31 | ```clojure 32 | (:require 33 | [hicgql.core :refer [graphql]]) 34 | ``` 35 | 36 | ## Examples 37 | GraphQL documents are described with nested vectors, where the first element of each vector is a keyword describing the type of the document element. 38 | 39 | ```clojure 40 | [: : :] 41 | ``` 42 | 43 | `hicgql.core/graphql` accepts an arbitrary number of operations and renders them concatenated with a`\n` inbetween. 44 | 45 | ### Operations 46 | Operations are described with `:*/` namespaced keywords, e.g. `:*/OperationName`. Valid types are `:query`, `:mutation`, `:subscription`, and can be set via `:*/type`. Variables are supplied as `:$var "Type"` pairs. 47 | 48 | ```clojure 49 | (graphql 50 | [:*/Op {:*/type :query 51 | :$var "Type" 52 | :$var2 ["Type2" "default-value"]} :id]) 53 | ``` 54 | 55 | ```graphql 56 | query Op($var: Type, $var2: Type2 = "default-value") { 57 | id 58 | } 59 | ``` 60 | 61 | ### Fields, Fragments, and Selections 62 | Fields with subfields are prefixed by `:+/` namespace. Data fields without subfields do not have a namespace. 63 | 64 | ```clojure 65 | [:+/fieldWithSubfields 66 | :subfield1 67 | :subfield2 68 | :.../fragment 69 | [:+/anotherSubfieldedField 70 | :andSoOn]] 71 | ``` 72 | 73 | ```graphql 74 | { 75 | fieldWithSubfields { 76 | subfield1 77 | subfield2 78 | ...fragment 79 | anotherSubfieldedField { 80 | andSoOn 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | #### Arguments and Values 87 | Arguments to fields can be set with a map which is the second element of the selection vector. Argument names are the keys, the values can be Clojure data structures that can be meaningfully translated with 88 | `js/JSON.stringify` or `cheshire.core/generate-string`. 89 | ```clojure 90 | [:+/_ 91 | [:fieldWithArgs {:stringArg "string" 92 | :numberArg 2 93 | :objArg {:size [27.32 "cm"]}}]] 94 | ``` 95 | 96 | ```graphql 97 | { 98 | fieldWithArgs(stringArg: "string", numberArg: 2, objArg: {size: [27.32 "cm"]}) 99 | } 100 | ``` 101 | 102 | To define a GraphQL document, that does not start with an operation, there is `:+/_`: `[:+/_ field]` → `{ field }`. 103 | 104 | #### Directives 105 | Directives can be set with the `:!` key in the property map. `:!`'s value has to be a list of directives. A directive is either a key `:directive` → `@directive`, or can be supplied with arguments, like a field. 106 | ```clojure 107 | [:+/_ 108 | [:fieldWithDirs {:! [[:dir2 {:arg1 :$var 109 | :arg2 'VAL}] 110 | :dir1]}]] 111 | ``` 112 | 113 | ```graphql 114 | { 115 | fieldWithDirs @dir2(arg1: $var, arg2: VAL) @dir1 116 | } 117 | ``` 118 | 119 | Directives are applied from bottom to top. 120 | 121 | ### Aliases 122 | Aliases can be set with the `:>/` namespace. The `name` of the keyword is the name of the aliased field. 123 | ```clojure 124 | [:+/_ 125 | [:>/alias [:someField {:arg "val"}]]] 126 | ``` 127 | 128 | ```graphql 129 | { 130 | alias: someField(arg: "val") 131 | } 132 | ``` 133 | 134 | ### Inline Fragments 135 | Inline fragments are defined with `:?/` prefixed keywords. The `name` of the keyword has to be the type the fragment is of. 136 | ```clojure 137 | [:+/_ 138 | [:?/TypeA :id]] 139 | ``` 140 | 141 | ```graphql 142 | { 143 | ... on TypeA { 144 | id 145 | } 146 | } 147 | ``` 148 | 149 | ### Fragment Definitions 150 | Fragments are defined as operations, but with `:§/` namespaced keywords. The fragment type has to be set via the `:on` property. 151 | ```clojure 152 | [:§/Fragment {:on :Type} 153 | :field] 154 | ``` 155 | 156 | ```graphql 157 | fragment Fragment on Type { 158 | field 159 | } 160 | ``` 161 | ### Clojure Sequences 162 | It is possible, to use for example `for`, to generate a list of fields. 163 | ```clojure 164 | [:+/_ 165 | (for [m ["M" "N"]] 166 | (for [a ["A" "B" "C"] 167 | x ["X" "Y" "Z"]] 168 | (keyword (str a x m))))] 169 | ``` 170 | 171 | ```graphql 172 | {AXM,AYM,AZM,BXM,BYM,BZM,CXM,CYM,CZM,AXN,AYN,AZN,BXN, 173 | BYN,BZN,CXN,CYN,CZN,AXP,AYP,AZP,BXP,BYP,BZP,CXP,CYP,CZP} 174 | ``` 175 | 176 | ## Usage with `re-graph` 177 | `re-graph` adds the operation type itself, so you don't need to add the `:*/type` keyword to the property map. 178 | 179 | ```clojure 180 | (re-graph/query 181 | :query-id 182 | (graphql 183 | [:*/MyQuery {:$var "String"} 184 | [:+/selection {:arg :$var} 185 | :subfield]]) 186 | {:var "value"} 187 | callback) 188 | ``` 189 | 190 | ## References 191 | 1. GraphQL Spec October 2021, http://spec.graphql.org/October2021/ 192 | 2. Hiccup by James Reeves, https://github.com/weavejester/hiccup 193 | 3. re-graph by Oliver Hine, https://github.com/oliyh/re-graph 194 | 195 | ## License 196 | Eclipse Public License 197 | https://www.eclipse.org/legal/epl-v10.html 198 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.0"} 2 | org.clojure/clojurescript {:mvn/version "1.10.520"} 3 | cheshire/cheshire {:mvn/version "5.10.2"}} 4 | :aliases {:test-cljs {:extra-paths ["test"] 5 | :extra-deps {olical/cljs-test-runner {:mvn/version "3.8.0"}} 6 | :main-opts ["-m" "cljs-test-runner.main"]} 7 | :test-clj {:extra-paths ["test"] 8 | :extra-deps {io.github.cognitect-labs/test-runner 9 | {:git/tag "v0.5.1" :sha "dfb30dd6605cb6c0efc275e1df1736f6e90d4d73"}} 10 | :main-opts ["-m" "cognitect.test-runner"] 11 | :exec-fn cognitect.test-runner.api/test}} 12 | :paths ["src"]} 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject io.github.timrichardt/hicgql "0.3.1" 2 | :description "GraphQL in Clojure data structures." 3 | :url "https://github.com/timrichardt/hicgql" 4 | 5 | :license {:name "Eclipse Public License" 6 | :url "http://www.eclipse.org/legal/epl-v10.html"} 7 | 8 | :dependencies 9 | [[org.clojure/clojurescript "1.10.520" :scope "provided"] 10 | [cheshire "5.10.2"]] 11 | 12 | :repositories 13 | {"clojars" {:url "https://clojars.org/repo" 14 | :sign-releases false}} 15 | 16 | :source-paths 17 | ["src"]) 18 | -------------------------------------------------------------------------------- /src/hicgql/core.cljc: -------------------------------------------------------------------------------- 1 | (ns hicgql.core 2 | "Hiccup for GraphQL." 3 | (:require [clojure.string] 4 | #?(:clj [cheshire.core]))) 5 | 6 | (defn- mapstr 7 | [f nodes] 8 | (->> (map f nodes) 9 | (interpose ",") 10 | (apply str))) 11 | 12 | (defn stringify 13 | [object] 14 | ;; TODO: support deep variables (now :$kw only catched at 15 | ;; first level 16 | (if (and (keyword? object) 17 | (clojure.string/starts-with? (name object) "$")) 18 | (name object) 19 | #?(:clj (cheshire.core/generate-string object) 20 | :cljs (js/JSON.stringify (clj->js object))))) 21 | 22 | (declare node->graphql) 23 | 24 | (defn- field->graphql 25 | [node] 26 | (let [node-name (name node)] 27 | (when-not (= node-name "_") 28 | node-name))) 29 | 30 | (defn- fragment->graphql 31 | [node] 32 | (str "..." (name node))) 33 | 34 | (defn- vars->graphql 35 | "```clojure 36 | {:$var1 \"Type1\" 37 | :$var2 [\"Type\" \"val2\"]} 38 | ;; => ($var1: Type1, $var2: Type2 = \"val 2\") 39 | ```" 40 | [varmap] 41 | (if (empty? varmap) 42 | "" 43 | (->> varmap 44 | (map (fn [[k v]] 45 | (if (vector? v) 46 | (str (name k) ":" (first v) "=" (stringify (second v))) 47 | ;; TODO: maybe represent type decls also 48 | ;; as data structures? 49 | (str (name k) ":" v)))) 50 | (interpose ",") 51 | (apply str) 52 | (#(str "(" % ")"))))) 53 | 54 | (defn- alias->graphql 55 | [[alias node]] 56 | (str (name alias) ":" (node->graphql node))) 57 | 58 | (defn- args->graphql 59 | "```clojure 60 | {:arg1 \"val\" 61 | :arg2 :val 62 | :arg3 :$var} 63 | ;; => \"(arg1: ''val'', arg2: val, arg3: $var)\" 64 | ```" 65 | [argmap] 66 | (if (empty? argmap) 67 | "" 68 | (->> argmap 69 | (map (fn [[k v]] 70 | (str (name k) ":" (stringify v)))) 71 | (interpose ",") 72 | (apply str) 73 | (#(str "(" % ")"))))) 74 | 75 | (defn- directives->graphql 76 | "```clojure 77 | [:dir1 78 | [:dir2 {:arg1 \"val1\"}]] => @dir1 @dir2(arg1: \"val1\") 79 | ```" 80 | [directives] 81 | (->> directives 82 | (map (fn [directive] 83 | (cond 84 | (keyword? directive) 85 | (str "@" (name directive)) 86 | 87 | (vector? directive) 88 | (let [[directive args] directive] 89 | (str "@" (name directive) 90 | (when args 91 | (args->graphql args))))))) 92 | (interpose " ") 93 | (apply str))) 94 | 95 | (declare node->graphql) 96 | 97 | (defn- operation->graphql 98 | "" 99 | [op vars selection] 100 | (let [type (:*/type vars) 101 | vars (dissoc vars :*/type)] 102 | (str 103 | (when type 104 | (str (name type) " ")) 105 | (name op) 106 | (vars->graphql vars) 107 | (when selection 108 | (str "{" (mapstr node->graphql selection) "}"))))) 109 | 110 | (defn- inline-fragment->graphql 111 | [[type & subfields]] 112 | (str "... on " (name type) "{" (mapstr node->graphql subfields) "}")) 113 | 114 | (defn- selection->graphql 115 | [field props subfields] 116 | (let [args (dissoc props :!) 117 | dirs (:! props)] 118 | (str (field->graphql field) 119 | (args->graphql args) 120 | (directives->graphql dirs) 121 | (when (seq subfields) 122 | (str "{" (mapstr node->graphql subfields) "}"))))) 123 | 124 | (defn- fragment-def->graphql 125 | [field {:keys [on]} selection] 126 | (str "fragment " (name field) 127 | (when on 128 | (str " on " (name on))) 129 | "{" (mapstr node->graphql selection) "}")) 130 | 131 | (defn- node->graphql 132 | "" 133 | [node] 134 | (cond 135 | (keyword? node) 136 | (if (= (namespace node) "...") 137 | (fragment->graphql node) 138 | (field->graphql node)) 139 | 140 | (sequential? node) 141 | (let [[field arg1] node] 142 | (if (and (= field :+/_) (= (count node) 1)) 143 | "{}" 144 | (if (sequential? field) 145 | (mapstr node->graphql node) 146 | (condp = (namespace field) 147 | "*" (cond (or (vector? arg1) (keyword? arg1)) 148 | (operation->graphql field {} (rest node)) 149 | 150 | (map? arg1) 151 | (operation->graphql field arg1 (drop 2 node))) 152 | 153 | "§" (if (map? arg1) 154 | (fragment-def->graphql field arg1 (drop 2 node)) 155 | (fragment-def->graphql field {} (rest node))) 156 | 157 | "?" (inline-fragment->graphql node) 158 | 159 | ">" (alias->graphql node) 160 | 161 | "+" (cond (map? arg1) 162 | (selection->graphql field arg1 (drop 2 node)) 163 | 164 | :else 165 | (selection->graphql field {} (rest node))) 166 | 167 | nil (if (map? arg1) 168 | (selection->graphql field arg1 nil) 169 | (mapstr node->graphql node)))))))) 170 | 171 | (defn graphql 172 | "" 173 | [& args] 174 | (->> args 175 | (filter seq) ;; TODO: why? (prolly convenience) 176 | (map node->graphql) 177 | (interpose \newline) 178 | (apply str))) 179 | 180 | (comment 181 | (graphql 182 | [:+/_ :id]) 183 | ;; => "{id}" 184 | 185 | (graphql 186 | [:*/Op :id]) 187 | 188 | (graphql 189 | [:§/Fragment {:on :Type} 190 | :field]) 191 | 192 | (graphql 193 | [:+/_ 194 | [:+/pizzas {:! [[:include {:if :$baked}]] 195 | :limit 1} 196 | [:+/self 197 | :id]]]) 198 | 199 | (graphql 200 | [:*/Op {:*/type :query 201 | :$var 'VAL} :id]) 202 | ;; => "query Op($var:VAL){id}" 203 | 204 | (graphql 205 | [:*/m {:$var "Type"} 206 | [:+/selection 207 | :subfield1 208 | [:subfield2 {:arg "val"}]]]) 209 | ;; => "m($var:Type){selection{subfield1,subfield2(arg:\"val\")}}" 210 | 211 | (graphql 212 | [:+/_ 213 | [:+/fieldWithSubfields 214 | :subfield1 215 | :subfield2 216 | [:+/anotherSubfieldedField 217 | :andSoOn]]]) 218 | 219 | (graphql 220 | [:+/_ 221 | [:fieldWithArgs {:stringArg "string" 222 | :numberArg 2 223 | :objArg {:size [27.32 "cm"]}}]]) 224 | 225 | (graphql 226 | [:+/_ 227 | [:fieldWithDirs {:! [[:dir2 {:arg1 :$var 228 | :arg2 'VAL}] 229 | :dir1]}]]) 230 | 231 | (graphql 232 | [:+/_ 233 | [:?/TypeA :id]]) 234 | 235 | (graphql 236 | [:+/_ 237 | [:>/alias [:someField {:arg "val"}]]]) 238 | "{alias:someField(arg:\"val\")}" 239 | 240 | (graphql 241 | [:+/_ 242 | (for [m ["M" "N" "P"]] 243 | (for [a ["A" "B" "C"] 244 | x ["X" "Y" "Z"]] 245 | [:+/_ (keyword (str a x m))]))])) 246 | -------------------------------------------------------------------------------- /test/hicgql/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hicgql.core-test 2 | (:require [hicgql.core :refer [graphql]] 3 | #?(:clj [clojure.test :refer [deftest is]] 4 | :cljs [cljs.test :refer [deftest is] :include-macros true]))) 5 | 6 | (deftest operation 7 | (is (= (graphql 8 | [:*/Op {:*/type :query 9 | :$var "Type" 10 | :$var2 ["Type2" "default-value"]} :id]) 11 | "query Op($var:Type,$var2:Type2=\"default-value\"){id}"))) 12 | 13 | (deftest unnamed-operation 14 | (is (= (graphql [:+/_]) 15 | "{}"))) 16 | 17 | 18 | (deftest field-with-subfields 19 | (is (= (graphql 20 | [:+/_ 21 | [:+/fieldWithSubfields 22 | :subfield1 23 | :subfield2 24 | :.../fragment 25 | [:+/anotherSubfieldedField 26 | :andSoOn]]]) 27 | "{fieldWithSubfields{subfield1,subfield2,...fragment,anotherSubfieldedField{andSoOn}}}"))) 28 | 29 | 30 | (deftest field-with-args 31 | (is (= (graphql 32 | [:+/_ 33 | [:fieldWithArgs {:stringArg "string" 34 | :numberArg 2 35 | :objArg {:size [27.32 "cm"]}}]]) 36 | "{fieldWithArgs(stringArg:\"string\",numberArg:2,objArg:{\"size\":[27.32,\"cm\"]})}"))) 37 | 38 | 39 | (deftest field-with-dirs 40 | (is (= (graphql 41 | [:+/_ 42 | [:fieldWithDirs {:! [[:dir2 {:arg1 :$var 43 | :arg2 'VAL}] 44 | :dir1]}]]) 45 | "{fieldWithDirs@dir2(arg1:$var,arg2:\"VAL\") @dir1}"))) 46 | 47 | 48 | (deftest aliases 49 | (is (= (graphql 50 | [:+/_ 51 | [:>/alias [:someField {:arg "val"}]]]) 52 | "{alias:someField(arg:\"val\")}"))) 53 | 54 | 55 | (deftest inline-fragment 56 | (is (= (graphql 57 | [:+/_ 58 | [:?/TypeA :id]]) 59 | "{... on TypeA{id}}"))) 60 | 61 | 62 | (deftest fragment-definition 63 | (is (= (graphql [:§/Fragment {:on :Type} 64 | :field]) 65 | "fragment Fragment on Type{field}"))) 66 | 67 | 68 | (deftest clojure-sequences 69 | (is (= (graphql 70 | [:+/_ 71 | (for [m ["M" "N" "P"]] 72 | (for [a ["A" "B" "C"] 73 | x ["X" "Y" "Z"]] 74 | (keyword (str a x m))))]) 75 | "{AXM,AYM,AZM,BXM,BYM,BZM,CXM,CYM,CZM,AXN,AYN,AZN,BXN,BYN,BZN,CXN,CYN,CZN,AXP,AYP,AZP,BXP,BYP,BZP,CXP,CYP,CZP}"))) 76 | 77 | 78 | (deftest multiple-forms 79 | (is (= (graphql 80 | [:+/_ 81 | (for [m ["M" "N" "P"]] 82 | (for [a ["A" "B" "C"] 83 | x ["X" "Y" "Z"]] 84 | (keyword (str a x m))))] 85 | nil 86 | [] 87 | '() 88 | {} 89 | [:+/_ 90 | (for [m ["M" "N" "P"]] 91 | (for [a ["A" "B" "C"] 92 | x ["X" "Y" "Z"]] 93 | (keyword (str a x m))))]) 94 | "{AXM,AYM,AZM,BXM,BYM,BZM,CXM,CYM,CZM,AXN,AYN,AZN,BXN,BYN,BZN,CXN,CYN,CZN,AXP,AYP,AZP,BXP,BYP,BZP,CXP,CYP,CZP} 95 | {AXM,AYM,AZM,BXM,BYM,BZM,CXM,CYM,CZM,AXN,AYN,AZN,BXN,BYN,BZN,CXN,CYN,CZN,AXP,AYP,AZP,BXP,BYP,BZP,CXP,CYP,CZP}"))) 96 | --------------------------------------------------------------------------------