mirror of
https://github.com/logos-storage/swarmsim.git
synced 2026-01-03 06:13:12 +00:00
factor out message type macro into more general type id macro
This commit is contained in:
parent
cb6bc67543
commit
cd64de4026
@ -18,26 +18,25 @@ type
|
||||
|
||||
type ArrayShuffler = proc (arr: var seq[PeerDescriptor]): void
|
||||
|
||||
type
|
||||
DHTTracker* = ref object of Protocol
|
||||
peerExpiration*: Duration
|
||||
maxPeers*: uint
|
||||
peers: OrderedTable[int, PeerDescriptor]
|
||||
shuffler: ArrayShuffler
|
||||
|
||||
|
||||
ExpirationTimer* = ref object of SchedulableEvent
|
||||
peerId*: int
|
||||
tracker: DHTTracker
|
||||
|
||||
typedMessage:
|
||||
withTypeId:
|
||||
type
|
||||
DHTTracker* = ref object of Protocol
|
||||
peerExpiration*: Duration
|
||||
maxPeers*: uint
|
||||
peers: OrderedTable[int, PeerDescriptor]
|
||||
shuffler: ArrayShuffler
|
||||
|
||||
PeerAnnouncement* = ref object of Message
|
||||
peerId*: int
|
||||
|
||||
SampleSwarm* = ref object of Message
|
||||
numPeers: uint
|
||||
|
||||
type
|
||||
ExpirationTimer* = ref object of SchedulableEvent
|
||||
peerId*: int
|
||||
tracker: DHTTracker
|
||||
|
||||
let RandomShuffler = proc (arr: var seq[PeerDescriptor]) =
|
||||
discard arr.nextPermutation()
|
||||
|
||||
@ -51,12 +50,11 @@ proc new*(
|
||||
): DHTTracker =
|
||||
DHTTracker(
|
||||
# This should in general be safe as those are always positive.
|
||||
id: "DHTTracker",
|
||||
peerExpiration: peerExpiration,
|
||||
maxPeers: maxPeers,
|
||||
shuffler: shuffler,
|
||||
peers: initOrderedTable[int, PeerDescriptor](),
|
||||
messageTypes: @[PeerAnnouncement.messageType, SampleSwarm.messageType]
|
||||
messageTypes: @[PeerAnnouncement.typeId, SampleSwarm.typeId]
|
||||
)
|
||||
|
||||
proc peers*(self: DHTTracker): seq[PeerDescriptor] = self.peers.values.toSeq()
|
||||
|
||||
@ -1,54 +1,7 @@
|
||||
import options
|
||||
import macros
|
||||
|
||||
import ./types
|
||||
|
||||
method `messageType`*(self: Message): string {.base.} =
|
||||
raise newException(CatchableError, "Method without implementation override")
|
||||
|
||||
method `messageType`*(self: FreelyTypedMessage): string = self.messageType
|
||||
method `messageType`*(self: Message): string {.base.} = self.typeId
|
||||
|
||||
proc allMessages*(self: type Message): string = "*"
|
||||
|
||||
func typeName(typeDef: NimNode): Option[NimNode] =
|
||||
expectKind typeDef, nnkTypeDef
|
||||
|
||||
return if typeDef[0].kind == nnkIdent:
|
||||
typeDef[0].some
|
||||
elif typeDef[0].kind == nnkPostfix:
|
||||
typeDef[0][1].some
|
||||
else:
|
||||
none(NimNode)
|
||||
|
||||
macro typedMessage*(body: untyped): untyped =
|
||||
expectKind body, nnkStmtList
|
||||
expectKind body[0], nnkTypeSection
|
||||
|
||||
for statement in body[0]:
|
||||
if statement.kind != nnkTypeDef:
|
||||
continue
|
||||
|
||||
let maybeTypename = typeName(statement)
|
||||
if maybeTypename.isNone:
|
||||
error("unable to get type name from AST. Sorry.")
|
||||
|
||||
let typeIdent = maybeTypename.get
|
||||
let typeName = newLit(typeIdent.strVal)
|
||||
|
||||
let typeProc = quote do:
|
||||
proc messageType*(self: type `typeIdent`): string = `typeName`
|
||||
|
||||
let instanceProc = quote do:
|
||||
method messageType*(self: `typeIdent`): string = `typeIdent`.messageType
|
||||
|
||||
# We replace the proc name with a quoted symbol so it turns into a
|
||||
# getter.
|
||||
typeProc[0][1] = newTree(nnkAccQuoted, typeProc[0][1])
|
||||
instanceProc[0][1] = newTree(nnkAccQuoted, instanceProc[0][1])
|
||||
|
||||
body.add(typeProc)
|
||||
body.add(instanceProc)
|
||||
|
||||
return body
|
||||
|
||||
export Message, FreelyTypedMessage
|
||||
export Message
|
||||
|
||||
@ -21,8 +21,8 @@ proc getProtocol*(self: Peer, id: string): Option[Protocol] =
|
||||
|
||||
none(Protocol)
|
||||
|
||||
proc addProtocol*(self: Peer, protocol: Protocol): void =
|
||||
self.protocols[protocol.id] = protocol
|
||||
proc addProtocol*[T: Protocol](self: Peer, protocol: T): void =
|
||||
self.protocols[protocol.protocolId] = protocol
|
||||
|
||||
proc deliverForType(self: Peer, messageType: string, message: Message,
|
||||
engine: EventDrivenEngine, network: Network): void =
|
||||
@ -44,7 +44,7 @@ proc initPeer*(self: Peer, protocols: seq[Protocol],
|
||||
for protocol in protocols:
|
||||
let protocol = protocol # https://github.com/nim-lang/Nim/issues/16740
|
||||
|
||||
self.protocols[protocol.id] = protocol
|
||||
self.protocols[protocol.protocolId] = protocol
|
||||
protocol.messageTypes.apply(proc (m: string): void =
|
||||
self.dispatch.add(m, protocol))
|
||||
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import typetraits
|
||||
|
||||
import ./types
|
||||
import ./eventdrivenengine
|
||||
|
||||
@ -7,8 +5,12 @@ export eventdrivenengine
|
||||
export Protocol
|
||||
export Message
|
||||
|
||||
method deliver*(self: Protocol, message: Message, engine: EventDrivenEngine,
|
||||
network: Network): void {.base.} =
|
||||
raise newException(CatchableError, "Method without implementation override")
|
||||
method `protocolId`*(self: Protocol): string {.base.} = self.typeId
|
||||
|
||||
proc protocolName*[T: Protocol](self: type T): string = name(T)
|
||||
method deliver*(
|
||||
self: Protocol,
|
||||
message: Message,
|
||||
engine: EventDrivenEngine,
|
||||
network: Network
|
||||
): void {.base.} =
|
||||
raise newException(CatchableError, "Method without implementation override")
|
||||
|
||||
@ -4,11 +4,13 @@ import std/sets
|
||||
import std/options
|
||||
import std/random
|
||||
|
||||
import ../lib/withtypeid
|
||||
import ../lib/multitable
|
||||
|
||||
export heapqueue
|
||||
export option
|
||||
export random
|
||||
export withtypeid
|
||||
|
||||
type
|
||||
SchedulableEvent* = ref object of RootObj
|
||||
@ -32,7 +34,6 @@ type
|
||||
## A `Protocol` defines a P2P protocol. It handles messages meant for it,
|
||||
## keeps internal state, and may expose services to other `Protocol`s within
|
||||
## the same `Peer`.
|
||||
id*: string
|
||||
messageTypes*: seq[string]
|
||||
|
||||
type
|
||||
@ -48,11 +49,6 @@ type
|
||||
sender*: Option[Peer] = none(Peer)
|
||||
receiver*: Peer
|
||||
|
||||
FreelyTypedMessage* = ref object of Message
|
||||
## A `FreelyTypedMessage` is a `Message` that can be of any type.
|
||||
##
|
||||
messageType*: string
|
||||
|
||||
type
|
||||
Network* = ref object of RootObj
|
||||
## A `Network` is a collection of `Peer`s that can communicate with each
|
||||
@ -61,3 +57,4 @@ type
|
||||
engine*: EventDrivenEngine
|
||||
defaultLinkDelay*: uint64
|
||||
peers*: HashSet[Peer] # TODO: use an array
|
||||
|
||||
|
||||
86
swarmsim/lib/withtypeid.nim
Normal file
86
swarmsim/lib/withtypeid.nim
Normal file
@ -0,0 +1,86 @@
|
||||
|
||||
## This package adds a very basic interface and a macro to annotate and query
|
||||
## types about their ids at runtime. Type information is queried over a method
|
||||
## and uses dynamic dispatch, which means you can always recover the actual
|
||||
## type of an object, even as it is upcasted to more general types.
|
||||
##
|
||||
## This is a stopgap measure to allow us to, for instance, register dispatchers
|
||||
## based on type information, and do type equality comparisons.
|
||||
##
|
||||
## NB. This is very naively implemented right now, and won't ensure by a long
|
||||
## shot that type IDs - which are currently just the type's name - are unique.
|
||||
## If this proves to be a worthwhile effort, however, it would be possible to
|
||||
## extend this to use hashes (e.g. signatureHash) or a global counter, and have
|
||||
## a separate "typeName" attribute to query the actual name of the type.
|
||||
##
|
||||
|
||||
import options
|
||||
import macros
|
||||
|
||||
method `typeId`*(self: RootObj): string {.base.} =
|
||||
## Returns the type id of an object. This is currently a string and
|
||||
## conflates with the type's human-readable name, but the only hard
|
||||
## requirement is that this is a hashable object and has well-defined
|
||||
## identity semantics (==).
|
||||
##
|
||||
## If a type is not created with the `withTypeId` macro, then the method will
|
||||
## raise an exception unless manually overriden by subtypes.
|
||||
raise (ref Defect)(msg: "Type has not been annotated with `withTypeId`.")
|
||||
|
||||
method `typeId`*(self: ref RootObj): string {.base.} =
|
||||
raise (ref Defect)(msg: "Type has not been annotated with `withTypeId`.")
|
||||
|
||||
func typeName(typeDef: NimNode): Option[NimNode] =
|
||||
expectKind typeDef, nnkTypeDef
|
||||
|
||||
return if typeDef[0].kind == nnkIdent:
|
||||
typeDef[0].some
|
||||
elif typeDef[0].kind == nnkPostfix:
|
||||
typeDef[0][1].some
|
||||
else:
|
||||
none(NimNode)
|
||||
|
||||
macro withTypeId*(body: untyped): untyped =
|
||||
## Creates a type with a `typeId` method and a proc bound to the type
|
||||
## itself which return the type's name.
|
||||
runnableExamples:
|
||||
withTypeId:
|
||||
type
|
||||
Foo = object of RootObj
|
||||
Bar* = object of RootObj
|
||||
|
||||
doAssert Foo.typeId == "Foo"
|
||||
doAssert Bar.typeId == "Bar"
|
||||
|
||||
doAssert Foo().typeId == "Foo"
|
||||
doAssert Bar().typeId == "Bar"
|
||||
|
||||
expectKind body, nnkStmtList
|
||||
expectKind body[0], nnkTypeSection
|
||||
|
||||
for statement in body[0]:
|
||||
if statement.kind != nnkTypeDef:
|
||||
continue
|
||||
|
||||
let maybeTypename = typeName(statement)
|
||||
if maybeTypename.isNone:
|
||||
error("unable to get type name from AST. Sorry.")
|
||||
|
||||
let typeIdent = maybeTypename.get
|
||||
let typeName = newLit(typeIdent.strVal)
|
||||
|
||||
let typeProc = quote do:
|
||||
proc typeId*(self: type `typeIdent`): string = `typeName`
|
||||
|
||||
let instanceProc = quote do:
|
||||
method typeId*(self: `typeIdent`): string = `typeIdent`.typeId
|
||||
|
||||
# We replace the proc name with a quoted symbol so it turns into a
|
||||
# getter.
|
||||
typeProc[0][1] = newTree(nnkAccQuoted, typeProc[0][1])
|
||||
instanceProc[0][1] = newTree(nnkAccQuoted, instanceProc[0][1])
|
||||
|
||||
body.add(typeProc)
|
||||
body.add(instanceProc)
|
||||
|
||||
return body
|
||||
@ -2,9 +2,9 @@ import engine/teventdrivenengine
|
||||
import engine/tschedulableevent
|
||||
import engine/tnetwork
|
||||
import engine/tpeer
|
||||
import engine/tmessage
|
||||
import codex/tdhttracker
|
||||
import lib/tmultitable
|
||||
import lib/twithtypeid
|
||||
|
||||
{.warning[UnusedImport]: off.}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import pkg/swarmsim/timeutils
|
||||
|
||||
proc getPeerArray(tracker: Peer): seq[PeerDescriptor] =
|
||||
DHTTracker(
|
||||
tracker.getProtocol(DHTTracker.protocolName).get()).peers
|
||||
tracker.getProtocol(DHTTracker.typeId).get()).peers
|
||||
|
||||
proc getPeerIdArray(tracker: Peer): seq[int] =
|
||||
getPeerArray(tracker).map(p => p.peerId)
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import unittest
|
||||
|
||||
import swarmsim/engine/message
|
||||
|
||||
typedMessage:
|
||||
type
|
||||
PeerAnnouncement* = object of Message
|
||||
peerId*: int
|
||||
|
||||
PrivateMessage = object of Message
|
||||
|
||||
suite "message":
|
||||
test "should automatically generate a type string for typedMessage types":
|
||||
check(PeerAnnouncement.messageType == "PeerAnnouncement")
|
||||
check(PrivateMessage.messageType == "PrivateMessage")
|
||||
|
||||
test "should automatically generate a type string for typedMessage instances":
|
||||
check(PeerAnnouncement(peerId: 1).messageType == "PeerAnnouncement")
|
||||
check(PrivateMessage().messageType == "PrivateMessage")
|
||||
|
||||
@ -7,14 +7,15 @@ import swarmsim/engine/peer
|
||||
import swarmsim/engine/protocol
|
||||
|
||||
import ../helpers/inbox
|
||||
import ../helpers/types
|
||||
|
||||
suite "network":
|
||||
test "should dispatch message to the correct peer":
|
||||
|
||||
let engine = EventDrivenEngine()
|
||||
|
||||
let i1 = Inbox(id: "inbox", messageTypes: @["m"])
|
||||
let i2 = Inbox(id: "inbox", messageTypes: @["m"])
|
||||
let i1 = Inbox(protocolId: "inbox", messageTypes: @["m"])
|
||||
let i2 = Inbox(protocolId: "inbox", messageTypes: @["m"])
|
||||
|
||||
let p1 = Peer.new(protocols = @[Protocol i1])
|
||||
let p2 = Peer.new(protocols = @[Protocol i2])
|
||||
|
||||
@ -7,6 +7,7 @@ import swarmsim/engine/peer
|
||||
import swarmsim/engine/message
|
||||
|
||||
import ../helpers/inbox
|
||||
import ../helpers/types
|
||||
|
||||
# We need this here as otherwise for some reason the nim compiler trips.
|
||||
proc `$`*(m: Message): string = repr m
|
||||
@ -32,8 +33,8 @@ suite "peer":
|
||||
check(not peerSet.contains(p1))
|
||||
|
||||
test "should dispatch message to correct protocol":
|
||||
let i1 = Inbox(id: "protocol1", messageTypes: @["m1"])
|
||||
let i2 = Inbox(id: "protocol2", messageTypes: @["m2"])
|
||||
let i1 = Inbox(protocolId: "protocol1", messageTypes: @["m1"])
|
||||
let i2 = Inbox(protocolId: "protocol2", messageTypes: @["m2"])
|
||||
|
||||
let peer = Peer.new(protocols = @[Protocol i1, i2])
|
||||
|
||||
@ -51,8 +52,8 @@ suite "peer":
|
||||
check(i2.messages == @[m2])
|
||||
|
||||
test "should dispatch a message to multiple protocols if they are listening on the same message type":
|
||||
let i1 = Inbox(id: "protocol1", messageTypes: @["m1"])
|
||||
let i2 = Inbox(id: "protocol2", messageTypes: @["m1"])
|
||||
let i1 = Inbox(protocolId: "protocol1", messageTypes: @["m1"])
|
||||
let i2 = Inbox(protocolId: "protocol2", messageTypes: @["m1"])
|
||||
|
||||
let peer = Peer.new(protocols = @[Protocol i1, i2])
|
||||
|
||||
@ -64,7 +65,7 @@ suite "peer":
|
||||
check(i2.messages == @[m1])
|
||||
|
||||
test "should allow protocol to listen on multiple message types":
|
||||
let i1 = Inbox(id: "protocol1", messageTypes: @["m1", "m2"])
|
||||
let i1 = Inbox(protocolId: "protocol1", messageTypes: @["m1", "m2"])
|
||||
|
||||
let peer = Peer.new(protocols = @[Protocol i1])
|
||||
|
||||
@ -80,7 +81,7 @@ suite "peer":
|
||||
|
||||
|
||||
test "should deliver all message types when listening to Message.allMessages":
|
||||
let i1 = Inbox(id: "protocol1", messageTypes: @[Message.allMessages])
|
||||
let i1 = Inbox(protocolId: "protocol1", messageTypes: @[Message.allMessages])
|
||||
|
||||
let peer = Peer.new(protocols = @[Protocol i1])
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import swarmsim/engine/network
|
||||
|
||||
type
|
||||
Inbox* = ref object of Protocol
|
||||
protocolId*: string
|
||||
messages*: seq[Message]
|
||||
|
||||
method deliver*(
|
||||
@ -15,6 +16,8 @@ method deliver*(
|
||||
) =
|
||||
self.messages.add(message)
|
||||
|
||||
method `protocolId`*(self: Inbox): string = self.protocolId
|
||||
|
||||
export Message
|
||||
export peer
|
||||
export protocol
|
||||
|
||||
26
tests/helpers/testpeer.nim
Normal file
26
tests/helpers/testpeer.nim
Normal file
@ -0,0 +1,26 @@
|
||||
import std/options
|
||||
import std/random
|
||||
|
||||
import swarmsim/engine
|
||||
import swarmsim/engine/peer
|
||||
|
||||
import ./inbox
|
||||
|
||||
type TestPeer* = ref object of Peer
|
||||
network: Network
|
||||
|
||||
proc new*(
|
||||
t: typedesc[TestPeer],
|
||||
network: Network,
|
||||
peerId: Option[int] = none(int),
|
||||
): TestPeer =
|
||||
let peer: TestPeer = TestPeer(network: network)
|
||||
discard peer.initPeer(protocols = @[Protocol Inbox()])
|
||||
peer
|
||||
|
||||
proc inbox*(peer: TestPeer): Inbox =
|
||||
Inbox peer.getProtocol(Inbox.protocolName).get()
|
||||
|
||||
proc send*(self: TestPeer, msg: Message): ScheduledEvent =
|
||||
msg.sender = Peer(self).some
|
||||
self.network.send(msg)
|
||||
9
tests/helpers/types.nim
Normal file
9
tests/helpers/types.nim
Normal file
@ -0,0 +1,9 @@
|
||||
import swarmsim/engine/message
|
||||
|
||||
type
|
||||
FreelyTypedMessage* = ref object of Message
|
||||
## A `FreelyTypedMessage` is a `Message` that can be of any type.
|
||||
##
|
||||
messageType*: string
|
||||
|
||||
method `messageType`*(self: FreelyTypedMessage): string = self.messageType
|
||||
38
tests/lib/twithtypeid.nim
Normal file
38
tests/lib/twithtypeid.nim
Normal file
@ -0,0 +1,38 @@
|
||||
import unittest
|
||||
|
||||
import swarmsim/lib/withtypeid
|
||||
|
||||
withTypeId:
|
||||
type
|
||||
Foo = object of RootObj
|
||||
Bar* = object of RootObj
|
||||
Qux* = ref object of RootObj
|
||||
|
||||
FooBar = object of Bar
|
||||
|
||||
type NonAnnotated = object of RootObj
|
||||
type NonAnnotatedRef = ref object of RootObj
|
||||
|
||||
suite "withtypeid":
|
||||
test "should allow querying a type for its id":
|
||||
check(Foo.typeId == "Foo")
|
||||
check(Bar.typeId == "Bar")
|
||||
check(Qux.typeId == "Qux")
|
||||
|
||||
test "should allow querying an instance for its id":
|
||||
check(Bar().typeId == "Bar")
|
||||
check(Foo().typeId == "Foo")
|
||||
check(Qux().typeId == "Qux")
|
||||
|
||||
test "should correctly return the id of the concrete type when upcasted":
|
||||
let instance: Bar = FooBar()
|
||||
check(instance.typeId == "FooBar")
|
||||
|
||||
test "should raise an error when trying to query the id of a non-annotated type":
|
||||
expect(Defect):
|
||||
discard NonAnnotated().typeId
|
||||
# Note we don't need NonAnnotated.typeId as that won't even compile.
|
||||
|
||||
test "should raise an error when trying to query the id of a non-annotated ref type":
|
||||
expect(Defect):
|
||||
discard NonAnnotatedRef().typeId
|
||||
Loading…
x
Reference in New Issue
Block a user