Skip to content

Commit

Permalink
Model resolution registry (#91)
Browse files Browse the repository at this point in the history
* Create model registry and lookup by registered alias

Toucan allows to query a model by either the model itself or the symbol
of the model `(db/select 'User :id 1)`. This lookup will look at
`models/root-namespace` for a model namespace. But this lookup will
obviously fail if you define models in namespaces that do not follow
this convention. For example multiple models in the same namespace
cannot possibly work.

This branch was added under the standard lookup so as not to be a
breaking change. In either order there is the chance for
ambiguity. Defining two models with the same name will never be
well-defined. Under this change, it will always find the model defined
by convention and never the one in a non-standard location.

This ambiguity cannot actually be fixed since there is no disambiguating
information in the query `(select 'Foo :id 1)`.

* Guard against multiple namespaces.

If there are multiple namespaces defining a model by the same name right
now, it will always resolve to one at the conventional location. Before
this change, no unconventional location models would ever be found by
using the symbol. This explains why the lookup by model symbol is the
second branch (so the current behavior, while unspecified before,
continues in the same manner), and why we do not choose an item if
multiple namespaces are registered (arbitrary model by set order, or
last namespace read if we just keep one namespace).
  • Loading branch information
dpsutton authored Feb 23, 2022
1 parent ee1acdf commit 762ad69
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 7 deletions.
32 changes: 26 additions & 6 deletions src/toucan/db.clj
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,33 @@
(defn- resolve-model-from-symbol
"Resolve the model associated with SYMB, calling `require` on its namespace if needed.
(resolve-model-from-symbol 'CardFavorite) -> my-project.models.card-favorite/CardFavorite"
(resolve-model-from-symbol 'CardFavorite) -> my-project.models.card-favorite/CardFavorite
If model is not found in the namespace as configured with [[models/root-namespace]], it checks a private registry
populated by `defmodel` to try again.
Multiple Models with the same name:
The first branch using the conventional namespace location will resolve to a model even if multiple are defined. The
second branch will not try to guess which model is intended if there are multiple models defined. Otherwise an error
will be throw containing data including the conventional namespace tried and any namespaces that have a model by
that name."
[symb]
(let [model-ns (model-symb->ns symb)]
@(try (ns-resolve model-ns symb)
(catch Throwable _
(require model-ns)
(ns-resolve model-ns symb)))))
(letfn [(sym->model [ns' symb']
(try
(some-> (requiring-resolve (symbol (str ns') (str symb')))
deref)
(catch java.io.FileNotFoundException _e nil)))]
(let [inferred-ns (model-symb->ns symb)
registered-nss (get @models/model-sym->namespace-sym symb)]
(or (sym->model inferred-ns symb)
(let [[registered-ns & ambig] registered-nss]
(when-not ambig
(sym->model registered-ns symb)))
(throw (ex-info (format "Could not find model for: %s" symb)
{:symbol symb
:configured-namespace inferred-ns
:registered-namespaces registered-nss}))))))

(defn resolve-model
"Resolve a model *if* it's quoted. This also unwraps entities when they're inside vectores.
Expand Down
7 changes: 7 additions & 0 deletions src/toucan/models.clj
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,12 @@
[(fully-qualified-symbol form) acc]
[type (assoc-in acc [type (keyword (first form))] `(fn ~@(drop 1 form)))])) [nil {}] forms)))

(defonce ^{:doc "Mapping from model name to namespaces containing the model. Useful in order to resolve models which
are not defined in a namespace matching the convention. Keys are sets of namespaces to help in error reporting if
there are multiple namespaces matching a single model name."}
model-sym->namespace-sym
(atom {}))

(defmacro defmodel
"Define a new \"model\". Models encapsulate information and behaviors related to a specific table in the application
DB, and have their own unique record type.
Expand Down Expand Up @@ -515,6 +521,7 @@
f))
(macroexpand defrecord-form))]
`(do
(swap! @#'model-sym->namespace-sym update '~model (fnil conj #{}) (ns-name *ns*))
~defrecord-form

(extend ~instance
Expand Down
14 changes: 13 additions & 1 deletion test/toucan/db_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,19 @@
;; Trying to resolve an model that cannot be found should throw an Exception
(expect
Exception
(db/resolve-model 'Fish))
(db/resolve-model 'Unfound))

(models/defmodel Nowfound :testing_model_resolution)

;; we can resolve models when their namespace doesn't follow convention
(expect
Nowfound
(db/resolve-model 'Nowfound))

;; We save the defining namespace in an atom keyed by model symbol
(expect
(get @models/model-sym->namespace-sym 'Nowfound)
#{'toucan.db-test})

;; ... as should trying to resolve things that aren't entities or symbols
(expect Exception (db/resolve-model {}))
Expand Down

0 comments on commit 762ad69

Please sign in to comment.