Skip to content

Commit

Permalink
Field -> Resolver registered in Resolver with Field Meta + Resolvers …
Browse files Browse the repository at this point in the history
…helpers
  • Loading branch information
Valentin Waeselynck committed Oct 12, 2018
1 parent 6c5bd5f commit 64727c1
Show file tree
Hide file tree
Showing 8 changed files with 456 additions and 158 deletions.
32 changes: 8 additions & 24 deletions src/d2q/api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -71,19 +70,26 @@
## 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
by one Field Call on one Entity in the given Query Context.
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.
Expand Down Expand Up @@ -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)})))




Expand Down
184 changes: 184 additions & 0 deletions src/d2q/helpers/resolvers.clj
Original file line number Diff line number Diff line change
@@ -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)))))))))
60 changes: 47 additions & 13 deletions src/d2q/impl.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down
34 changes: 27 additions & 7 deletions src/d2q/impl/utils.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Loading

0 comments on commit 64727c1

Please sign in to comment.