I am building a web application and using an ORM called norm. As such, I have an SQL database with a bunch of tables, all of which correspond to various Models that I have in nim. Often enough, they are in many-to-many relationships such as this:
type
A = ref object of Model
name: string
B = ref object of Model
anotherName: string
C = ref object of Model
myA: A
myB: B
Due to the way norm is currently implemented, I have to query C if I want all entries B for a given A or all entries A for a given B.
That leaves me with a seq[C] when I actually wanted seq[A] or seq[B].
I can, of course, for a specific set of A, B and C iterate over the seq with the mapIt
function of sequtils
:
import sequtils
let myASeq: seq[A] = myCSeq.mapit(it.myA)
But given that I have around 20 or so of these relationships and I'd like to not implement the same thing 20 times there. I'd prefer a generic way, where I can just type in the fieldName (e.g. myA
) in some way and resolve it like that on the fly. How can I achieve this?
Thanks to the incredibly helpful folks on the nim discord server (shoutout to ElegantBeef), that educated me on this. There is 2 ways you can go about this:
import norm/[model, pragmas]
import std/[sequtils, typetraits, macros, json]
template mapModel[T: Model](mySeq: seq[T], field: untyped): seq[untyped] = mySeq.mapIt(it.field)
Example usage:
type
A = ref object of Model
name: string
D = ref object of Model
myothernameid: string
myDA: A
var myDSeq: seq[D] = @[]
let anA: A = A(name: "this is an A")
myDSeq.add(D(myothernameid: "la", myDA: anA))
myDSeq.add(D(myothernameid: "le", myDA: anA))
echo %*myDSeq # [{"myothernameid":"la","myDA":{"name":"this is an A","id":0},"id":0},{"myothernameid":"le","myDA":{"name":"this is an A","id":0},"id":0}]
let myASeq: seq[A] = mapModel(myDSeq, "myDA")
echo %*myASeq # [{"name":"this is an A","id":0},{"name":"this is an A","id":0}]
This one is useful if you only have access to the field name at compile time and not the field itself, because that might be different depending on a when
condition.
import norm/[model, pragmas]
import std/[sequtils, typetraits, macros, json]
macro mapModel[T: Model](mySeq: seq[T], field: static string): untyped =
newCall(bindSym"mapIt", mySeq, nnkDotExpr.newTree(ident"it", ident field))
Example usage:
var myDSeq: seq[D] = @[]
let anA: A = A(name: "this is an A")
myDSeq.add(D(myothernameid: "la", myDA: anA))
myDSeq.add(D(myothernameid: "le", myDA: anA))
echo %*myDSeq # As Before
let myASeq: seq[A] = mapModel(myDSeq, "myDA")
echo %*myASeq # As Before
For an explanation what the macro does, I do not have a full understanding of it myself, but wrote down what I got out of my chat with ElegantBeef:
newCall
--> You're about to receive something to execute which I'll refer to as callable, do that
bindSym"mapIt"
--> Call a function that is bound to the symbol "mapIt", and do so with the following arguments
mySeq
--> the first argument to pass to the function of the "mapIt" symbol
nnkDotExpr.newTree
--> This is going to be an execution tree or whatever they're called this resolves into a callable, such as a proc or a func. Specifically a callable that is a dot expression.
ident"it"
--> The value of the variable that has the symbol "it", this is supplied by the "mapIt" function
ident field
--> The value of the variable that has the symbol contained within the field variable.
nnkDotExpr.newTree(ident"it", ident field)
--> Call a dot expression on it to grab the value of its field that goes by the name contained within field. This is equivalent to it.la
if field contains "la"