├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── boot.properties ├── build.boot ├── docs ├── 01-types.md ├── 02-queries.md ├── 03-defpermission.md ├── 04-defentity.md ├── 05-defcommand.md ├── 06-defservice.md ├── 07-defview.md └── 08-defscreen.md ├── resources ├── codox │ └── theme │ │ └── macros │ │ ├── macros.css │ │ └── theme.edn ├── docs.cljs.edn ├── index.html ├── screen-app.cljs.edn └── screen-app.html ├── scripts └── update-gh-pages └── src ├── docs └── workflo │ └── macros │ ├── docs.cljs │ └── docs │ ├── screen.cljs │ ├── util │ └── string.cljs │ └── view.cljs ├── examples └── workflo │ └── macros │ └── examples │ └── screen_app.cljs ├── main └── workflo │ └── macros │ ├── bind.clj │ ├── bind.cljs │ ├── command.clj │ ├── command.cljs │ ├── command │ └── util.cljc │ ├── config.clj │ ├── config.cljs │ ├── entity.clj │ ├── entity.cljs │ ├── entity │ ├── datascript.cljc │ ├── datomic.clj │ ├── refs.cljc │ └── schema.cljc │ ├── hooks.clj │ ├── hooks.cljs │ ├── jscomponents.clj │ ├── jscomponents.cljs │ ├── permission.clj │ ├── permission.cljs │ ├── query.cljc │ ├── query │ ├── bind.cljc │ ├── om_next.cljc │ ├── om_util.cljc │ └── util.cljc │ ├── registry.clj │ ├── registry.cljs │ ├── screen.clj │ ├── screen.cljs │ ├── screen │ ├── bidi.cljs │ ├── om_next.cljs │ └── util.cljc │ ├── service.cljc │ ├── service │ └── util.cljc │ ├── specs │ ├── bind.cljc │ ├── command.cljc │ ├── conforming_query.cljc │ ├── entity.cljc │ ├── om_query.cljc │ ├── parsed_query.cljc │ ├── permission.cljc │ ├── query.cljc │ ├── screen.cljc │ ├── service.cljc │ ├── types.cljc │ └── view.cljc │ ├── util │ ├── form.cljc │ ├── js.cljs │ ├── macro.clj │ ├── misc.cljc │ ├── string.cljc │ └── symbol.cljc │ ├── view.clj │ ├── view.cljs │ └── view │ └── util.cljc └── test └── workflo └── macros ├── bind_test.cljc ├── command_run_test.cljc ├── command_test.clj ├── config_test.cljc ├── entity ├── datomic_schema_test.clj ├── refs_test.cljc ├── schema_test.cljc └── test_entities.cljc ├── entity_test.clj ├── permission_test.clj ├── query └── om_next_test.cljc ├── query_test.cljc ├── registry_test.cljc ├── screen └── bidi_test.cljs ├── screen_test.cljs ├── service_test.cljc ├── spec_test.cljc ├── util └── string_test.cljc └── view_test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *jar 4 | /lib/ 5 | /classes/ 6 | /target/ 7 | /checkouts/ 8 | .lein-deps-sum 9 | .lein-repl-history 10 | .lein-plugins/ 11 | .lein-failures 12 | .nrepl-port 13 | .nrepl-history 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | env: 3 | - BOOT_JVM_OPTIONS="-Xmx1024m -Xms256m -Xss1m" 4 | language: java 5 | script: boot test-once 6 | install: 7 | - mkdir -p ~/bin 8 | - export PATH=~/bin:$PATH 9 | - curl -L https://github.com/boot-clj/boot-bin/releases/download/latest/boot.sh -o ~/bin/boot 10 | - chmod +x ~/bin/boot 11 | jdk: 12 | - oraclejdk8 13 | cache: 14 | directories: 15 | - $HOME/.m2 16 | - $HOME/.boot/cache/bin 17 | - $HOME/.boot/cache/lib 18 | - $HOME/bin 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Workflo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # workflo/macros 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/workflo/macros.svg)](https://clojars.org/workflo/macros) 4 | [![Build Status](https://travis-ci.org/functionalfoundry/macros.svg?branch=master)](https://travis-ci.org/functionalfoundry/macros) 5 | 6 | [API documentation](https://functionalfoundry.github.io/macros/) | 7 | [Changes](CHANGELOG.md) 8 | 9 | A collection of Clojure and ClojureScript macros (and related utilities) 10 | for web and mobile development. The main goal of these macros is to 11 | provide all main, high-level building blocks of an application: 12 | 13 | * Data (`defentity`) 14 | * Permissions (`defpermission`) 15 | * Services (`defservice`) 16 | * Commands / Actions (`defcommand`) 17 | * Views (`defview`) 18 | * Screens (`defscreen`) 19 | 20 | How the building blocks created using these macros are combined into 21 | working applications is left open. All macros provide hooks to make 22 | this as easy as possible. 23 | 24 | ## License 25 | 26 | `workflo/macros` is copyright (C) 2016-2017 Workflo, Inc. 27 | 28 | Licensed under the MIT License. 29 | 30 | For more information [see the LICENSE file](LICENSE). 31 | -------------------------------------------------------------------------------- /boot.properties: -------------------------------------------------------------------------------- 1 | BOOT_CLOJURE_VERSION=1.9.0-alpha17 2 | BOOT_VERSION=2.7.1 3 | BOOT_EMIT_TARGET=no 4 | -------------------------------------------------------------------------------- /build.boot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env boot 2 | 3 | (def +project+ 'workflo/macros) 4 | (def +version+ "0.2.63") 5 | 6 | (set-env! 7 | :resource-paths #{"resources" "src/main" "src/docs"} 8 | :dependencies '[;; Boot setup 9 | [adzerk/boot-cljs "2.0.0" :scope "test"] 10 | [adzerk/boot-reload "0.5.1" :scope "test"] 11 | [adzerk/boot-test "1.2.0" :scope "test"] 12 | [adzerk/bootlaces "0.1.13" :scope "test"] 13 | [boot-codox "0.10.3" :scope "test"] 14 | [pandeiro/boot-http "0.8.3" :scope "test"] 15 | [crisptrutski/boot-cljs-test "0.3.0" :scope "test"] 16 | [com.cemerick/piggieback "0.2.2" :scope "test" 17 | :exclusions [com.google.guava/guava]] 18 | 19 | ;; Testing 20 | [org.clojure/test.check "0.10.0-alpha2" :scope "test"] 21 | 22 | ;; Library dependencies 23 | [bidi "2.1.2"] 24 | [com.datomic/datomic-free "0.9.5561.50" :scope "test" 25 | :exclusions [com.google.guava/guava]] 26 | [com.stuartsierra/component "0.3.2"] 27 | [datomic-schema "1.3.0"] 28 | [inflections "0.13.0"] 29 | [org.clojure/clojure "1.9.0-alpha17"] 30 | [org.clojure/clojurescript "1.9.671"] 31 | [org.clojure/core.async "0.3.443"] 32 | [org.clojure/spec.alpha "0.1.123"] 33 | [org.omcljs/om "1.0.0-alpha48"] 34 | [org.clojure/data.json "0.2.6"] 35 | 36 | ;; Development dependencies 37 | [org.clojure/tools.nrepl "0.2.13" :scope "test"] 38 | [devcards "0.2.3" :scope "test"] 39 | [datascript "0.16.1" :scope "test"]]) 40 | 41 | 42 | (require '[adzerk.boot-cljs :refer [cljs]] 43 | '[adzerk.boot-reload :refer [reload]] 44 | '[adzerk.boot-test :refer :all] 45 | '[adzerk.bootlaces :refer :all] 46 | '[boot.git :refer [last-commit]] 47 | '[codox.boot :refer [codox]] 48 | '[crisptrutski.boot-cljs-test :refer [test-cljs]] 49 | '[pandeiro.boot-http :refer [serve]]) 50 | 51 | (bootlaces! +version+ :dont-modify-paths? true) 52 | 53 | (task-options! 54 | test-cljs {:js-env :phantom 55 | :update-fs? true 56 | :exit? true 57 | :optimizations :none} 58 | push {:repo "deploy-clojars" 59 | :ensure-branch "master" 60 | :ensure-clean true 61 | :ensure-tag (last-commit) 62 | :ensure-version +version+} 63 | pom {:project +project+ 64 | :version +version+ 65 | :description "Clojure macros for web and mobile development" 66 | :url "https://github.com/functionalfoundry/macros" 67 | :scm {:url "https://github.com/functionalfoundry/macros"} 68 | :license {"MIT License" 69 | "https://opensource.org/licenses/MIT"}} 70 | repl {:middleware '[cemerick.piggieback/wrap-cljs-repl]}) 71 | 72 | (deftask examples 73 | [] 74 | (merge-env! :source-paths #{"src/examples"}) 75 | identity) 76 | 77 | (deftask build-dev 78 | [] 79 | (comp 80 | (cljs :source-map true 81 | :optimizations :none 82 | :compiler-options {:devcards true 83 | :parallel-build true}))) 84 | 85 | (deftask build-production 86 | [] 87 | (comp 88 | (cljs :optimizations :advanced 89 | :compiler-options {:devcards true 90 | :parallel-build true}))) 91 | 92 | (deftask dev 93 | [] 94 | (comp 95 | (examples) 96 | (watch) 97 | (reload :on-jsload 'workflo.macros.examples.screen-app/reload) 98 | (build-dev) 99 | (serve) 100 | (repl :server true))) 101 | 102 | (deftask production 103 | [] 104 | (comp 105 | (examples) 106 | (watch) 107 | (build-production) 108 | (serve))) 109 | 110 | (deftask testing 111 | [] 112 | (merge-env! :source-paths #{"src/test"}) 113 | identity) 114 | 115 | (deftask building-docs 116 | [] 117 | (merge-env! :source-paths #{"docs"}) 118 | identity) 119 | 120 | (deftask docs 121 | [] 122 | (comp 123 | (building-docs) 124 | (codox :name "workflo/macros" 125 | :version +version+ 126 | :source-paths #{"src/main"} 127 | :output-path "api-docs" 128 | :doc-paths #{"docs/"} 129 | :metadata {:doc/format :markdown} 130 | :themes [:default :macros] 131 | :source-uri "https://github.com/functionalfoundry/macros/blob/{version}/{filepath}#L{line}") 132 | (target))) 133 | 134 | (deftask test-once 135 | [] 136 | (comp 137 | (testing) 138 | (test-cljs) 139 | (test))) 140 | 141 | (deftask test-auto 142 | [] 143 | (comp 144 | (testing) 145 | (watch) 146 | (test-cljs) 147 | (test))) 148 | 149 | (deftask install-local 150 | [] 151 | (comp 152 | (pom) 153 | (jar) 154 | (install))) 155 | 156 | (deftask deploy-snapshot 157 | [] 158 | (comp 159 | (pom) 160 | (jar) 161 | (push-snapshot))) 162 | 163 | (deftask deploy-release 164 | [] 165 | (comp 166 | (pom) 167 | (jar) 168 | (push-release))) 169 | -------------------------------------------------------------------------------- /docs/01-types.md: -------------------------------------------------------------------------------- 1 | # Type specs 2 | 3 | `workflo/macros` comes with specs—as in `clojure.spec` specs—for 4 | various fundamental types. We've decided to focus on those supported 5 | by [Datomic](http://www.datomic.com/), plus special types for relationships 6 | between what we call [entities](04-defentity.md). 7 | 8 | Please note, however, that the type specs are useful by themselves 9 | and can be used in any application, regardless of whether you are 10 | using Datomic or not. 11 | 12 | ## Basic types 13 | 14 | * `:workflo.macros.specs.types/any` 15 | * `:workflo.macros.specs.types/keyword` 16 | * `:workflo.macros.specs.types/string` 17 | * `:workflo.macros.specs.types/boolean` 18 | * `:workflo.macros.specs.types/long` 19 | * `:workflo.macros.specs.types/bigint` 20 | * `:workflo.macros.specs.types/float` 21 | * `:workflo.macros.specs.types/double` 22 | * `:workflo.macros.specs.types/bigdec` 23 | * `:workflo.macros.specs.types/instant` 24 | * `:workflo.macros.specs.types/uuid` 25 | * `:workflo.macros.specs.types/bytes` 26 | * `:workflo.macros.specs.types/enum` 27 | 28 | ## Identifier types 29 | 30 | * `:workflo.macros.specs.types/id` — a 32-character string. 31 | * `:workflo/id` — the same as `:workflo.macros.specs.types/id` but with the 32 | `unique-identity` and `indexed` type hints added. 33 | 34 | ## Relationship types 35 | 36 | * `:workflo.macros.specs.types/ref` — a map that contains at least a `:workflo/id`. 37 | * `:workflo.macros.specs.types/ref-many` — a vector or set containing an arbitrary 38 | number of values of that match the `:workflo.macros.specs.types/ref` spec. 39 | 40 | ## Entity relationship types 41 | 42 | * `(workflo.macros.specs.types/entity-ref ENTITY-SYM OPTIONS)` — a type spec 43 | that is equivalent to `:workflo.macros.specs.types/ref` or 44 | `:workflo.macros.specs.types/ref-many` (depending on `OPTIONS`) but also 45 | stores the name of the target entity (see [defentity](04-defentity.md)). 46 | 47 | ## Datomic-compatible type hints 48 | 49 | * `:workflo.macros.specs.types/unique-value` — values of this type should be 50 | unique across all entity attributes with the same name (see 51 | [defentity](04-defentity.md)). 52 | * `:workflo.macros.specs.types/unique-identity` — values of this type should 53 | be unique across the entire system. 54 | * `:workflo.macros.specs.types/indexed` — values of this type should be indexed 55 | by databases, if possible. 56 | * `:workflo.macros.specs.types/fulltext` 57 | * `:workflo.macros.specs.types/component` 58 | * `:workflo.macros.specs.types/no-history` 59 | 60 | ## Other hints 61 | 62 | * `:workflo.macros.specs.types/non-persistent` — values of this types are not 63 | to be persisted (e.g. to Datomic, local storage or whatever your project 64 | uses for persistence). 65 | -------------------------------------------------------------------------------- /docs/02-queries.md: -------------------------------------------------------------------------------- 1 | # Queries 2 | 3 | Most of the macros provided by `workflo/macros` involve some sort of 4 | query. `defentity` defines an optional authorization query, `defcommand` 5 | defines a query for additional input data, as does `defservice`, and 6 | `defview` defines a query for data to display in the view. 7 | 8 | All queries use the same query language. This query language is fully 9 | specified with `clojure.spec`, via `:workflo.macros.specs.query/query`. 10 | 11 | ## The query language 12 | 13 | The query language used by the macros is based on the 14 | [Om Next query language](https://github.com/omcljs/om/blob/master/src/main/om/next/impl/parser.cljc#L5). 15 | Its structure is the almost identical but its syntax is very different. What our 16 | query language adds in terms of actual functionality is 17 | 18 | * Aliases 19 | * Query fragments — i.e. re-use of queries defined elsewhere 20 | * Advanced parameterization 21 | 22 | ### Elements of a query 23 | 24 | A query is an interleaved vector of different subqueries, some spanning 25 | multiple vector elements. These subqueries can be: 26 | 27 | * **Properties** — e.g. `user` or `post`, `ui.route` 28 | * **Links** — e.g. `[user 100]` or `[ui.route _]` 29 | * **Joins** — of the form `{SOURCE TARGET}`, where `SOURCE` can be either a property 30 | (e.g. `user`) or a link (e.g. `[user 100]`) 31 | * **Prefixed properties** — e.g. `user [name email]` or `ui.route [url params]` 32 | * **Aliased properties** — e.g. `user :as jeff` or `ui.route :as current-route` 33 | * **Parameterizations** – of the form `(QUERY PARAMS)`, where `QUERY` is either a 34 | property, an aliased property, a link or a join, and `PARAMS` is a parameter 35 | map (see the section about query parameters below) 36 | * **Fragments** — e.g. `...route-query` or `...user-query` 37 | 38 | And since queries are Clojure data structures, they can include comments anywhere. 39 | 40 | ### Example queries 41 | 42 | A query for the name and email address of a user with a specific `:workflo/id`: 43 | ```clojure 44 | [({user [workflo [id] user [name email]]} 45 | {workflo/id "some-id"})] 46 | ``` 47 | This query is identical to the following Om Next query: 48 | ```clojure 49 | [({:user [:workflo/id :user/name :user/email]} 50 | {:workflo/id "some-id"})] 51 | ``` 52 | 53 | A query for all users that have the first name `Linda`, including all posts 54 | they have written, sorted by their last names, plus the post with the 55 | ID with the value `"foo"`: 56 | ```clojure 57 | [(;; The subquery for the users 58 | {users [workflo [id] 59 | user [first-name 60 | last-name 61 | {posts [workflo [id] 62 | post [title content]]}]]} 63 | 64 | ;; An alias for these users 65 | :as all-lindas 66 | 67 | ;; The query parameters 68 | {user/first-name "Linda" 69 | sort/attr :user/last-name}) 70 | 71 | {[user "foo"]}] 72 | ``` 73 | This query is equivalent to the following Om Next query, only adding 74 | some meta data for the alias: 75 | ```clojure 76 | [({:users [:workflo/id 77 | :user/first-name 78 | :user/last-name 79 | {:user/posts [:workflo/id 80 | :post/title 81 | :post/content]}]} 82 | {:user/first-name "Linda" 83 | :sort/attr :user/last-name})] 84 | ``` 85 | 86 | ### Query parameters 87 | 88 | Each query parameter is represented by a key-value pair in the parameter map 89 | of a parameterized query. The key can take two forms: 90 | 91 | 1. a parameter name — a symbol like `foo`, `user/name` or `workflo/id` 92 | 2. a parameter path — a vector of symbols, like `[user/organization workflo/id]` 93 | or `[post/blog blog/author author/name]` 94 | 95 | The second form is also called **deep parameterization**, as it allows to 96 | match properties deep inside an entity and its relationships against the 97 | value of a parameter. 98 | 99 | The value of a parameter can take three forms: 100 | 101 | 1. a literal value — e.g. `"Linda"`, `10` or `:foo/bar` 102 | 2. a variable — e.g. `?first-name`, `?id` or `?user` 103 | 3. a variable path — a vector of variables e.g. `[?url ?params ?user]` 104 | 105 | How these variables are resolved depends on the situation. Various of the 106 | macros define functions that take in data and bind query parameters against 107 | this data. In these cases a variable path is similar to a call to `get-in`, 108 | allowing to extract values from deep inside a data structure. 109 | 110 | For example, given the input data 111 | ``` 112 | {:url {:params {:user "123"}}} 113 | ``` 114 | the variable path `[?url ?params ?user]` would be resolved to `"123"` by 115 | following the keys `:url`, `:params` and `:user`. 116 | 117 | ## API documentation / utilities 118 | 119 | The macros come with various utilities for parsing queries into an AST, 120 | binding query parameters against data, translating queries to Om Next 121 | queries, creating bindings for query results in code that consumes 122 | them and much more. 123 | 124 | The following is a list of pointers to the API documentation for 125 | all query utilities. 126 | 127 | * [workflo.macros.bind](workflo.macros.bind.html) 128 | - Create local bindings for query results (used for binding query 129 | results in macro function bodies) 130 | * [workflo.macros.query](workflo.macros.query.html) 131 | - Parse queries into an AST 132 | - Conform queries using `clojure.spec` 133 | - Bind query parameters 134 | * [workflo.macros.query.bind](workflo.macros.query.bind.html) 135 | - Resolve variables and variable paths from query parameters: 136 | [workflo.macros.query.bind](workflo.macros.query.bind.html) 137 | * [workflo.macros.query.om-next](workflo.macros.query.om-next.html) 138 | - Convert query ASTs to Om Next queries 139 | - Split queries (with potential conflicts) into minimal sequences 140 | of non-conflicting queries (useful for sending queries to 141 | backends) 142 | * [workflo.macros.query.om-util](workflo.macros.query.om-util.html) 143 | - Internal utilities for working with Om Next queries 144 | * [workflo.macros.query.util](workflo.macros.query.util.html) 145 | - Internal utilities for working with queries. -------------------------------------------------------------------------------- /docs/03-defpermission.md: -------------------------------------------------------------------------------- 1 | # Permissions (defpermission) 2 | 3 | The `defpermission` macro essentially allows to give different permissions 4 | a name and some human-readable information for documentation purposes. 5 | 6 | Other parts of the system can then use the names of these permissions 7 | to restrict access to e.g. entities, commands or user-facing features. 8 | 9 | ## Usage 10 | 11 | ### General structure 12 | 13 | ```clojure 14 | (require '[workflo.macros.permission :refer [defpermission]]) 15 | 16 | (defpermission 17 | (title "...") ; Required 18 | (description "...") ; Optional 19 | ) 20 | ``` 21 | 22 | ### Simple example 23 | 24 | ```clojure 25 | (require '[workflo.macros.permission :refer [defpermission]]) 26 | 27 | (defpermission administer-organization 28 | (title "Administer organization") 29 | (description 30 | "* Can view, add, remove and change roles in the organization 31 | * Can add and remove roles to or from users (cannot remove 32 | predefined roles) 33 | * Can view and change the billing info of the organization 34 | - No other permission can access this information 35 | * Can view and change the subscription of the organization")) 36 | 37 | (defpermission manage-organization 38 | (title "Manage organization") 39 | (description 40 | "* Can view users of the organization and invite new users to 41 | the organization 42 | * Can add and remove roles to or from users (except for the 43 | Admin role) 44 | * Can reassign permissions inside the organization 45 | * Can view but not change the subscription of the organization")) 46 | 47 | (defpermission view-organization 48 | (title "View organization") 49 | (description 50 | "* Can view non-admin information of the organization 51 | * Can view users of the organization 52 | * Can view roles and permission assignments of the organization")) 53 | ``` 54 | 55 | ## API documentation 56 | 57 | * [workflo.macros.permission](workflo.macros.permission.html) 58 | - The `defpermission` macro 59 | - Permission registry 60 | * [workflo.macros.specs.permission](workflo.macros.specs.permission.html) 61 | - Specs for `defpermission` arguments -------------------------------------------------------------------------------- /docs/04-defentity.md: -------------------------------------------------------------------------------- 1 | # Entities (defentity) 2 | 3 | Entities in `workflo/macros` define the types of data in your system (e.g. 4 | users, accounts, articles). The following list sums up the key features of 5 | entities: 6 | 7 | * **Specs** — entities are fully specified using `clojure.spec` 8 | * **Relationships** — relationships between entities can easily be defined. 9 | This includes one-to-one, one-to-many and many-to-many relations 10 | * **Entity registry** — entities defined with `defentity` are stored 11 | in a global registry, from where they can be looked up at any time 12 | * **Schema generation** — schemas for popular Clojure(Script) databases 13 | such as Datomic and DataScript can be derived from entities easily 14 | * **Authorization** — `defentity` comes with built-in support for 15 | authorization to control access to entities in your system 16 | * **Hints** — entities can be tagged with arbitrary hints to allow 17 | special-casing entity data in the system (e.g. prevent certain entities 18 | from being persisted) 19 | 20 | ## Usage 21 | 22 | ### General structure 23 | 24 | ```clojure 25 | (require '[workflo.macros.entity :refer [defentity]]) 26 | 27 | (defentity 28 | "description" ; Optional 29 | (hints [...]) ; Optional 30 | (spec ...) ; Required 31 | (auth-query ...) ; Optional 32 | (auth ...) ; Optional 33 | ) 34 | ``` 35 | 36 | ### Simple example 37 | 38 | ```clojure 39 | (require '[clojure.spec.alpha :as s]) 40 | (require '[workflo.macros.specs.types :as types]) 41 | (require '[workflo.macros.entity :refer [defentity]]) 42 | 43 | ;;;; Specs for user attributes 44 | 45 | (s/def :user/name ::types/string) 46 | (s/def :user/email ::types/string) 47 | (s/def :user/friends (types/entity-ref 'user :many? true)) 48 | (s/def :user/todos (types/entity-ref 'todo :many? true)) 49 | 50 | ;;;; User entity 51 | 52 | (defentity user 53 | "A user in a multi-user todo app" 54 | (spec 55 | (s/keys :req [:workflo/id 56 | :user/name 57 | :user/email] 58 | :opt [:user/friends 59 | :user/todos])) 60 | 61 | ;;;; Specs for todo attributes 62 | 63 | (s/def :todo/text ::types/string) 64 | (s/def :todo/done? ::types/boolean) 65 | (s/def :todo/complexity (s/and ::types/enum 66 | {:todo.complexity/easy 67 | :todo.complexity/medium 68 | :todo.complexity/hard})) 69 | 70 | ;;;; Todo entity 71 | 72 | (defentity todo 73 | "A todo item in a multi-user todo app" 74 | (spec 75 | (s/keys :req [:workflo/id 76 | :todo/text 77 | :todo/done? 78 | :todo/complexity]))) 79 | ``` 80 | 81 | ## API documentation 82 | 83 | The following is a list of pointers to the namespaces that are related to 84 | entities: 85 | 86 | * [workflo.macros.entity](workflo.macros.entity.html) 87 | - The `defentity` macro 88 | - Entity registry 89 | - Entity hooks 90 | - Entity references lookup 91 | - Entity validation 92 | - Entity authorization 93 | * [workflo.macros.entity.datascript](workflo.macros.entity.datascript.html) 94 | - DataScript schema generation from registered entities 95 | * [workflo.macros.entity.datomic](workflo.macros.entity.datomic.html) 96 | - Datomic schema generation from registered entities 97 | * [workflo.macros.entity.refs](workflo.macros.entity.refs.html) 98 | - Internal registry of references between entities 99 | * [workflo.macros.entity.schema](workflo.macros.entity.schema.html) 100 | - Intermediate schema representation for entities (from which 101 | schemas for any database can be derived) 102 | * [workflo.macros.specs.entity](workflo.macros.specs.entity.html) 103 | - Specs for `defentity` arguments -------------------------------------------------------------------------------- /docs/05-defcommand.md: -------------------------------------------------------------------------------- 1 | # Commands (defcommand) 2 | 3 | Commands represent actions that can be performed in a system, either 4 | triggered by the user or by any other part of the system. 5 | 6 | A command takes in input data (the payload) and optionally asks the 7 | system to run a query to fetch additional data required for it to run. 8 | It then executes code and eventually emits data, which typically would 9 | be delivered to registered services (see 10 | [Services](06-defservice.html) for more information on those. 11 | 12 | Commands are defined and registered with the `defcommand` macro. Some 13 | of the key features of commands are: 14 | 15 | * **Interface similar to reducers** — commands are designed to encourage 16 | concise, side-effect-free, self-contained code; see the section about 17 | the reducing nature of commands below 18 | * **Queries, parameterized using command input data** — every command 19 | has an optional [query](02-queries.html) to fetch additional data; 20 | values of query parameters are resolved using the data passed to 21 | the command 22 | * **Local bindings for query results** — query results are bound to 23 | names in the command execution scope automatically 24 | * **Authorization** — like with [entities](04-defentity.html), support for 25 | authorization comes built-in through an auth query and an auth function 26 | * **Command registry** — all commands are stored in a registry from 27 | which they can be looked up again by their names at any time 28 | 29 | ## Commands are like reducers 30 | 31 | Commands can be thought of as being similar to reducers of the 32 | form 33 | 34 | ```elm 35 | Reducer :: (State, Data) -> State 36 | ``` 37 | 38 | with their query and output data added into the mix as follows: 39 | 40 | ```elm 41 | BindQuery :: UnboundQuery -> Data -> BoundQuery 42 | 43 | -- In reality, how queries are executed is entirely up to you 44 | RunQuery :: State -> BoundQuery -> QueryResults 45 | 46 | -- Command implementations are, ideally, free of side-effects 47 | Implementation :: QueryResults -> Data -> EmitData 48 | 49 | -- Rough outline of how commands are executed 50 | RunCommand :: State -> UnboundQuery -> Data -> Implementation -> EmitData 51 | RunCommand state, unboundQuery, data, implementation = 52 | implementation (RunQuery state (BindQuery unboundQuery data)) data 53 | ``` 54 | 55 | ## Usage 56 | 57 | ### General structure 58 | 59 | ```clojure 60 | (require '[workflo.macros.command :refer [defcommand]]) 61 | 62 | (defcommand 63 | "description" ; Optional 64 | (hints [...]) ; Optional 65 | (spec ...) ; Optional (clojure.spec for the input data) 66 | (query ...) ; Optional 67 | (auth-query ...) ; Optional 68 | (auth ...) ; Optional 69 | ... ; Optional (arbitrary (foo ...) forms) 70 | (emit ...) ; Required (the implementation of the code) 71 | ) 72 | ``` 73 | 74 | ### Simple example 75 | 76 | ```clojure 77 | (require '[clojure.spec.alpha :as s] 78 | '[workflo.macros.command :as commands :refer [defcommand]] 79 | '[workflo.macros.entity :refer [defentity]] 80 | '[workflo.macros.specs.types :as types]) 81 | 82 | 83 | ;; User entity (this establishes specs for a user's attributes) 84 | 85 | (s/def :user/name ::types/string) 86 | (s/def :user/email ::types/string) 87 | 88 | (defentity user 89 | (spec 90 | (s/keys :req [:workflo/id 91 | :user/name 92 | :user/email]))) 93 | 94 | 95 | ;; Specs for the command input data 96 | 97 | (s/def :create-user/user (get user :spec)) ; Reuse the user spec here 98 | (s/def :create-user/timestamp ::types/instant) 99 | 100 | 101 | ;; Implementation of a command to create new users 102 | 103 | (defcommand create-user 104 | "Creates a user and sends them a welcome email." 105 | (spec 106 | ;; The command expects a user and a timestamp 107 | (s/keys :req-un [:create-user/user 108 | :create-user/timestamp])) 109 | (query 110 | ;; Query all existing users with their emails 111 | [{users [user [email]]} :as existing-users]) 112 | (emit 113 | (let [;; Extract the new user from the input data 114 | new-user (get data :user) 115 | ;; Define a predicate for checking whether the user already exists 116 | matches-new-user? (fn [existing-user] 117 | (= (get existing-user :user/email) 118 | (get new-user :user/email)))] 119 | (if (some matches-new-user? existing-user) 120 | ;; Emit nothing if the user exists 121 | {} 122 | ;; Otherwise, create the user in the database and send an email 123 | ;; What you emit here is entirely up to you 124 | {:db [[:create new-user]] 125 | :email [{:from "Great App " 126 | :to (get new-user :user/email) 127 | :subject (str "Welcome to Great App, " 128 | (get new-user :user/email)}]}))))) 129 | 130 | 131 | ;; Run the command 132 | 133 | (commands/run-command! 'create-user 134 | {:user {:workflo/id "..." 135 | :user/name "John" 136 | :user/email "john@doe.org"} 137 | :timestamp (java.util.Date.now.)}) 138 | 139 | ;; -> {:db [[:create {:workflo/id "..." 140 | ;; :user/name "John" 141 | ;; :user/email "john@doe.org"}]] 142 | ;; :email [{:from "Great App " 143 | ;; :to "john@doe.org" 144 | ;; :subject (str "Welcome to Great App, John")}]} 145 | 146 | 147 | ;; Run the command again 148 | 149 | (commands/run-command! 'create-user 150 | {:user {:workflo/id "..." 151 | :user/name "John" 152 | :user/email "john@doe.org"} 153 | :timestamp (java.util.Date.now.)}) 154 | 155 | ;; -> {} 156 | ``` 157 | 158 | ## API documentation 159 | 160 | The following is a list of pointers to namespaces related to commands: 161 | 162 | * [workflo.macros.command](workflo.macros.command.html) 163 | - The `defcommand` macro 164 | - Command execution 165 | - Command registry 166 | - Command hooks 167 | * [workflo.macros.command.util](workflo.macros.command.util.html) 168 | - Internal utilities for `defcommand` 169 | * [workflo.macros.specs.command](workflo.macros.specs.command.html) 170 | - Specs for `defcommand` arguments -------------------------------------------------------------------------------- /docs/06-defservice.md: -------------------------------------------------------------------------------- 1 | # Services (defservice) 2 | 3 | In `workflo/macros`, services are components in the system that data (events, 4 | instructions or any other data) can be delivered to for processing. Unlike 5 | the reducer-flavored [commands](05-defcommand.md), services are (or can be) 6 | stateful and are expected to (but not required to) generate side-effects 7 | as part of their processing. 8 | 9 | Like commands, services can—as part of their processing—emit data for the 10 | system to act on further. 11 | 12 | In a *(Screens-)Views-Commands-Services* architecture, screens might render 13 | views, views might trigger commands, commands might emit to services 14 | and services might emit side-effects and updates to the app state so that the 15 | screens/views update. 16 | 17 | Some key features of services include: 18 | 19 | * **Service components** — `defservice` automatically creates 20 | [components](https://github.com/stuartsierra/component) for each service, 21 | allowing services to integrate seeminglessly in most Clojure systems; 22 | what is more, all running instances of service components are 23 | stored in a global registry so that they can be looked up by the 24 | service name (e.g. for data deliveries) 25 | * **Dependencies** — services can declare that they depend on other 26 | services; this can be used by frameworks such as 27 | [system](https://github.com/danielsz/system) to start service 28 | components in the right order and inject depenencies into dependent 29 | service components. 30 | * **Service queries** — like [commands](05-defcommand.md), services support 31 | a query to fetch additional information before processing incoming data 32 | * **Service registry** — services defined with `defservice` are stored 33 | in a global registry, from where they can be looked up at any time using 34 | their name 35 | * **Multi-service dispatching** — data in the form of a collection 36 | of `[SERVICE_NAME DATA]` (e.g. a key/value map or vector of tuples) 37 | can be delivered to multiple services at once by dispatching on 38 | the service names in the tuples; this is a handy feature when forwarding 39 | data emitted from [commands](05-defcommand.md) to services 40 | * **Delayed and debounced delivery** — services support hints that 41 | allow to delay deliveries by a certain time or to debounce deliveries 42 | (either all per service or based on the contents of the delivered data) 43 | 44 | ## Usage 45 | 46 | ### General structure 47 | 48 | ```clojure 49 | (defservice 50 | "description" ; Optional 51 | (hints [...]) ; Optional 52 | (dependencies [...]) ; Optional 53 | (query ...) ; Optional 54 | (spec ...) ; Optional (spec for delivery data) 55 | (start ...) ; Optional (executed when the service component starts) 56 | (stop ...) ; Optional (executed when the service component stops) 57 | (process ...) ; Optional (handles data deliveries) 58 | ) 59 | ``` 60 | 61 | ### Simple example 62 | 63 | ```clojure 64 | (require '[clojure.spec.alpha :as s] 65 | '[com.stuartsierra.component :as component] 66 | '[workflo.macros.service :as services :refer [defservice]]) 67 | 68 | 69 | ;; Specs for delivery data 70 | 71 | (s/def ::record (s/keys :req [:db/id])) 72 | 73 | (s/def ::create-operation (s/tuple #{:create} ::record)) 74 | (s/def ::update-operation (s/tuple #{:update} ::record)) 75 | (s/def ::delete-operation (s/tuple #{:delete} ::record)) 76 | 77 | (s/def ::operations 78 | (s/or :create ::create-operation 79 | :update ::update-operation 80 | :delete ::delete-operation)) 81 | 82 | 83 | ;; DB service 84 | 85 | (defservice db 86 | "A very simple database service." 87 | (spec 88 | (s/coll-of ::operations :kind vector?)) 89 | (start 90 | (println "Starting db") 91 | (assoc this :store (atom {}))) 92 | (stop 93 | (println "Stopping db") 94 | (dissoc this :store)) 95 | (process 96 | (doseq [[op op-data] data] 97 | (let [id (get op-data :db/id)] 98 | (case op 99 | :create (swap! (get this :store) assoc id op-data) 100 | :update (swap! (get this :store) update id merge op-data) 101 | :delete (swap! (get this :store) dissoc id)))))) 102 | 103 | 104 | ;; Create and start a DB service component 105 | 106 | (-> (services/new-service-component 'db {:optional :config}) 107 | (component/start)) 108 | 109 | 110 | ;; Deliver some data 111 | 112 | (services/deliver-to-services! 113 | {:db [[:create {:db/id 1 :user/name "John"}] 114 | [:create {:db/id 2 :user/name "Linda"}] 115 | [:update {:db/id 2 :user/email "linda@email.com"}] 116 | [:delete {:db/id 1}]]}) 117 | 118 | 119 | ;; Obtain the service component and print its store 120 | 121 | (-> (services/resolve-service-component 'db) 122 | (get :store) 123 | (deref) 124 | (println)) 125 | 126 | ;; -> {2 {:db/id 2 :user/name "Linda" :user/email "linda@email.com"}} 127 | ``` 128 | 129 | ## API documentation 130 | 131 | The following is a list of pointers to namespaces that are related to services: 132 | 133 | * [workflo.macros.service](workflo.macros.service.html) 134 | - The `defservice` macro 135 | - Service registry 136 | - Service component creation and registry 137 | - Service delivery 138 | - Service hooks 139 | * [workflo.macros.specs.service](workflo.macros.specs.service.html) 140 | - Specs for `defservice` arguments 141 | -------------------------------------------------------------------------------- /docs/07-defview.md: -------------------------------------------------------------------------------- 1 | # Views (defview) 2 | 3 | *Defining Om Next components in a compact way.* 4 | 5 | Om Next views are a combination of an Om Next component defined 6 | with `om.next/defui` and a component factory created with 7 | `om.next/factory`. The `defview` macro combines these two into 8 | a single definition and reduces the boilerplate code needed to 9 | define properties, idents, React keys, queries and component 10 | functions. 11 | 12 | ### Defining views 13 | 14 | Views are defined using `defview`, accepting the following 15 | information: 16 | 17 | * A query for properties (optional) 18 | * A query for computed properties (optional) 19 | * A `key` or `keyfn` function (optional) 20 | * A `validate` or `validator` function (optional) 21 | * An arbitrary number of regular Om Next, React or JS 22 | functions (e.g. `query`, `ident`, `componentWillMount` 23 | or `render`). 24 | 25 | A `(defview UserProfile ...)` expression defines both a 26 | `UserProfile` component and a `user-profile` component 27 | factory. 28 | 29 | ### Properties destructuring 30 | 31 | All properties declared via the queries are made available in 32 | component functions via an implicit `let` statement wrapping the 33 | original function body in the view definition. 34 | 35 | As an example, the properties query 36 | 37 | ```clojure 38 | [user [name email {friends ...}] [current-user _]] 39 | ``` 40 | 41 | would destructure the resulting properties as follows: 42 | 43 | ```clojure 44 | :user/name -> name 45 | :user/email -> email 46 | :user/friends -> friends 47 | :current-user -> current-user 48 | ``` 49 | 50 | ### Automatic Om Next query generation 51 | 52 | The `defview` macro predefines `(query ...)` for any view based 53 | on the properties query. Auto-generation currently supports joins, 54 | links, parameterization but no unions. 55 | 56 | As an example, the properties query 57 | 58 | ```clojure 59 | [user [name email {friends User}] [current-user _]] 60 | ``` 61 | 62 | would generate the following `query` function: 63 | 64 | ```clojure 65 | static om.next/IQuery 66 | (query [this] 67 | [:user/name 68 | :user/email 69 | {:user/friends (om/get-query User)} 70 | [:current-user _]]) 71 | ``` 72 | 73 | This can be overriden simply by implementing your own `query`: 74 | 75 | ```clojure 76 | (defview User 77 | [...] 78 | (query 79 | [:name :email])) 80 | ``` 81 | 82 | In the future, we will likely add a simple way to transform 83 | auto-generated queries. On idea is to implicitly bind the 84 | auto-generated query when overriding `query` and providing 85 | convenient methods to parameterize sub-queries, e.g. 86 | 87 | ```clojure 88 | (query 89 | (-> auto-query 90 | (set-param :user/friends :param :value) 91 | (set-param :current-user :id [:user 15]))) 92 | ``` 93 | 94 | ### Automatic inference of `ident` and `:keyfn` 95 | 96 | If the properties query includes `[db [id]]`, corresponding to 97 | the Om Next query attribute `:db/id`, it is assumed that the 98 | view represents data from DataScript or Datomic. In this case, 99 | `defview` will automatically infer `(ident ...)` and 100 | `(key ...)` / `:keyfn` functions based on the database ID. This 101 | behavior can be overriden by specifically defining both ident 102 | and key. 103 | 104 | As an example: 105 | 106 | ``` 107 | (defview User 108 | [db [id] user [name]]) 109 | ``` 110 | 111 | will result in the equivalent of: 112 | 113 | ``` 114 | (defui User 115 | static om/Ident 116 | (ident [this {:keys [db/id]}] 117 | [:db/id id])) 118 | 119 | (def user (om/factory User {:keyfn :db/id})) 120 | ``` 121 | 122 | ### Implicit binding of `this` and `props` in functions 123 | 124 | The names `this` and `props` are available inside 125 | function bodies depending on their signature (e.g. `render` 126 | only makes `this` available, whereas `ident` pre-binds `this` 127 | and `props`). 128 | 129 | ### Custom (raw) functions with their own argument vectors 130 | 131 | By default, `defview` implicitly assumes the arguments to custom 132 | functions, i.e., functions other than Om Next and React lifecycle 133 | functions, are `[this]`. However, when adding JS object functions 134 | to a view, you'll often want additional arguments. Here is an example 135 | highlighting the problem: 136 | 137 | ```clojure 138 | (defview UserList 139 | [users] 140 | (select ;; implictly adds [this] 141 | ;; where should the user ID come from? 142 | (om/transact! this `[(users/select {:user ~???})]))) 143 | ``` 144 | 145 | To solve this problem, `defview` supports a `.` syntax for custom 146 | function definitions. Any function that starts with a `.` will 147 | become a JS object function and its arguments will remain untouched: 148 | 149 | ```clojure 150 | (defview UserList 151 | [users] 152 | (.select [this user] 153 | (om/transact! this `[(users/select {:user ~user})]))) 154 | ``` 155 | 156 | Inside the view, this function can now be called with 157 | `(.select this )`, just like any other JS object function. 158 | 159 | ### Example 160 | 161 | ```clojure 162 | (ns foo.bar 163 | (:require [workflo.macros.view :refer [defview]])) 164 | 165 | (defview User 166 | [user [name email address {friends ...}]] 167 | [ui [selected?] select-fn] 168 | (key name) 169 | (validate (string? name)) 170 | (ident [:user/by-name name]) 171 | (render 172 | (dom/div #js {:className (when selected? "selected") 173 | :onClick (select-fn (om/get-ident this))} 174 | (dom/h1 nil "User: " name) 175 | (dom/p nil "Address: " street " " house)))) 176 | ``` 177 | 178 | Example usage in Om Next: 179 | 180 | ```clojure 181 | (user (om/computed {:user/name "Jeff" 182 | :user/email "jeff@jeff.org" 183 | :user/address {:street "Elmstreet" 184 | :house 13}} 185 | {:ui/selected? true 186 | :select-fn #(js/alert "Selected!"))) 187 | ``` 188 | -------------------------------------------------------------------------------- /docs/08-defscreen.md: -------------------------------------------------------------------------------- 1 | # Screens (defscreen) 2 | 3 | TODO -------------------------------------------------------------------------------- /resources/codox/theme/macros/macros.css: -------------------------------------------------------------------------------- 1 | /* Header */ 2 | 3 | #header { 4 | background: white; 5 | color: black; 6 | box-shadow: none; 7 | box-sizing: border-box; 8 | border-bottom: thin solid #e9edde; 9 | height: 2.5em; 10 | padding: 0 13px 0 13px; 11 | box-sizing: border-box; 12 | } 13 | 14 | #header h1 { 15 | text-shadow: none; 16 | height: 2.5rem; 17 | box-sizing: border-box; 18 | padding-top: 0.5em; 19 | } 20 | 21 | #header h2 { 22 | padding-top: 0.5em; 23 | color: black; 24 | } 25 | 26 | #header a { 27 | color: #ff3366; 28 | font-weight: 500; 29 | } 30 | 31 | /* Sidebar */ 32 | 33 | .sidebar { 34 | top: 2.5em; 35 | background: transparent !important; 36 | } 37 | 38 | .sidebar.primary { 39 | border-right: none; 40 | } 41 | 42 | .sidebar.secondary { 43 | border-right: none; 44 | } 45 | 46 | .sidebar a { 47 | color: black !important; 48 | } 49 | 50 | .sidebar h3 { 51 | padding-top: 1em; 52 | } 53 | .sidebar.primary li.current a { 54 | border-left: 3px solid #ff3366; 55 | color: #ff3366 !important; 56 | } 57 | 58 | .sidebar.secondary li.current a { 59 | border-left: 3px solid #4d5061; 60 | color: #4d5061 !important; 61 | } 62 | 63 | /* Content */ 64 | 65 | #content { 66 | top: 2.5em; 67 | } 68 | 69 | #content h1 { 70 | padding-top: 0.25em; 71 | } 72 | 73 | #content a, 74 | #content a:link, 75 | #content a:visited { 76 | text-decoration: none; 77 | border-bottom: thin solid #eee; 78 | } 79 | 80 | .index { 81 | font-size: 100%; 82 | line-height: 1.5em; 83 | } 84 | 85 | /* General */ 86 | 87 | body { 88 | font-family: 'Avenir Next', sans-serif; 89 | font-weight: 400; 90 | font-size: 15px; 91 | line-height: 150%; 92 | } 93 | 94 | pre, 95 | code { 96 | font-size: 13px; 97 | color: #5c80bc; 98 | background: transparent !important; 99 | font-family: 'Fira Mono', 'Monaco', 'DejaVu Sans Mono', Consolas, monospace; 100 | } 101 | 102 | pre { 103 | background: #fafafa !important; 104 | border: #e9edde !important; 105 | } 106 | 107 | h1 { 108 | color: #ff3366; 109 | } 110 | 111 | h2, 112 | h3, 113 | h4, 114 | h5, 115 | h6 { 116 | color: #011627; 117 | } 118 | 119 | a, 120 | a:visited, 121 | a:link { 122 | color: black; 123 | } 124 | -------------------------------------------------------------------------------- /resources/codox/theme/macros/theme.edn: -------------------------------------------------------------------------------- 1 | {:resources ["macros.css"] 2 | :transforms [[:head] [:append [:link {:rel "stylesheet" 3 | :type "text/css" 4 | :href "macros.css"}] 5 | [:script {:src "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/languages/elm.min.js"}]]]} 6 | -------------------------------------------------------------------------------- /resources/docs.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [workflo.macros.docs]} 2 | -------------------------------------------------------------------------------- /resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Workflo Macros Documentation 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /resources/screen-app.cljs.edn: -------------------------------------------------------------------------------- 1 | {:require [workflo.macros.examples.screen-app] 2 | :init-fns [workflo.macros.examples.screen-app/init]} 3 | -------------------------------------------------------------------------------- /resources/screen-app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Workflo Macros Screen App 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /scripts/update-gh-pages: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | TMPDIR=$(mktemp -d) 6 | 7 | boot docs \ 8 | && scp -r target/api-docs/ "$TMPDIR" \ 9 | && git checkout gh-pages \ 10 | && rm -rf target/ \ 11 | && scp -r "$TMPDIR/" . \ 12 | && rm -rf "$TMPDIR" \ 13 | && git add --all \ 14 | && git commit -m "Update API documentation" \ 15 | && git push origin gh-pages \ 16 | && git checkout master 17 | -------------------------------------------------------------------------------- /src/docs/workflo/macros/docs.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.docs 2 | (:require [devcards.core :refer [defcard start-devcard-ui!]] 3 | [workflo.macros.docs.util.string] 4 | [workflo.macros.docs.screen] 5 | [workflo.macros.docs.view])) 6 | 7 | (enable-console-print!) 8 | (start-devcard-ui!) 9 | 10 | (defcard 11 | "# Workflo Macros 12 | 13 | `workflo.macros` is a collection of Clojure and ClojureScript macros 14 | for web and mobile development.") 15 | -------------------------------------------------------------------------------- /src/docs/workflo/macros/docs/screen.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.docs.screen 2 | (:require [bidi.bidi :as bidi] 3 | [cljs.pprint :refer [pprint]] 4 | [devcards.core :refer-macros [defcard defcard-doc dom-node]] 5 | [workflo.macros.screen :as screen :refer [defscreen]] 6 | [workflo.macros.screen.bidi :as screen-bidi] 7 | [workflo.macros.view :as view :refer [defview]])) 8 | 9 | (defcard-doc 10 | "# `defscreen` macro") 11 | 12 | (defview UserList 13 | (render)) 14 | 15 | (defview UserProfile 16 | (render)) 17 | 18 | (defview UserSettings 19 | (render)) 20 | 21 | (defscreen users 22 | (url "users") 23 | (navigation 24 | {:title "Users"}) 25 | (sections 26 | {:content user-list})) 27 | 28 | (defscreen user 29 | (url "users/:user-id") 30 | (navigation 31 | {:title "User"}) 32 | (sections 33 | {:content user-profile})) 34 | 35 | (defscreen user-settings 36 | (url "users/:user-id/settings") 37 | (navigation 38 | {:title "User Settings"}) 39 | (sections 40 | {:content user-settings})) 41 | 42 | (defcard 43 | "## Registered screens 44 | 45 | ``` 46 | (defscreen users 47 | (url \"users\") 48 | (navigation 49 | (title \"Users\")) 50 | (sections 51 | {:content user-list})) 52 | 53 | (defscreen user 54 | (url \"user\") 55 | (navigation 56 | (title \"User\")) 57 | (sections 58 | {:content user-profile})) 59 | 60 | (defscreen user-settings 61 | (url \"users/:user-id/settings\") 62 | (navigation 63 | (title \"User Settings\")) 64 | (sections 65 | {:content user-settings})) 66 | ```" 67 | (screen/registered-screens)) 68 | 69 | (defcard 70 | "## Resolved `users` screen" 71 | (screen/resolve-screen 'users)) 72 | 73 | (defcard 74 | "## Routes 75 | 76 | ``` 77 | (workflo.macros.screen.bidi/routes) 78 | ```" 79 | (screen-bidi/routes)) 80 | 81 | (defcard 82 | "## Routing for users 83 | 84 | ``` 85 | [(bidi/path-for 'users) 86 | (bidi/match-route \"/users\") 87 | (workflo.macros.screen.bidi/match \"/users\")] 88 | ```" 89 | [(bidi/path-for (screen-bidi/routes) 'users) 90 | (bidi/match-route (screen-bidi/routes) "/users") 91 | (screen-bidi/match "/users")]) 92 | 93 | (defcard 94 | "## Routing for a user 95 | 96 | ``` 97 | [(bidi/path-for 'user :user-id 5) 98 | (bidi/match-route \"/users/10\") 99 | (workflo.macros.screen.bidi/match \"/users/10\")] 100 | ```" 101 | [(bidi/path-for (screen-bidi/routes) 'user :user-id 5) 102 | (bidi/match-route (screen-bidi/routes) "/users/10") 103 | (screen-bidi/match "/users/10")]) 104 | 105 | (defcard 106 | "## Routing for a user's settings 107 | 108 | ``` 109 | [(bidi/path-for 'user-settings :user-id 5) 110 | (bidi/match-route \"/users/15/settings\") 111 | (workflo.macros.screen.bidi/match \"/users/15/settings\")] 112 | ```" 113 | [(bidi/path-for (screen-bidi/routes) 'user-settings :user-id 5) 114 | (bidi/match-route (screen-bidi/routes) "/users/15/settings") 115 | (screen-bidi/match "/users/15/settings")]) 116 | -------------------------------------------------------------------------------- /src/docs/workflo/macros/docs/util/string.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.docs.util.string 2 | (:require [devcards.core :refer-macros [defcard defcard-doc]] 3 | [om.dom :as dom] 4 | [workflo.macros.util.string :refer [camel->kebab 5 | kebab->camel]])) 6 | 7 | (defcard-doc 8 | "# String utilities") 9 | 10 | (defcard 11 | "## Camel case to kebab case conversion" 12 | (fn [state _] 13 | (dom/table nil 14 | (dom/tbody nil 15 | (for [string @state] 16 | (dom/tr #js {:key string} 17 | (dom/td nil string) 18 | (dom/td nil (camel->kebab string))))))) 19 | ["User" "UserProfile" "UserProfileHeader" 20 | "user" "user-profile" "user-profile-header"]) 21 | 22 | (defcard 23 | "## Kebab case to camel case conversion" 24 | (fn [state _] 25 | (dom/table nil 26 | (dom/tbody nil 27 | (for [string @state] 28 | (dom/tr #js {:key string} 29 | (dom/td nil string) 30 | (dom/td nil (kebab->camel string))))))) 31 | ["User" "UserProfile" "UserProfileHeader" 32 | "user" "user-profile" "user-profile-header"]) 33 | -------------------------------------------------------------------------------- /src/docs/workflo/macros/docs/view.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.docs.view 2 | (:require [cljs.pprint :refer [pprint]] 3 | [devcards.core :refer-macros [defcard defcard-doc dom-node]] 4 | [om.next :as om] 5 | [om.dom :as dom] 6 | [workflo.macros.view :as view :refer [defview]])) 7 | 8 | (defcard-doc 9 | "# `defview` macro") 10 | 11 | ;;;; Example state with two users 12 | 13 | (def users-state 14 | {:users [{:user/name "Jeff" 15 | :user/email "jeff@jeff.org"} 16 | {:user/name "Joe" 17 | :user/email "joe@joe.org"}]}) 18 | 19 | ;;;; A minimalistic user parser with only a read function 20 | 21 | (defmulti read om/dispatch) 22 | 23 | (defmethod read :users 24 | [{:keys [query state]} key _] 25 | (let [st @state] 26 | {:value (om/db->tree query (get st key) st)})) 27 | 28 | (def users-parser 29 | (om/parser {:read read :mutate #()})) 30 | 31 | (def users-reconciler 32 | (om/reconciler {:state users-state 33 | :parser users-parser})) 34 | 35 | (defcard-doc 36 | "# Example: Users 37 | 38 | The User view renders a user and triggers a command 39 | when the user name is clicked. The command is executed 40 | using the `:run-command` hook that is configured 41 | with 42 | 43 | ``` 44 | (workflo.macros.view/configure-views! {:run-command ...}) 45 | ``` 46 | 47 | This allows arbitrary code to be executed, such as 48 | generating `om/transact!` calls or calling external 49 | services. 50 | 51 | ``` 52 | (defview User 53 | (query [user [name email]]) 54 | (key name) 55 | (ident [:user name]) 56 | (commands [open]) 57 | (render 58 | (dom/p nil 59 | \"Name: \" 60 | (dom/button #js {:onClick #(open {:user/name name})} 61 | name)) 62 | (dom/p nil \"Email: \" email)))) 63 | ``` 64 | 65 | The User and UserList views render multiple children, exercising 66 | the `:wrapper-view` feature that automatically wraps the content 67 | of `(render ...)` if it does not return a single element. 68 | 69 | The UserList view also demonstrates that, with views, the 70 | properties argument is optional: if no props are being used, 71 | no `nil` props need to be passed in: 72 | 73 | ``` 74 | (defview UserList 75 | (query [{users User}]) 76 | (render 77 | (header {:text \"First user\"}) 78 | (user (first users)) 79 | (header {:text \"Second user\"}) 80 | (user (second users)) 81 | (header {:text \"All users\"}) 82 | (for [u users] 83 | (user u)))) 84 | ```") 85 | 86 | (defview User 87 | (query [user [name email]]) 88 | (key name) 89 | (ident [:user name]) 90 | (commands [open]) 91 | (render 92 | (dom/p nil 93 | "Name: " 94 | (dom/button #js {:onClick #(open {:user/name name})} 95 | name)) 96 | (dom/p nil "Email: " email))) 97 | 98 | (defview Header 99 | (query [text]) 100 | (key text) 101 | (render 102 | (dom/h3 nil text))) 103 | 104 | (defview Wrapper 105 | (render 106 | (dom/section nil (om/children this)))) 107 | 108 | (view/configure-views! {:wrapper-view om.dom/article}) 109 | 110 | (defview UserList 111 | (query [{users User}]) 112 | (render 113 | (header {:text "First user"}) 114 | (user (first users)) 115 | (header {:text "Second user"}) 116 | (user (second users)) 117 | (header {:text "All users"}) 118 | (for [u users] 119 | (user u)))) 120 | 121 | (defcard 122 | "## The output of rendering a UserList with two users:" 123 | (dom-node 124 | (fn [_ node] 125 | (om/add-root! users-reconciler UserList node))) 126 | users-state) 127 | -------------------------------------------------------------------------------- /src/main/workflo/macros/bind.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.bind 2 | (:require [clojure.core.specs.alpha :as core-specs] 3 | [clojure.test :refer [is]] 4 | [clojure.spec.alpha :as s] 5 | [clojure.spec.gen.alpha :as gen] 6 | [clojure.spec.test.alpha :as st] 7 | [clojure.string :as string] 8 | [workflo.macros.query :as q] 9 | [workflo.macros.query.util :as util] 10 | [workflo.macros.specs.bind :as sb] 11 | [workflo.macros.specs.parsed-query :as spq] 12 | [workflo.macros.specs.query :as sq])) 13 | 14 | (s/fdef property-binding-paths 15 | :args (s/cat :property ::spq/typed-property 16 | :path (s/? (s/nilable ::sb/path))) 17 | :ret ::sb/paths) 18 | 19 | (defn property-binding-paths 20 | "Takes a `property` and an optional `path` of parents. Returns 21 | a flat vector of all paths inside the property and its 22 | parents. Each path is again a sequence of properties, 23 | starting from the leaf (e.g. a regular property) and 24 | ending at the root of the query. 25 | 26 | As an example, the query `[{user [user [name email]]]}]` 27 | would result in the query AST 28 | 29 | ```clojure 30 | [{:name user :type :join 31 | :join-source {:name user :type :property} 32 | :join-target [{:name user/name :type :property} 33 | {:name user/email :type :property}]}]. 34 | ``` 35 | 36 | Calling `property-binding-paths` with the join property 37 | would result in two paths, one for `user -> user/name` and 38 | one for `user -> user/email`: 39 | 40 | ```clojure 41 | [({:name user/name :type :property} 42 | {:name user :type :join :join-source ... :join-target ...}) 43 | ({:name user/email :type :property} 44 | {:name user :type :join :join-source ... :join-target ...})]. 45 | ```" 46 | ([property] 47 | (property-binding-paths property nil)) 48 | ([property path] 49 | (case (:type property) 50 | :property [(cons property path)] 51 | :link [(cons property path)] 52 | :join (let [new-path (cons property path)] 53 | (if (vector? (:join-target property)) 54 | (->> (:join-target property) 55 | (map #(property-binding-paths % new-path)) 56 | (apply concat) 57 | (into [new-path])) 58 | [new-path]))))) 59 | 60 | (s/fdef binding-paths 61 | :args (s/cat :query (s/or :query ::sq/query 62 | :parsed-query ::spq/query)) 63 | :ret ::sb/paths) 64 | 65 | (defn binding-paths 66 | "Returns a vector of all property binding paths for a `query` 67 | or parsed query." 68 | [query] 69 | (transduce (map property-binding-paths) concat [] 70 | (cond-> query 71 | (s/valid? ::sq/query query) q/conform-and-parse))) 72 | 73 | (s/fdef simplified-name 74 | :args (s/cat :sym-or-kw (s/or :sym (s/and symbol? #(not= "." (name %))) 75 | :kw (s/and keyword? #(not= "." (name %))))) 76 | :ret string?) 77 | 78 | (defn simplified-name 79 | [sym-or-kw] 80 | (last (string/split (name sym-or-kw) #"\."))) 81 | 82 | (s/fdef path-bindings 83 | :args (s/cat :path ::sb/path) 84 | :ret ::core-specs/map-bindings) 85 | 86 | (defn path-bindings 87 | "Takes a property binding path and returns destructuring map 88 | that can be used in combination with e.g. let to pluck the 89 | value of the corresponding property from a query result 90 | and bind it to the name of the property. 91 | 92 | E.g. for a property path `(a b/c d)` (simplified notation with 93 | only the property names), it would return `{{{a :a} :b/c} :d}`, 94 | allowing to destructure a map like `{:d {:b/c {:a }}}` 95 | and bind a to ``. 96 | 97 | It treats backref properties like `_b/c` in joins special by 98 | binding to `b` instead of `c` (the regular case) or `_b`." 99 | [[leaf & path]] 100 | (loop [form {(or (some-> leaf :alias simplified-name symbol) 101 | (if (util/backref-attr? (:name leaf)) 102 | (some-> leaf :name namespace simplified-name symbol) 103 | (some-> leaf :name simplified-name symbol))) 104 | (keyword (:name leaf))} 105 | path path] 106 | (if (empty? path) 107 | form 108 | (recur {form (keyword (:name (first path)))} 109 | (rest path))))) 110 | 111 | (s/fdef query-bindings 112 | :args (s/cat :query (s/or :query ::sq/query 113 | :parsed-query ::spq/query)) 114 | :ret ::core-specs/map-bindings) 115 | 116 | (defn query-bindings 117 | "Takes a `query` or parsed query and returns map bindings to 118 | be applied to the corresponding query result in order to 119 | destructure and bind all possible properties in the query 120 | result to their names. 121 | 122 | E.g. for a query `[a :as b c [d e {f [g :as h]}]]` it 123 | would return `{b :a, d :c/d, e :c/e, f :f, {{h :g} :f}}`." 124 | [query] 125 | (let [paths (binding-paths query) 126 | bindings (mapv path-bindings paths) 127 | merge-fn (fn [a b] 128 | (if (and (map? a) (map? b)) 129 | (merge a b) 130 | b)) 131 | combined (apply (partial merge-with merge-fn) bindings)] 132 | combined)) 133 | 134 | (s/fdef with-query-bindings* 135 | :args (s/cat :query (s/or :query ::sq/query 136 | :parsed-query ::spq/query) 137 | :result map? 138 | :body any?) 139 | :ret any?) 140 | 141 | (defn with-query-bindings* 142 | [query result body] 143 | (let [bindings (query-bindings query)] 144 | `(let [~bindings ~result] 145 | ~@body))) 146 | 147 | (defmacro with-query-bindings 148 | "Takes a `query`, a query `result` and an arbitrary code `body`. 149 | Wraps the code block so that all possible bindings derived 150 | from the query are bound to the values in the query result." 151 | [query result & body] 152 | (with-query-bindings* query result body)) 153 | -------------------------------------------------------------------------------- /src/main/workflo/macros/bind.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.bind 2 | (:require-macros [workflo.macros.bind :refer [with-query-bindings]]) 3 | (:require [clojure.spec.alpha] 4 | [workflo.macros.specs.query])) 5 | -------------------------------------------------------------------------------- /src/main/workflo/macros/command.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.command 2 | (:require-macros [workflo.macros.command :refer [defcommand]]) 3 | (:require [clojure.spec.alpha :as s] 4 | [workflo.macros.bind] 5 | [workflo.macros.hooks :refer [defhooks]] 6 | [workflo.macros.query :as q] 7 | [workflo.macros.registry :refer [defregistry]] 8 | [workflo.macros.specs.query :as specs.query])) 9 | 10 | ;;;; Command hooks 11 | 12 | ;; The following hooks are supported: 13 | ;; 14 | ;; :query - a function that takes a parsed query and the context 15 | ;; that was passed to `run-command!` by the caller; 16 | ;; this function is used to query a cache for data that 17 | ;; the command being executed needs to run. 18 | ;; 19 | ;; :auth-query - a function that takes a parsed auth query and the context 20 | ;; that was passed to `run-command!` by the caller; 21 | ;; this function is used to query a cache for data that 22 | ;; is needed to authorize the command execution. 23 | ;; 24 | ;; :before-emit - a function that is called just before a command emit 25 | ;; implementation is executed; gets passed the command 26 | ;; name, the command query result, the command data and 27 | ;; the command context; the return value of this hook 28 | ;; has no effect 29 | ;; 30 | ;; :process-emit - a function that is called after a command has 31 | ;; been executed; it takes the data returned from 32 | ;; the command emit function as well as the context 33 | ;; that was passed to `run-command` by the caller 34 | ;; and processes or transforms the command output in 35 | ;; whatever way is desirable. 36 | 37 | (defhooks command) 38 | 39 | ;;;; Command registry 40 | 41 | (defregistry command) 42 | 43 | ;;;; Command execution 44 | 45 | (defn run-command! 46 | ([cmd-name data] 47 | (run-command! cmd-name data nil)) 48 | ([cmd-name data context] 49 | (let [definition (resolve-command cmd-name)] 50 | (when (:spec definition) 51 | (assert (s/valid? (:spec definition) data) 52 | (str "Command data is invalid:" 53 | (s/explain-str (:spec definition) data)))) 54 | (process-command-hooks :before {:command cmd-name 55 | :data data 56 | :context context}) 57 | (let [bind-data (if (map? data) 58 | (merge context data) 59 | (merge context {:data data})) 60 | query (some-> (:query definition) 61 | (q/bind-query-parameters bind-data)) 62 | query-result (when query 63 | (-> (process-command-hooks :query 64 | {:command cmd-name 65 | :data data 66 | :query query 67 | :context context}) 68 | (get :query-result))) 69 | auth-query (some-> (:auth-query definition) 70 | (q/bind-query-parameters bind-data)) 71 | auth-query-result (when auth-query 72 | (-> (process-command-hooks :auth-query 73 | {:command cmd-name 74 | :data data 75 | :query-result query-result 76 | :query auth-query 77 | :context context}) 78 | (get :query-result))) 79 | authorized? (if-let [auth-fn (:auth definition)] 80 | (auth-fn query-result auth-query-result) 81 | true)] 82 | (if authorized? 83 | (do 84 | (process-command-hooks :before-emit {:command cmd-name 85 | :query-result query-result 86 | :data data 87 | :context context}) 88 | (let [output ((:emit definition) query-result data)] 89 | (-> (process-command-hooks :process-emit {:command cmd-name 90 | :output output 91 | :context context}) 92 | (get :output))) 93 | (process-command-hooks :after {:command cmd-name 94 | :data data 95 | :context context})) 96 | (do 97 | (process-command-hooks :after {:command cmd-name 98 | :data data 99 | :context context}) 100 | (throw (ex-info (str "Not authorized to run command: " cmd-name) 101 | {:command cmd-name 102 | :data data 103 | :context context})))))))) 104 | -------------------------------------------------------------------------------- /src/main/workflo/macros/command/util.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.command.util 2 | (:require [clojure.spec.alpha :as s] 3 | [workflo.macros.query] 4 | [workflo.macros.specs.command :as sc] 5 | [workflo.macros.specs.parsed-query :as spq] 6 | [workflo.macros.util.symbol])) 7 | 8 | ;;;; Utilities 9 | 10 | (s/fdef wrap-with-query-bindings 11 | :args (s/cat :body (s/spec ::sc/command-form-body) 12 | :query ::spq/query) 13 | :ret ::sc/command-form-body) 14 | 15 | (defn wrap-with-query-bindings 16 | [body query] 17 | `((~'workflo.macros.bind/with-query-bindings 18 | ~query ~'query-result ~@body))) 19 | -------------------------------------------------------------------------------- /src/main/workflo/macros/config.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.config 2 | (:require [clojure.spec.alpha :as s] 3 | [inflections.core :refer [plural]])) 4 | 5 | (s/def ::defconfig-args 6 | (s/cat :name symbol? 7 | :options (s/map-of keyword? any?))) 8 | 9 | (s/fdef defconfig* 10 | :args ::defconfig-args 11 | :ret any?) 12 | 13 | (defn defconfig* 14 | ([name options] 15 | (defconfig* name options nil)) 16 | ([name options env] 17 | (let [config-sym (symbol (str "+" name "-config+")) 18 | configure-sym (symbol (str "configure-" (plural name) "!")) 19 | get-config-sym (symbol (str "get-" name "-config"))] 20 | `(do 21 | (defonce ^:private ~config-sym (atom ~options)) 22 | (defn ~configure-sym 23 | [~'options] 24 | (swap! ~config-sym merge ~'options)) 25 | (defn ~get-config-sym 26 | ([] 27 | (deref ~config-sym)) 28 | ([~'option] 29 | (get (~get-config-sym) ~'option))))))) 30 | 31 | (defmacro defconfig 32 | [name options] 33 | (defconfig* name options &env)) 34 | -------------------------------------------------------------------------------- /src/main/workflo/macros/config.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.config 2 | (:require-macros [workflo.macros.config :refer [defconfig]])) 3 | -------------------------------------------------------------------------------- /src/main/workflo/macros/entity.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as string] 4 | [workflo.macros.bind] 5 | [workflo.macros.config :refer [defconfig]] 6 | [workflo.macros.entity.refs :as refs] 7 | [workflo.macros.entity.schema :as schema] 8 | [workflo.macros.query :as q] 9 | [workflo.macros.registry :refer [defregistry]] 10 | [workflo.macros.specs.entity] 11 | [workflo.macros.specs.query :as specs.query] 12 | [workflo.macros.util.form :as f] 13 | [workflo.macros.util.symbol :refer [unqualify]])) 14 | 15 | ;;;; Configuration options for the defentity macro 16 | 17 | (defconfig entity 18 | ;; Configures how entities are created with defentity and how aspects 19 | ;; like authorization are performed against them. Supports the 20 | ;; following options: 21 | ;; 22 | ;; :auth-query - a function that takes an environment and a parsed query 23 | ;; the query result from this function is then passed 24 | ;; to the entity's auth function to perform authorization. 25 | {:auth-query nil}) 26 | 27 | ;;;; Entity registry 28 | 29 | (declare entity-registered) 30 | (declare entity-unregistered) 31 | 32 | (defregistry entity (fn [event entity-name] 33 | (case event 34 | :register (entity-registered entity-name) 35 | :unregister (entity-unregistered entity-name)))) 36 | 37 | (defn entity-registered 38 | [entity-name] 39 | (let [entity-def (resolve-entity entity-name)] 40 | (refs/register-entity-refs! entity-name entity-def))) 41 | 42 | (defn entity-unregistered 43 | [entity-name] 44 | (refs/unregister-entity-refs! entity-name)) 45 | 46 | (defn entity-refs 47 | [entity-name] 48 | (refs/entity-refs entity-name)) 49 | 50 | (defn entity-backrefs 51 | [entity-name] 52 | (refs/entity-backrefs entity-name)) 53 | 54 | ;;;; Validation 55 | 56 | (defn validate 57 | [entity-or-name data] 58 | (let [entity (cond-> entity-or-name 59 | (symbol? entity-or-name) 60 | resolve-entity) 61 | spec (:spec entity)] 62 | (or (s/valid? spec data) 63 | (throw (Exception. 64 | (format "Validation of %s entity data failed: %s" 65 | (:name entity) 66 | (s/explain-str spec data))))))) 67 | 68 | ;;;; Authorization 69 | 70 | (defn authorized? 71 | [entity-or-name env entity-id viewer-id] 72 | (let [entity (cond-> entity-or-name 73 | (symbol? entity-or-name) 74 | resolve-entity) 75 | auth-fn (:auth entity)] 76 | (if auth-fn 77 | (let [auth-data {:entity-id entity-id 78 | :viewer-id viewer-id} 79 | query-result (when-let [query-hook (some-> (get-entity-config :auth-query) 80 | (partial env))] 81 | (some-> (:auth-query entity) 82 | (q/bind-query-parameters auth-data) 83 | (query-hook)))] 84 | (auth-fn query-result entity-id viewer-id)) 85 | true))) 86 | 87 | ;;;; Look up entity definitions for entity data 88 | 89 | (defn- entity-attrs [entity] 90 | (concat (schema/required-keys entity) 91 | (schema/optional-keys entity))) 92 | 93 | (def memoized-entity-attrs 94 | (memoize entity-attrs)) 95 | 96 | (defn entity-for-attr [attr] 97 | (some (fn [[_ entity]] 98 | (when (some #{attr} (memoized-entity-attrs entity)) 99 | entity)) 100 | (registered-entities))) 101 | 102 | (def memoized-entity-for-attr 103 | (memoize entity-for-attr)) 104 | 105 | (defn entity-for-data [data] 106 | (some (fn [[attr _]] 107 | (memoized-entity-for-attr attr)) 108 | (dissoc data :db/id))) 109 | 110 | ;;;; The defentity macro 111 | 112 | (s/fdef defentity* 113 | :args :workflo.macros.specs.entity/defentity-args 114 | :ret any?) 115 | 116 | (defn defentity* 117 | ([name forms] 118 | (defentity* name forms nil)) 119 | ([name forms env] 120 | (let [args-spec :workflo.macros.specs.entity/defentity-args 121 | args (if (s/valid? args-spec [name forms]) 122 | (s/conform args-spec [name forms]) 123 | (throw (Exception. 124 | (s/explain-str args-spec 125 | [name forms])))) 126 | description (:description (:forms args)) 127 | hints (:form-body (:hints (:forms args))) 128 | target-cljs? (boolean (:ns env)) 129 | auth-query (when-not target-cljs? 130 | (some-> args :forms :auth-query :form-body)) 131 | parsed-auth-query (when (and auth-query (not target-cljs?)) 132 | (q/parse auth-query)) 133 | auth (when-not target-cljs? 134 | (some-> args :forms :auth :form-body)) 135 | validation (:validation (:forms args)) 136 | spec (:form-body (:spec (:forms args))) 137 | name-sym (unqualify name) 138 | forms (-> (:forms args) 139 | (select-keys [:spec]) 140 | (vals) 141 | (cond-> 142 | true (conj {:form-name 'name}) 143 | description (conj {:form-name 'description}) 144 | hints (conj {:form-name 'hints}) 145 | auth-query (conj {:form-name 'auth-query}) 146 | auth (conj {:form-name 'auth}))) 147 | def-sym (f/qualified-form-name 'definition name-sym)] 148 | `(do 149 | ~(f/make-def name-sym 'name `'~name) 150 | ~@(when description 151 | `(~(f/make-def name-sym 'description description))) 152 | ~@(when hints 153 | `(~(f/make-def name-sym 'hints hints))) 154 | ~(f/make-def name-sym 'spec spec) 155 | ~@(when auth-query 156 | `((~'def ~(f/prefixed-form-name 'auth-query name-sym) 157 | '~parsed-auth-query))) 158 | ~@(when auth 159 | `(~(f/make-defn name-sym 'auth 160 | '[auth-query-result entity-id viewer-id] 161 | (if auth-query 162 | `((workflo.macros.bind/with-query-bindings 163 | ~parsed-auth-query ~'auth-query-result 164 | ~@auth)) 165 | auth)))) 166 | ~@(when validation 167 | `(~(f/make-def name-sym 'validation 168 | (:form-body validation)))) 169 | ~(f/make-def name-sym 'definition 170 | (f/forms-map forms name-sym)) 171 | (register-entity! '~name ~def-sym))))) 172 | 173 | (defmacro defentity 174 | [name & forms] 175 | (defentity* name forms &env)) 176 | -------------------------------------------------------------------------------- /src/main/workflo/macros/entity.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity 2 | (:require-macros [workflo.macros.entity :refer [defentity]]) 3 | (:require [workflo.macros.bind] 4 | [workflo.macros.config :refer [defconfig]] 5 | [workflo.macros.entity.refs :as refs] 6 | [workflo.macros.entity.schema :as schema] 7 | [workflo.macros.query :as q] 8 | [workflo.macros.registry :refer [defregistry]])) 9 | 10 | ;;;; Entity configuration 11 | 12 | (defconfig entity 13 | ;; Configures how entities are created with defentity and how aspects 14 | ;; like authorization are performed against them. Supports the 15 | ;; following options: 16 | ;; 17 | ;; :auth-query - a function that takes an environment and a parsed query 18 | ;; the query result from this function is then passed 19 | ;; to the entity's auth function to perform authorization. 20 | {:auth-query nil}) 21 | 22 | ;;;; Entity registry 23 | 24 | (declare entity-registered) 25 | (declare entity-unregistered) 26 | 27 | (defregistry entity (fn [event entity-name] 28 | (case event 29 | :register (entity-registered entity-name) 30 | :unregister (entity-unregistered entity-name)))) 31 | 32 | (defn entity-registered 33 | [entity-name] 34 | (let [entity-def (resolve-entity entity-name)] 35 | (refs/register-entity-refs! entity-name entity-def))) 36 | 37 | (defn entity-unregistered 38 | [entity-name] 39 | (refs/unregister-entity-refs! entity-name)) 40 | 41 | (defn entity-refs 42 | [entity-name] 43 | (refs/entity-refs entity-name)) 44 | 45 | (defn entity-backrefs 46 | [entity-name] 47 | (refs/entity-backrefs entity-name)) 48 | 49 | ;;;; Authorization 50 | 51 | (defn authorized? 52 | [entity-or-name env entity-id viewer-id] 53 | (let [entity (cond-> entity-or-name 54 | (symbol? entity-or-name) 55 | resolve-entity) 56 | auth-fn (:auth entity)] 57 | (if auth-fn 58 | (let [auth-data {:entity-id entity-id 59 | :viewer-id viewer-id} 60 | query-result (when-let [query-hook (some-> (get-entity-config :auth-query) 61 | (partial env))] 62 | (some-> (:auth-query entity) 63 | (q/bind-query-parameters auth-data) 64 | (query-hook)))] 65 | (auth-fn query-result entity-id viewer-id)) 66 | true))) 67 | 68 | ;;;; Look up entity definitions for entity data 69 | 70 | (defn- entity-attrs [entity] 71 | (concat (schema/required-keys entity) 72 | (schema/optional-keys entity))) 73 | 74 | (def memoized-entity-attrs 75 | (memoize entity-attrs)) 76 | 77 | (defn- entity-with-attr [attr] 78 | (some (fn [[_ entity]] 79 | (when (some #{attr} (memoized-entity-attrs entity)) 80 | entity)) 81 | (registered-entities))) 82 | 83 | (def memoized-entity-with-attr 84 | (memoize entity-with-attr)) 85 | 86 | (defn entity-for-data [data] 87 | (some (fn [[attr _]] 88 | (memoized-entity-with-attr attr)) 89 | (dissoc data :db/id))) 90 | -------------------------------------------------------------------------------- /src/main/workflo/macros/entity/datascript.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity.datascript 2 | (:require [datascript.core :as d] 3 | [workflo.macros.entity.schema :as es])) 4 | 5 | (defn attr-schema 6 | [attr-opts] 7 | (reduce (fn [opts opt] 8 | (cond-> opts 9 | (= :ref opt) 10 | (assoc :db/valueType :db.type/ref) 11 | (= :many opt) 12 | (assoc :db/cardinality :db.cardinality/many) 13 | (= :unique-value opt) 14 | (assoc :db/unique :db.unique/value) 15 | (= :unique-identity opt) 16 | (assoc :db/unique :db.unique/identity))) 17 | {} attr-opts)) 18 | 19 | (defn entity-schema 20 | "Returns the DataScript schema for an entity." 21 | [entity] 22 | (reduce (fn [schema [attr-name attr-opts]] 23 | (let [aschema (attr-schema attr-opts)] 24 | (cond-> schema 25 | (not (empty? aschema)) 26 | (assoc attr-name aschema)))) 27 | {} 28 | (es/entity-schema entity))) 29 | 30 | (defn merge-attr-schema 31 | "\"Merges\" two schemas for an attribute with the same name 32 | by throwing an exception if they are different and the 33 | first is not nil, and otherwise picking the second." 34 | [[attr-name prev-schema] [_ next-schema]] 35 | (if (nil? prev-schema) 36 | next-schema 37 | (if (= prev-schema next-schema) 38 | next-schema 39 | (let [err-msg (str "Conflicting schemas for attribute \"" 40 | attr-name "\": " 41 | (pr-str prev-schema) " != " 42 | (pr-str next-schema))] 43 | (throw #?(:cljs (js/Error. err-msg) 44 | :clj (Exception. err-msg))))))) 45 | 46 | (defn merge-schemas 47 | "Merge multiple DataScript schemas so that there are no 48 | conflicting attributes. The default behavior is to throw 49 | an exception if two schemas for the same attribute are 50 | different." 51 | ([schemas] 52 | (merge-schemas schemas merge-attr-schema)) 53 | ([schemas merge-fn] 54 | (reduce (fn [ret [attr-name attr-schema]] 55 | (update ret attr-name 56 | (fn [existing-schema] 57 | (merge-fn [attr-name existing-schema] 58 | [attr-name attr-schema])))) 59 | {} (apply concat schemas)))) 60 | -------------------------------------------------------------------------------- /src/main/workflo/macros/entity/datomic.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity.datomic 2 | (:require [datomic-schema.schema :as ds] 3 | [workflo.macros.entity.schema :as es] 4 | [workflo.macros.util.misc :refer [drop-keys]])) 5 | 6 | (defn split-schema 7 | "Takes an entity schema and splits it into multiple 8 | entity schemas based on attribute prefixes. An entity 9 | with a spec like 10 | 11 | (s/keys :req [:foo/bar :baz/ruux]) 12 | 13 | would yield an entity schema like 14 | 15 | {:foo/bar [...] 16 | :baz/ruux [...]} 17 | 18 | and would be split into 19 | 20 | {:foo {:foo/bar [...]} 21 | :baz {:bar/ruux [...]}}" 22 | [schema] 23 | (reduce (fn [ret [attr-name attr-def]] 24 | (let [prefix (keyword (namespace attr-name))] 25 | (if (nil? prefix) 26 | (throw (IllegalArgumentException. 27 | (str "Unqualified attribute not supported " 28 | "when generating a Datomic schema: " 29 | attr-name)))) 30 | (update ret prefix 31 | (fn [m] 32 | (assoc (or m {}) attr-name attr-def))))) 33 | {} schema)) 34 | 35 | (defn attr->field 36 | "Returns an attribute name and definition pair. Returns 37 | a pair suitable to be passed to datomic-schema's `schema*` 38 | function as a field." 39 | [[attr-name [attr-type & attr-opts]]] 40 | [(name attr-name) [attr-type (set attr-opts)]]) 41 | 42 | (defn datomic-schema 43 | "Generates a Datomic schema from an entity schema with 44 | a specific prefix." 45 | [[prefix schema]] 46 | (ds/schema* (name prefix) {:fields (map attr->field schema)})) 47 | 48 | (defn entity-schema 49 | "Returns the Datomic schema for an entity." 50 | [entity] 51 | (let [raw-schema (es/entity-schema entity) 52 | prefix-schemas (->> raw-schema 53 | (filter #(not= :db/id (first %))) 54 | (into {}) 55 | (split-schema)) 56 | datomic-schemas (map datomic-schema prefix-schemas)] 57 | (ds/generate-schema datomic-schemas))) 58 | 59 | (defn normalize-attr 60 | "Normalizes a Datomic schema attribute by converting 61 | it to a map if it's of the form `[:db/add ...]`." 62 | [attr] 63 | (if (vector? attr) 64 | (do 65 | (assert (= :db/add (first attr))) 66 | (let [kvs (into [:db/id] (rest attr))] 67 | (apply hash-map kvs))) 68 | attr)) 69 | 70 | (defn- merge-attr-schema 71 | "\"Merges\" two schemas for an attribute with the same name 72 | by throwing an exception if they are different and the 73 | first is not nil, and otherwise picking the second." 74 | [a b] 75 | (if (nil? a) 76 | b 77 | (let [a-without-id (drop-keys a [:db/id]) 78 | b-without-id (drop-keys b [:db/id])] 79 | (if (= a-without-id b-without-id) 80 | b 81 | (throw (Exception. 82 | (str "Conflicting schemas for attribute: " 83 | (pr-str a-without-id) " != " 84 | (pr-str b-without-id)))))))) 85 | 86 | (defn merge-schemas 87 | "Merge multiple Datomic schemas so that there are no 88 | conflicting attributes. The default behavior is to throw 89 | an exception if two schemas for the same attribute are 90 | different." 91 | ([schemas] 92 | (merge-schemas schemas merge-attr-schema)) 93 | ([schemas merge-fn] 94 | (transduce (map normalize-attr) 95 | (completing (fn [ret attr] 96 | (update ret (:db/ident attr) merge-fn attr)) 97 | vals) 98 | {} (apply concat schemas)))) 99 | -------------------------------------------------------------------------------- /src/main/workflo/macros/entity/refs.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity.refs 2 | (:require [workflo.macros.entity.schema :as es])) 3 | 4 | (def +refmap+ (atom {})) 5 | 6 | (defn- add-ref-and-backref! 7 | [source-entity attr ref-info] 8 | (let [target-entity (:entity ref-info)] 9 | (swap! +refmap+ (fn [refmap] 10 | (-> refmap 11 | (assoc-in [source-entity :refs attr] 12 | {:entity target-entity 13 | :many? (or (:many? ref-info) false)}) 14 | (assoc-in [target-entity :backrefs attr] 15 | {:entity source-entity 16 | :many? true})))))) 17 | 18 | (defn remove-backrefs-to 19 | [entity-name backrefs] 20 | (let [new-backrefs (into {} (remove (fn [[attr ref-info]] 21 | (= entity-name 22 | (:entity ref-info)))) 23 | backrefs)] 24 | (when (seq new-backrefs) 25 | new-backrefs))) 26 | 27 | (defn register-entity-refs! 28 | "Adds all refs defined by an entity to the intetnal refmap. 29 | This includes adding backrefs in the reverse direction." 30 | [entity-name entity-def] 31 | (when-not (es/simple-entity? entity-def) 32 | (let [refs (es/entity-refs entity-def)] 33 | (doseq [[attr ref-info] refs] 34 | (add-ref-and-backref! entity-name attr ref-info))))) 35 | 36 | (defn unregister-entity-refs! 37 | "Removes all refs defined by an entity from the internal refmap. 38 | This includes backrefs created for the refs of the entity." 39 | [entity-name] 40 | (swap! +refmap+ (fn [refmap] 41 | (reduce (fn [out [source-entity refs-and-backrefs]] 42 | (conj out [source-entity 43 | (update refs-and-backrefs :backrefs 44 | (partial remove-backrefs-to entity-name))])) 45 | {} (update refmap entity-name dissoc :refs))))) 46 | 47 | (defn entity-refs 48 | "Returns all references from an entity to other entities. The result 49 | is a map that maps attribute names (e.g. `:user/friends`) to reference 50 | infos (e.g. `{:entity 'user :many? true}`." 51 | [entity-name] 52 | (get-in @+refmap+ [entity-name :refs])) 53 | 54 | (defn entity-backrefs 55 | "Returns all references to an entity from other entities. The result 56 | is a map that maps attribute names (e.g. `:post/author`) to reference 57 | infos (e.g. `{:entity 'post :many? true}`." 58 | [entity-name] 59 | (get-in @+refmap+ [entity-name :backrefs])) 60 | -------------------------------------------------------------------------------- /src/main/workflo/macros/hooks.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.hooks 2 | (:require [clojure.spec.alpha :as s])) 3 | 4 | (s/fdef defhooks* 5 | :args (s/cat :name symbol?) 6 | :ret any?) 7 | 8 | (defn defhooks* 9 | ([name] 10 | (defhooks* name nil nil)) 11 | ([name env] 12 | (defhooks* name nil env)) 13 | ([name callback env] 14 | (let [hooks-sym (symbol (str "+" name "-hooks+")) 15 | register-sym (symbol (str "register-" name "-hook!")) 16 | unregister-sym (symbol (str "unregister-" name "-hook!")) 17 | registered-sym (symbol (str "registered-" name "-hooks")) 18 | reset-sym (symbol (str "reset-registered-" name "-hooks!")) 19 | process-sym (symbol (str "process-" name "-hooks"))] 20 | `(do 21 | (defonce ^:private ~hooks-sym (atom (sorted-map))) 22 | (defn ~register-sym [~'name ~'hook] 23 | (swap! ~hooks-sym update ~'name (comp set conj) ~'hook) 24 | ~@(when callback 25 | `((~callback :register ~'name)))) 26 | (defn ~unregister-sym [~'name ~'hook] 27 | ~@(when callback 28 | `((~callback :unregister ~'name))) 29 | (swap! ~hooks-sym update ~'name (fn [hooks#] 30 | (remove #{~'hook} hooks#)))) 31 | (defn ~registered-sym [] 32 | (deref ~hooks-sym)) 33 | (defn ~reset-sym [] 34 | (reset! ~hooks-sym (sorted-map))) 35 | (defn ~process-sym [~'name ~'args] 36 | (let [~'hooks (get (~registered-sym) ~'name)] 37 | (reduce (fn [~'args-out ~'hook] 38 | (~'hook ~'args-out)) 39 | ~'args ~'hooks))))))) 40 | 41 | (defmacro defhooks 42 | "Defines a set of hooks under the given name. See defhook* for 43 | more information." 44 | ([name] 45 | (defhooks* name &env)) 46 | ([name callback] 47 | (defhooks* name callback &env))) 48 | -------------------------------------------------------------------------------- /src/main/workflo/macros/hooks.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.hooks 2 | (:require-macros [workflo.macros.hooks :refer [defhooks]])) 3 | -------------------------------------------------------------------------------- /src/main/workflo/macros/jscomponents.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.jscomponents 2 | (:require [clojure.data.json :as json] 3 | [clojure.java.io :as io] 4 | [clojure.string :as str] 5 | [workflo.macros.util.string :refer [camel->kebab]])) 6 | 7 | (def camelize-keys workflo.macros.util.string/camelize-keys) 8 | 9 | (defn defjscomponent* 10 | [module name] 11 | (let [fn-sym (symbol (camel->kebab name)) 12 | module-sym (symbol "js" (str module))] 13 | `(defn ~fn-sym [props# & children#] 14 | (.apply ~(symbol "js" "React.createElement") nil 15 | (into-array 16 | (cons (~'aget ~module-sym ~(str name)) 17 | (cons (-> props# 18 | workflo.macros.jscomponents/camelize-keys 19 | ~'clj->js) 20 | children#))))))) 21 | 22 | (defn defjscomponents* 23 | [module] 24 | (let [filename (str module ".json") 25 | components (-> filename io/resource slurp json/read-str)] 26 | `(do 27 | ~@(map (partial defjscomponent* module) components)))) 28 | 29 | (defmacro defjscomponents 30 | "Defines ClojureScript functions of the form 31 | 32 | (defn [props & children] 33 | (js/React.createElement js/. 34 | (clj->js props) 35 | ... children ..)) 36 | 37 | for all component names listed in the file 38 | .json in the classpath. 39 | 40 | This allows to integrate an entire JavaScript React component 41 | library into a ClojureScript project (e.g. using Om Next) with 42 | a single (defjscomponents ComponentLibraryName) expression and 43 | a ComponentLibraryName.json file." 44 | [module] 45 | (defjscomponents* module)) 46 | -------------------------------------------------------------------------------- /src/main/workflo/macros/jscomponents.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.jscomponents 2 | (:require-macros [workflo.macros.jscomponents :refer [defjscomponents]]) 3 | (:require [workflo.macros.util.string])) 4 | 5 | (def camelize-keys workflo.macros.util.string/camelize-keys) 6 | -------------------------------------------------------------------------------- /src/main/workflo/macros/permission.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.permission 2 | (:require [clojure.spec.alpha :as s] 3 | [workflo.macros.registry :refer [defregistry]] 4 | [workflo.macros.specs.permission] 5 | [workflo.macros.util.macro :refer [definition-symbol]])) 6 | 7 | ;;;; Permission registry 8 | 9 | (defregistry permission) 10 | 11 | ;;;; The defpermission macro 12 | 13 | (defn make-permission-definition 14 | [permission-name {:keys [forms]}] 15 | (let [title (:form-body (:title forms)) 16 | description (:form-body (:description forms))] 17 | `(def ~(symbol (name (definition-symbol permission-name))) 18 | {:name ~(keyword permission-name) 19 | :title ~title 20 | :description ~description}))) 21 | 22 | (defn make-register-call 23 | [name args] 24 | `(register-permission! '~name ~(definition-symbol name))) 25 | 26 | (s/fdef defpermission* 27 | :args :workflo.macros.specs.permission/defpermission-args 28 | :ret any?) 29 | 30 | (defn defpermission* 31 | ([name forms] 32 | (defpermission* name forms nil)) 33 | ([name forms env] 34 | (let [args-spec :workflo.macros.specs.permission/defpermission-args 35 | args (if (s/valid? args-spec [name forms]) 36 | (s/conform args-spec [name forms]) 37 | (throw (Exception. (s/explain-str args-spec [name forms]))))] 38 | `(do 39 | ~(make-permission-definition name args) 40 | ~(make-register-call name args))))) 41 | 42 | (defmacro defpermission 43 | [name & args] 44 | (defpermission* name args &env)) 45 | -------------------------------------------------------------------------------- /src/main/workflo/macros/permission.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.permission 2 | (:require-macros [workflo.macros.permission :refer [defpermission]]) 3 | (:require [workflo.macros.registry :refer [defregistry]])) 4 | 5 | ;;;; Permission registry 6 | 7 | (defregistry permission) 8 | -------------------------------------------------------------------------------- /src/main/workflo/macros/query.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.query 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as str] 4 | [clojure.spec.gen.alpha :as gen] 5 | [workflo.macros.query.bind :as bind] 6 | [workflo.macros.registry :refer [defregistry]] 7 | [workflo.macros.specs.conforming-query :as conforming-query] 8 | [workflo.macros.specs.parsed-query :as parsed-query] 9 | [workflo.macros.specs.query :as q])) 10 | 11 | (declare conform-and-parse) 12 | 13 | ;;;; Registry for query fragments 14 | 15 | (defregistry query-fragment) 16 | 17 | ;;;; Query parsing 18 | 19 | (s/fdef query-type 20 | :args (s/cat :query (s/with-gen 21 | (s/and vector? 22 | (s/cat :first keyword? 23 | :rest (s/* any?))) 24 | #(gen/vector (s/gen keyword?) 25 | 1 10))) 26 | :ret keyword? 27 | :fn #(= (:ret %) (:first (:query (:args %))))) 28 | 29 | (defn query-type [query] 30 | (first query)) 31 | 32 | (s/def ::subquery 33 | (s/or :simple ::conforming-query/simple 34 | :link ::conforming-query/link 35 | :join ::conforming-query/join 36 | :property ::conforming-query/property 37 | :aliased-property ::conforming-query/aliased-property 38 | :prefixed-properties ::conforming-query/prefixed-properties 39 | :parameterization ::conforming-query/parameterization)) 40 | 41 | (s/fdef parse-subquery 42 | :args (s/cat :query ::subquery) 43 | :ret ::parsed-query/query) 44 | 45 | (defmulti parse-subquery 46 | "Takes a subquery and returns a vector of parsed properties, each 47 | in one of the following forms: 48 | 49 | {:name user/name :type :property :alias user-name} 50 | {:name user/email :type :property} 51 | {:name user/friends :type :join 52 | :join-source {name user/friends :type :property} 53 | :join-target User} 54 | {:name user/friends :type :join 55 | :join-source {:name user/friends :type :property} 56 | :join-target [{:name user/name :type :property}]} 57 | {:name current-user :type :link :link-id _}. 58 | 59 | Each of these may in addition contain an optional :parameters 60 | key with a {symbol ?variable}-style map and an :alias key 61 | with a symbol to use when destructuring instead of the 62 | original name." 63 | query-type) 64 | 65 | (defmethod parse-subquery :simple 66 | [[_ name]] 67 | [{:name name :type :property}]) 68 | 69 | (defmethod parse-subquery :link 70 | [[_ [link-name link-id]]] 71 | [{:name link-name :type :link :link-id link-id}]) 72 | 73 | (defmethod parse-subquery :join 74 | [[_ join]] 75 | (let [[type value] join 76 | [source target] (first value) 77 | join-source (first (parse-subquery source)) 78 | res [{:name (:name join-source) 79 | :type :join 80 | :join-source join-source}]] 81 | (case type 82 | :properties (assoc-in res [0 :join-target] 83 | (->> target 84 | (map parse-subquery) 85 | (apply concat) 86 | (into []))) 87 | :recursive (assoc-in res [0 :join-target] (second target)) 88 | :view (assoc-in res [0 :join-target] (second target))))) 89 | 90 | (defmethod parse-subquery :property 91 | [[_ q]] 92 | (parse-subquery q)) 93 | 94 | (defmethod parse-subquery :aliased-property 95 | [[_ q]] 96 | (let [{:keys [property alias]} q] 97 | (mapv #(assoc % :alias alias) (parse-subquery property)))) 98 | 99 | (defmethod parse-subquery :prefixed-properties 100 | [[_ {:keys [base children]}]] 101 | (letfn [(prefixed-name [sym] 102 | (symbol (str base) (str sym))) 103 | (prefix-name [x] 104 | (update x :name prefixed-name)) 105 | (prefix-join-source-name [x] 106 | (cond-> x 107 | (:join-source x) 108 | (update-in [:join-source :name] prefixed-name)))] 109 | (->> children 110 | (map parse-subquery) 111 | (apply concat) 112 | (map prefix-name) 113 | (map prefix-join-source-name) 114 | (into [])))) 115 | 116 | (defmethod parse-subquery :fragment 117 | [[_ q]] 118 | (let [fragment-name (keyword (subs (name q) 3))] 119 | (conform-and-parse (resolve-query-fragment fragment-name)))) 120 | 121 | (defmethod parse-subquery :parameterization 122 | [[_ {:keys [query parameters]}]] 123 | (assoc-in (parse-subquery query) [0 :parameters] parameters)) 124 | 125 | (defmethod parse-subquery :default 126 | [q] 127 | (cond 128 | ;; Workaround for extra [] around 129 | ;; [:aliased-property ...] in output of 130 | ;; conform (JIRA issue CLJ-2003) 131 | (and (vector? q) 132 | (= 1 (count q)) 133 | (vector? (first q)) 134 | (keyword? (ffirst q))) 135 | (parse-subquery (first q)) 136 | 137 | ;; Again, JIRA issue CLJ-2003 probably, triggered by 138 | ;; [a [b c] d [e f] h [i]] 139 | (and (vector? q) 140 | (every? vector? q) 141 | (every? (comp keyword? first) q)) 142 | (vec (mapcat parse-subquery q)) 143 | 144 | :else 145 | (let [msg (str "Unknown subquery: " q)] 146 | (throw (new #?(:cljs js/Error :clj Exception) msg))))) 147 | 148 | (s/fdef parse 149 | :args (s/cat :query ::conforming-query/query) 150 | :ret ::parsed-query/query) 151 | 152 | (defn parse [query] 153 | (transduce (map parse-subquery) 154 | (comp vec concat) 155 | [] query)) 156 | 157 | (s/fdef conform 158 | :args (s/cat :query ::q/query) 159 | :ret ::conforming-query/query) 160 | 161 | (defn conform [query] 162 | (s/conform ::q/query query)) 163 | 164 | (s/fdef conform-and-parse 165 | :args (s/cat :query ::q/query) 166 | :ret ::parsed-query/query) 167 | 168 | (defn conform-and-parse 169 | "Conforms and parses a query expression like 170 | 171 | [user [name :as nm email {friends User}] [current-user _]] 172 | 173 | into a flat vector of parsed properties with the following 174 | structure: 175 | 176 | [{:name user/name :type :property :alias nm} 177 | {:name user/email :type :property} 178 | {:name user/friends :type :join :join-target User} 179 | {:name current-user :type :link :link-id _}]. 180 | 181 | From this it is trivial to generate queries for arbitrary 182 | frameworks (e.g. Om Next) as well as keys for destructuring 183 | the results." 184 | [query] 185 | (parse (conform query))) 186 | 187 | (s/def ::map-destructuring-keys 188 | (s/coll-of symbol? :kind vector?)) 189 | 190 | (s/fdef map-destructuring-keys 191 | :args (s/cat :query ::parsed-query/query) 192 | :ret ::map-destructuring-keys 193 | :fn (s/and #(= (into #{} (:ret %)) 194 | (into #{} (map :name) (:query (:args %)))))) 195 | 196 | (defn map-destructuring-keys 197 | "Generates keys for destructuring a map of query results." 198 | [query] 199 | (into [] (map :name) query)) 200 | 201 | (defn bind-query-parameters 202 | "Takes a parsed query and a map of named parameters and their 203 | values. Binds the unbound parameters in the query (that is, 204 | those where the value is either a symbol beginning with a ? 205 | or a vector of such symbols) to values of the corresponding 206 | parameters in the parameter map and returns the result. 207 | 208 | As an example, the :db/id parameter in the query 209 | 210 | [{:name user :type :join 211 | :join-target [{:name name :type :property}] 212 | :parameters {:db/id ?foo 213 | :user/friend [?bar ?baz]}}] 214 | 215 | would be bound to the value 10 if the parameter map was 216 | {:foo 10 :bar {:baz :ruux}} and the :user/friend parameter 217 | would be bound to the value :ruux." 218 | [query params] 219 | (letfn [(bind-param [[k v]] 220 | [k (cond-> v 221 | (or (bind/var? v) 222 | (bind/path? v)) 223 | (bind/resolve params))]) 224 | (bind-params [unbound-params] 225 | (into {} (map bind-param) unbound-params)) 226 | (bind-query-params [subquery] 227 | (if (contains? subquery :parameters) 228 | (update subquery :parameters bind-params) 229 | subquery)) 230 | (follow-and-bind-joins [subquery] 231 | (if (vector? (get subquery :join-target)) 232 | (update subquery :join-target 233 | (partial mapv bind-query-params)) 234 | subquery))] 235 | (mapv (comp follow-and-bind-joins 236 | bind-query-params) 237 | query))) 238 | -------------------------------------------------------------------------------- /src/main/workflo/macros/query/bind.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.query.bind 2 | (:refer-clojure :exclude [resolve var?]) 3 | (:require [clojure.spec.alpha :as s] 4 | [clojure.spec.gen.alpha :as gen] 5 | [workflo.macros.specs.parsed-query])) 6 | 7 | (s/def ::var 8 | (s/with-gen 9 | (s/and symbol? 10 | #(= \? (first (str %)))) 11 | #(gen/fmap (fn [sym] 12 | (symbol (str "?" (str sym)))) 13 | (s/gen symbol?)))) 14 | 15 | (s/fdef var? 16 | :args (s/cat :x any?) 17 | :ret boolean? 18 | :fn #(= (:ret %) (s/valid? ::var (-> % :args :x)))) 19 | 20 | (defn var? 21 | [x] 22 | (and (symbol? x) 23 | (= \? (first (str x))))) 24 | 25 | (s/def ::path 26 | (s/coll-of ::var :kind vector? :min-count 0 :gen-max 10)) 27 | 28 | (s/fdef path? 29 | :args (s/cat :x any?) 30 | :ret boolean? 31 | :fn #(= (:ret %) (s/valid? ::path (-> % :args :x)))) 32 | 33 | (defn path? 34 | [x] 35 | (and (vector? x) 36 | (every? var? x))) 37 | 38 | (s/fdef denamespace 39 | :args (s/cat :x any?) 40 | :ret any? 41 | :fn (s/or :unnamed #(= (-> % :ret) (-> % :args :x)) 42 | :symbol (s/and #(symbol? (-> % :args :x)) 43 | #(= (-> % :ret) 44 | (symbol (name (-> % :args :x))))) 45 | :keyword (s/and #(keyword? (-> % :args :x)) 46 | #(= (-> % :ret) 47 | (keyword (name (-> % :args :x))))))) 48 | 49 | (defn denamespace 50 | [x] 51 | (cond-> x 52 | (keyword? x) ((comp keyword name)) 53 | (symbol? x) ((comp symbol name)))) 54 | 55 | (s/fdef denamespace-keys 56 | :args (s/cat :m (s/map-of any? any?)) 57 | :ret (s/map-of any? any?) 58 | :fn (s/and #(= (set (keys (-> % :ret))) 59 | (set (map denamespace 60 | (keys (-> % :args :m))))) 61 | (fn [{:keys [args ret]}] 62 | (every? (fn [[k v]] 63 | (= v (ret (denamespace k)))) 64 | (:m args))))) 65 | 66 | (defn denamespace-keys 67 | [m] 68 | (if (map? m) 69 | (zipmap (map denamespace (keys m)) 70 | (vals m)) 71 | m)) 72 | 73 | (s/fdef resolve-var 74 | :args (s/cat :var ::var :m any?) 75 | :ret any?) 76 | 77 | (defn resolve-var 78 | [var m] 79 | (let [vname (subs (str var) 1) 80 | kw (keyword vname)] 81 | (or (get m kw) 82 | (when (and (nil? (namespace kw)) 83 | (map? m)) 84 | (get (denamespace-keys m) kw))))) 85 | 86 | (s/fdef resolve-path 87 | :args (s/cat :path ::path 88 | :m any?) 89 | :ret any?) 90 | 91 | (defn resolve-path [path m] 92 | (loop [path path m m] 93 | (let [[var & remainder] path 94 | val (when (var? var) 95 | (resolve-var var m))] 96 | (cond->> val 97 | (and val (not (empty? remainder))) 98 | (recur remainder))))) 99 | 100 | (s/fdef resolve 101 | :args (s/cat :var-or-path (s/or :var ::var 102 | :path ::path) 103 | :m any?) 104 | :ret any?) 105 | 106 | (defn resolve 107 | [var-or-path m] 108 | (cond 109 | (var? var-or-path) (resolve-var var-or-path m) 110 | (path? var-or-path) (resolve-path var-or-path m) 111 | :else nil)) 112 | -------------------------------------------------------------------------------- /src/main/workflo/macros/query/om_util.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.query.om-util) 2 | 3 | 4 | (defn ident-expr? 5 | "Returns true if expr is an ident expression." 6 | [expr] 7 | (and (vector? expr) 8 | (= (count expr) 2) 9 | (keyword? (first expr)))) 10 | 11 | 12 | (defn join-expr? 13 | "Returns true if expr is a join expression." 14 | [expr] 15 | (and (map? expr) 16 | (= (count expr) 1))) 17 | 18 | 19 | (defn param-expr? 20 | "Returns true if q is a parameterized query expression." 21 | [expr] 22 | (or (and (seq? expr) 23 | (= (count expr) 2)) 24 | (and (seq? expr) 25 | (= (count expr) 3) 26 | (= 'clojure.core/list (first expr))))) 27 | 28 | 29 | (def join-source 30 | "Returns the source of a join query expression." 31 | ffirst) 32 | 33 | 34 | (def join-target 35 | "Return the target of a join query expression." 36 | (comp second first)) 37 | 38 | 39 | (defn param-query 40 | "Returns the query of a parameterized query expression." 41 | [expr] 42 | (if (= (count expr) 3) 43 | (second expr) 44 | (first expr))) 45 | 46 | 47 | (def param-map 48 | "Returns the parameter map of a parameterized query expression." 49 | last) 50 | 51 | 52 | (def ident-name 53 | "Returns the name of an ident expression." 54 | first) 55 | 56 | 57 | (defn dispatch-key 58 | "Returns the key the results for a query expression will be 59 | stored under in the query result map. E.g. the query 60 | `{:foo [:bar :baz]}` would store its results under `:foo`, 61 | whereas the query `{:bar [:foo]}` would store its results 62 | under `:bar`." 63 | [expr] 64 | (cond 65 | (keyword? expr) expr 66 | (number? expr) nil 67 | (ident-expr? expr) (dispatch-key (ident-name expr)) 68 | (join-expr? expr) (dispatch-key (join-source expr)) 69 | (param-expr? expr) (dispatch-key (param-query expr)) 70 | :else (throw (ex-info "Invalid query expression passed to `dispatch-key`" 71 | {:expression expr})))) 72 | 73 | 74 | (defn expr-type 75 | "Returns the type of the expression as a keyword (e.g. 76 | :keyword, :ident, :join, :param)." 77 | [expr] 78 | (cond 79 | (keyword? expr) :keyword 80 | (number? expr) :limited-recursion 81 | (= '... expr) :unlimited-recursion 82 | (ident-expr? expr) :ident 83 | (join-expr? expr) :join 84 | (param-expr? expr) :param)) 85 | -------------------------------------------------------------------------------- /src/main/workflo/macros/query/util.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.query.util 2 | (:require [clojure.pprint :refer [pprint]] 3 | [clojure.spec.alpha :as s] 4 | [clojure.spec.gen.alpha :as gen] 5 | [clojure.string :refer [capitalize]] 6 | [inflections.core :as inflections])) 7 | 8 | (s/fdef one-item? 9 | :args (s/cat :coll coll?) 10 | :ret boolean? 11 | :fn (s/or :true (s/and #(= 1 (count (:coll (:args %)))) 12 | #(true? (:ret %))) 13 | :false (s/and #(not= 1 (count (:coll (:args %)))) 14 | #(false? (:ret %))))) 15 | 16 | (defn one-item? 17 | [coll] 18 | (= 1 (count coll))) 19 | 20 | (s/fdef combine-properties-and-groups 21 | :args (s/cat :props-and-groups vector?) 22 | :ret vector?) 23 | 24 | (defn combine-properties-and-groups 25 | "Takes a vector of property names or [base subprops] groups, 26 | e.g. [foo [bar [baz ruux]]], and returns a flat vector into which 27 | the group vectors are spliced, e.g. [foo bar [bax ruux]]." 28 | [props-and-groups] 29 | (transduce (map (fn [prop-or-group] 30 | (cond-> prop-or-group 31 | (not (vector? prop-or-group)) 32 | vector))) 33 | (comp vec concat) 34 | [] 35 | props-and-groups)) 36 | 37 | 38 | (s/fdef capitalized-name 39 | :args (s/cat :x (s/and symbol? 40 | #(not (nil? (name %))))) 41 | :ret string? 42 | :fn (s/and #(= (first (:ret %)) 43 | (first (capitalize (name (:x (:args %)))))) 44 | #(= (rest (:ret %)) 45 | (rest (name (:x (:args %))))))) 46 | 47 | (defn capitalized-name 48 | "Returns the name of a symbol, keyword or string, with the first 49 | letter capitalized." 50 | [x] 51 | (apply str 52 | (capitalize (first (name x))) 53 | (rest (name x)))) 54 | 55 | (s/fdef capitalized-symbol? 56 | :args (s/cat :x any?) 57 | :ret boolean? 58 | :fn (s/or 59 | :capitalized-symbol 60 | (s/and #(symbol? (:x (:args %))) 61 | #(= (first (name (:x (:args %)))) 62 | (first (capitalize (first (name (:x (:args %))))))) 63 | #(true? (:ret %))) 64 | :other 65 | #(false? (:ret %)))) 66 | 67 | (defn capitalized-symbol? 68 | "Returns true if x is a symbol that starts with a capital letter." 69 | [x] 70 | (and (symbol? x) 71 | (= (name x) 72 | (capitalized-name x)))) 73 | 74 | (defn print-spec-gen 75 | "Takes a spec (e.g. as a keyword or symbol) and pretty-prints 76 | 10 random values generated for this spec." 77 | [spec] 78 | (println "Values generated from spec" (name spec)) 79 | (try 80 | (pprint (gen/sample (s/gen spec) 10)) 81 | (catch #?(:cljs js/Object :clj Exception) e 82 | (println "Error: Spec not found" e)))) 83 | 84 | ;;;; Backref attributes 85 | 86 | (defn backref-attr? 87 | "Returns true if an attribute is a backref attribute, that is, 88 | if it has a namespace and name and the name that starts with `_`." 89 | [attr] 90 | (let [ns (namespace attr) 91 | nm (name attr)] 92 | (and ns (= (subs nm 0 1) "_")))) 93 | 94 | (defn singular-backref-attr? 95 | "Returns true if a backref attribute represents a singular (as 96 | opposed to plural) result, that is, if the namespace is 97 | singular." 98 | [attr] 99 | (let [ns (namespace attr)] 100 | (and (backref-attr? attr) 101 | (= ns (inflections/singular ns))))) 102 | -------------------------------------------------------------------------------- /src/main/workflo/macros/registry.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.registry 2 | (:require [clojure.spec.alpha :as s] 3 | [inflections.core :refer [plural]])) 4 | 5 | (defn throw-registry-error 6 | "Throws an error generated by a registry." 7 | [msg] 8 | (throw (Exception. msg))) 9 | 10 | (s/fdef defregistry* 11 | :args (s/cat :name symbol?) 12 | :ret any?) 13 | 14 | (defn defregistry* 15 | "Defines a registry with the given name. The resulting registry 16 | maps names (e.g. screen, view or command names) to definitions 17 | (e.g. the definition of a screen, a command or a view). 18 | 19 | Definitions in the registry can be looked up using one of the 20 | utility functions that are defined implicitly. 21 | 22 | (defregistry* 'command) 23 | (defregistry* 'command 24 | (fn [event entity-name] 25 | ...)) 26 | 27 | will result in the following functions to be defined: 28 | 29 | (defn register-command! [name def] ...) 30 | (defn unregister-command! [name] ...) 31 | (defn registered-commands [] ...) 32 | (defn reset-registered-commands! [] ...) 33 | (defn resolve-command [name] ...)." 34 | ([name] 35 | (defregistry* name nil nil)) 36 | ([name env] 37 | (defregistry* name nil nil)) 38 | ([name callback env] 39 | (let [registry-sym (symbol (str "+" name "-registry+")) 40 | register-sym (symbol (str "register-" name "!")) 41 | unregister-sym (symbol (str "unregister-" name "!")) 42 | registered-sym (symbol (str "registered-" (plural name))) 43 | reset-sym (symbol (str "reset-registered-" 44 | (plural name) "!")) 45 | resolve-sym (symbol (str "resolve-" name))] 46 | `(do 47 | (defonce ^:private ~registry-sym (atom (sorted-map))) 48 | (defn ~register-sym 49 | [~'name ~'def] 50 | (swap! ~registry-sym assoc ~'name ~'def) 51 | ~@(when callback 52 | `((~callback :register ~'name)))) 53 | (defn ~unregister-sym 54 | [~'name] 55 | ~@(when callback 56 | `((~callback :unregister ~'name))) 57 | (swap! ~registry-sym dissoc ~'name)) 58 | (defn ~registered-sym 59 | [] 60 | (deref ~registry-sym)) 61 | (defn ~reset-sym 62 | [] 63 | (reset! ~registry-sym (sorted-map))) 64 | (defn ~resolve-sym 65 | [~'name] 66 | (let [~'definition (get (~registered-sym) ~'name)] 67 | (when (nil? ~'definition) 68 | (let [~'msg (str "Failed to resolve " ~(str name) 69 | " '" ~'name "'")] 70 | (throw-registry-error ~'msg))) 71 | ~'definition)))))) 72 | 73 | (defmacro defregistry 74 | "Defines a registry with the given name. See defregistry* for 75 | more information." 76 | ([name] 77 | (defregistry* name &env)) 78 | ([name callback] 79 | (defregistry* name callback &env))) 80 | -------------------------------------------------------------------------------- /src/main/workflo/macros/registry.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.registry 2 | (:require-macros [workflo.macros.registry :refer [defregistry]])) 3 | 4 | (defn throw-registry-error 5 | [msg] 6 | (throw (js/Error. msg))) 7 | -------------------------------------------------------------------------------- /src/main/workflo/macros/screen.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.screen 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as string] 4 | [workflo.macros.screen.util :as util] 5 | [workflo.macros.specs.screen] 6 | [workflo.macros.util.form :as f] 7 | [workflo.macros.util.string :refer [camel->kebab]] 8 | [workflo.macros.util.symbol :refer [unqualify]])) 9 | 10 | (s/fdef defscreen* 11 | :args :workflo.macros.specs.screen/defscreen-args 12 | :ret any?) 13 | 14 | (defn defscreen* 15 | ([name forms] 16 | (defscreen* name forms nil)) 17 | ([name forms env] 18 | (let [args-spec :workflo.macros.specs.screen/defscreen-args 19 | args (if (s/valid? args-spec [name forms]) 20 | (s/conform args-spec [name forms]) 21 | (throw (Exception. 22 | (s/explain-str args-spec 23 | [name forms])))) 24 | description (:description (:forms args)) 25 | name-sym (unqualify name) 26 | forms (-> (:forms args) 27 | (select-keys [:url :sections]) 28 | (vals) 29 | (cond-> 30 | true (conj {:form-name 'name}) 31 | true (conj {:form-name 'forms}) 32 | description (conj {:form-name 'description}))) 33 | field-forms (:forms (:forms args))] 34 | `(do 35 | ~(f/make-def-quoted name-sym 'name name) 36 | ~@(when description 37 | `(~(f/make-def name-sym 'description description))) 38 | ~(f/make-def name-sym 'url 39 | (let [url-str (:form-body (:url (:forms args)))] 40 | {:string url-str 41 | :segments (util/url-segments url-str)})) 42 | ~(f/make-def name-sym 'forms 43 | (zipmap (map (comp keyword :form-name) field-forms) 44 | (map :form-body field-forms))) 45 | ~(f/make-def name-sym 'sections 46 | (:form-body (:sections (:forms args)))) 47 | ~(f/make-def name-sym 'definition 48 | (f/forms-map forms name-sym)) 49 | (register-screen! '~name-sym 50 | ~(f/qualified-form-name 51 | 'definition name-sym)))))) 52 | 53 | 54 | (defmacro defscreen 55 | "Create a new screen with the given name." 56 | [name & forms] 57 | (defscreen* name forms &env)) 58 | -------------------------------------------------------------------------------- /src/main/workflo/macros/screen.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.screen 2 | (:require-macros [workflo.macros.screen :refer [defscreen]]) 3 | (:require [workflo.macros.config :refer [defconfig]] 4 | [workflo.macros.registry :refer [defregistry]] 5 | [workflo.macros.util.js :refer [js-resolve]])) 6 | 7 | ;;;; Configuration options for the defscreen macro 8 | 9 | (defconfig screen {}) 10 | 11 | ;;;; Screen registry 12 | 13 | (defregistry screen) 14 | -------------------------------------------------------------------------------- /src/main/workflo/macros/screen/bidi.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.screen.bidi 2 | (:require [bidi.bidi :as bidi] 3 | [bidi.router :as br] 4 | [workflo.macros.screen :as scr])) 5 | 6 | (defn route 7 | "Returns a bidi route for the given screen." 8 | [[screen-name screen]] 9 | (let [segments (:segments (:url screen))] 10 | [(-> segments 11 | (interleave (repeat "/")) 12 | (butlast) 13 | (vec)) 14 | screen-name])) 15 | 16 | (defn routes 17 | "Returns combined bidi routes for all registered screens." 18 | [] 19 | ["/" (mapv route (scr/registered-screens))]) 20 | 21 | (defn match-location 22 | [location] 23 | {:screen (scr/resolve-screen (-> location :handler name symbol)) 24 | :params (:route-params location)}) 25 | 26 | (defn match 27 | "Matches a URL against all screen routes. Returns a 28 | {:params :screen } map, where :screen 29 | holds the screen for the URL and the route params map all 30 | parameterizable URL segments (e.g. :user-id) to their values 31 | in the URL." 32 | [url] 33 | (when-let [location (bidi/match-route (routes) url)] 34 | (match-location location))) 35 | 36 | (defn path 37 | "Returns a URL path for the given screen and the given URL 38 | parameters. Accepts both screen names and screens. For example, 39 | 40 | (path 'user :user-id 1) 41 | (path (workflo.macros.screen/resolve-screen 'user) :user-id 1) 42 | 43 | are both acceptable uses of this function." 44 | [screen-or-name & params] 45 | (let [screen-name (cond-> screen-or-name 46 | (map? screen-or-name) 47 | :name)] 48 | (apply (partial bidi/path-for (routes) screen-name) 49 | params))) 50 | 51 | (defn- on-navigate 52 | [env location] 53 | (let [{:keys [screen params]} (match-location location)] 54 | (some-> env :mount-screen (apply [screen params])))) 55 | 56 | (defn router 57 | [{:keys [default-screen 58 | default-params 59 | mount-screen] 60 | :or {default-screen 'home 61 | default-params {}} 62 | :as env}] 63 | (br/start-router! (routes) 64 | {:on-navigate #(on-navigate env %) 65 | :default-location {:handler default-screen 66 | :route-params default-params}})) 67 | 68 | (defn goto! 69 | [router screen params] 70 | (let [location {:handler screen :route-params params}] 71 | (br/set-location! router location))) 72 | -------------------------------------------------------------------------------- /src/main/workflo/macros/screen/om_next.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.screen.om-next 2 | (:require [clojure.walk :refer [walk]] 3 | [com.stuartsierra.component :as component] 4 | [om.next :as om] 5 | [workflo.macros.screen.bidi :as sb] 6 | [workflo.macros.screen :as scr] 7 | [workflo.macros.util.string :refer [kebab->camel]] 8 | [workflo.macros.view :refer [defview resolve-view]])) 9 | 10 | ;;;; Remember the active screen 11 | 12 | (defonce ^:private +active-screen+ 13 | (atom {:screen nil :params nil})) 14 | 15 | (defn active-screen 16 | "Returns the active screen as a map, with :screen storing 17 | the current screen and :params storing the URL parameters 18 | for the current screen." 19 | [] 20 | @+active-screen+) 21 | 22 | (defn set-active-screen! 23 | "Sets the active screen and its parameters." 24 | [screen params] 25 | (swap! +active-screen+ assoc 26 | :screen screen 27 | :params params)) 28 | 29 | (defn reload-active-screen! 30 | "Reloads the implementation of the active screen by replacing 31 | it with a freshly resolved instance from the screen registry." 32 | [] 33 | (swap! +active-screen+ update :screen 34 | (fn [screen] 35 | (some-> screen :name scr/resolve-screen)))) 36 | 37 | ;;;; Wrapping Om Next parser to handle screen-based routing 38 | 39 | (defn read-screen 40 | "Executes a query agains the active screen." 41 | [read {:keys [parser query] :as env} _ _] 42 | {:value (parser (assoc env :screen (:screen (active-screen))) 43 | query nil)}) 44 | 45 | (defn read-forms 46 | "Queries the forms of the active screen." 47 | [_ {:keys [screen]} _ _] 48 | {:value (:forms screen)}) 49 | 50 | (defn read-sections 51 | "Executes all queries for views in the sections of the 52 | active screen." 53 | [_ {:keys [parser query screen] :as env} _ _] 54 | {:value 55 | (reduce (fn [res section-item] 56 | (let [[section query] (first section-item)] 57 | (assoc res section 58 | {:view (-> screen :sections section resolve-view :factory) 59 | :props (parser (dissoc env :path) query nil)}))) 60 | {} query)}) 61 | 62 | (defn- wrapping-read 63 | "Wraps a user-provided parser read function for Om Next by 64 | special-casing queries for screen-based routing information 65 | (such as :workflo/screen, :workflo/forms and 66 | :workflo/sections.)" 67 | [read env key params] 68 | (case key 69 | :workflo/screen (read-screen read env key params) 70 | :workflo/forms (read-forms read env key params) 71 | :workflo/sections (read-sections read env key params) 72 | (read env key params))) 73 | 74 | (defn parser 75 | "Returns a parser that wraps the provided read function to 76 | catch queries for screen-based routing information." 77 | [{:keys [read mutate]}] 78 | (om/parser {:read (partial wrapping-read read) 79 | :mutate mutate})) 80 | 81 | ;;;; Root component 82 | 83 | (defonce ^:private +root-component+ 84 | (atom {:factory nil :js? false})) 85 | 86 | (defn root-component 87 | "Returns the root component to be used." 88 | [] 89 | @+root-component+) 90 | 91 | (defn set-root-component! 92 | "Configures the root component to be used." 93 | [factory js?] 94 | (swap! +root-component+ assoc 95 | :factory factory 96 | :js? js?)) 97 | 98 | ;;;; Wrapping application 99 | 100 | (defn- realize-sections 101 | "Realizes the results of a sections query by instantiating 102 | all returned views with their props / query results." 103 | [sections] 104 | (zipmap (keys sections) 105 | (map (fn [{:keys [view props] :as item}] 106 | (view (vary-meta props assoc :om-path (-> item meta :om-path)))) 107 | (vals sections)))) 108 | 109 | (defn camel-cased-prop-map 110 | "Convert a Clojure map with Om Next properties to a map 111 | where all keys are camel-cased strings that can be accessed 112 | like object properties in JS/React." 113 | [m] 114 | (walk (fn [[k v]] 115 | [(kebab->camel (name k)) 116 | (cond-> v (map? v) camel-cased-prop-map)]) 117 | identity 118 | m)) 119 | 120 | (defview RootWrapper 121 | (query [{workflo/screen [workflo [forms sections]]}]) 122 | (render 123 | (let [forms (:workflo/forms screen) 124 | sections (realize-sections (:workflo/sections screen)) 125 | root-info (root-component)] 126 | ((:factory root-info) 127 | (if (:js? root-info) 128 | (do 129 | (clj->js (merge {:sections (camel-cased-prop-map sections)} 130 | (camel-cased-prop-map forms)))) 131 | (merge {:sections sections} forms)))))) 132 | 133 | ;;;; Routing 134 | 135 | (defn mount-screen 136 | "Mounts a screen with parameters by setting it as the active 137 | screen and updating the root wrapper's component query 138 | according to the screen's sections." 139 | [app screen params] 140 | (let [c (om/app-root (:reconciler (:config app))) 141 | query [{:workflo/screen 142 | [:workflo/forms 143 | {:workflo/sections 144 | (mapv (fn [[section view-name]] 145 | {section (or (some-> view-name resolve-view :view om/get-query) 146 | [])}) 147 | (:sections screen))}]}]] 148 | (set-active-screen! screen params) 149 | (some-> app :config :screen-mounted 150 | (apply [app screen params])) 151 | (om/set-query! c {:params params :query query}))) 152 | 153 | ;;;; Application bootstrapping 154 | 155 | (defprotocol IApplication 156 | (mount [this]) 157 | (reload [this]) 158 | (goto [this screen params])) 159 | 160 | (defrecord Application [config router] 161 | component/Lifecycle 162 | (start [this] 163 | (mount this) 164 | (assoc this :router 165 | (sb/router 166 | {:default-screen (:default-screen config) 167 | :mount-screen (partial mount-screen this)}))) 168 | 169 | (stop [this] 170 | (dissoc this :router)) 171 | 172 | IApplication 173 | (mount [this] 174 | (om/add-root! (:reconciler config) RootWrapper (:target config))) 175 | 176 | (reload [this] 177 | (reload-active-screen!) 178 | (mount this) 179 | (let [{:keys [screen params]} (active-screen)] 180 | (mount-screen this screen params))) 181 | 182 | (goto [this screen params] 183 | (sb/goto! router screen params))) 184 | 185 | (defn application 186 | "Creates an Om Next application that implements screen-based 187 | routing. Takes an Om Next reconciler, a default screen, a 188 | target DOM element, a root component and a flag as to whether 189 | or not the root component is a JS component. 190 | 191 | The root component is expected to accept the properties 192 | defined for screens, so `:sections` and arbitrary other forms. 193 | `:sections` is a map of section keys to instantiated components. 194 | The root component can then decide which of these components 195 | to render where based on these section keys. 196 | 197 | During rendering, the provided root component is wrapped 198 | in an application component that handles the screen routing 199 | logic and generates the `:sections` and other form props 200 | based on the active screen." 201 | [{:keys [default-screen reconciler root 202 | root-js? target screen-mounted] 203 | :or {root-js? false}}] 204 | (set-root-component! root root-js?) 205 | (map->Application {:config {:default-screen default-screen 206 | :reconciler reconciler 207 | :target target 208 | :screen-mounted screen-mounted}})) 209 | -------------------------------------------------------------------------------- /src/main/workflo/macros/screen/util.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.screen.util 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as string])) 4 | 5 | (s/def ::typed-url-segment 6 | (s/cat :qualifier '#{long keyword uuid} 7 | :keyword keyword?)) 8 | 9 | (s/fdef url-segment 10 | :args (s/cat :s string?) 11 | :ret (s/or :string string? 12 | :keyword keyword? 13 | :typed ::typed-url-segment) 14 | :fn (s/or :string (s/and #(not= \: (-> % :args :s first)) 15 | #(= :string (-> % :ret first)) 16 | #(string? (-> % :ret second))) 17 | :keyword (s/and #(= \: (-> % :args :s first)) 18 | #(= :keyword (-> % :ret first)) 19 | #(keyword? (-> % :ret second))) 20 | :typed (s/and #(= \: (-> % :args :s first)) 21 | #(re-find #"\^" (-> % :args :s)) 22 | #(= :typed (-> % :ret first)) 23 | #(vector? (-> % :ret second)) 24 | #(= 2 (count (-> % :ret second))) 25 | #(some #{(-> % :ret second first)} 26 | '[long keyword uuid]) 27 | #(keyword? (-> % :ret second second))))) 28 | 29 | (defn url-segment 30 | [s] 31 | (if (= \: (first s)) 32 | (let [[name type] (string/split (subs s 1) #"\^")] 33 | (if type 34 | [(symbol type) (keyword name)] 35 | (keyword name))) 36 | s)) 37 | 38 | (s/fdef url-segments 39 | :args (s/cat :s string?) 40 | :ret (s/coll-of (s/or :string string? 41 | :keyword keyword? 42 | :typed ::typed-url-segment) 43 | :kind vector?)) 44 | 45 | (defn url-segments 46 | "Returns a vector of URL segments, converting keyword-like 47 | string segments into actual keywords." 48 | [s] 49 | (mapv url-segment (string/split s #"/"))) 50 | -------------------------------------------------------------------------------- /src/main/workflo/macros/service/util.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.service.util) 2 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/bind.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.bind 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.specs.parsed-query :as spq])) 5 | 6 | (s/def ::path 7 | (s/coll-of ::spq/typed-property :min-count 1 :gen-max 10)) 8 | 9 | (s/def ::paths 10 | (s/coll-of ::path :gen-max 10)) 11 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/command.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.command 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.specs.query])) 5 | 6 | (s/def ::command-name 7 | symbol?) 8 | 9 | (s/def ::command-description 10 | string?) 11 | 12 | (s/def ::command-spec 13 | (s/with-gen 14 | any? 15 | #(s/gen #{symbol? map? vector?}))) 16 | 17 | (s/def ::command-form-name 18 | (s/with-gen 19 | (s/and symbol? 20 | #(not (some #{%} '[description 21 | spec 22 | query 23 | auth-query 24 | auth 25 | emit])) 26 | #(not (some #{\/} (str %)))) 27 | #(s/gen '#{foo bar foo-bar}))) 28 | 29 | (s/def ::command-form-body 30 | (s/* any?)) 31 | 32 | (s/def ::command-form 33 | (s/spec (s/cat :form-name ::command-form-name 34 | :form-body ::command-form-body))) 35 | 36 | (s/def ::command-hints 37 | (s/coll-of keyword? :kind vector? :min-count 1)) 38 | 39 | (s/def ::command-hints-form 40 | (s/spec (s/cat :form-name #{'hints} 41 | :form-body ::command-hints))) 42 | 43 | (s/def ::command-spec-form 44 | (s/spec (s/cat :form-name #{'spec} 45 | :form-body ::command-spec))) 46 | 47 | (s/def ::command-query-form 48 | (s/spec (s/cat :form-name #{'query} 49 | :form-body :workflo.macros.specs.query/query))) 50 | 51 | (s/def ::command-auth-query-form 52 | (s/spec (s/cat :form-name #{'auth-query} 53 | :form-body :workflo.macros.specs.query/query))) 54 | 55 | (s/def ::command-auth-form 56 | (s/spec (s/cat :form-name #{'auth} 57 | :form-body ::command-form-body))) 58 | 59 | (s/def ::command-emit-form 60 | (s/spec (s/cat :form-name #{'emit} 61 | :form-body ::command-form-body))) 62 | 63 | (s/def ::defcommand-args 64 | (s/cat :name ::command-name 65 | :forms 66 | (s/spec (s/cat :description (s/? ::command-description) 67 | :hints (s/? ::command-hints-form) 68 | :spec (s/? ::command-spec-form) 69 | :query (s/? ::command-query-form) 70 | :auth-query (s/? ::command-auth-query-form) 71 | :auth (s/? ::command-auth-form) 72 | :forms (s/* ::command-form) 73 | :emit ::command-emit-form)) 74 | :env (s/? any?))) 75 | 76 | (s/def ::form-name 77 | ::command-form-name) 78 | 79 | (s/def ::form-body 80 | ::command-form-body) 81 | 82 | (s/def ::conforming-command-form 83 | (s/keys :req-un [::form-name ::form-body])) 84 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/conforming_query.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.conforming-query 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.query.util :as util] 5 | [workflo.macros.specs.query])) 6 | 7 | (s/def ::simple 8 | (s/tuple #{:simple} :workflo.macros.specs.query/property-name)) 9 | 10 | (s/def ::link 11 | (s/tuple #{:link} :workflo.macros.specs.query/link)) 12 | 13 | (s/def ::join-source 14 | (s/or :simple ::simple 15 | :link ::link)) 16 | 17 | (s/def ::join-properties-value 18 | (s/with-gen 19 | (s/map-of ::join-source ::query :count 1) 20 | #(gen/map (s/gen ::join-source) 21 | (gen/vector 22 | (s/gen #{[:property [:simple 'a]] 23 | [:property [:simple 'b]] 24 | [:prefixed-properties 25 | {:base 'a 26 | :children [[:property [:simple 'b]] 27 | [:property [:simple 'c]]]}] 28 | [:property [:link '[a _]]]}) 29 | 1 10) 30 | {:num-elements 1}))) 31 | 32 | (s/def ::join-properties 33 | (s/tuple #{:properties} ::join-properties-value)) 34 | 35 | (s/def ::unlimited-join-recursion 36 | (s/tuple #{:unlimited} #{'... ''...})) 37 | 38 | (s/def ::limited-join-recursion 39 | (s/tuple #{:limited} (s/and int? pos?))) 40 | 41 | (s/def ::join-recursion-value 42 | (s/map-of ::join-source 43 | (s/or :unlimited ::unlimited-join-recursion 44 | :limited ::limited-join-recursion) 45 | :count 1)) 46 | 47 | (s/def ::join-recursion 48 | (s/tuple #{:recursive} ::join-recursion-value)) 49 | 50 | (s/def ::join-view-value 51 | (s/map-of ::join-source 52 | (s/tuple #{:view} :workflo.macros.specs.query/view-name) 53 | :count 1)) 54 | 55 | (s/def ::join-view 56 | (s/tuple #{:view} ::join-view-value)) 57 | 58 | (s/def ::join 59 | (s/tuple #{:join} (s/or :properties ::join-properties 60 | :recursion ::join-recursion 61 | :view ::join-view))) 62 | 63 | (s/def ::property-value 64 | (s/or :simple ::simple 65 | :link ::link 66 | :join ::join)) 67 | 68 | (s/def ::property 69 | (s/tuple #{:property} ::property-value)) 70 | 71 | (s/def :aliased-property-value/property 72 | ::property-value) 73 | 74 | (s/def :aliased-property-value/alias 75 | :workflo.macros.specs.query/property-name) 76 | 77 | (s/def :aliased-property-value/as 78 | #{:as}) 79 | 80 | (s/def ::aliased-property-value 81 | (s/keys :req-un [:aliased-property-value/property 82 | :aliased-property-value/alias] 83 | :opt-un [:aliased-property-value/as])) 84 | 85 | (s/def ::aliased-property 86 | (s/tuple #{:aliased-property} ::aliased-property-value)) 87 | 88 | (s/def :prefixed-properties-value/base 89 | :workflo.macros.specs.query/property-name) 90 | 91 | (s/def :prefixed-properties-value/children 92 | (s/with-gen 93 | (s/and ::query) 94 | #(gen/vector 95 | (s/gen (s/or :property ::property 96 | :aliased-property ::aliased-property 97 | :parameterization ::parameterization)) 98 | 1 10))) 99 | 100 | (s/def ::prefixed-properties-value 101 | (s/keys :req-un [:prefixed-properties-value/base 102 | :prefixed-properties-value/children])) 103 | 104 | (s/def ::prefixed-properties 105 | (s/tuple #{:prefixed-properties} ::prefixed-properties-value)) 106 | 107 | (s/def :parameterization-value/query 108 | (s/or :property ::property 109 | :aliased-property ::aliased-property)) 110 | 111 | (s/def :parameterization-value/parameters 112 | :workflo.macros.specs.query/parameters) 113 | 114 | (s/def ::parameterization-value 115 | (s/keys :req-un [:parameterization-value/query 116 | :parameterization-value/parameters])) 117 | 118 | (s/def ::parameterization 119 | (s/tuple #{:parameterization} ::parameterization-value)) 120 | 121 | (s/def ::fragment 122 | (s/tuple #{:fragment} :workflo.macros.specs.query/fragment)) 123 | 124 | ;; Workaround for extra [] around [:aliased-property ...] and 125 | ;; [:prefixed-properties ...] in output of conform (JIRA issue 126 | ;; CLJ-2003) 127 | (s/def ::bug-vector 128 | (s/coll-of (s/or :property ::property 129 | :aliased-property ::aliased-property 130 | :prefixed-properties ::prefixed-properties) 131 | :kind vector? :count 1)) 132 | 133 | (s/def ::query-value 134 | (s/or :property ::property 135 | :aliased-property ::aliased-property 136 | :prefixed-properties ::prefixed-properties 137 | :parameterization ::parameterization 138 | :fragment ::fragment 139 | :bug-vector ::bug-vector)) 140 | 141 | (s/def ::query 142 | (s/coll-of ::query-value :kind vector? 143 | :min-count 1 :gen-max 10)) 144 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/entity.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.entity 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.specs.query :as q])) 5 | 6 | ;;;; Specs for defentity arguments 7 | 8 | (s/def ::entity-name 9 | symbol?) 10 | 11 | (s/def ::entity-description 12 | string?) 13 | 14 | (s/def ::entity-form-body 15 | (s/* any?)) 16 | 17 | (s/def ::entity-auth-query-form 18 | (s/spec (s/cat :form-name #{'auth-query} 19 | :form-body ::q/query))) 20 | 21 | (s/def ::entity-auth-form 22 | (s/spec (s/cat :form-name #{'auth} 23 | :form-body ::entity-form-body))) 24 | 25 | (s/def ::entity-spec-form 26 | (s/spec (s/cat :form-name #{'spec} 27 | :form-body any?))) 28 | 29 | (s/def ::entity-hints 30 | (s/coll-of keyword? :kind vector? :min-count 1)) 31 | 32 | (s/def ::entity-hints-form 33 | (s/spec (s/cat :form-name #{'hints} 34 | :form-body ::entity-hints))) 35 | 36 | (s/def ::defentity-args 37 | (s/cat :name ::entity-name 38 | :forms (s/spec (s/cat :description (s/? ::entity-description) 39 | :hints (s/? ::entity-hints-form) 40 | :spec ::entity-spec-form 41 | :auth-query (s/? ::entity-auth-query-form) 42 | :auth (s/? ::entity-auth-form))) 43 | :env (s/? any?))) 44 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/om_query.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.om-query 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.specs.query :as q] 5 | [workflo.macros.query.util :as util])) 6 | 7 | (s/def ::keyword 8 | keyword?) 9 | 10 | (s/def ::link 11 | (s/tuple ::keyword :workflo.macros.specs.query/link-id)) 12 | 13 | (s/def ::component-query 14 | (s/cat :call '#{om.next/get-query} 15 | :component symbol?)) 16 | 17 | (s/def ::join-source 18 | (s/or :property ::keyword 19 | :link ::link)) 20 | 21 | (s/def ::join-target 22 | (s/or :query ::query 23 | :recursion ::q/join-recursion 24 | :component ::component-query)) 25 | 26 | (s/def ::join 27 | (s/and (s/map-of ::join-source ::join-target) 28 | util/one-item?)) 29 | 30 | (s/def ::regular-property 31 | (s/or :keyword ::keyword 32 | :link ::link 33 | :join ::join)) 34 | 35 | (s/def ::parameter-name 36 | keyword?) 37 | 38 | (s/def ::parameter-path 39 | (s/coll-of ::parameter-name :kind vector? :min-count 1 :gen-max 3)) 40 | 41 | (s/def ::parameter-name-or-path 42 | (s/or :parameter-name ::parameter-name 43 | :parameter-path ::parameter-path)) 44 | 45 | (s/def ::parameters 46 | (s/map-of ::parameter-name-or-path any? :gen-max 5)) 47 | 48 | (s/def ::parameterized-property 49 | (s/spec (s/cat :list (s/? #{'clojure.core/list}) 50 | :property ::regular-property 51 | :parameters ::parameters))) 52 | 53 | (s/def ::property 54 | (s/or :regular ::regular-property 55 | :parameterized ::parameterized-property)) 56 | 57 | (s/def ::query 58 | (s/coll-of ::property 59 | :kind? vector? :min-count 1 60 | :gen-max 10)) 61 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/parsed_query.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.parsed-query 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.specs.query])) 5 | 6 | (s/def :property/type 7 | #{:property}) 8 | 9 | (s/def :link/type 10 | #{:link}) 11 | 12 | (s/def :join/type 13 | #{:join}) 14 | 15 | (s/def ::name 16 | :workflo.macros.specs.query/property-name) 17 | 18 | (s/def ::link-id 19 | :workflo.macros.specs.query/link-id) 20 | 21 | (s/def ::join-source 22 | (s/with-gen 23 | (s/or :property ::unparameterized-property 24 | :link ::unparameterized-link) 25 | #(gen/one-of [(s/gen ::unparameterized-property) 26 | (s/gen '#{{:type :link :name foo :link-id _} 27 | {:type :link :name bar :link-id 5}})]))) 28 | 29 | (s/def ::join-target 30 | (s/with-gen 31 | (s/or :view :workflo.macros.specs.query/view-name 32 | :recursion :workflo.macros.specs.query/join-recursion 33 | :properties ::query) 34 | #(gen/one-of [(s/gen :workflo.macros.specs.query/view-name) 35 | (s/gen :workflo.macros.specs.query/join-recursion) 36 | (s/gen '#{[{:type :property :name name}] 37 | [{:type :property :name name} 38 | {:type :property :name email}] 39 | [{:type :property :name name} 40 | {:type :link :name current-user :link-id '_} 41 | {:type :join :name friends 42 | :join-target 'User}]})]))) 43 | 44 | (s/def ::parameters 45 | :workflo.macros.specs.query/parameters) 46 | 47 | (s/def ::alias 48 | :workflo.macros.specs.query/property-name) 49 | 50 | (s/def ::property 51 | (s/keys :req-un [:property/type ::name] 52 | :opt-un [::parameters ::alias])) 53 | 54 | (s/def ::unparameterized-property 55 | (s/keys :req-un [:property/type ::name])) 56 | 57 | (s/def ::link 58 | (s/keys :req-un [:link/type ::name ::link-id] 59 | :opt-un [::parameters ::alias])) 60 | 61 | (s/def ::unparameterized-link 62 | (s/keys :req-un [:link/type ::name ::link-id])) 63 | 64 | (s/def ::join 65 | (s/keys :req-un [:join/type ::name ::join-source ::join-target] 66 | :opt-un [::parameters ::alias])) 67 | 68 | (defmulti typed-property-spec :type) 69 | 70 | (defmethod typed-property-spec :property [_] 71 | ::property) 72 | 73 | (defmethod typed-property-spec :link [_] 74 | ::link) 75 | 76 | (defmethod typed-property-spec :join [_] 77 | ::join) 78 | 79 | (s/def ::typed-property 80 | (s/multi-spec typed-property-spec :type)) 81 | 82 | (s/def ::query 83 | (s/coll-of ::typed-property 84 | :kind vector? :min-count 1 85 | :gen-max 10)) 86 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/permission.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.permission 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen])) 4 | 5 | (s/def ::permission-name (or symbol? keyword?)) 6 | 7 | (s/def ::permission-title string?) 8 | (s/def ::permission-title-form 9 | (s/spec (s/cat :form-name #{'title} 10 | :form-body ::permission-title))) 11 | 12 | (s/def ::permission-description string?) 13 | (s/def ::permission-description-form 14 | (s/spec (s/cat :form-name #{'description} 15 | :form-body ::permission-description))) 16 | 17 | (s/def ::defpermission-args 18 | (s/cat :name ::permission-name 19 | :forms (s/spec (s/cat :title ::permission-title-form 20 | :description (s/? ::permission-description-form))) 21 | :env (s/? any?))) 22 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/query.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.query 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.query.util :as util])) 5 | 6 | ;;;; Simple properties 7 | 8 | (s/def ::property-name 9 | (s/and symbol? #(not (re-matches #"^[\.]+.*" (name %))))) 10 | 11 | ;;;; Links 12 | 13 | (s/def ::link-id 14 | (s/with-gen any? gen/simple-type)) 15 | 16 | (s/def ::link 17 | (s/tuple ::property-name ::link-id)) 18 | 19 | ;;;; Joins 20 | 21 | ;; Join source 22 | 23 | (s/def ::join-source 24 | (s/or :simple ::property-name 25 | :link ::link)) 26 | 27 | ;; Recursive joins 28 | 29 | (s/def ::join-recursion 30 | (s/or :unlimited #{'... ''...} 31 | :limited (s/and int? pos?))) 32 | 33 | (s/def ::recursive-join 34 | (s/map-of ::join-source ::join-recursion 35 | :count 1 :conform-keys true)) 36 | 37 | ;; Property joins 38 | 39 | (s/def ::join-properties 40 | (s/with-gen 41 | ;; NOTE: The s/and here is a hack to make looking up ::query 42 | ;; work even though ::query is only defined later in this 43 | ;; namespace. It *should* work without it but it may be the 44 | ;; s/with-gen around it that makes it fail. 45 | (s/and ::query) 46 | #(s/gen '#{[user] 47 | [db [id] user [name email]] 48 | [[current-user _]] 49 | [{users [user [name email]]}]}))) 50 | 51 | (s/def ::properties-join 52 | (s/map-of ::join-source ::join-properties 53 | :count 1 :conform-keys true)) 54 | 55 | ;; View joins 56 | 57 | (s/def ::view-name 58 | symbol?) 59 | 60 | (s/def ::join-view 61 | (s/or :view ::view-name)) 62 | 63 | (s/def ::view-join 64 | (s/map-of ::join-source ::join-view 65 | :count 1 :conform-keys true)) 66 | 67 | ;; All possible joins 68 | 69 | (s/def ::join 70 | (s/or :recursive ::recursive-join 71 | :properties ::properties-join 72 | :view ::view-join)) 73 | 74 | ;;;; Individual properties, prefixed properties, aliased properties 75 | 76 | (s/def ::property 77 | (s/or :simple ::property-name 78 | :link ::link 79 | :join ::join)) 80 | 81 | (s/def ::prefixed-properties-value 82 | (s/with-gen 83 | ;; NOTE: The s/and here is a hack to make looking up ::query 84 | ;; work even though ::query is only defined later in this 85 | ;; namespace. It *should* work without it but it may be the 86 | ;; s/with-gen around it that makes it fail. 87 | (s/and ::query) 88 | #(s/gen '#{[id] 89 | [name email] 90 | [name email [current-user _]]}))) 91 | 92 | (s/def ::prefixed-properties 93 | (s/cat :base ::property-name 94 | :children ::prefixed-properties-value)) 95 | 96 | (s/def ::aliased-property 97 | (s/cat :property ::property 98 | :as #{:as} 99 | :alias ::property-name)) 100 | 101 | (s/def ::fragment 102 | (s/with-gen 103 | (s/and symbol? #(re-matches #"^[\.]{3}.+" (name %))) 104 | #(s/gen '#{...example-fragment}))) 105 | 106 | (s/def ::parameterization-query 107 | (s/alt :property ::property 108 | :aliased-property ::aliased-property)) 109 | 110 | (s/def ::parameter-name 111 | symbol?) 112 | 113 | (s/def ::parameter-path 114 | (s/coll-of ::parameter-name :kind vector? :min-count 1 :gen-max 3)) 115 | 116 | (s/def ::parameter-name-or-path 117 | (s/or :parameter-name ::parameter-name 118 | :parameter-path ::parameter-path)) 119 | 120 | (s/def ::parameter-value 121 | (s/with-gen any? gen/simple-type)) 122 | 123 | (s/def ::parameters 124 | (s/map-of ::parameter-name-or-path ::parameter-value 125 | :gen-max 5)) 126 | 127 | (s/def ::parameterization 128 | (s/with-gen 129 | (s/and list? 130 | (s/cat :query ::parameterization-query 131 | :parameters ::parameters)) 132 | #(gen/fmap (fn [[query parameters]] 133 | (apply list (conj query parameters))) 134 | (gen/tuple (s/gen ::parameterization-query) 135 | (s/gen ::parameters))))) 136 | 137 | (s/def ::query 138 | (s/with-gen 139 | (s/and vector? 140 | (s/+ (s/alt :property ::property 141 | :prefixed-properties ::prefixed-properties 142 | :aliased-property ::aliased-property 143 | :parameterization ::parameterization 144 | :fragment ::fragment))) 145 | #(gen/fmap util/combine-properties-and-groups 146 | (gen/vector (gen/one-of 147 | [(s/gen ::property) 148 | (s/gen ::prefixed-properties) 149 | (s/gen ::aliased-property) 150 | (s/gen ::parameterization) 151 | (s/gen ::fragment)]) 152 | 1 3)))) 153 | 154 | (comment 155 | ;;;; Non-recursive queries 156 | 157 | ;; Regular property 158 | (s/conform ::query '[a]) 159 | (s/conform ::query '[a b]) 160 | (s/conform ::query '[a b c]) 161 | 162 | ;; Link property 163 | (s/conform ::query '[[a _]]) 164 | (s/conform ::query '[[a _] [b 1]]) 165 | (s/conform ::query '[[a _] [b 1] [c :x]]) 166 | 167 | ;; Join with property source 168 | (s/conform ::query '[{a [b]}]) 169 | (s/conform ::query '[{a [b c]}]) 170 | (s/conform ::query '[{a [b c]} d]) 171 | (s/conform ::query '[{a [b c]} {d [e f]}]) 172 | (s/conform ::query '[{a ...}]) 173 | (s/conform ::query '[{a 5}]) 174 | (s/conform ::query '[{a User}]) 175 | 176 | ;; Join with link source 177 | (s/conform ::query '[{[a _] [b]}]) 178 | (s/conform ::query '[{[a 1] [b c]}]) 179 | (s/conform ::query '[{[a :x] [b c d]}]) 180 | 181 | ;; Prefixed properties 182 | (s/conform ::query '[a [b]]) 183 | (s/conform ::query '[a [b c]]) 184 | (s/conform ::query '[a [b c] d]) 185 | (s/conform ::query '[a [b c] d [e f]]) 186 | 187 | ;; Aliased property 188 | (s/conform ::query '[a :as b]) 189 | (s/conform ::query '[a :as b c :as d]) 190 | 191 | ;; Aliased link 192 | (s/conform ::query '[[a _] :as b]) 193 | (s/conform ::query '[[a 1] :as b]) 194 | (s/conform ::query '[[a :x] :as b]) 195 | (s/conform ::query '[[a _] :as b [c _] :as d]) 196 | 197 | ;; Aliased join 198 | (s/conform ::query '[{a [b]} :as c]) 199 | (s/conform ::query '[{a [b c]} :as d {e [f g]} :as h]) 200 | 201 | ;; Aliased properties 202 | (s/conform ::query '[a [b :as c]]) 203 | (s/conform ::query '[a [b :as c d :as e]]) 204 | 205 | ;; Parameterization 206 | (s/conform ::query '[(a {b c})]) 207 | (s/conform ::query '[(a {b c d e})]) 208 | 209 | ;;;; Recursive queries 210 | 211 | ;; Join with sub-joins 212 | (s/conform ::query '[{users [db [id] 213 | user [name] 214 | {friends [db [id] 215 | user [name]]}]}]) 216 | (s/conform ::query '[{users [{friends [{friends [db [id]]}]}]}]) 217 | 218 | ;; Join with sub-links 219 | (s/conform ::query '[{users [db [id] [current-user _]]}]) 220 | (s/conform ::query '[{users [user [name] 221 | {[current-user _] [user [name]]}]}]) 222 | 223 | ;; Join with sub-aliases 224 | (s/conform ::query '[{[user 1] [db [id :as db-id] 225 | name :as nm]}])) 226 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/screen.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.screen 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.specs.query])) 5 | 6 | (s/def ::screen-name 7 | symbol?) 8 | 9 | (s/def ::screen-description 10 | string?) 11 | 12 | (s/def ::url-form 13 | (s/spec (s/cat :form-name #{'url} 14 | :form-body string?))) 15 | 16 | (s/def ::screen-form-name 17 | symbol?) 18 | 19 | (s/def ::screen-form-body 20 | any?) 21 | 22 | (s/def ::screen-form 23 | (s/spec (s/cat :form-name ::screen-form-name 24 | :form-body ::screen-form-body))) 25 | 26 | (s/def ::sections-form 27 | (s/spec (s/cat :form-name #{'sections} 28 | :form-body (s/map-of keyword? any?)))) 29 | 30 | (s/def ::defscreen-args 31 | (s/cat :name ::screen-name 32 | :forms (s/spec (s/cat :description (s/? ::screen-description) 33 | :url ::url-form 34 | :forms (s/* ::screen-form) 35 | :sections ::sections-form)) 36 | :env (s/? any?))) 37 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/service.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.service 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.specs.query])) 5 | 6 | (s/def ::service-name 7 | symbol?) 8 | 9 | (s/def ::service-description 10 | string?) 11 | 12 | (s/def ::service-hints 13 | (s/coll-of keyword? :kind vector? :min-count 1)) 14 | 15 | (s/def ::service-hints-form 16 | (s/spec (s/cat :form-name #{'hints} 17 | :form-body ::service-hints))) 18 | 19 | (s/def ::service-spec 20 | (s/with-gen 21 | any? 22 | #(s/gen #{symbol? map? vector?}))) 23 | 24 | (s/def ::service-dependencies 25 | (s/coll-of symbol? :kind vector?)) 26 | 27 | (s/def ::service-form-body 28 | (s/* any?)) 29 | 30 | (s/def ::service-dependencies-form 31 | (s/spec (s/cat :form-name #{'dependencies} 32 | :form-body ::service-dependencies))) 33 | 34 | (s/def ::service-replay?-form 35 | (s/spec (s/cat :form-name #{'replay?} 36 | :form-body boolean?))) 37 | 38 | (s/def ::service-query-form 39 | (s/spec (s/cat :form-name #{'query} 40 | :form-body :workflo.macros.specs.query/query))) 41 | 42 | (s/def ::service-spec-form 43 | (s/spec (s/cat :form-name #{'spec} 44 | :form-body ::service-spec))) 45 | 46 | (s/def ::service-start-form 47 | (s/spec (s/cat :form-name #{'start} 48 | :form-body ::service-form-body))) 49 | 50 | (s/def ::service-stop-form 51 | (s/spec (s/cat :form-name #{'stop} 52 | :form-body ::service-form-body))) 53 | 54 | (s/def ::service-process-form 55 | (s/spec (s/cat :form-name #{'process} 56 | :form-body ::service-form-body))) 57 | 58 | (s/def ::defservice-args 59 | (s/cat :name ::service-name 60 | :forms 61 | (s/spec (s/cat :description (s/? ::service-description) 62 | :hints (s/? ::service-hints-form) 63 | :dependencies (s/? ::service-dependencies-form) 64 | :replay? (s/? ::service-replay?-form) 65 | :query (s/? ::service-query-form) 66 | :spec (s/? ::service-spec-form) 67 | :start (s/? ::service-start-form) 68 | :stop (s/? ::service-stop-form) 69 | :process (s/? ::service-process-form))) 70 | :env (s/? any?))) 71 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/types.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.types 2 | (:refer-clojure :exclude [bigdec? bytes? double? float? uri?]) 3 | (:require [clojure.spec.alpha :as s :refer [Spec]] 4 | [clojure.spec.gen.alpha :as gen] 5 | [clojure.string :as str] 6 | [workflo.macros.util.misc :refer [val-after]])) 7 | 8 | ;;;; Helpers 9 | 10 | (defn long? [x] 11 | #?(:cljs (and (number? x) 12 | (not (js/isNaN x)) 13 | (not (identical? x js/Infinity)) 14 | (= 0 (rem x 1))) 15 | :clj (instance? java.lang.Long x))) 16 | 17 | (defn bigint? [x] 18 | #?(:cljs (long? x) 19 | :clj (instance? clojure.lang.BigInt x))) 20 | 21 | (defn float? [x] 22 | #?(:cljs (and (number? x) 23 | (not (js/isNaN x)) 24 | (not (identical? x js/Infinity)) 25 | (not= (js/parseFloat x) (js/parseInt x 10))) 26 | :clj (clojure.core/float? x))) 27 | 28 | (defn double? [x] 29 | #?(:cljs (float? x) 30 | :clj (clojure.core/double? x))) 31 | 32 | (defn bigdec? [x] 33 | #?(:cljs (float? x) 34 | :clj (clojure.core/bigdec? x))) 35 | 36 | (defn bytes? [x] 37 | #?(:cljs (array? x) 38 | :clj (clojure.core/bytes? x))) 39 | 40 | ;;;; Fundamental types 41 | 42 | (s/def ::any any?) 43 | (s/def ::keyword keyword?) 44 | (s/def ::string string?) 45 | (s/def ::boolean boolean?) 46 | (s/def ::long long?) 47 | (s/def ::bigint bigint?) 48 | (s/def ::float float?) 49 | (s/def ::double double?) 50 | (s/def ::bigdec bigdec?) 51 | (s/def ::instant inst?) 52 | (s/def ::uuid uuid?) 53 | (s/def ::bytes bytes?) 54 | (s/def ::enum keyword?) 55 | 56 | ;;;; Type options 57 | 58 | (s/def ::unique-value any?) 59 | (s/def ::unique-identity any?) 60 | (s/def ::indexed any?) 61 | (s/def ::fulltext any?) 62 | (s/def ::component any?) 63 | (s/def ::no-history any?) 64 | 65 | ;;;; Types whose values are not to be persisted 66 | 67 | (s/def ::non-persistent any?) 68 | 69 | ;;;; Entity IDs 70 | 71 | (s/def ::id 72 | (s/with-gen 73 | (s/and string? #(= (count %) 32)) 74 | #(gen/fmap (fn [uuid] 75 | (str/replace (str uuid) "-" "")) 76 | (gen/uuid)))) 77 | 78 | (s/def :workflo/id 79 | (s/and ::id ::unique-identity ::indexed)) 80 | 81 | ;;;; Simple reference types 82 | 83 | (s/def ::ref (s/keys :req [:workflo/id])) 84 | (s/def ::ref-many (s/or :vector (s/coll-of ::ref :kind vector?) 85 | :set (s/coll-of ::ref :kind set?))) 86 | 87 | ;;;; Entity references 88 | 89 | (defn entity-ref-impl 90 | [entity-sym opts gfn] 91 | (let [subspec (s/spec (if (:many? opts) ::ref-many ::ref))] 92 | (reify Spec 93 | (conform* [_ x] 94 | (s/conform* subspec x)) 95 | (unform* [_ x] 96 | (s/unform* subspec x)) 97 | (explain* [_ path via in x] 98 | (s/explain* subspec path via in x)) 99 | (gen* [_ overrides path rmap] 100 | (if gfn (gfn) (s/gen* subspec overrides path rmap))) 101 | (with-gen* [_ gfn] 102 | (entity-ref-impl entity-sym opts gfn)) 103 | (describe* [_] 104 | `(entity-ref ~entity-sym ~@(mapcat identity opts)))))) 105 | 106 | (defn entity-ref 107 | [entity-sym & {:keys [many?] :or {many false} :as opts}] 108 | (entity-ref-impl entity-sym opts nil)) 109 | 110 | (defn entity-ref-opts 111 | [spec] 112 | (->> (s/describe spec) 113 | (drop-while (complement keyword?)) 114 | (partition 2 2) 115 | (transduce (map vec) conj {}))) 116 | 117 | (defn entity-ref-info 118 | [spec] 119 | (merge {:entity (val-after (s/describe spec) 'entity-ref)} 120 | (entity-ref-opts spec))) 121 | 122 | (defn entity-ref-from-description 123 | "Takes an `(entity-ref & )` description and returns 124 | an instance of the corresponding spec." 125 | [[_ entity-name & args]] 126 | (let [;; "Unquote" the entity name (if it is quoted) 127 | entity-name (cond-> entity-name 128 | (and (seq? entity-name) 129 | (= 'quote (first entity-name)) 130 | (symbol? (second entity-name))) 131 | second)] 132 | (apply entity-ref (cons entity-name args)))) 133 | -------------------------------------------------------------------------------- /src/main/workflo/macros/specs/view.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.specs.view 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.query.util :as util] 5 | [workflo.macros.specs.query])) 6 | 7 | (s/def ::view-name 8 | (s/with-gen 9 | util/capitalized-symbol? 10 | #(s/gen '#{Foo Bar FooBar}))) 11 | 12 | (s/def ::view-form-args 13 | (s/with-gen 14 | (s/coll-of symbol? :kind vector? :min-count 1) 15 | #(s/gen '#{[this] [props] [this props]}))) 16 | 17 | (s/def ::raw-view-form-name 18 | (s/with-gen 19 | (s/and symbol? #(= \. (first (str %)))) 20 | #(s/gen '#{.do-this .do-that}))) 21 | 22 | (s/def ::raw-view-form 23 | (s/spec (s/cat :form-name ::raw-view-form-name 24 | :form-args ::view-form-args 25 | :form-body (s/* any?)))) 26 | 27 | (s/def ::regular-view-form 28 | (s/spec (s/cat :form-name (s/and symbol? #(not= \. (first (str %)))) 29 | :form-args (s/? ::view-form-args) 30 | :form-body (s/* any?)))) 31 | 32 | (s/def ::view-form 33 | (s/alt :raw-view-form ::raw-view-form 34 | :regular-view-form ::regular-view-form)) 35 | 36 | (s/def ::view-query-form 37 | (s/spec (s/cat :form-name #{'query} 38 | :form-body :workflo.macros.specs.query/query))) 39 | 40 | (s/def ::view-computed-form 41 | (s/spec (s/cat :form-name #{'computed} 42 | :form-body :workflo.macros.specs.query/query))) 43 | 44 | (s/def ::defview-args 45 | (s/cat :name ::view-name 46 | :forms (s/spec (s/cat :query (s/? ::view-query-form) 47 | :computed (s/? ::view-computed-form) 48 | :forms (s/* ::view-form))))) 49 | -------------------------------------------------------------------------------- /src/main/workflo/macros/util/form.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.util.form 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [workflo.macros.util.symbol])) 5 | 6 | (s/fdef prefixed-form-name 7 | :args (s/cat :form-name :workflo.macros.util.symbol/unqualified-symbol 8 | :prefix :workflo.macros.util.symbol/unqualified-symbol) 9 | :ret symbol? 10 | :fn #(= (-> % :ret) 11 | (symbol (str (-> % :args :prefix) "-" 12 | (-> % :args :form-name))))) 13 | 14 | (defn prefixed-form-name 15 | [form-name prefix] 16 | (symbol (str prefix "-" form-name))) 17 | 18 | #?(:clj 19 | (s/fdef qualified-form-name 20 | :args (s/cat :form-name 21 | :workflo.macros.util.symbol/unqualified-symbol 22 | :prefix 23 | :workflo.macros.util.symbol/unqualified-symbol) 24 | :ret symbol? 25 | :fn (s/and #(= (ns-name *ns*) (symbol (namespace (-> % :ret)))) 26 | #(= (prefixed-form-name (-> % :args :form-name) 27 | (-> % :args :prefix)) 28 | (symbol (name (-> % :ret))))))) 29 | 30 | (defn qualified-form-name 31 | [form-name prefix] 32 | (symbol (str (ns-name *ns*)) 33 | (str (prefixed-form-name form-name prefix)))) 34 | 35 | (s/def ::form-name 36 | :workflo.macros.util.symbol/unqualified-symbol) 37 | 38 | (s/def ::form-body 39 | any?) 40 | 41 | (s/def ::form-args 42 | vector?) 43 | 44 | (s/def ::conforming-form 45 | (s/with-gen 46 | (s/keys ::req-un [::form-name ::form-body] 47 | ::opt-un [::form-args]) 48 | #(gen/hash-map :form-name (s/gen ::form-name) 49 | :form-body (s/gen ::form-body)))) 50 | 51 | (s/def ::conforming-forms 52 | (s/spec (s/* ::conforming-form))) 53 | 54 | (s/fdef forms-map 55 | :args (s/cat :forms ::conforming-forms 56 | :prefix :workflo.macros.util.symbol/unqualified-symbol) 57 | :ret (s/map-of keyword? symbol?)) 58 | 59 | (defn forms-map 60 | [forms prefix] 61 | (zipmap (map (comp keyword :form-name) forms) 62 | (map (comp #(qualified-form-name % prefix) 63 | :form-name) 64 | forms))) 65 | 66 | (defn make-def 67 | ([name body] 68 | `(~'def ~name ~body)) 69 | ([prefix name body] 70 | `(~'def ~(prefixed-form-name name prefix) ~body))) 71 | 72 | (defn make-def-quoted 73 | ([name body] 74 | `(~'def ~name '~body)) 75 | ([prefix name body] 76 | `(~'def ~(prefixed-form-name name prefix) '~body))) 77 | 78 | (defn make-defn 79 | ([name args body] 80 | `(~'defn ~name 81 | ~(cond-> args 82 | (not (vector? args)) vec) 83 | ~@(cond-> body (not (sequential? body)) vector))) 84 | ([prefix name args body] 85 | `(~'defn ~(prefixed-form-name name prefix) 86 | ~(cond-> args 87 | (not (sequential? args)) vector 88 | (not (vector? args)) vec) 89 | ~@(cond-> body (not (sequential? body)) vector)))) 90 | 91 | (s/fdef form->defn 92 | :args (s/cat :form ::conforming-form) 93 | :ret (s/cat :defn #{'defn} 94 | :name :workflo.macros.util.symbol/unqualified-symbol 95 | :body (s/* any?))) 96 | 97 | (defn form->defn 98 | [form] 99 | (make-defn (:form-name form) 100 | (:form-args form) 101 | (:form-body form))) 102 | -------------------------------------------------------------------------------- /src/main/workflo/macros/util/js.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.util.js 2 | (:require [clojure.string :as string])) 3 | 4 | (defn sym->var-string 5 | "Converts a ClojureScript symbol to a JS variable string." 6 | [sym] 7 | (-> (str sym) 8 | (string/replace #"/" ".") 9 | (string/replace #"-" "_"))) 10 | 11 | (defn js-resolve 12 | "Resolves a ClojureScript symbol into the value of a JS 13 | variable or function." 14 | [sym] 15 | (js/eval (sym->var-string sym))) 16 | -------------------------------------------------------------------------------- /src/main/workflo/macros/util/macro.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.util.macro 2 | (:require [clojure.string :as string] 3 | [workflo.macros.query :refer [map-destructuring-keys]] 4 | [workflo.macros.util.string :refer [kebab->camel]])) 5 | 6 | (defn definition-symbol 7 | "Returns a fully qualified definition symbol for a name. 8 | The definition symbol is the name under which the definition 9 | of e.g. a service or a command will be stored." 10 | [name] 11 | (symbol (str (ns-name *ns*)) 12 | (str name))) 13 | 14 | (defn record-symbol 15 | "Returns the symbol for a record. E.g. for `user`, it will 16 | return `User`." 17 | [name] 18 | (-> name str kebab->camel string/capitalize symbol)) 19 | 20 | (defn component-record-symbol 21 | "Returns the symbol for a component record. E.g. for `user`, it 22 | will return `UserComponent`." 23 | [name] 24 | (symbol (str (record-symbol name) 'Component))) 25 | -------------------------------------------------------------------------------- /src/main/workflo/macros/util/misc.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.util.misc) 2 | 3 | (defn val-after 4 | [coll x] 5 | (loop [coll coll] 6 | (if (= x (first coll)) 7 | (first (rest coll)) 8 | (when (not (empty? (rest coll))) 9 | (recur (rest coll)))))) 10 | 11 | (defn drop-keys 12 | [m ks] 13 | (letfn [(drop-kv? [[k v]] 14 | (some #{k} ks))] 15 | (into {} (remove drop-kv?) m))) 16 | -------------------------------------------------------------------------------- /src/main/workflo/macros/util/string.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.util.string 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.string :as string] 4 | [clojure.walk :refer [walk]])) 5 | 6 | (s/fdef camel->kebab 7 | :args (s/cat :s string?) 8 | :ret string?) 9 | 10 | (defn camel->kebab 11 | "Converts from camel case (e.g. Foo or FooBar) to kebab case 12 | (e.g. foo or foo-bar)." 13 | [s] 14 | (let [segments (re-seq #"[A-Z][a-z0-9_-]*" s)] 15 | (cond 16 | segments (->> segments (string/join "-") (string/lower-case)) 17 | :else s))) 18 | 19 | (s/fdef kebab->camel 20 | :args (s/cat :s string?) 21 | :ret string?) 22 | 23 | (defn kebab->camel 24 | "Converts from kebab case (e.g. foo-bar) to camel case (e.g. 25 | fooBar)." 26 | [s] 27 | (let [words (re-seq #"\w+" s)] 28 | (apply str 29 | (cons (first words) 30 | (map string/capitalize (rest words)))))) 31 | 32 | (defn camelize-keys 33 | "Convert a Clojure map to a map where all keys are 34 | camel-cased strings that can be accessed like object 35 | properties in JS/React." 36 | [m] 37 | (walk (fn [[k v]] 38 | [(kebab->camel (name k)) 39 | (cond-> v (map? v) camelize-keys)]) 40 | identity 41 | m)) 42 | -------------------------------------------------------------------------------- /src/main/workflo/macros/util/symbol.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.util.symbol 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.spec.gen.alpha :as gen] 4 | [clojure.string :as string])) 5 | 6 | (s/def ::unqualified-symbol 7 | (s/with-gen 8 | (s/and symbol? #(not (some #{\/} (str %)))) 9 | #(gen/fmap (comp symbol name) (s/gen symbol?)))) 10 | 11 | (s/fdef unqualify 12 | :args (s/cat :x symbol?) 13 | :ret ::unqualified-symbol) 14 | 15 | (defn unqualify 16 | "Take a symbol and generate a non-namespaced version of 17 | it by replacing slashes (/) and dots (.) with dashes (-)." 18 | [x] 19 | (symbol (string/replace (str x) #"[\./]" "-"))) 20 | -------------------------------------------------------------------------------- /src/main/workflo/macros/view.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.view 2 | (:require-macros [workflo.macros.view :refer [defview]]) 3 | (:require [om.next :as om] 4 | [om.dom] 5 | [workflo.macros.bind] 6 | [workflo.macros.config :refer [defconfig]] 7 | [workflo.macros.registry :refer [defregistry]])) 8 | 9 | ;;;; Configuration options for the defview macro 10 | 11 | (defn default-run-command! 12 | "Default command handler, generating an Om Next transaction 13 | with a mutation and queries that correspond 1:1 to the 14 | command, its parameters and the optional reads." 15 | [cmd-name view params reads] 16 | (om/transact! view `[(~cmd-name ~{:cmd-data params}) 17 | ~@(or reads [])])) 18 | 19 | (defconfig view 20 | ;; Supports the following options: 21 | ;; 22 | ;; :wrapper-view - a React element factory to use for wrapping 23 | ;; the body of render functions if render has 24 | ;; more than a single child expression. 25 | ;; 26 | ;; :run-command - a function that takes a view, a command name 27 | ;; and a parameter map and an optional vector 28 | ;; of things to re-query after running the 29 | ;; command. 30 | {:wrapper-view nil 31 | :run-command default-run-command!}) 32 | 33 | (defn wrapper 34 | "Returns a wrapper factory for use in render functions. If no 35 | wrapper function is defined, issues a warning and returns 36 | om.dom/div to avoid breaking apps entirely." 37 | [] 38 | (if-not (get-view-config :wrapper-view) 39 | (do (js/console.warn "No wrapper view defined for defview.") 40 | om.dom/div) 41 | (get-view-config :wrapper-view))) 42 | 43 | (defn run-command! 44 | "Runs a given command using the configured :run-command 45 | handler, if defined." 46 | [cmd-name view params reads] 47 | (if-not (get-view-config :run-command) 48 | (js/console.warn "No :run-command handler defined for defview.") 49 | (some-> (get-view-config :run-command) 50 | (apply [cmd-name view params reads])))) 51 | 52 | (defn factory 53 | "A wrapper factory around om.next/factory that makes the nil 54 | argument for properties optional." 55 | [& args] 56 | (let [om-factory (apply om.next/factory args)] 57 | (fn [& children] 58 | (if (or (map? (first children)) 59 | (object? (first children)) 60 | (nil? (first children))) 61 | (apply (partial om-factory (first children)) 62 | (rest children)) 63 | (apply (partial om-factory {}) 64 | children))))) 65 | 66 | ;;;; View registry 67 | 68 | (defregistry view) 69 | -------------------------------------------------------------------------------- /src/main/workflo/macros/view/util.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.view.util 2 | (:require [clojure.string :as string] 3 | [workflo.macros.util.string :refer [camel->kebab]])) 4 | 5 | (defn factory-name 6 | [sym] 7 | (symbol (camel->kebab (name sym)))) 8 | -------------------------------------------------------------------------------- /src/test/workflo/macros/bind_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.bind-test 2 | (:require [clojure.test :refer [are deftest is]] 3 | [workflo.macros.bind :refer [with-query-bindings]])) 4 | 5 | (deftest regular-properties 6 | (with-query-bindings [a b b/c c.d c.d/e] 7 | {:a :aval :b :bval :b/c :bcval :c.d :cdval :c.d/e :cdeval} 8 | (is (= [a b c d e] [:aval :bval :bcval :cdval :cdeval])))) 9 | 10 | (deftest links 11 | (with-query-bindings [[a _] [b 1] [c :x] [c/d _] [d.e _] [e.f/g _]] 12 | {:a :aval :b :bval :c :cval :c/d :cdval :d.e :deval :e.f/g :efgval} 13 | (is (= [a b c d e g] [:aval :bval :cval :cdval :deval :efgval])))) 14 | 15 | (deftest joins-with-property-sources 16 | (with-query-bindings [{a [b c]} {d ...} {e 5}] 17 | {:a {:b :bval :c :cval} :d :dval :e :eval} 18 | (is (= [a b c d e] [{:b :bval :c :cval} 19 | :bval :cval :dval :eval])))) 20 | 21 | (deftest joins-with-link-sources 22 | (with-query-bindings [{[a _] [b c]} {[d _] ...}] 23 | {:a {:b :bval :c :cval} :d :dval} 24 | (is (= [a b c d] [{:b :bval :c :cval} :bval :cval :dval])))) 25 | 26 | (deftest prefixed-properties 27 | (with-query-bindings [a [b c] d [e f] g.h [i]] 28 | {:a/b :abval :a/c :acval :d/e :deval :d/f :dfval :g.h/i :ghival} 29 | (is (= [b c e f i] [:abval :acval :deval :dfval :ghival])))) 30 | 31 | (deftest aliased-regular-properties 32 | (with-query-bindings [a :as b b :as c] 33 | {:a :aval :b :bval} 34 | (is (= [b c] [:aval :bval])))) 35 | 36 | (deftest aliased-links 37 | (with-query-bindings [[a _] :as b [b _] :as c] 38 | {:a :aval :b :bval} 39 | (is (= [b c] [:aval :bval])))) 40 | 41 | (deftest aliased-joins 42 | (with-query-bindings [{a [b]} :as c {b [d]} :as e] 43 | {:a {:b :abval} :b {:d :bdval}} 44 | (is (= [c b e d] [{:b :abval} :abval 45 | {:d :bdval} :bdval])))) 46 | 47 | (deftest aliased-prefixed-properties 48 | (with-query-bindings [a [b :as c c :as d]] 49 | {:a/b :abval :a/c :acval} 50 | (is (= [c d] [:abval :acval])))) 51 | 52 | (deftest parameterizations 53 | (with-query-bindings [(a {b c d e}) (b {f g})] 54 | {:a :aval :b :bval} 55 | (is (= [a b] [:aval :bval])))) 56 | 57 | (deftest joins-with-sub-joins 58 | (with-query-bindings [{a [b [c] 59 | d [e] 60 | {f [g 61 | {h [i]}]}]}] 62 | {:a {:b/c :abcval 63 | :d/e :adeval 64 | :f {:g :afgval 65 | :h {:i :afhival}}}} 66 | (is (= [a c e f g h i] 67 | [{:b/c :abcval 68 | :d/e :adeval 69 | :f {:g :afgval 70 | :h {:i :afhival}}} 71 | :abcval 72 | :adeval 73 | {:g :afgval 74 | :h {:i :afhival}} 75 | :afgval 76 | {:i :afhival} 77 | :afhival])))) 78 | 79 | (deftest joins-with-sub-links 80 | (with-query-bindings [{a [b [c] [d _]]}] 81 | {:a {:b/c :abcval :d :adval}} 82 | (is (= [a c d] [{:b/c :abcval :d :adval} :abcval :adval])))) 83 | 84 | (deftest joins-with-sub-aliases 85 | (with-query-bindings [{[a _] [b [c :as d] d :as e]}] 86 | {:a {:b/c :abcval :d :adval}} 87 | (is (= [a d e] [{:b/c :abcval :d :adval} :abcval :adval])))) 88 | 89 | (deftest deeply-nested-query-with-joins 90 | (with-query-bindings [{a [b {c [d :as e]}]}] 91 | {:a {:b :abval :c {:d :acdval}}} 92 | (is (= [a b c e] 93 | [{:b :abval :c {:d :acdval}} 94 | :abval 95 | {:d :acdval} 96 | :acdval])))) 97 | 98 | (deftest backlink-joins 99 | (with-query-bindings [{a [b [{_c [d]}]]}] 100 | {:a {:b/_c {:d :abcdval}}} 101 | (is (= [a b d] 102 | [{:b/_c {:d :abcdval}} {:d :abcdval} :abcdval])))) 103 | -------------------------------------------------------------------------------- /src/test/workflo/macros/command_run_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.command-run-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :refer [deftest is]] 4 | [workflo.macros.command :as c :refer [defcommand]])) 5 | 6 | ;; Use a user-counters atom for testing 7 | (def user-counters 8 | (atom [{:user/name :jeff :user/counter 0} 9 | {:user/name :joe :user/counter 0}])) 10 | 11 | ;; Example query, auth-query and process-emit hooks 12 | (defn example-query-hook 13 | [{:keys [query context] :as data}] 14 | ;; Verify the query is what we'd expect 15 | (is (= [{:name 'user 16 | :type :join 17 | :join-source {:name 'user :type :property} 18 | :join-target [{:name 'user/name :type :property :alias 'user-name} 19 | {:name 'user/counter :type :property}] 20 | :parameters {'user/name (:user context)}}] 21 | query)) 22 | ;; Simulate running the query 23 | (assoc data :query-result {:user (first (filter (fn [user-counter] 24 | (= (:user/name user-counter) 25 | (:user context))) 26 | @user-counters))})) 27 | 28 | (defn example-auth-query-hook 29 | [{:keys [query context] :as data}] 30 | ;; Verify the auth query is what we'd expect 31 | (is (= '[{:name permissions 32 | :type :join 33 | :join-source {:name permissions :type :property} 34 | :join-target [{:name name :type :property}]}] 35 | query)) 36 | ;; Simulate running the auth query, returning permissions only for jeff 37 | (assoc data :query-result (if (= :jeff (:user context)) 38 | {:permissions [{:name :update}]} 39 | {:permissions []}))) 40 | 41 | (defn example-process-emit-hook 42 | [{:keys [output context]}] 43 | (swap! user-counters 44 | (fn [user-counters] 45 | (mapv (fn [user-counter] 46 | (cond-> user-counter 47 | (= (:user/name output) 48 | (:user/name user-counter)) 49 | (assoc :user/counter (:user/counter output)))) 50 | user-counters)))) 51 | 52 | (deftest exercise-run-command-with-authorization 53 | (c/register-command-hook! :query example-query-hook) 54 | (c/register-command-hook! :auth-query example-auth-query-hook) 55 | (c/register-command-hook! :process-emit example-process-emit-hook) 56 | 57 | ;; Define a spec for the command data 58 | (s/def :update-user-counter/user keyword?) 59 | (s/def ::update-user-counter-data 60 | (s/keys :req [:update-user-counter/user])) 61 | 62 | (defcommand update-user-counter 63 | (spec ::update-user-counter-data) 64 | (query 65 | [({user [user [name :as user-name counter]]} 66 | {user/name ?user})]) 67 | (auth-query 68 | [{permissions [name]}]) 69 | (auth 70 | (is (or (= {:user/name :jeff :user/counter 0} user) 71 | (= {:user/name :joe :user/counter 0} user))) 72 | (if (= :jeff user-name) 73 | (is (= [{:name :update}] permissions)) 74 | (is (= [] permissions))) 75 | ;; Only users for which permissions are available are authorized 76 | ;; to update counters 77 | (and user (seq permissions))) 78 | (emit 79 | {:user/name user-name 80 | :user/counter (inc counter)})) 81 | 82 | ;; Run the command as an authorized user 83 | (c/run-command! 'update-user-counter 84 | {:update-user-counter/user :jeff} 85 | {:user :jeff}) 86 | 87 | ;; Verify that the user's counter has changed 88 | (is (= [{:user/name :jeff :user/counter 1} 89 | {:user/name :joe :user/counter 0}] 90 | @user-counters)) 91 | 92 | ;; Verify that running the command as an unauthorized user 93 | ;; throws an exception in Clojure but not in ClojureScript (where 94 | ;; authorization is disabled) 95 | #?(:cljs 96 | (c/run-command! 'update-user-counter 97 | {:update-user-counter/user :joe} 98 | {:user :joe}) 99 | :clj 100 | (is (thrown? #?(:cljs js/Error :clj Exception) 101 | (c/run-command! 'update-user-counter 102 | {:update-user-counter/user :joe} 103 | {:user :joe})))) 104 | 105 | ;; Verify that, in Clojure, the unauthorized command wasn't executed, 106 | ;; whereas it was executed in ClojureScript 107 | #?(:cljs 108 | (is (= [{:user/name :jeff :user/counter 1} 109 | {:user/name :joe :user/counter 1}] 110 | @user-counters)) 111 | :clj 112 | (is (= [{:user/name :jeff :user/counter 1} 113 | {:user/name :joe :user/counter 0}] 114 | @user-counters)))) 115 | -------------------------------------------------------------------------------- /src/test/workflo/macros/command_test.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.command-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :refer [deftest is]] 4 | [workflo.macros.command :as c :refer [defcommand]])) 5 | 6 | (deftest minimal-defcommand 7 | (is (= '(do 8 | (defn user-create-emit 9 | [query-result data] 10 | (:foo :bar)) 11 | (def user-create-spec 12 | vector?) 13 | (def user-create-definition 14 | {:spec pod/user-create-spec 15 | :emit pod/user-create-emit}) 16 | (workflo.macros.command/register-command! 17 | 'user/create pod/user-create-definition)) 18 | (macroexpand-1 `(defcommand user/create 19 | (~'spec ~'vector?) 20 | (~'emit (:foo :bar))))))) 21 | 22 | (deftest fully-qualified-command-name 23 | (is (= '(do 24 | (defn my-user-create-emit 25 | [query-result data] 26 | (:foo :bar)) 27 | (def my-user-create-spec 28 | vector?) 29 | (def my-user-create-definition 30 | {:spec pod/my-user-create-spec 31 | :emit pod/my-user-create-emit}) 32 | (workflo.macros.command/register-command! 33 | 'my.user/create pod/my-user-create-definition)) 34 | (macroexpand-1 `(defcommand my.user/create 35 | (~'spec ~'vector?) 36 | (~'emit (:foo :bar))))))) 37 | 38 | (deftest defcommand-with-hints 39 | (is (= '(do 40 | (defn my-user-create-emit 41 | [query-result data] 42 | (:foo :bar)) 43 | (def my-user-create-hints 44 | [:bar :baz]) 45 | (def my-user-create-spec 46 | vector?) 47 | (def my-user-create-definition 48 | {:spec pod/my-user-create-spec 49 | :hints pod/my-user-create-hints 50 | :emit pod/my-user-create-emit}) 51 | (workflo.macros.command/register-command! 52 | 'my.user/create pod/my-user-create-definition)) 53 | (macroexpand-1 `(defcommand my.user/create 54 | (~'hints [:bar :baz]) 55 | (~'spec ~'vector?) 56 | (~'emit (:foo :bar))))))) 57 | 58 | (deftest defcommand-with-query 59 | (is (= '(do 60 | (defn user-update-emit 61 | [query-result data] 62 | (workflo.macros.bind/with-query-bindings 63 | [{:name user/name :type :property} 64 | {:name user/email :type :property}] 65 | query-result 66 | {:some :data})) 67 | (def user-update-query 68 | '[{:name user/name :type :property} 69 | {:name user/email :type :property}]) 70 | (def user-update-spec 71 | vector?) 72 | (def user-update-definition 73 | {:spec pod/user-update-spec 74 | :query pod/user-update-query 75 | :emit pod/user-update-emit}) 76 | (workflo.macros.command/register-command! 77 | 'user/update pod/user-update-definition)) 78 | (macroexpand-1 `(defcommand user/update 79 | (~'spec ~'vector?) 80 | (~'query ~'[user [name email]]) 81 | (~'emit {:some :data})))))) 82 | 83 | (deftest defcommand-with-forms 84 | (is (= '(do 85 | (defn user-update-foo 86 | [query-result data] 87 | [:bar]) 88 | (defn user-update-emit 89 | [query-result data] 90 | {:emit :result}) 91 | (def user-update-spec 92 | vector?) 93 | (def user-update-definition 94 | {:foo pod/user-update-foo 95 | :emit pod/user-update-emit 96 | :spec pod/user-update-spec}) 97 | (workflo.macros.command/register-command! 98 | 'user/update pod/user-update-definition)) 99 | (macroexpand-1 `(defcommand user/update 100 | (~'spec ~'vector?) 101 | (~'foo [:bar]) 102 | (~'emit {:emit :result})))))) 103 | 104 | (deftest defcommand-with-query-and-forms 105 | (is (= '(do 106 | (defn user-create-foo 107 | [query-result data] 108 | (workflo.macros.bind/with-query-bindings 109 | [{:name db/id :type :property}] 110 | query-result 111 | [:bar])) 112 | (defn user-create-emit 113 | [query-result data] 114 | (workflo.macros.bind/with-query-bindings 115 | [{:name db/id :type :property}] 116 | query-result 117 | :result)) 118 | (def user-create-query 119 | '[{:name db/id :type :property}]) 120 | (def user-create-spec 121 | map?) 122 | (def user-create-definition 123 | {:foo pod/user-create-foo 124 | :emit pod/user-create-emit 125 | :query pod/user-create-query 126 | :spec pod/user-create-spec}) 127 | (workflo.macros.command/register-command! 128 | 'user/create pod/user-create-definition)) 129 | (macroexpand-1 `(defcommand user/create 130 | (~'spec ~'map?) 131 | (~'query ~'[db [id]]) 132 | (~'foo [:bar]) 133 | (~'emit :result)))))) 134 | 135 | (deftest defcommand-with-query-and-auth 136 | (is (= '(do 137 | (defn user-update-emit 138 | [query-result data] 139 | (workflo.macros.bind/with-query-bindings 140 | [{:name db/id :type :property}] 141 | query-result 142 | :result)) 143 | (def user-update-query 144 | '[{:name db/id :type :property}]) 145 | (defn user-update-auth 146 | [query-result auth-query-result] 147 | (workflo.macros.bind/with-query-bindings 148 | [{:name db/id :type :property}] 149 | query-result 150 | :foo)) 151 | (def user-update-definition 152 | {:emit pod/user-update-emit 153 | :query pod/user-update-query 154 | :auth pod/user-update-auth}) 155 | (workflo.macros.command/register-command! 156 | 'user/update pod/user-update-definition)) 157 | (macroexpand-1 `(defcommand user/update 158 | (~'query ~'[db [id]]) 159 | (~'auth :foo) 160 | (~'emit :result)))))) 161 | 162 | (deftest defcommand-with-query-and-auth-and-auth-query 163 | (is (= '(do 164 | (defn user-update-emit 165 | [query-result data] 166 | (workflo.macros.bind/with-query-bindings 167 | [{:name db/id :type :property}] 168 | query-result 169 | :result)) 170 | (def user-update-query 171 | '[{:name db/id :type :property}]) 172 | (def user-update-auth-query 173 | '[{:name foo/bar :type :property}]) 174 | (defn user-update-auth 175 | [query-result auth-query-result] 176 | (workflo.macros.bind/with-query-bindings 177 | [{:name db/id :type :property}] 178 | query-result 179 | (workflo.macros.bind/with-query-bindings 180 | [{:name foo/bar :type :property}] 181 | auth-query-result 182 | :foo))) 183 | (def user-update-definition 184 | {:emit pod/user-update-emit 185 | :query pod/user-update-query 186 | :auth-query pod/user-update-auth-query 187 | :auth pod/user-update-auth}) 188 | (workflo.macros.command/register-command! 189 | 'user/update pod/user-update-definition)) 190 | (macroexpand-1 `(defcommand user/update 191 | (~'query ~'[db [id]]) 192 | (~'auth-query ~'[foo [bar]]) 193 | (~'auth :foo) 194 | (~'emit :result)))))) 195 | -------------------------------------------------------------------------------- /src/test/workflo/macros/config_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.config-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [workflo.macros.config :refer [defconfig]])) 4 | 5 | (deftest defconfig-defines-all-expected-functions 6 | (defconfig view {}) 7 | (and (is (fn? configure-views!)) 8 | (is (fn? get-view-config)))) 9 | 10 | (deftest defconfig-works-as-expected 11 | (defconfig item {:foo :default-foo 12 | :bar "Default bar" 13 | :baz nil}) 14 | (and (is (= {:foo :default-foo 15 | :bar "Default bar" 16 | :baz nil} 17 | (get-item-config))) 18 | (is (= :default-foo (get-item-config :foo))) 19 | (is (= "Default bar" (get-item-config :bar))) 20 | (is (= nil (get-item-config :baz))) 21 | (is (= nil (get-item-config :ruux))) 22 | (do 23 | (configure-items! {:foo :other-foo}) 24 | (and (is (= :other-foo (get-item-config :foo))) 25 | (is (= "Default bar" (get-item-config :bar))) 26 | (is (= nil (get-item-config :baz))) 27 | (is (= nil (get-item-config :ruux))))))) 28 | -------------------------------------------------------------------------------- /src/test/workflo/macros/entity/datomic_schema_test.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity.datomic-schema-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :refer [are deftest is]] 4 | [workflo.macros.entity :refer [resolve-entity defentity]] 5 | [workflo.macros.entity.datomic :as datomic-schema] 6 | [workflo.macros.entity.test-entities] 7 | [workflo.macros.specs.types :as types] 8 | [workflo.macros.util.misc :refer [val-after]])) 9 | 10 | ;;;; Helpers 11 | 12 | (defn datomic-attrs [schema] 13 | (into #{} 14 | (map (fn [attr-schema] 15 | (if (vector? attr-schema) 16 | (val-after attr-schema :db/ident) 17 | (:db/ident attr-schema)))) 18 | schema)) 19 | 20 | ;;;; Tests 21 | 22 | (deftest entity-with-value-spec-type-id 23 | (let [entity (resolve-entity 'url/selected-user)] 24 | (and (is (not (nil? entity))) 25 | (is (= #{:url/selected-user} 26 | (-> entity datomic-schema/entity-schema 27 | datomic-attrs)))))) 28 | 29 | (deftest entity-with-value-spec-type-string 30 | (let [entity (resolve-entity 'ui/search-text)] 31 | (and (is (not (nil? entity))) 32 | (is (= #{:ui/search-text} 33 | (-> entity datomic-schema/entity-schema 34 | datomic-attrs)))))) 35 | 36 | (deftest entity-with-keys-spec 37 | (let [entity (resolve-entity 'user)] 38 | (and (is (not (nil? entity))) 39 | (is (= #{:base/id :user/email :user/name :user/role :user/bio 40 | :user.role/user :user.role/admin :user.role/owner} 41 | (-> entity datomic-schema/entity-schema 42 | datomic-attrs)))))) 43 | 44 | (deftest entity-with-and-keys-spec 45 | (let [entity (resolve-entity 'user-with-extended-spec)] 46 | (and (is (not (nil? entity))) 47 | (is (= #{:base/id :user/email :user/name 48 | :user/role :user/bio :user/address 49 | :user.role/user :user.role/admin :user.role/owner} 50 | (-> entity datomic-schema/entity-schema 51 | datomic-attrs)))))) 52 | 53 | (deftest entity-with-and-value-spec 54 | (let [entity (resolve-entity 'user-with-extended-spec)] 55 | (and (is (not (nil? entity))) 56 | (is (= #{:base/id :user/email :user/name 57 | :user/role :user/bio :user/address 58 | :user.role/user :user.role/admin :user.role/owner} 59 | (-> entity datomic-schema/entity-schema 60 | datomic-attrs)))))) 61 | 62 | (deftest schema-merging 63 | (let [entities (map resolve-entity '[user user-with-extended-spec]) 64 | schemas (map datomic-schema/entity-schema entities)] 65 | (is (= #{:base/id :user/email :user/name 66 | :user/role :user/bio :user/address 67 | :user.role/user :user.role/admin :user.role/owner} 68 | (-> schemas datomic-schema/merge-schemas 69 | datomic-attrs))))) 70 | 71 | ;;;; Entity refs 72 | 73 | ;;; Entities with refs between them 74 | 75 | (deftest entity-with-entity-refs 76 | (let [entity (resolve-entity 'post)] 77 | (and (is (not (nil? entity))) 78 | (is (= #{:post/author :post/text :post/comments} 79 | (-> entity datomic-schema/entity-schema 80 | datomic-attrs)))))) 81 | 82 | ;;; Entities with a top-level entity-ref spec 83 | 84 | (deftest entity-with-entity-ref-spec 85 | (let [entity (resolve-entity 'current-post)] 86 | (and (is (not (nil? entity))) 87 | (is (thrown? IllegalArgumentException 88 | (-> entity datomic-schema/entity-schema 89 | datomic-attrs)))))) 90 | 91 | (deftest entity-with-entity-ref-many-spec 92 | (let [entity (resolve-entity 'previous-posts)] 93 | (and (is (not (nil? entity))) 94 | (is (thrown? IllegalArgumentException 95 | (-> entity datomic-schema/entity-schema 96 | datomic-attrs)))))) 97 | 98 | -------------------------------------------------------------------------------- /src/test/workflo/macros/entity/refs_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity.refs-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :refer [are deftest is use-fixtures]] 4 | [workflo.macros.entity :as e] 5 | [workflo.macros.entity.schema :as schema] 6 | [workflo.macros.entity.test-entities] 7 | [workflo.macros.specs.types :as types])) 8 | 9 | (deftest refs-for-an-entity 10 | (is (nil? (e/entity-refs 'url/selected-user))) 11 | (is (nil? (e/entity-refs 'ui/search-text))) 12 | (is (nil? (e/entity-refs 'ui/search-text-with-extended-spec))) 13 | (is (nil? (e/entity-refs 'user))) 14 | (is (nil? (e/entity-refs 'user-with-extended-spec))) 15 | (is (nil? (e/entity-refs 'author))) 16 | (is (nil? (e/entity-refs 'current-post))) 17 | (is (nil? (e/entity-refs 'previous-post))) 18 | 19 | (is (= {:post/author {:entity 'author :many? false} 20 | :post/comments {:entity 'comment :many? true}} 21 | (e/entity-refs 'post))) 22 | 23 | (is (= {:comment/author {:entity 'author :many? false}} 24 | (e/entity-refs 'comment)))) 25 | 26 | (deftest backrefs-for-an-entity 27 | (is (nil? (e/entity-backrefs 'url/selected-user))) 28 | (is (nil? (e/entity-backrefs 'ui/search-text))) 29 | (is (nil? (e/entity-backrefs 'ui/search-text-with-extended-spec))) 30 | (is (nil? (e/entity-backrefs 'user))) 31 | (is (nil? (e/entity-backrefs 'user-with-extended-spec))) 32 | (is (nil? (e/entity-backrefs 'post))) 33 | 34 | (is (= {:post/author {:entity 'post :many? true} 35 | :comment/author {:entity 'comment :many? true}} 36 | (e/entity-backrefs 'author))) 37 | 38 | (is (= {:post/comments {:entity 'post :many? true}} 39 | (e/entity-backrefs 'comment)))) 40 | 41 | (deftest refs-and-backrefs-before-and-after-entity 42 | (s/def :foo/bar ::types/string) 43 | (e/defentity foo (spec (s/keys :req [:foo/bar]))) 44 | 45 | (let [refs-before (e/entity-refs 'foo) 46 | backrefs-before (e/entity-backrefs 'foo)] 47 | 48 | (s/def :bar/foo (types/entity-ref 'foo)) 49 | (e/defentity bar (spec (s/keys :req [:bar/foo]))) 50 | (e/unregister-entity! 'bar) 51 | 52 | (let [refs-after (e/entity-refs 'foo) 53 | backrefs-after (e/entity-backrefs 'foo)] 54 | (is (= refs-before refs-after)) 55 | (is (= backrefs-before backrefs-after))))) 56 | -------------------------------------------------------------------------------- /src/test/workflo/macros/entity/schema_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity.schema-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :refer [are deftest is]] 4 | [workflo.macros.entity :refer [registered-entities resolve-entity]] 5 | [workflo.macros.entity.schema :as schema] 6 | [workflo.macros.entity.test-entities] 7 | [workflo.macros.specs.types :as types])) 8 | 9 | (deftest entity-with-value-spec-type-id 10 | (let [entity (resolve-entity 'url/selected-user)] 11 | (and (is (not (nil? entity))) 12 | (is (= {:url/selected-user [:string]} 13 | (schema/entity-schema entity)))))) 14 | 15 | (deftest entity-with-value-spec-type-string 16 | (let [entity (resolve-entity 'ui/search-text)] 17 | (and (is (not (nil? entity))) 18 | (is (= {:ui/search-text [:string]} 19 | (schema/entity-schema entity)))))) 20 | 21 | (deftest entity-with-keys-spec 22 | (let [entity (resolve-entity 'user)] 23 | (and (is (not (nil? entity))) 24 | (is (= {:base/id [:uuid :unique-identity] 25 | :user/email [:string :unique-value] 26 | :user/name [:string] 27 | :user/role [:enum [:admin :user :owner]] 28 | :user/bio [:string]} 29 | (schema/entity-schema entity)))))) 30 | 31 | (deftest entity-with-and-keys-spec 32 | (let [entity (resolve-entity 'user-with-extended-spec)] 33 | (and (is (not (nil? entity))) 34 | (is (= {:base/id [:uuid :unique-identity] 35 | :user/email [:string :unique-value] 36 | :user/name [:string] 37 | :user/role [:enum [:admin :user :owner]] 38 | :user/bio [:string] 39 | :user/address [:string]} 40 | (schema/entity-schema entity)))))) 41 | 42 | (deftest entity-with-and-value-spec 43 | (let [entity (resolve-entity 'user-with-extended-spec)] 44 | (and (is (not (nil? entity))) 45 | (is (= {:base/id [:uuid :unique-identity] 46 | :user/email [:string :unique-value] 47 | :user/name [:string] 48 | :user/role [:enum [:admin :user :owner]] 49 | :user/bio [:string] 50 | :user/address [:string]} 51 | (schema/entity-schema entity)))))) 52 | 53 | ;;;; Matching schemas 54 | 55 | (deftest matching-entity-schemas 56 | (is (= {:url/selected-user [:string] 57 | :ui/search-text [:string] 58 | :ui/search-text-with-extended-spec [:string]} 59 | (schema/matching-entity-schemas (registered-entities) 60 | #"^(url|ui)/.*")))) 61 | 62 | ;;;; Keys, required keys, optional keys 63 | 64 | (deftest all-keys 65 | (are [x y] (= x (-> y resolve-entity schema/keys)) 66 | {} 'url/selected-user 67 | {} 'ui/search-text 68 | {} 'ui/search-text-with-extended-spec 69 | {:required [:base/id 70 | :user/name 71 | :user/email 72 | :user/role] 73 | :optional [:user/bio]} 'user 74 | {:required [:base/id 75 | :user/name 76 | :user/email 77 | :user/role] 78 | :optional [:user/bio 79 | :user/address]} 'user-with-extended-spec)) 80 | 81 | (deftest required-keys 82 | (are [x y] (= x (-> y resolve-entity schema/required-keys)) 83 | [] 'url/selected-user 84 | [] 'ui/search-text 85 | [] 'ui/search-text-with-extended-spec 86 | [:base/id 87 | :user/name 88 | :user/email 89 | :user/role] 'user 90 | [:base/id 91 | :user/name 92 | :user/email 93 | :user/role] 'user-with-extended-spec)) 94 | 95 | (deftest optional-keys 96 | (are [x y] (= x (-> y resolve-entity schema/optional-keys)) 97 | [] 'url/selected-user 98 | [] 'ui/search-text 99 | [] 'ui/search-text-with-extended-spec 100 | [:user/bio] 'user 101 | [:user/bio 102 | :user/address] 'user-with-extended-spec)) 103 | 104 | ;;;; Non-persistent keys 105 | 106 | (deftest non-persistent-keys 107 | (are [x y] (= x (-> y resolve-entity schema/non-persistent-keys)) 108 | [] 'url/selected-user 109 | [] 'ui/search-text 110 | [] 'ui/search-text-with-extended-spec 111 | [] 'user 112 | [:user/address] 'user-with-extended-spec)) 113 | 114 | ;;;; Entity refs 115 | 116 | ;;; Entities with refs between them 117 | 118 | (deftest entity-ref-describe 119 | (are [desc spec] (= desc (s/describe spec)) 120 | ;; Without options 121 | '(entity-ref user) (types/entity-ref 'user) 122 | '(entity-ref comment) (types/entity-ref 'comment) 123 | 124 | ;; With options 125 | '(entity-ref user :many? true) 126 | (types/entity-ref 'user :many? true))) 127 | 128 | (deftest entity-with-entity-refs 129 | (let [entity (resolve-entity 'post)] 130 | (and (is (not (nil? entity))) 131 | (is (= {:post/author [:ref :indexed] 132 | :post/text [:string] 133 | :post/comments [:ref :many]} 134 | (schema/entity-schema entity)))))) 135 | 136 | ;;; Entities with a top-level entity-ref spec 137 | 138 | (deftest entity-with-entity-ref-spec 139 | (let [entity (resolve-entity 'current-post)] 140 | (and (is (not (nil? entity))) 141 | (is (= {:current-post [:ref]} 142 | (schema/entity-schema entity)))))) 143 | 144 | (deftest entity-with-entity-ref-many-spec 145 | (let [entity (resolve-entity 'previous-posts)] 146 | (and (is (not (nil? entity))) 147 | (is (= {:previous-posts [:ref :many]} 148 | (schema/entity-schema entity)))))) 149 | 150 | ;;;; Entity refs 151 | 152 | (deftest entity-refs 153 | (are [joins entity] (= joins (-> entity resolve-entity 154 | schema/entity-refs)) 155 | {:post/author {:entity 'author} 156 | :post/comments {:entity 'comment :many? true}} 'post 157 | {:comment/author {:entity 'author}} 'comment 158 | {:entity 'post} 'current-post 159 | {:entity 'post :many? true} 'previous-posts 160 | nil 'ui/search-text)) 161 | -------------------------------------------------------------------------------- /src/test/workflo/macros/entity/test_entities.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity.test-entities 2 | (:require [clojure.spec.alpha :as s] 3 | [workflo.macros.entity :refer [defentity]] 4 | [workflo.macros.specs.types :as types])) 5 | 6 | (defentity url/selected-user 7 | (spec ::types/id)) 8 | 9 | (defentity ui/search-text 10 | (spec ::types/string)) 11 | 12 | (defentity ui/search-text-with-extended-spec 13 | (spec 14 | (s/and ::types/string 15 | #(> (count %) 5)))) 16 | 17 | (s/def :base/id (s/and ::types/uuid ::types/unique-identity)) 18 | (s/def :user/email (s/and ::types/string 19 | ::types/unique-value 20 | #(> (count %) 5))) 21 | (s/def :user/name ::types/string) 22 | (s/def :user/role (s/and ::types/enum #{:user :admin :owner})) 23 | (s/def :user/bio ::types/string) 24 | 25 | (defentity user 26 | (spec 27 | (s/keys :req [:base/id :user/name :user/email :user/role] 28 | :opt [:user/bio]))) 29 | 30 | (s/def :user/address (s/and ::types/string ::types/non-persistent)) 31 | 32 | (defentity user-with-extended-spec 33 | (spec 34 | (s/and (s/keys :req [:base/id :user/name :user/email :user/role] 35 | :opt [:user/bio :user/address]) 36 | #(> (count (:user/name %)) 5)))) 37 | 38 | ;;;; Entities with refs between them 39 | 40 | (s/def :post/author (s/and (types/entity-ref 'author) ::types/indexed)) 41 | (s/def :post/text ::types/string) 42 | (s/def :post/comments (types/entity-ref 'comment :many? true)) 43 | 44 | (defentity post 45 | (spec (s/keys :req [:post/author 46 | :post/text] 47 | :opt [:post/comments]))) 48 | 49 | (s/def :author/name ::types/string) 50 | 51 | (defentity author 52 | (spec (s/keys :req [:author/name]))) 53 | 54 | (s/def :comment/author (types/entity-ref 'author)) 55 | (s/def :comment/text ::types/string) 56 | 57 | (defentity comment 58 | (spec (s/keys :req [:comment/author 59 | :comment/text]))) 60 | 61 | ;;;; Entities with a top-level entity-ref spec 62 | 63 | (defentity current-post 64 | (spec (types/entity-ref 'post))) 65 | 66 | (defentity previous-posts 67 | (spec (types/entity-ref 'post :many? true))) 68 | -------------------------------------------------------------------------------- /src/test/workflo/macros/entity_test.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.entity-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :refer [deftest is]] 4 | [workflo.macros.entity :as e :refer [defentity]])) 5 | 6 | (deftest minimal-defentity 7 | (is (= '(do 8 | (def macros-user-name 'macros/user) 9 | (def macros-user-spec map?) 10 | (def macros-user-definition 11 | {:name pod/macros-user-name 12 | :spec pod/macros-user-spec}) 13 | (workflo.macros.entity/register-entity! 14 | 'macros/user pod/macros-user-definition)) 15 | (macroexpand-1 `(defentity macros/user 16 | (~'spec ~'map?)))))) 17 | 18 | (deftest fully-qualified-entity-name 19 | (is (= '(do 20 | (def my-ui-app-name 'my.ui/app) 21 | (def my-ui-app-spec map?) 22 | (def my-ui-app-definition 23 | {:name pod/my-ui-app-name 24 | :spec pod/my-ui-app-spec}) 25 | (workflo.macros.entity/register-entity! 26 | 'my.ui/app pod/my-ui-app-definition)) 27 | (macroexpand-1 `(defentity my.ui/app 28 | (~'spec ~'map?)))))) 29 | 30 | (deftest defentity-with-hints 31 | (is (= '(do 32 | (def x-foo-name 'x/foo) 33 | (def x-foo-hints [:bar :baz]) 34 | (def x-foo-spec map?) 35 | (def x-foo-definition 36 | {:name pod/x-foo-name 37 | :hints pod/x-foo-hints 38 | :spec pod/x-foo-spec}) 39 | (workflo.macros.entity/register-entity! 40 | 'x/foo pod/x-foo-definition)) 41 | (macroexpand-1 `(defentity x/foo 42 | (~'hints [:bar :baz]) 43 | (~'spec ~'map?)))))) 44 | 45 | (deftest defentity-with-auth 46 | (is (= '(do 47 | (def macros-user-name 'macros/user) 48 | (def macros-user-spec map?) 49 | (def macros-user-auth-query 50 | '[{:name foo :type :property} 51 | {:name bar :type :property}]) 52 | (defn macros-user-auth 53 | [auth-query-result entity-id viewer-id] 54 | (workflo.macros.bind/with-query-bindings 55 | [{:name foo :type :property} 56 | {:name bar :type :property}] 57 | auth-query-result 58 | (println foo bar))) 59 | (def macros-user-definition 60 | {:name pod/macros-user-name 61 | :spec pod/macros-user-spec 62 | :auth pod/macros-user-auth 63 | :auth-query pod/macros-user-auth-query}) 64 | (workflo.macros.entity/register-entity! 65 | 'macros/user pod/macros-user-definition)) 66 | (macroexpand-1 `(defentity macros/user 67 | ~'(spec map?) 68 | ~'(auth-query [foo bar]) 69 | ~'(auth 70 | (println foo bar))))))) 71 | -------------------------------------------------------------------------------- /src/test/workflo/macros/permission_test.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.permission-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :refer [deftest is]] 4 | [workflo.macros.permission :as p :refer [defpermission]])) 5 | 6 | (deftest simple-defpermission 7 | (is (= '(do 8 | (def do-something 9 | {:name :do-something 10 | :title "Able to do something" 11 | :description "A good description"}) 12 | (workflo.macros.permission/register-permission! 13 | 'do-something pod/do-something)) 14 | (macroexpand '(workflo.macros.permission/defpermission do-something 15 | (title "Able to do something") 16 | (description "A good description")))))) 17 | -------------------------------------------------------------------------------- /src/test/workflo/macros/query/om_next_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.query.om-next-test 2 | (:require [clojure.test :refer [are deftest]] 3 | [workflo.macros.query.om-next :as om])) 4 | 5 | 6 | (deftest disambiguate-queries-without-conflicts 7 | (are [out in] (= out (om/disambiguate in)) 8 | ;; Identical keywords are merged 9 | [[:foo :bar :baz]] 10 | [:foo :foo :bar :bar :baz :baz] 11 | 12 | ;; Queries of joins with identical join sources are combined 13 | [[{:foo [:bar :baz]} 14 | {:bar [:baz :ruux]}]] 15 | [{:foo [:bar]} 16 | {:foo [:baz]} 17 | {:bar [:baz]} 18 | {:bar [:baz :ruux]}] 19 | 20 | ;; Queries of joins with identical join sources are 21 | ;; combined and disambiguated recursively 22 | [[{:foo [{:bar [:baz]}]}]] 23 | [{:foo [{:bar [:baz :baz]} 24 | {:bar [:baz :baz]}]}] 25 | 26 | ;; Queries of joins with different limited recursions 27 | ;; combined so that the higher recursion number wins 28 | [[{:foo 10}]] 29 | [{:foo 5} {:foo 2} {:foo 10} {:foo 3}] 30 | 31 | ;; Identical parameterized queries are merged 32 | '[[(:foo {:bar :baz})]] 33 | '[(:foo {:bar :baz}) 34 | (:foo {:bar :baz})] 35 | 36 | ;; Parameterized queries are merged and combined if their 37 | ;; parameters are identical 38 | '[[({:foo [:bar :baz]} {:bar :baz})]] 39 | '[(:foo {:bar :baz}) 40 | ({:foo [:bar]} {:bar :baz}) 41 | ({:foo [:bar :baz]} {:bar :baz})])) 42 | 43 | 44 | (deftest disambiguate-queries-with-conflicts 45 | (are [out in] (= out (om/disambiguate in)) 46 | ;; Identical parameterized queries are split if their 47 | ;; parameters differ 48 | '[[(:foo {:bar :baz})] 49 | [(:foo {:bar :ruux})]] 50 | '[(:foo {:bar :baz}) 51 | (:foo {:bar :ruux})] 52 | 53 | ;; Parameterized and non-parameterized queries are split 54 | ;; even if their dispatch keys match 55 | '[[:foo] 56 | [(:foo {:bar :baz})]] 57 | '[:foo (:foo {:bar :baz})] 58 | 59 | ;; Recursive and non-recursive joins are split even if 60 | ;; their dispatch keys match 61 | '[[{:foo ...}] 62 | [{:foo [:bar]}]] 63 | '[{:foo ...} {:foo [:bar]}] 64 | 65 | ;; Joins with limited recursion and no limited recursion 66 | ;; are split even if their dispatch keys match 67 | '[[{:foo 1}] 68 | [{:foo ...}] 69 | [{:foo [:bar]}]] 70 | '[{:foo 1} {:foo ...} {:foo [:bar]}] 71 | 72 | 73 | ;; Ident and non-ident queries are split even if their 74 | ;; dispatch keys match 75 | [[:foo] 76 | [[:foo 1]]] 77 | [:foo 78 | [:foo 1]] 79 | 80 | ;; Idents with different values are split even if their 81 | ;; dispatch keys match 82 | [[[:foo 1]] 83 | [[:foo 2]]] 84 | [[:foo 1] 85 | [:foo 2]] 86 | 87 | ;; Joins with a conflict in their subqueries are split 88 | '[[{:foo [(:bar {:a :b}) :baz]}] 89 | [{:foo [(:bar {:b :c}) :baz]}]] 90 | '[{:foo [(:bar {:a :b}) :baz]} 91 | {:foo [(:bar {:b :c}) :baz]}])) 92 | 93 | 94 | (deftest disambiguate-queries-with-and-without-conflicts 95 | (are [out in] (= out (om/disambiguate in)) 96 | ;; Duplicate keywords are merged, conflicts are grouped 97 | ;; into two a sequence of two non-conflicting queries 98 | '[[:foo :bar :baz :ruux] 99 | [(:foo {:a :b}) 100 | (:bar {:a :b}) 101 | (:baz {:a :b})]] 102 | '[:foo (:foo {:a :b}) :foo 103 | :bar (:bar {:a :b}) :bar 104 | :baz (:baz {:a :b}) :baz 105 | :ruux])) 106 | -------------------------------------------------------------------------------- /src/test/workflo/macros/registry_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.registry-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [workflo.macros.registry :refer [defregistry]])) 4 | 5 | (deftest defregistry-defines-all-expected-functions 6 | (defregistry view) 7 | (and (is (fn? register-view!)) 8 | (is (fn? registered-views)) 9 | (is (fn? reset-registered-views!)) 10 | (is (fn? resolve-view)))) 11 | 12 | (deftest defregistry-works-as-expected 13 | (defregistry item) 14 | (register-item! 'foo :bar) 15 | (register-item! 'bar :baz) 16 | (and (is (= {'foo :bar 'bar :baz} 17 | (registered-items))) 18 | (is (= :bar (resolve-item 'foo))) 19 | (is (= :baz (resolve-item 'bar))) 20 | (do 21 | (reset-registered-items!) 22 | (is (= (sorted-map) 23 | (registered-items)))))) 24 | -------------------------------------------------------------------------------- /src/test/workflo/macros/screen/bidi_test.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.screen.bidi-test 2 | (:require [cljs.test :refer [deftest is use-fixtures]] 3 | [workflo.macros.screen :as screen :refer [defscreen]] 4 | [workflo.macros.screen.bidi :as screen-bidi])) 5 | 6 | (defn empty-screen-registry [f] 7 | (screen/reset-registered-screens!) 8 | (f)) 9 | 10 | (use-fixtures :each empty-screen-registry) 11 | 12 | (deftest routes 13 | (def user-view :user-view) 14 | (def user-settings-view :user-settings-view) 15 | (def users-view :users-view) 16 | (defscreen user 17 | (url "users/:user-id") 18 | (navigation 19 | {:title "User"}) 20 | (sections 21 | {:content user-view})) 22 | (defscreen user-settings 23 | (url "users/:user-id/settings") 24 | (navigation 25 | {:title "User Settings"}) 26 | (sections 27 | {:content user-settings-view})) 28 | (defscreen users 29 | (url "users") 30 | (navigation 31 | {:title "Users"}) 32 | (sections 33 | {:content users-view})) 34 | (and 35 | ;; Check routes generation 36 | (is (= '["/" [[["users" "/" :user-id] user] 37 | [["users" "/" :user-id "/" "settings"] user-settings] 38 | [["users"] users]]] 39 | (screen-bidi/routes))) 40 | ;; Check route matching 41 | (is (= {:screen (screen/resolve-screen 'users) 42 | :params {}} 43 | (screen-bidi/match "/users"))) 44 | (is (= {:screen (screen/resolve-screen 'user) 45 | :params {:user-id "10"}} 46 | (screen-bidi/match "/users/10"))) 47 | (is (= {:screen (screen/resolve-screen 'user-settings) 48 | :params {:user-id "15"}} 49 | (screen-bidi/match "/users/15/settings"))) 50 | ;; Check path generation from screens 51 | (is (= "/users" (screen-bidi/path 'users))) 52 | (is (= "/users" (screen-bidi/path (screen/resolve-screen 'users)))) 53 | (is (= "/users/1" (screen-bidi/path 'user :user-id 1))) 54 | (is (= "/users/1" (screen-bidi/path (screen/resolve-screen 'user) 55 | :user-id 1))) 56 | (is (= "/users/1/settings" (screen-bidi/path 'user-settings 57 | :user-id 1))))) 58 | -------------------------------------------------------------------------------- /src/test/workflo/macros/screen_test.cljs: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.screen-test 2 | (:require [cljs.test :refer [deftest is]] 3 | [workflo.macros.screen :as screen :refer [defscreen]])) 4 | 5 | (deftest minimal-defscreen 6 | (is (= '(do 7 | (def users-name 'users) 8 | (def users-url 9 | {:string "screens/users" 10 | :segments ["screens" "users"]}) 11 | (def users-forms 12 | {:navigation {:title "Users"}}) 13 | (def users-sections {:content user}) 14 | (def users-definition 15 | {:name workflo.macros.screen-test/users-name 16 | :url workflo.macros.screen-test/users-url, 17 | :forms workflo.macros.screen-test/users-forms, 18 | :sections workflo.macros.screen-test/users-sections}) 19 | (workflo.macros.screen/register-screen! 20 | 'users workflo.macros.screen-test/users-definition))) 21 | (macroexpand-1 22 | '(defscreen users 23 | (url "screens/users") 24 | (navigation 25 | {:title "Users"}) 26 | (sections 27 | {:content user}))))) 28 | 29 | (deftest defscreen-with-description-forms-and-sections 30 | (is (= '(do 31 | (def users-name 'users) 32 | (def users-description "Displays all users") 33 | (def users-url 34 | {:string "screens/users" 35 | :segments ["screens" "users"]}) 36 | (def users-forms 37 | {:navigation {:title "Users"}}) 38 | (def users-sections {:content user}) 39 | (def users-definition 40 | {:name workflo.macros.screen-test/users-name 41 | :description workflo.macros.screen-test/users-description 42 | :url workflo.macros.screen-test/users-url 43 | :forms workflo.macros.screen-test/users-forms 44 | :sections workflo.macros.screen-test/users-sections}) 45 | (workflo.macros.screen/register-screen! 46 | 'users workflo.macros.screen-test/users-definition)) 47 | (macroexpand-1 48 | '(defscreen users 49 | "Displays all users" 50 | (url "screens/users") 51 | (navigation 52 | {:title "Users"}) 53 | (sections 54 | {:content user})))))) 55 | -------------------------------------------------------------------------------- /src/test/workflo/macros/service_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.service-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :refer [deftest is]] 4 | [com.stuartsierra.component :as component] 5 | [workflo.macros.service :as service :refer [defservice]])) 6 | 7 | (deftest a-simple-service 8 | ;; Specs for email data 9 | (s/def ::to string?) 10 | (s/def ::subject string?) 11 | (s/def ::body string?) 12 | (s/def ::email (s/keys :req-un [::to ::subject ::body])) 13 | 14 | (def email-service-info (atom {})) 15 | (def sent-emails (atom [])) 16 | 17 | ;; Email service definition 18 | (defservice email 19 | "Email service" 20 | (spec ::email) 21 | (start 22 | (swap! email-service-info assoc :started? true) 23 | this) 24 | (stop 25 | (swap! email-service-info dissoc :started?) 26 | this) 27 | (process (swap! sent-emails conj data))) 28 | 29 | (and 30 | ;; Verify that the email service is defined as a record 31 | (is (record? email)) 32 | 33 | ;; Verify that it has all the expected fields 34 | (is (= "Email service" (:description email))) 35 | (is (= ::email (:spec email))) 36 | (is (fn? (:start email))) 37 | (is (fn? (:stop email))) 38 | (is (fn? (:process email))) 39 | 40 | ;; Verify that the email service is registered 41 | (is (= email (service/resolve-service 'email))) 42 | 43 | ;; Verify that sending an email doesn't work yet, since the 44 | ;; service hasn't been instantiated as a component yet 45 | (do 46 | (service/deliver-to-services! 47 | {:email {:to "Recipient" 48 | :subject "Subject" 49 | :body "Body"}}) 50 | (is (empty? @sent-emails))) 51 | 52 | ;; Verify that the start function of the email service 53 | ;; hasn't been called yet and that there is no registered 54 | ;; email service component 55 | (and 56 | (is (nil? (:started? @email-service-info))) 57 | (is (nil? (get (service/registered-service-components) 'email)))) 58 | 59 | (and 60 | ;; Start the email service 61 | (do 62 | (let [c (service/new-service-component 'email)] 63 | (component/start c)) 64 | 65 | ;; Verify that the start function has been called and 66 | ;; that there is a registered email service component now 67 | (and 68 | (is (true? (:started? @email-service-info))) 69 | (is (not (nil? (get (service/registered-service-components) 70 | 'email)))))) 71 | (do 72 | ;; Verify that sending emails works 73 | (service/deliver-to-services! 74 | {:email {:to "Recipient 1" 75 | :subject "Subject 1" 76 | :body "Body 1"}}) 77 | (= [{:to "Recipient 1" :subject "Subject 1" :body "Body 1"}] 78 | @sent-emails)) 79 | (do 80 | ;; Verify that sending another email works 81 | (service/deliver-to-services! 82 | {:email {:to "Recipient 2" 83 | :subject "Subject 2" 84 | :body "Body 2"}}) 85 | (= [{:to "Recipient 1" :subject "Subject 1" :body "Body 1"} 86 | {:to "Recipient 2" :subject "Subject 2" :body "Body 2"}] 87 | @sent-emails)) 88 | (do 89 | ;; Stop the email service 90 | (let [c (get (service/registered-service-components) 'email)] 91 | (component/stop c)) 92 | 93 | ;; Verify that the stop function has been called and 94 | ;; there no longer is a registered email service component 95 | (and 96 | (is (nil? (:started? @email-service-info))) 97 | (is (nil? (get (service/registered-service-components) 98 | 'email))))) 99 | (do 100 | ;; Verify that sending emails no longer works 101 | (service/deliver-to-services! 102 | {:email {:to "Recipient" 103 | :subject "Subject" 104 | :body "Body"}}) 105 | (is (= [{:to "Recipient 1" :subject "Subject 1" :body "Body 1"} 106 | {:to "Recipient 2" :subject "Subject 2" :body "Body 2"}] 107 | @sent-emails)))))) 108 | -------------------------------------------------------------------------------- /src/test/workflo/macros/spec_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.spec-test 2 | (:require [clojure.string :as string] 3 | [clojure.test :refer [deftest is]] 4 | [clojure.spec.alpha :as s] 5 | [clojure.spec.test.alpha :as st] 6 | [workflo.macros.entity] 7 | [workflo.macros.command] 8 | [workflo.macros.command.util] 9 | [workflo.macros.jscomponents] 10 | [workflo.macros.query] 11 | [workflo.macros.query.bind] 12 | [workflo.macros.query.om-next] 13 | [workflo.macros.query.util] 14 | [workflo.macros.util.form] 15 | [workflo.macros.util.string] 16 | [workflo.macros.util.symbol] 17 | [workflo.macros.screen] 18 | [workflo.macros.view])) 19 | 20 | (workflo.macros.query/register-query-fragment! :example-fragment '[foo bar]) 21 | 22 | (def check-opts 23 | {:clojure.spec.test.check/opts {:num-tests 10 24 | :max-size 10}}) 25 | 26 | (defn workflo-sym? [sym] 27 | (string/starts-with? (str sym) "workflo.macros.")) 28 | 29 | ;; #?(:cljs (deftest test-specs 30 | ;; (doseq [sym (st/checkable-syms)] 31 | ;; (println " Testing" sym) 32 | ;; (when-not 33 | ;; (some #{sym} 34 | ;; ['workflo.macros.query/parse-subquery 35 | ;; 'workflo.macros.query/parse 36 | ;; 'workflo.macros.query.om-next/query 37 | ;; 'workflo.macros.query.om-next/property-query 38 | ;; 'workflo.macros.util.form/forms-map]) 39 | ;; (let [result (st/check sym check-opts)] 40 | ;; (println " >" result) 41 | ;; (and (is (map? result)) 42 | ;; (is (true? (:result result))))))))) 43 | 44 | #?(:clj (deftest test-specs 45 | (doseq [s (->> (st/checkable-syms) 46 | (filter workflo-sym?))] 47 | (println " Testing" s) 48 | (let [result (first (st/check s check-opts))] 49 | (and (is (map? result)) 50 | (is (-> result :clojure.spec.test.check/ret)) 51 | (is (-> result :clojure.spec.test.check/ret :result true?) 52 | (-> result :clojure.spec.test.check/ret))))))) 53 | -------------------------------------------------------------------------------- /src/test/workflo/macros/util/string_test.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.macros.util.string-test 2 | (:require [clojure.test :refer [deftest is]] 3 | [workflo.macros.util.string :refer [camel->kebab]])) 4 | 5 | (deftest camel->kebab-works [] 6 | (and (is (= "user" (camel->kebab "User"))) 7 | (is (= "user-profile" (camel->kebab "UserProfile"))) 8 | (is (= "user-profile-title" (camel->kebab "UserProfileTitle"))))) 9 | --------------------------------------------------------------------------------