Repository: wilkerlucio/pathom
Pull request #127 (2019-09-19 13:57:18)

Parallel wait improvements


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea46f522..c94eff88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 - Connect process with skip a resolver in case the desired key is already fulfilled
 - Add `com.wsscode.pathom.sugar` namespace with new helpers to quickly define parsers
 - Improve handling of external watcher responses in parallel reader
+- Add new resolver definition helpers: `constantly-resolver`, `single-attr-resolver` and `single-attr-resolver2`
 
 ## [2.2.24]
 - Add ::p/root-query to environment
diff --git a/docs-src/modules/ROOT/nav.adoc b/docs-src/modules/ROOT/nav.adoc
index 6e7fce16..d7a95a6f 100644
--- a/docs-src/modules/ROOT/nav.adoc
+++ b/docs-src/modules/ROOT/nav.adoc
@@ -8,6 +8,7 @@
 ** xref:connect/indexes.adoc[Understanding the Indexes]
 ** xref:connect/exploration.adoc[Exploration with Pathom Viz]
 ** xref:connect/thread-pool.adoc[Using a Thread Pool]
+* xref:sugar.adoc[Sugar Helpers]
 * xref:plugins.adoc[Plugins]
 * xref:core.adoc[Core Engine]
 ** xref:core/getting-started.adoc[Getting Started]
diff --git a/docs-src/modules/ROOT/pages/connect/resolvers.adoc b/docs-src/modules/ROOT/pages/connect/resolvers.adoc
index 9d294f8f..19f62371 100644
--- a/docs-src/modules/ROOT/pages/connect/resolvers.adoc
+++ b/docs-src/modules/ROOT/pages/connect/resolvers.adoc
@@ -256,7 +256,12 @@ of resolvers called by providing some base information that you already have, yo
   [:customer/full-name]}]
 ----
 
-== Alias resolvers
+== Resolver builders
+
+There are some common resolver patterns that emerge from usage, Pathom connect provides
+helpers for such cases, the next sections describes the Connect core available helpers.
+
+=== Alias resolvers
 
 Sometimes you might want to make an alias, that is, create a resolver which just converts one
 name to another. For example:
@@ -280,6 +285,64 @@ that easy, Pathom provides some helpers:
 
 If you want to create an alias that goes in both directions, use `pc/alias-resolver2`.
 
+=== Constant resolvers
+
+This is a resolver that always returns a constant value for a given key. A common use
+case is use this to set some default.
+
+[source,clojure]
+----
+; using helper
+(def answer-to-everything
+  (pc/constantly-resolver :douglas.adams/answer-to-everything 42))
+
+; is equivalente to:
+(pc/defresolver answer-to-everything [_ _]
+  {::pc/output [:douglas.adams/answer-to-everything]}
+  {:douglas.adams/answer-to-everything 42})
+----
+
+=== Single attribute resolvers
+
+For cases of a single transition from one attribute to another, you can use the
+`single-attr-resolver` helper:
+
+[source,clojure]
+----
+; convertion fn
+(defn fahrenheit->celcius [x]
+  (-> x (- 32) (* 5/9)))
+
+; resolver with helper
+(def f->c-resolver
+  (pc/single-attr-resolver :unit/fahrenheit :unit/celcius fahrenheit->celcius))
+
+; is equivalent to:
+(pc/defresolver answer-to-everything [_ {:keys [unit/fahrenheit]}]
+  {::pc/input  #{:unit/fahrenheit}
+   ::pc/output [:unit/celcius]}
+  {:unit/celcius (fahrenheit->celcius fahrenheit)})
+----
+
+Sometimes you also need to get some data from the environment, for those cases use
+`single-attr-resolver2`, the difference is that this one will send `env` and the value
+input as arguments to the provided function:
+
+[source,clojure]
+----
+(defn single-with-env [env value]
+  (* value (::multiplier env))
+
+(def env-demo-resolver
+  (pc/single-attr-resolver ::some-value ::other-value single-with-env))
+
+; is equivalent to
+(pc/defresolver answer-to-everything [env {::keys [some-value]}]
+  {::pc/input  #{::some-value}
+   ::pc/output [::other-value]}
+  {::other-value (single-with-env env some-value)})
+----
+
 == Parameters
 
 Parameters enable another dimension of information to be added to the request. Params have
diff --git a/docs-src/modules/ROOT/pages/introduction.adoc b/docs-src/modules/ROOT/pages/introduction.adoc
index d16210aa..93f02b05 100644
--- a/docs-src/modules/ROOT/pages/introduction.adoc
+++ b/docs-src/modules/ROOT/pages/introduction.adoc
@@ -107,6 +107,7 @@ aliases used, assume that you have the following namespace requires in your envi
     [com.wsscode.pathom.connect :as pc]
     [com.wsscode.pathom.connect.graphql2 :as pcg]
     [com.wsscode.pathom.graphql :as pg]
+    [com.wsscode.pathom.sugar :as ps]
     [com.wsscode.pathom.trace :as pt]
     ; clj only
     [com.wsscode.common.async-clj :refer [let-chan go-catch <? <?maybe]
diff --git a/docs-src/modules/ROOT/pages/sugar.adoc b/docs-src/modules/ROOT/pages/sugar.adoc
new file mode 100644
index 00000000..fe0b88d6
--- /dev/null
+++ b/docs-src/modules/ROOT/pages/sugar.adoc
@@ -0,0 +1,45 @@
+= Sugar Helpers
+
+Since version `2.2.15` Pathom provides helpers to start parsers with very little code.
+
+These helpers are available at the namespace `com.wsscode.pathom.sugar`.
+
+== Connect parser helpers
+
+First we have helpers to create parser with connect setup. There are three, one for
+the `serial parser`, one for `async parser` and one for the `parallel parser`.
+
+Usage example:
+
+[source,clojure]
+----
+(ns my-app.ns
+  (:require [com.wsscode.pathom.connect :as pc]
+            [com.wsscode.pathom.sugar :as ps]))
+
+(def name-alias-parser
+  (ps/connect-serial-parser
+    [(pc/alias-resolver :system-a/name :system-b/name)]))
+
+(name-alias-parser {::p/entity {:system-a/name "Jane"}} [:system-b/name])
+; => #:system-b{:name "Jane"}
+----
+
+The argument for all the sugar parsers is the Connect registry.
+
+== Parser context interface
+
+Continuing the previous example, the sugar namespace provides a parser interface wrapper
+to facilitate calling the parser sending an entity (as we did in the last example) with
+a simpler interface:
+
+[source,clojure]
+----
+(def context-name-parser (ps/context-parser name-alias-parser))
+
+(context-name-parser {:system-a/name "Jane"} [:system-b/name])
+; => #:system-b{:name "Jane"}
+----
+
+You can also use a custom `env` with the context parser, to do so add the env as the
+first argument (on arity 3).
diff --git a/project.clj b/project.clj
index a0cb53f0..faf9db1a 100644
--- a/project.clj
+++ b/project.clj
@@ -1,4 +1,4 @@
-(defproject com.wsscode/pathom "2.2.24"
+(defproject com.wsscode/pathom "2.2.25-SNAPSHOT"
   :description "A Clojure library designed to provide a collection of helper functions to support Clojure(script) graph parsers using\nom.next graph syntax."
   :url "https://github.com/wilkerlucio/pathom"
   :license {:name "MIT"
diff --git a/src/com/wsscode/pathom/connect.cljc b/src/com/wsscode/pathom/connect.cljc
index 6ad29cc6..a6761dea 100644
--- a/src/com/wsscode/pathom/connect.cljc
+++ b/src/com/wsscode/pathom/connect.cljc
@@ -1183,12 +1183,6 @@
                                 (contains? entity key')
                                 (select-keys entity [key'])
 
-                                (contains? waiting key')
-                                (do
-                                  (pt/trace env (assoc trace-data ::pt/event ::waiting-resolver ::waiting-key key'))
-                                  (let [{::pp/keys [error]} (<! (pp/watch-pending-key env key'))]
-                                    (or error ::watch-ready)))
-
                                 cache?
                                 (if (and batch? processing-sequence)
                                   (<! (parallel-batch env))
@@ -1198,6 +1192,12 @@
                                       (p/cached-async env [resolver-sym e params]
                                         #(go-catch (or (<!maybe (call-resolver env e)) {}))))))
 
+                                (contains? waiting key')
+                                (do
+                                  (pt/trace env (assoc trace-data ::pt/event ::waiting-resolver ::waiting-key key'))
+                                  (let [{::pp/keys [error]} (<! (pp/watch-pending-key env key'))]
+                                    (or error ::watch-ready)))
+
                                 :else
                                 (try
                                   (or (<?maybe (call-resolver env e)) {})
@@ -1335,6 +1335,39 @@
   [(alias-resolver from to)
    (alias-resolver to from)])
 
+(defn constantly-resolver
+  "Create a simple resolver that always return `value` for `attribute`."
+  ([attribute value]
+   (constantly-resolver {::attribute attribute
+                         :value         value}))
+  ([{::keys [attribute sym] :keys [value]}]
+   (let [sym (or sym (symbol (str (munge (subs (str attribute) 1)) "-constant")))]
+     (resolver sym
+       {::output [attribute]}
+       (fn [_ _] {attribute value})))))
+
+(defn single-attr-resolver
+  "Apply fn `f` to input `from` and spits the result with the name `to`.
+
+  `f` receives a single argument, which is the input value from `from`."
+  [from to f]
+  (let [sym (symbol (str (attr-alias-name from to) "-single-attr-transform"))]
+    (resolver sym
+      {::input  #{from}
+       ::output [to]}
+      (fn [_ input]
+        {to (f (get input from))}))))
+
+(defn single-attr-resolver2
+  "Similar single-attr-resolver, but `f` receives two arguments, `env` and the input."
+  [from to f]
+  (let [sym (symbol (str (attr-alias-name from to) "-single-attr-transform"))]
+    (resolver sym
+      {::input  #{from}
+       ::output [to]}
+      (fn [env input]
+        {to (f env (get input from))}))))
+
 (defn mutation
   "Helper to return a mutation map"
   [sym {::keys [transform] :as options} mutate]
diff --git a/src/com/wsscode/pathom/sugar.clj b/src/com/wsscode/pathom/sugar.clj
deleted file mode 100644
index f4307b7e..00000000
--- a/src/com/wsscode/pathom/sugar.clj
+++ /dev/null
@@ -1,34 +0,0 @@
-(ns com.wsscode.pathom.sugar
-  (:require [com.wsscode.pathom.core :as p]
-            [com.wsscode.pathom.connect :as pc]))
-
-(defn connect-serial-parser
-  "Create a standard connect parser using the serial parser.
-
-  This parser recommended for handling small and simple queries, like
-  resolvers to process missing configuration options."
-  [register]
-  (p/parser
-    {::p/env     {::p/reader               [p/map-reader
-                                            pc/reader2
-                                            pc/open-ident-reader
-                                            p/env-placeholder-reader]
-                  ::p/placeholder-prefixes #{">"}}
-     ::p/mutate  pc/mutate
-     ::p/plugins [(pc/connect-plugin {::pc/register register})
-                  p/error-handler-plugin
-                  p/trace-plugin]}))
-
-(defn context-parser
-  "Transforms the signature of a regular parser to one that takes
-  some initial context to run the query. This returns a fn with
-  the following arities:
-
-  [context query] => runs query using context as initial data.
-  [env context query] => same as before but accepts initial environment."
-  [parser]
-  (fn context-parser-internal
-    ([context query]
-     (parser {::p/entity (atom context)} query))
-    ([env context query]
-     (parser (assoc env ::p/entity (atom context)) query))))
diff --git a/src/com/wsscode/pathom/sugar.cljc b/src/com/wsscode/pathom/sugar.cljc
new file mode 100644
index 00000000..0c15d347
--- /dev/null
+++ b/src/com/wsscode/pathom/sugar.cljc
@@ -0,0 +1,71 @@
+(ns com.wsscode.pathom.sugar
+  "This namespace contains easy ways to setup common configurations for Pathom parsers"
+  (:require [com.wsscode.pathom.core :as p]
+            [com.wsscode.pathom.connect :as pc]))
+
+(defn connect-serial-parser
+  "Create a standard connect parser using the serial parser.
+
+  This parser recommended for handling small and simple queries, like
+  resolvers to process missing configuration options."
+  [register]
+  (p/parser
+    {::p/env     {::p/reader               [p/map-reader
+                                            pc/reader2
+                                            pc/open-ident-reader
+                                            p/env-placeholder-reader]
+                  ::p/placeholder-prefixes #{">"}}
+     ::p/mutate  pc/mutate
+     ::p/plugins [(pc/connect-plugin {::pc/register register})
+                  p/error-handler-plugin
+                  p/trace-plugin]}))
+
+(defn connect-async-parser
+  "Create a standard connect parser using the async parser.
+
+  Just like the serial parser, but supports waiting for core.async channels
+  in responses. The most common usage of this one is in ClojureScript land, where
+  most of the IO needs to be async."
+  [register]
+  (p/async-parser
+    {::p/env     {::p/reader               [p/map-reader
+                                            pc/async-reader2
+                                            pc/open-ident-reader
+                                            p/env-placeholder-reader]
+                  ::p/placeholder-prefixes #{">"}}
+     ::p/mutate  pc/mutate-async
+     ::p/plugins [(pc/connect-plugin {::pc/register register})
+                  p/error-handler-plugin
+                  p/trace-plugin]}))
+
+(defn connect-parallel-parser
+  "Create a standard connect parser using the parallel parser.
+
+  This is recommended if you have a lot of different information sources that
+  are IO bound. This parser can handle things in parallel, but adds extra overhead
+  to processing, use it in case your system has good parallelism opportunities."
+  [register]
+  (p/parallel-parser
+    {::p/env     {::p/reader               [p/map-reader
+                                            pc/parallel-reader
+                                            pc/open-ident-reader
+                                            p/env-placeholder-reader]
+                  ::p/placeholder-prefixes #{">"}}
+     ::p/mutate  pc/mutate-async
+     ::p/plugins [(pc/connect-plugin {::pc/register register})
+                  p/error-handler-plugin
+                  p/trace-plugin]}))
+
+(defn context-parser
+  "Transforms the signature of a regular parser to one that takes
+  some initial context to run the query. This returns a fn with
+  the following arities:
+
+  [context query] => runs query using context as initial data.
+  [env context query] => same as before but accepts initial environment."
+  [parser]
+  (fn context-parser-internal
+    ([context query]
+     (parser {::p/entity (atom context)} query))
+    ([env context query]
+     (parser (assoc env ::p/entity (atom context)) query))))


Test patch:
diff --git a/test/com/wsscode/pathom/connect_test.cljc b/test/com/wsscode/pathom/connect_test.cljc
index 2da43baa..ccb97b8a 100644
--- a/test/com/wsscode/pathom/connect_test.cljc
+++ b/test/com/wsscode/pathom/connect_test.cljc
@@ -3,13 +3,13 @@
             #?(:clj
                [com.wsscode.common.async-clj :refer [go-catch go-promise <!maybe <?]])
             [nubank.workspaces.core :refer [deftest]]
-            #?(:clj [clojure.core.async :as async :refer [go <! <!!]]
-               :cljs [cljs.core.async :as async :refer-macros [go] :refer [<!]])
+            [clojure.core.async :as async :refer [go <! <!!]]
             [com.wsscode.pathom.core :as p]
             [com.wsscode.pathom.connect :as pc]
             [com.wsscode.pathom.connect.test :as pct]
             [com.wsscode.pathom.parser :as pp]
             [com.wsscode.pathom.trace :as pt]
+            [com.wsscode.pathom.sugar :as ps]
             [clojure.walk :as walk])
   #?(:clj
      (:import (clojure.lang ExceptionInfo))))
@@ -804,6 +804,16 @@
     (is (= ((::pc/resolve resolver) {} {:foo "value"})
            {:bar "value"}))))
 
+(deftest test-constantly-resolver
+  (let [parser (ps/connect-serial-parser [(pc/constantly-resolver :foo "bar")])]
+    (is (= (parser {} [:foo])
+           {:foo "bar"}))))
+
+(deftest test-single-attr-resolver
+  (let [parser (ps/context-parser (ps/connect-serial-parser [(pc/single-attr-resolver :n :x inc)]))]
+    (is (= (parser {:n 10} [:x])
+           {:x 11}))))
+
 (def parser
   (p/parser {:mutate pc/mutate
              ::p/plugins
@@ -2386,6 +2396,7 @@
                  :com.wsscode.pathom.trace/event :com.wsscode.pathom.connect/merge-resolver-response
                  :key                            :b}])))
 
+     #_
      (testing "multi step resolver waiting"
        (let [kw (atom {})
              e  (atom {})]
@@ -3246,6 +3257,33 @@
        (reset! quick-parser-trace* @trace)
        res)))
 
+(comment
+  (quick-parser {::p/env       {:counter c}
+                 ::pc/register [(pc/resolver 'branch-resolver
+                                  {::pc/output [:guitar/id :camera/id]}
+                                  (fn [env _]
+                                    (Thread/sleep 100)
+                                    {:camera/id 123}))
+
+                                (pc/resolver 'guitar-name
+                                  {::pc/input  #{:guitar/id}
+                                   ::pc/output [:guitar/name :guitar/factory-name]}
+                                  (fn [_ {:keys [guitar/id]}]
+                                    (Thread/sleep 100)
+                                    {:guitar/name (str id " name")
+                                     :guitar/factory-name "Guitar Factory"}))
+
+                                (pc/resolver 'camera-name
+                                  {::pc/input  #{:camera/id}
+                                   ::pc/output [:camera/name]}
+                                  (fn [_ {:keys [camera/id]}]
+                                    (Thread/sleep 100)
+                                    {:camera/name (str id " name")}))
+
+                                (pc/alias-resolver :guitar/name :thing/name)
+                                (pc/alias-resolver :camera/name :thing/name)]}
+    '[:b :z]))
+
 #?(:clj
    (deftest test-parallel-parser-with-connect
      (testing "skip resolver if value is resolved"
@@ -3470,8 +3508,8 @@
          (is (= (async/<!!
                   (custom-pparser [thing->dep provide-nothing require-a-b-from-nothing]
                     {} [:a :c]))
-                {:a                              :com.wsscode.pathom.core/not-found
-                 :c                              :com.wsscode.pathom.core/reader-error
+                {:a                              ::p/not-found
+                 :c                              ::p/not-found
                  :com.wsscode.pathom.core/errors {[:c] "class clojure.lang.ExceptionInfo: Insufficient resolver output - {:com.wsscode.pathom.parser/response-value {}, :key :a}"}})))
 
        (testing "when a response has an error value but a later resolver gets a good one, the later must be used."