diff --git a/src/d2q/api.clj b/src/d2q/api.clj index 332ee91..2adc25a 100644 --- a/src/d2q/api.clj +++ b/src/d2q/api.clj @@ -54,7 +54,6 @@ * a Field is either scalar-typed (`:d2q.field/ref? false`) or ref-typed (`:d2q.field/ref? true`), * a ref-typed Field can be cardinality-one (`:d2q.field/cardinality :d2q.field.cardinality/one`) or cardinality-many (`:d2q.field/cardinality :d2q.field.cardinality/many`) - * a Field has a Resolver (:d2q.field/resolver) which knows how to compute it. Scalar-typed Fields evaluate to the terminal values which will appear in the d2q Query Result, whereas ref-typed Fields evaluate to other Entities, which will then be further processed by the query engine. @@ -71,6 +70,7 @@ ## About Resolvers: d2q Resolvers are essentially named (:d2q.resolver/name) functions (:d2q.resolver/compute), + dedicated to a set of d2q Fields (:d2q.resolver/field->meta), which asynchronously compute a (potentially sparse) table of requested data: given a Query Context, a list of M Field Calls, and a batch of N entities (called an Entity Selection), a Resolver will compute up to MxN Result Cells, where each Result Cell represents the value taken @@ -78,12 +78,18 @@ In a pseudo type notation, the signature of a :d2q.resolver/compute function would be: - (QCtx, [[FieldCallIndex, FieldCall]], [[EntityIndex, Entity]]) + (QCtx, [[FieldCallIndex, FieldCall, FieldMeta]], [[EntityIndex, Entity]]) -> manifold.Deferred<{:d2q-res-cells [{:d2q-entcell-i EntityIndex, :d2q-fcall-i FieldCallIndex, :d2q-rescell-value V}], :d2q-errors [Throwable]}> + :d2q.resolver/field->meta must be a map, which keys are the names (:d2q.field/name) of the Fields + that the Resolver knows how to compute, and the values are optional metadata, which as a convenience + for implementing resolvers are passed to the resolver functions alongside with the Field Calls: here + you can put any Field-specific configuration to help you resolve it. Each Field must be computed by + exactly one Resolver. + Notes: 1. Resolvers are asynchronous, therefore the returned result should be wrapped in a Manifold Deferred. @@ -140,28 +146,6 @@ (d2q.datatypes/->ResultCell d2q-entcell-i d2q-fcall-i d2q-rescell-value)) -(defn into-resolver-result - "Transforms a sequence which elements are either d2q Result Cells or errors into a proper d2q Resolver Result map. - Useful for constructing Resolver Results by processing entities sequentially. - - An optional transducer may be supplied, which must transform inputs to Result Cells or errors. - - A value `v` is considered an error when `(instance? Throwable v)` is true." - ([rcells-or-errors] - (into-resolver-result identity rcells-or-errors)) - ([xform xs] - (let [^ArrayList l-errs (ArrayList.) - rcells (into [] - (comp - xform - (remove (fn [v] - (when (instance? Throwable v) - (.add l-errs v) - true)))) - xs)] - {:d2q-res-cells rcells - :d2q-errors (vec l-errs)}))) - diff --git a/src/d2q/helpers/resolvers.clj b/src/d2q/helpers/resolvers.clj new file mode 100644 index 0000000..38a1136 --- /dev/null +++ b/src/d2q/helpers/resolvers.clj @@ -0,0 +1,184 @@ +(ns d2q.helpers.resolvers + "Utilities for implementing Resolvers." + (:require [manifold.deferred :as mfd] + [d2q.api :as d2q] + [d2q.impl.utils :as impl.utils]) + (:import (java.util ArrayList))) + + +(defn into-resolver-result + "Transforms a sequence which elements are either d2q Result Cells or errors into a proper d2q Resolver Result map. + Useful for constructing Resolver Results by processing entities sequentially. + + An optional transducer may be supplied, which must transform inputs to Result Cells or errors. + + A value `v` is considered an error when `(instance? Throwable v)` is true." + ([rcells-or-errors] + (into-resolver-result identity rcells-or-errors)) + ([xform xs] + (let [^ArrayList l-errs (ArrayList.) + rcells (into [] + (comp + xform + (remove (fn [v] + (when (instance? Throwable v) + (.add l-errs v) + true)))) + xs)] + {:d2q-res-cells rcells + :d2q-errors (vec l-errs)}))) + +(defn entities-independent-resolver + "Convenience for implementing resolvers which computations are independent from the entities to which they are applied. + + Some Fields are independent of the Entities to which they are applied (example: :myapp/find-user-by-id); + such Fields will typically be executed only at the top-level of a Query, and only on one Entity, + but a Resolver cannot make this assumption. + + resolver-fn must be a function with arguments [qctx i+fcalls] + (like an ordinary Resolver function, without the last j+entities argument), + and returns the same results as an ordinary + resolver function, with dummy values for :d2q-entcell-i in Result Cells (typically -1 or nil). + Each Result Cell returned by resolver-fn will be repeated once per input Entity." + [resolver-fn] + (fn [qctx i+fcalls j+entities] + (mfd/chain (resolver-fn qctx i+fcalls) + (fn [{:as ret, res-cells :d2q-res-cells}] + (let [js (mapv first j+entities)] + (assoc ret + :d2q-res-cells + (into [] + (mapcat + (fn [partial-res-cell] + (->> js + (mapv + (fn [j] + (assoc partial-res-cell :d2q-entcell-i j)))))) + res-cells))))))) + +(defn fields-independent-resolver + "Convenience for implementing a Resolver function which ignores its i+fcalls argument, + typically because it computes only one Field with no argument." + [resolver-fn] + (fn [qctx i+fcalls j+entities] + (mfd/chain (resolver-fn qctx j+entities) + (fn [{:as ret, res-cells :d2q-res-cells}] + (let [is (mapv first i+fcalls)] + (assoc ret + :d2q-res-cells + (into [] + (mapcat + (fn [partial-res-cell] + (->> is + (mapv + (fn [i] + (assoc partial-res-cell :d2q-fcall-i i)))))) + res-cells))))))) + +(defn validating-fcall-args + "Adds validation of :d2q-fcall-arg in Field Calls to a Resolver function. + + Given an options map and Resolver compute function `resolver-fn`, + returns a Resolver function which checks the :d2q-fcall-arg of each Field Call, + passing only valid Field Call tuples to the wrapped `resolver-fn` and adding errors + to the result for the invalid ones. + + Validation is performed via a user-supplied 'checker' function in the Field metadata, + at key :d2q.helpers.field-meta/check-fcall-args. + + This checker function must accept the :d2q-fcall-arg for a Field, and do one of the following: + - Throw an Exception: the Field Call is invalid, and an error will be added to the output map. + - Return a truthy result: the Field Call is valid, and will be passed to the wrapped `resolver-fn`. + - Return a falsey result: the Field Call is invalid, but will simply be discarded (not passed to + the wrapped `resolver-fn`) without yielding and error. + + The :d2q.helpers.validating-fcall-args/checker-required? options determines the checking behaviour + in case :d2q.helpers.field-meta/check-fcall-args is not supplied in a Field meta: + - if false (the default): the corresponding Field Calls are considered valid. + - if true: an Exception is returned for the corresponding Field Calls." + [{:as opts, + required? :d2q.helpers.validating-fcall-args/checker-required? + :or {required? false}} + resolver-fn] + (fn [qctx i+fcalls j+entities] + (let [{valid-i+fcalls :valid validation-errors :error} + (impl.utils/group-and-map-by + (map (fn [i+fcall] + (let [[i fcall field-meta] i+fcall + checker (:d2q.helpers.field-meta/check-fcall-args field-meta)] + (if (nil? checker) + (if required? + [:error (ex-info + (str "Missing required checker function at key " (pr-str :d2q.helpers.validating-fcall-args/check-fcall-args) + " in metadata for Field " (pr-str (:d2q-fcall-field fcall))) + {:d2q-fcall-i i})] + [:valid i+fcall]) + (try + [(if (checker (:d2q-fcall-arg fcall)) + :valid + :ignored) + i+fcall] + (catch Throwable err + [:error (ex-info + (str "Invalid " (pr-str :d2q-fcall-arg) " when calling Field " (pr-str (:d2q-fcall-field fcall)) + ". See the cause of this Exception for details.") + {:d2q-fcall-i i} + err)])))))) + first second + i+fcalls)] + (if (empty? valid-i+fcalls) + (mfd/success-deferred + {:d2q-res-cells [] + :d2q-errors (vec validation-errors)}) + (mfd/chain + (resolver-fn qctx valid-i+fcalls j+entities) + (fn [ret] + (update ret :d2q-errors + (fn [ret-errors] + (concat validation-errors ret-errors))))))))) + +(defn validating-input-entities + "Adds validation of Entities to a Resolver function. + + Given a `check-entity` and Resolver compute function `resolver-fn`, + returns a Resolver function which checks the input Entities, + passing only valid Entity tuples to the wrapped `resolver-fn` and adding errors + to the result for the invalid ones. + + The `check-entity` function must accept an Entity, and do one of the following: + - Throw an Exception: the Entity is invalid, and an error will be added to the output map. + - Return a truthy result: the Entity is valid, and will be passed to the wrapped `resolver-fn`. + - Return a falsey result: the Entity will be discarded (not passed to the wrapped `resolver-fn`) + without yielding and error." + [check-entity resolver-fn] + {:pre [(fn? check-entity) (fn? resolver-fn)]} + (fn [qctx i+fcalls j+entities] + (let [{valid-entities :valid validation-errors :error} + (impl.utils/group-and-map-by + (map (fn [j+entity] + (let [[j entity] j+entity] + (try + [(if (check-entity entity) + :valid + :ignored) + j+entity] + (catch Throwable err + [:error (ex-info + (str "Invalid Entity passed to the Resolver." + " To fix, make sure your `check-entity` function is correct, " + " and that the upstream Resolver returns valid Entities." + " See the cause of this Exception for details.") + {:d2q-entcell-i j} + err)]))))) + first second + j+entities)] + (if (empty? valid-entities) + (mfd/success-deferred + {:d2q-res-cells [] + :d2q-errors (vec validation-errors)}) + (mfd/chain + (resolver-fn qctx i+fcalls valid-entities) + (fn [ret] + (update ret :d2q-errors + (fn [ret-errors] + (concat validation-errors ret-errors))))))))) diff --git a/src/d2q/impl.clj b/src/d2q/impl.clj index 286398a..bf78963 100644 --- a/src/d2q/impl.clj +++ b/src/d2q/impl.clj @@ -452,28 +452,62 @@ (get fieldByNames field-name))) (defn server - [tabular-resolvers fields transform-entities-fn] + [resolvers fields transform-entities-fn] {:pre [(fn? transform-entities-fn)]} - (let [trs-by-name (->> tabular-resolvers + (let [trs-by-name (->> resolvers (map (fn [resolver-opts] (->TabularResolver (impl.utils/get-safe resolver-opts :d2q.resolver/name) (impl.utils/get-safe resolver-opts :d2q.resolver/compute)))) (impl.utils/index-by :name)) + field-names (into #{} + (map (fn [field-spec] (impl.utils/get-safe field-spec :d2q.field/name))) + fields) + field->resolver+meta + (let [field->resolver+metas + (->> resolvers + (mapcat (fn [{:as resolver-opts, res-name :d2q.resolver/name}] + (for [[field-name field-meta] (impl.utils/get-safe resolver-opts :d2q.resolver/field->meta)] + [field-name res-name field-meta]))) + (reduce + (fn [m [field-name res-name field-meta]] + (update m field-name (fn [v] (-> v (or []) (conj [res-name field-meta]))))) + {})) + undeclared-fields + (->> field->resolver+metas keys (remove field-names) set) + non-implemented + (->> field-names (remove field->resolver+metas) set) + implemented-several-times + (->> field->resolver+metas + (filter (fn [[_field-name resolver+metas]] + (> (count resolver+metas) 1))) + (map (fn [[field-name resolver+metas]] + [field-name (mapv first resolver+metas)])) + (into (sorted-map)))] + (if-not (and (empty? undeclared-fields) (empty? non-implemented) (empty? implemented-several-times)) + (throw (ex-info + (str "Each Field must be implemented by exactly one Resolver; found problems with Fields " + (pr-str (into (sorted-set) cat [undeclared-fields non-implemented (keys implemented-several-times)])) + " and Resolvers " + (pr-str (into (sorted-set) cat (vals implemented-several-times)))) + {:undeclared-fields (into (sorted-set) undeclared-fields) + :non-implemented-fields (into (sorted-set) non-implemented) + :fields-implemented-by-several-resolvers implemented-several-times})) + (->> field->resolver+metas + (map (fn [[field-name resolver+meta]] + [field-name (first resolver+meta)])) + (into {})))) fields-by-name (->> fields (map (fn [field-spec] - (->Field - (impl.utils/get-safe field-spec :d2q.field/name) - (not (impl.utils/get-safe field-spec :d2q.field/ref?)) - (= :d2q.field.cardinality/many (:d2q.field/cardinality field-spec)) - (-> field-spec (impl.utils/get-safe :d2q.field/resolver) - trs-by-name - (or (throw (ex-info - (str "Unregistered resolver " (pr-str (:d2q.field/resolver field-spec)) - " referenced in field " (:d2q.field/name field-spec)) - {:field field-spec})))) - (:d2q.field/meta field-spec)))) + (let [field-name (:d2q.field/name field-spec) + [resolver-name field-meta] (get field->resolver+meta field-name)] + (->Field + field-name + (not (impl.utils/get-safe field-spec :d2q.field/ref?)) + (= :d2q.field.cardinality/many (:d2q.field/cardinality field-spec)) + (get trs-by-name resolver-name) + field-meta)))) (impl.utils/index-by :fieldName))] (->Server fields-by-name transform-entities-fn))) diff --git a/src/d2q/impl/utils.clj b/src/d2q/impl/utils.clj index 633251d..7c2e31d 100644 --- a/src/d2q/impl/utils.clj +++ b/src/d2q/impl/utils.clj @@ -10,13 +10,33 @@ (transient {}) coll))) (defn index-and-map-by - [kf vf coll] - (persistent! - (reduce (fn [tm v] - (assoc! tm - (kf v) - (vf v))) - (transient {}) coll))) + ([kf vf coll] + (persistent! + (reduce (fn [tm v] + (assoc! tm + (kf v) + (vf v))) + (transient {}) coll)))) + +(defn group-and-map-by + ([kf vf coll] + (persistent! + (reduce (fn [tm x] + (let [k (kf x) + v (vf x) + old-vs (get tm k [])] + (assoc! tm k (conj old-vs v)))) + (transient {}) coll))) + ([xform kf vf coll] + (persistent! + (transduce + xform + (completing (fn [tm x] + (let [k (kf x) + v (vf x) + old-vs (get tm k [])] + (assoc! tm k (conj old-vs v))))) + (transient {}) coll)))) (defmacro doarr-indexed! "Runs an sequence of expressions `body` across an array `a`, diff --git a/test/d2q/test/api.clj b/test/d2q/test/api.clj index b1e0517..64a6855 100644 --- a/test/d2q/test/api.clj +++ b/test/d2q/test/api.clj @@ -2,28 +2,38 @@ (:require [clojure.test :refer :all] [midje.sweet :refer :all] + [d2q.test.test-utils :as tu] [d2q.api :as d2q :refer :all] [d2q.datatypes] [manifold.deferred :as mfd] [vvvvalvalval.supdate.api :as supd])) -(defn dataify-ex - [^Throwable ex] - (let [cause (.getCause ex)] - (cond-> {:error/type (-> ex class .getName symbol) - :error/message (.getMessage ex) - :error/data (ex-data ex)} - (some? cause) - (assoc :error/cause (dataify-ex cause))))) - -(defn errors->ex-data - [res] - (update res :d2q-errors - #(->> % - (map dataify-ex) - (sort-by :error/message) - vec))) - +(fact "Errors in Fields <-> Resolvers mappings" + (try + (d2q/server + {:d2q.server/fields + [{:d2q.field/name :f1 + :d2q.field/ref? false} + {:d2q.field/name :f2 + :d2q.field/ref? false} + {:d2q.field/name :f3 + :d2q.field/ref? false}] + :d2q.server/resolvers + [{:d2q.resolver/name :r1 + :d2q.resolver/field->meta {:f1 nil + :f4 nil} + :d2q.resolver/compute (constantly nil)} + {:d2q.resolver/name :r2 + :d2q.resolver/field->meta {:f1 nil + :f2 nil} + :d2q.resolver/compute (constantly nil)}]}) + :should-have-thrown + (catch Throwable err + [(.getMessage err) (ex-data err)])) + => ["Each Field must be implemented by exactly one Resolver; found problems with Fields #{:f1 :f3 :f4} and Resolvers #{:r1 :r2}" + {:undeclared-fields #{:f4}, + :non-implemented-fields #{:f3}, + :fields-implemented-by-several-resolvers {:f1 [:r1 :r2]}}]) ;; ------------------------------------------------------------------------------ ;; Example with 'synthetic' fields - not representing a real-word domain, but useful for testing @@ -83,7 +93,7 @@ ] [[0 (->SynthEnt "a")] [1 (->SynthEnt "b")]]) - errors->ex-data) + tu/errors->ex-data) => '{:d2q-res-cells [#d2q/result-cell{:d2q-entcell-i 0, :d2q-fcall-i 0, :d2q-rescell-value "a"} #d2q/result-cell{:d2q-entcell-i 1, :d2q-fcall-i 0, :d2q-rescell-value "b"} @@ -123,83 +133,83 @@ ] []) - errors->ex-data) + tu/errors->ex-data) => {:d2q-res-cells [], :d2q-errors []}) ) (def synthetic-fields [{:d2q.field/name :synth.fields/ent-id - :d2q.field/resolver :synth.resolvers/resolver-1 :d2q.field/ref? false} {:d2q.field/name :synth.fields/always-42 - :d2q.field/resolver :synth.resolvers/resolver-2 :d2q.field/ref? false} {:d2q.field/name :synth.fields/return-arg - :d2q.field/resolver :synth.resolvers/resolver-1 :d2q.field/ref? false} {:d2q.field/name :synth.fields/inc-arg - :d2q.field/resolver :synth.resolvers/resolver-2 :d2q.field/ref? false} {:d2q.field/name :synth.fields/missing - :d2q.field/resolver :synth.resolvers/resolver-1 :d2q.field/ref? false} {:d2q.field/name :synth.fields/throw-arg - :d2q.field/resolver :synth.resolvers/resolver-2 :d2q.field/ref? false} {:d2q.field/name :synth.fields/qctx - :d2q.field/resolver :synth.resolvers/resolver-1 :d2q.field/ref? false} {:d2q.field/name :synth.fields.refs/one-child - :d2q.field/resolver :synth.resolvers/resolver-1 :d2q.field/ref? true :d2q.field/cardinality :d2q.field.cardinality/one} {:d2q.field/name :synth.fields.refs/one-missing - :d2q.field/resolver :synth.resolvers/resolver-2 :d2q.field/ref? true :d2q.field/cardinality :d2q.field.cardinality/one} {:d2q.field/name :synth.fields.refs/one-throw-arg - :d2q.field/resolver :synth.resolvers/resolver-1 :d2q.field/ref? true :d2q.field/cardinality :d2q.field.cardinality/one} {:d2q.field/name :synth.fields.refs/many-children - :d2q.field/resolver :synth.resolvers/resolver-2 :d2q.field/ref? true :d2q.field/cardinality :d2q.field.cardinality/many} {:d2q.field/name :synth.fields.refs/many-missing - :d2q.field/resolver :synth.resolvers/resolver-1 :d2q.field/ref? true :d2q.field/cardinality :d2q.field.cardinality/many} {:d2q.field/name :synth.fields.refs/many-throw-arg - :d2q.field/resolver :synth.resolvers/resolver-2 :d2q.field/ref? true :d2q.field/cardinality :d2q.field.cardinality/many} {:d2q.field/name :synth.fields/resolver-throws - :d2q.field/resolver :synth.resolvers/throwing-resolver :d2q.field/ref? false} {:d2q.field/name :synth.fields/resolvers-returns-error - :d2q.field/resolver :synth.resolvers/error-returning-resolver :d2q.field/ref? false} {:d2q.field/name :synth.fields/resolver-deferred-error - :d2q.field/resolver :synth.resolvers/error-deferred-resolver :d2q.field/ref? false} ]) (def synthetic-resolvers [{:d2q.resolver/name :synth.resolvers/resolver-1 + :d2q.resolver/field->meta {:synth.fields/ent-id nil, + :synth.fields/missing nil, + :synth.fields/qctx nil, + :synth.fields/return-arg nil, + :synth.fields.refs/many-missing nil, + :synth.fields.refs/one-child nil, + :synth.fields.refs/one-throw-arg nil} :d2q.resolver/compute #'synthetic-resolve} {:d2q.resolver/name :synth.resolvers/resolver-2 + :d2q.resolver/field->meta {:synth.fields/always-42 nil, + :synth.fields/inc-arg nil, + :synth.fields/throw-arg nil, + :synth.fields.refs/many-children nil, + :synth.fields.refs/many-throw-arg nil, + :synth.fields.refs/one-missing nil} :d2q.resolver/compute #'synthetic-resolve} {:d2q.resolver/name :synth.resolvers/throwing-resolver + :d2q.resolver/field->meta {:synth.fields/resolver-throws nil} :d2q.resolver/compute (fn [qctx [[fcall-i {[msg data] :d2q-fcall-arg}]] i+ents] (throw (ex-info msg data)))} {:d2q.resolver/name :synth.resolvers/error-deferred-resolver + :d2q.resolver/field->meta {:synth.fields/resolver-deferred-error nil} :d2q.resolver/compute (fn [qctx [[fcall-i {[msg data] :d2q-fcall-arg}]] i+ents] (mfd/error-deferred (ex-info msg data)))} {:d2q.resolver/name :synth.resolvers/error-returning-resolver + :d2q.resolver/field->meta {:synth.fields/resolvers-returns-error nil} :d2q.resolver/compute (fn [qctx [[fcall-i {[msg data] :d2q-fcall-arg}]] i+ents] (mfd/success-deferred {:d2q-errors [(ex-info msg data)]}))}]) @@ -275,13 +285,13 @@ (fact "Empty entities" (-> @(d2q.api/query (synth-server) qctx q []) - errors->ex-data) + tu/errors->ex-data) => {:d2q-results [] :d2q-errors []} ) (fact "Empty query" (-> @(d2q.api/query (synth-server) qctx [] ents) - errors->ex-data) + tu/errors->ex-data) => '{:d2q-results [{} {}], :d2q-errors [{:error/type clojure.lang.ExceptionInfo, :error/message "Error in d2q Transform-Entities phase.", @@ -298,7 +308,7 @@ ;; FIXME fix tests for new error reporting (Val, 10 Apr 2018) (-> @(d2q.api/query (synth-server) qctx q ents) - errors->ex-data)) + tu/errors->ex-data)) => '{:d2q-results [{:synth.fields/ent-id 0, "scalar-k0" "HI", @@ -423,59 +433,3 @@ ) - - -(fact "into-resolver-result" - (fact "with transducer" - (-> - (d2q.api/into-resolver-result - (map-indexed - (fn [ent-i n] - (try - (d2q.api/result-cell ent-i 0 - (/ 1 n)) - (catch Throwable err - (ex-info "aaaaaarrrrg" - {:n n} - err))))) - (range -2 3)) - errors->ex-data) - - => - '{:d2q-res-cells [#d2q/result-cell{:d2q-entcell-i 0, :d2q-fcall-i 0, :d2q-rescell-value -1/2} - #d2q/result-cell{:d2q-entcell-i 1, :d2q-fcall-i 0, :d2q-rescell-value -1} - #d2q/result-cell{:d2q-entcell-i 3, :d2q-fcall-i 0, :d2q-rescell-value 1} - #d2q/result-cell{:d2q-entcell-i 4, :d2q-fcall-i 0, :d2q-rescell-value 1/2}], - :d2q-errors [{:error/type clojure.lang.ExceptionInfo, - :error/message "aaaaaarrrrg", - :error/data {:n 0}, - :error/cause {:error/type java.lang.ArithmeticException, - :error/message "Divide by zero", - :error/data nil}}]}) - - (fact "without transducer" - (-> - (d2q.api/into-resolver-result - (map-indexed - (fn [ent-i n] - (try - (d2q.api/result-cell ent-i 0 - (/ 1 n)) - (catch Throwable err - (ex-info "aaaaaarrrrg" - {:n n} - err)))) - (range -2 3))) - errors->ex-data) - => - '{:d2q-res-cells [#d2q/result-cell{:d2q-entcell-i 0, :d2q-fcall-i 0, :d2q-rescell-value -1/2} - #d2q/result-cell{:d2q-entcell-i 1, :d2q-fcall-i 0, :d2q-rescell-value -1} - #d2q/result-cell{:d2q-entcell-i 3, :d2q-fcall-i 0, :d2q-rescell-value 1} - #d2q/result-cell{:d2q-entcell-i 4, :d2q-fcall-i 0, :d2q-rescell-value 1/2}], - :d2q-errors [{:error/type clojure.lang.ExceptionInfo, - :error/message "aaaaaarrrrg", - :error/data {:n 0}, - :error/cause {:error/type java.lang.ArithmeticException, - :error/message "Divide by zero", - :error/data nil}}]}) - ) diff --git a/test/d2q/test/example/persons.clj b/test/d2q/test/example/persons.clj index 9757846..c2df5a5 100644 --- a/test/d2q/test/example/persons.clj +++ b/test/d2q/test/example/persons.clj @@ -288,56 +288,45 @@ ;;;; **************************************************************** - - (def fields [;; NOTE this Field is ref-typed, cardinality-one, and parameterized {:d2q.field/name :myapp.persons/person-of-id :doc "Resolves a Person given its :myapp.person/id" :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/one - :d2q.field/resolver :myapp.resolvers/person-of-id} + :d2q.field/cardinality :d2q.field.cardinality/one} ;; NOTE the following 4 Fields are scalar-typed, and not parameterized {:d2q.field/name :myapp.person/id :doc "A unique identifier of this Person" - :d2q.field/ref? false - :d2q.field/resolver :myapp.resolvers/person-fields} + :d2q.field/ref? false} {:d2q.field/name :myapp.person/first-name :doc "The first name of this Person" - :d2q.field/ref? false - :d2q.field/resolver :myapp.resolvers/person-fields} + :d2q.field/ref? false} {:d2q.field/name :myapp.person/last-name :doc "The last name of this Person" - :d2q.field/ref? false - :d2q.field/resolver :myapp.resolvers/person-fields} + :d2q.field/ref? false} {:d2q.field/name :myapp.person/full-name :doc "The full name of this Person, i.e the concatenation of her first and last names" - :d2q.field/ref? false - :d2q.field/resolver :myapp.resolvers/person-fields} + :d2q.field/ref? false} ;; NOTE the following 4 Fields are ref-typed and not parameterized {:d2q.field/name :myapp.person/mother :doc "The biological mother of this Person" :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/one - :d2q.field/resolver :myapp.resolvers/person-parents} + :d2q.field/cardinality :d2q.field.cardinality/one} {:d2q.field/name :myapp.person/father :doc "The biological father of this Person" :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/one - :d2q.field/resolver :myapp.resolvers/person-parents} + :d2q.field/cardinality :d2q.field.cardinality/one} {:d2q.field/name :myapp.person/parents :doc "The biological parents of this Person (mother then father when both are known)" :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/many - :d2q.field/resolver :myapp.resolvers/person-parents} + :d2q.field/cardinality :d2q.field.cardinality/many} {:d2q.field/name :myapp.person/children :doc "The biological children of this Person" :d2q.field/ref? true - :d2q.field/cardinality :d2q.field.cardinality/many - :d2q.field/resolver :myapp.resolvers/person-children}]) + :d2q.field/cardinality :d2q.field.cardinality/many}]) (defn resolve-person-fields [{:as qctx, :keys [db]} i+fcalls j+entities] @@ -528,11 +517,18 @@ {:d2q.server/fields fields :d2q.server/resolvers [{:d2q.resolver/name :myapp.resolvers/person-of-id + :d2q.resolver/field->meta {:myapp.persons/person-of-id nil} :d2q.resolver/compute #'resolve-persons-by-ids} {:d2q.resolver/name :myapp.resolvers/person-fields + :d2q.resolver/field->meta {:myapp.person/first-name nil, + :myapp.person/full-name nil, + :myapp.person/id nil, + :myapp.person/last-name nil} :d2q.resolver/compute #'resolve-person-fields} {:d2q.resolver/name :myapp.resolvers/person-parents + :d2q.resolver/field->meta {:myapp.person/father nil, :myapp.person/mother nil, :myapp.person/parents nil} :d2q.resolver/compute #'resolve-person-parents} {:d2q.resolver/name :myapp.resolvers/person-children + :d2q.resolver/field->meta {:myapp.person/children nil} :d2q.resolver/compute #'resolve-person-children}]})) diff --git a/test/d2q/test/helpers/resolvers.clj b/test/d2q/test/helpers/resolvers.clj new file mode 100644 index 0000000..fc6c394 --- /dev/null +++ b/test/d2q/test/helpers/resolvers.clj @@ -0,0 +1,106 @@ +(ns d2q.test.helpers.resolvers + (:require [clojure.test :refer :all] + [midje.sweet :refer :all] + [d2q.helpers.resolvers :as d2q-res] + [d2q.test.test-utils :as tu] + [d2q.api :as d2q] + [vvvvalvalval.supdate.api :as supd])) + +(fact "into-resolver-result" + (fact "with transducer" + (-> + (d2q-res/into-resolver-result + (map-indexed + (fn [ent-i n] + (try + (d2q.api/result-cell ent-i 0 + (/ 1 n)) + (catch Throwable err + (ex-info "aaaaaarrrrg" + {:n n} + err))))) + (range -2 3)) + tu/errors->ex-data) + + => + '{:d2q-res-cells [#d2q/result-cell{:d2q-entcell-i 0, :d2q-fcall-i 0, :d2q-rescell-value -1/2} + #d2q/result-cell{:d2q-entcell-i 1, :d2q-fcall-i 0, :d2q-rescell-value -1} + #d2q/result-cell{:d2q-entcell-i 3, :d2q-fcall-i 0, :d2q-rescell-value 1} + #d2q/result-cell{:d2q-entcell-i 4, :d2q-fcall-i 0, :d2q-rescell-value 1/2}], + :d2q-errors [{:error/type clojure.lang.ExceptionInfo, + :error/message "aaaaaarrrrg", + :error/data {:n 0}, + :error/cause {:error/type java.lang.ArithmeticException, + :error/message "Divide by zero", + :error/data nil}}]}) + + (fact "without transducer" + (-> + (d2q-res/into-resolver-result + (map-indexed + (fn [ent-i n] + (try + (d2q.api/result-cell ent-i 0 + (/ 1 n)) + (catch Throwable err + (ex-info "aaaaaarrrrg" + {:n n} + err)))) + (range -2 3))) + tu/errors->ex-data) + => + '{:d2q-res-cells [#d2q/result-cell{:d2q-entcell-i 0, :d2q-fcall-i 0, :d2q-rescell-value -1/2} + #d2q/result-cell{:d2q-entcell-i 1, :d2q-fcall-i 0, :d2q-rescell-value -1} + #d2q/result-cell{:d2q-entcell-i 3, :d2q-fcall-i 0, :d2q-rescell-value 1} + #d2q/result-cell{:d2q-entcell-i 4, :d2q-fcall-i 0, :d2q-rescell-value 1/2}], + :d2q-errors [{:error/type clojure.lang.ExceptionInfo, + :error/message "aaaaaarrrrg", + :error/data {:n 0}, + :error/cause {:error/type java.lang.ArithmeticException, + :error/message "Divide by zero", + :error/data nil}}]}) + ) + +(fact "entities-independent-resolver" + (-> + @((d2q-res/entities-independent-resolver + (fn square-root [_qctx i+fcalls] + (d2q-res/into-resolver-result + (map (fn [[i {:as fcall, x :d2q-fcall-arg}]] + (try + (assert (number? x)) + (assert (not (neg? x))) + (d2q/result-cell -1 i + (Math/sqrt (double x))) + (catch Throwable err + (ex-info + (str "Failed to take square root of " (pr-str x)) + {:d2q-fcall-i i} + err))))) + i+fcalls))) + :qctx + [[0 {:d2q-fcall-arg 0.0}] + [1 {:d2q-fcall-arg 4.0}] + [2 {:d2q-fcall-arg -1.0}] + [3 {:d2q-fcall-arg nil}]] + [[10 :entity-1] + [20 :entity-2]]) + tu/errors->ex-data + (supd/supdate {:d2q-res-cells set})) + => '{:d2q-res-cells #{#d2q/result-cell{:d2q-entcell-i 20, :d2q-fcall-i 1, :d2q-rescell-value 2.0} + #d2q/result-cell{:d2q-entcell-i 20, :d2q-fcall-i 0, :d2q-rescell-value 0.0} + #d2q/result-cell{:d2q-entcell-i 10, :d2q-fcall-i 0, :d2q-rescell-value 0.0} + #d2q/result-cell{:d2q-entcell-i 10, :d2q-fcall-i 1, :d2q-rescell-value 2.0}}, + :d2q-errors [{:error/type clojure.lang.ExceptionInfo, + :error/message "Failed to take square root of -1.0", + :error/data {:d2q-fcall-i 2}, + :error/cause {:error/type java.lang.AssertionError, + :error/message "Assert failed: (not (neg? x))", + :error/data nil}} + {:error/type clojure.lang.ExceptionInfo, + :error/message "Failed to take square root of nil", + :error/data {:d2q-fcall-i 3}, + :error/cause {:error/type java.lang.AssertionError, + :error/message "Assert failed: (number? x)", + :error/data nil}}]} + ) diff --git a/test/d2q/test/test_utils.clj b/test/d2q/test/test_utils.clj new file mode 100644 index 0000000..fb26c59 --- /dev/null +++ b/test/d2q/test/test_utils.clj @@ -0,0 +1,20 @@ +(ns d2q.test.test-utils + (:require [clojure.test :refer :all])) + +(defn dataify-ex + [^Throwable ex] + (let [cause (.getCause ex)] + (cond-> {:error/type (-> ex class .getName symbol) + :error/message (.getMessage ex) + :error/data (ex-data ex)} + (some? cause) + (assoc :error/cause (dataify-ex cause))))) + +(defn errors->ex-data + [res] + (update res :d2q-errors + #(->> % + (map dataify-ex) + (sort-by :error/message) + vec))) +