mirror of
https://github.com/waku-org/nwaku.git
synced 2025-01-26 23:02:30 +00:00
feat(message): differentiate content and pubsub topic namespacing
This commit is contained in:
parent
d5f93e385d
commit
67db35e29d
@ -20,7 +20,7 @@ proc toV1Topic*(contentTopic: ContentTopic): waku_protocol.Topic {.raises: [Valu
|
||||
## Extracts the 4-byte array v1 topic from a content topic
|
||||
## with format `/waku/1/<v1-topic-bytes-as-hex>/rfc26`
|
||||
|
||||
let ns = NamespacedTopic.parse(contentTopic)
|
||||
let ns = NsContentTopic.parse(contentTopic)
|
||||
if ns.isErr():
|
||||
let err = ns.tryError()
|
||||
raise newException(ValueError, $err)
|
||||
@ -33,11 +33,11 @@ proc toV2ContentTopic*(v1Topic: waku_protocol.Topic): ContentTopic =
|
||||
## with format `/waku/1/<v1-topic-bytes-as-hex>/rfc26`
|
||||
##
|
||||
## <v1-topic-bytes-as-hex> should be prefixed with `0x`
|
||||
var namespacedTopic = NamespacedTopic()
|
||||
var namespacedTopic = NsContentTopic()
|
||||
|
||||
namespacedTopic.application = ContentTopicApplication
|
||||
namespacedTopic.version = ContentTopicAppVersion
|
||||
namespacedTopic.name = "0x" & v1Topic.toHex()
|
||||
namespacedTopic.name = v1Topic.to0xHex()
|
||||
namespacedTopic.encoding = "rfc26"
|
||||
|
||||
return ContentTopic($namespacedTopic)
|
||||
@ -45,7 +45,7 @@ proc toV2ContentTopic*(v1Topic: waku_protocol.Topic): ContentTopic =
|
||||
|
||||
proc isBridgeable*(msg: WakuMessage): bool =
|
||||
## Determines if a Waku v2 msg is on a bridgeable content topic
|
||||
let ns = NamespacedTopic.parse(msg.contentTopic)
|
||||
let ns = NsContentTopic.parse(msg.contentTopic)
|
||||
if ns.isErr():
|
||||
return false
|
||||
|
||||
|
@ -6,14 +6,14 @@ import
|
||||
import
|
||||
../../waku/v2/protocol/waku_message/topics
|
||||
|
||||
suite "Waku Message - Topics namespacing":
|
||||
suite "Waku Message - Content topics namespacing":
|
||||
|
||||
test "Stringify namespaced topic":
|
||||
test "Stringify namespaced content topic":
|
||||
## Given
|
||||
var ns = NamespacedTopic()
|
||||
ns.application = "waku"
|
||||
var ns = NsContentTopic()
|
||||
ns.application = "toychat"
|
||||
ns.version = "2"
|
||||
ns.name = "default-waku"
|
||||
ns.name = "huilong"
|
||||
ns.encoding = "proto"
|
||||
|
||||
## When
|
||||
@ -21,79 +21,173 @@ suite "Waku Message - Topics namespacing":
|
||||
|
||||
## Then
|
||||
check:
|
||||
topic == "/waku/2/default-waku/proto"
|
||||
topic == "/toychat/2/huilong/proto"
|
||||
|
||||
test "Parse topic string - Valid string":
|
||||
test "Parse content topic string - Valid string":
|
||||
## Given
|
||||
let topic = "/waku/2/default-waku/proto"
|
||||
let topic = "/toychat/2/huilong/proto"
|
||||
|
||||
## When
|
||||
let nsRes = NamespacedTopic.parse(topic)
|
||||
let nsRes = NsContentTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check nsRes.isOk()
|
||||
|
||||
let ns = nsRes.get()
|
||||
check:
|
||||
ns.application == "waku"
|
||||
ns.application == "toychat"
|
||||
ns.version == "2"
|
||||
ns.name == "default-waku"
|
||||
ns.name == "huilong"
|
||||
ns.encoding == "proto"
|
||||
|
||||
test "Parse topic string - Invalid string: doesn't start with slash":
|
||||
test "Parse content topic string - Invalid string: missing leading slash":
|
||||
## Given
|
||||
let topic = "waku/2/default-waku/proto"
|
||||
let topic = "toychat/2/huilong/proto"
|
||||
|
||||
## When
|
||||
let ns = NamespacedTopic.parse(topic)
|
||||
let ns = NsContentTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check ns.isErr()
|
||||
let err = ns.tryError()
|
||||
check:
|
||||
err.kind == NamespacingErrorKind.InvalidFormat
|
||||
err.kind == ParsingErrorKind.InvalidFormat
|
||||
err.cause == "topic must start with slash"
|
||||
|
||||
test "Parse topic string - Invalid string: not namespaced":
|
||||
test "Parse content topic string - Invalid string: not namespaced":
|
||||
## Given
|
||||
let topic = "/this-is-not-namespaced"
|
||||
|
||||
## When
|
||||
let ns = NamespacedTopic.parse(topic)
|
||||
let ns = NsContentTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check ns.isErr()
|
||||
let err = ns.tryError()
|
||||
check:
|
||||
err.kind == NamespacingErrorKind.InvalidFormat
|
||||
err.kind == ParsingErrorKind.InvalidFormat
|
||||
err.cause == "invalid topic structure"
|
||||
|
||||
|
||||
test "Parse topic string - Invalid string: missing encoding part":
|
||||
test "Parse content topic string - Invalid string: missing encoding part":
|
||||
## Given
|
||||
let topic = "/waku/2/default-waku"
|
||||
let topic = "/toychat/2/huilong"
|
||||
|
||||
## When
|
||||
let ns = NamespacedTopic.parse(topic)
|
||||
let ns = NsContentTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check ns.isErr()
|
||||
let err = ns.tryError()
|
||||
check:
|
||||
err.kind == NamespacingErrorKind.InvalidFormat
|
||||
err.kind == ParsingErrorKind.InvalidFormat
|
||||
err.cause == "invalid topic structure"
|
||||
|
||||
test "Parse topic string - Invalid string: too many parts":
|
||||
test "Parse content topic string - Invalid string: too many parts":
|
||||
## Given
|
||||
let topic = "/waku/2/default-waku/proto/33"
|
||||
let topic = "/toychat/2/huilong/proto/33"
|
||||
|
||||
## When
|
||||
let ns = NamespacedTopic.parse(topic)
|
||||
let ns = NsContentTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check ns.isErr()
|
||||
let err = ns.tryError()
|
||||
check:
|
||||
err.kind == NamespacingErrorKind.InvalidFormat
|
||||
err.kind == ParsingErrorKind.InvalidFormat
|
||||
err.cause == "invalid topic structure"
|
||||
|
||||
|
||||
suite "Waku Message - Pub-sub topics namespacing":
|
||||
|
||||
test "Stringify named sharding pub-sub topic":
|
||||
## Given
|
||||
var ns = NsPubsubTopic.named("waku-dev")
|
||||
|
||||
## When
|
||||
let topic = $ns
|
||||
|
||||
## Then
|
||||
check:
|
||||
topic == "/waku/2/waku-dev"
|
||||
|
||||
test "Stringify static sharding pub-sub topic":
|
||||
## Given
|
||||
var ns = NsPubsubTopic.staticSharding(cluster=0, shard=2)
|
||||
|
||||
## When
|
||||
let topic = $ns
|
||||
|
||||
## Then
|
||||
check:
|
||||
topic == "/waku/2/rs/0/2"
|
||||
|
||||
test "Parse named pub-sub topic string - Valid string":
|
||||
## Given
|
||||
let topic = "/waku/2/waku-dev"
|
||||
|
||||
## When
|
||||
let nsRes = NsPubsubTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check nsRes.isOk()
|
||||
|
||||
let ns = nsRes.get()
|
||||
check:
|
||||
ns.name == "waku-dev"
|
||||
|
||||
test "Parse static sharding pub-sub topic string - Valid string":
|
||||
## Given
|
||||
let topic = "/waku/2/rs/16/42"
|
||||
|
||||
## When
|
||||
let nsRes = NsPubsubTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check nsRes.isOk()
|
||||
|
||||
let ns = nsRes.get()
|
||||
check:
|
||||
ns.cluster == 16
|
||||
ns.shard == 42
|
||||
|
||||
test "Parse pub-sub topic string - Invalid string: invalid protocol version":
|
||||
## Given
|
||||
let topic = "/waku/1/rs/16/42"
|
||||
|
||||
## When
|
||||
let ns = NsPubsubTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check ns.isErr()
|
||||
let err = ns.tryError()
|
||||
check:
|
||||
err.kind == ParsingErrorKind.InvalidFormat
|
||||
|
||||
test "Parse static sharding pub-sub topic string - Invalid string: empty shard value":
|
||||
## Given
|
||||
let topic = "/waku/2/rs//02"
|
||||
|
||||
## When
|
||||
let ns = NsPubsubTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check ns.isErr()
|
||||
let err = ns.tryError()
|
||||
check:
|
||||
err.kind == ParsingErrorKind.MissingPart
|
||||
err.part == "shard_cluster_index"
|
||||
|
||||
|
||||
test "Parse static sharding pub-sub topic string - Invalid string: cluster value":
|
||||
## Given
|
||||
let topic = "/waku/2/rs/xx/77"
|
||||
|
||||
## When
|
||||
let ns = NsPubsubTopic.parse(topic)
|
||||
|
||||
## Then
|
||||
check ns.isErr()
|
||||
let err = ns.tryError()
|
||||
check:
|
||||
err.kind == ParsingErrorKind.InvalidFormat
|
||||
|
@ -1,94 +1,7 @@
|
||||
## Waku topics definition and namespacing utils
|
||||
##
|
||||
## See 14/WAKU2-MESSAGE RFC: https://rfc.vac.dev/spec/14/
|
||||
## See 23/WAKU2-TOPICS RFC: https://rfc.vac.dev/spec/23/
|
||||
|
||||
when (NimMajor, NimMinor) < (1, 4):
|
||||
{.push raises: [Defect].}
|
||||
else:
|
||||
{.push raises: [].}
|
||||
|
||||
|
||||
import
|
||||
std/strutils,
|
||||
stew/results
|
||||
./topics/content_topic,
|
||||
./topics/pubsub_topic
|
||||
|
||||
|
||||
## Topics
|
||||
|
||||
type
|
||||
PubsubTopic* = string
|
||||
ContentTopic* = string
|
||||
|
||||
const
|
||||
DefaultPubsubTopic* = PubsubTopic("/waku/2/default-waku/proto")
|
||||
DefaultContentTopic* = ContentTopic("/waku/2/default-content/proto")
|
||||
|
||||
|
||||
## Namespacing
|
||||
|
||||
type
|
||||
NamespacedTopic* = object
|
||||
application*: string
|
||||
version*: string
|
||||
name*: string
|
||||
encoding*: string
|
||||
|
||||
type
|
||||
NamespacingErrorKind* {.pure.} = enum
|
||||
InvalidFormat
|
||||
|
||||
NamespacingError* = object
|
||||
case kind*: NamespacingErrorKind
|
||||
of InvalidFormat:
|
||||
cause*: string
|
||||
|
||||
NamespacingResult*[T] = Result[T, NamespacingError]
|
||||
|
||||
|
||||
proc invalidFormat(T: type NamespacingError, cause = "invalid format"): T =
|
||||
NamespacingError(kind: NamespacingErrorKind.InvalidFormat, cause: cause)
|
||||
|
||||
proc `$`*(err: NamespacingError): string =
|
||||
case err.kind:
|
||||
of NamespacingErrorKind.InvalidFormat:
|
||||
return "invalid format: " & err.cause
|
||||
|
||||
|
||||
proc parse*(T: type NamespacedTopic, topic: PubsubTopic|ContentTopic|string): NamespacingResult[NamespacedTopic] =
|
||||
## Splits a namespaced topic string into its constituent parts.
|
||||
## The topic string has to be in the format `/<application>/<version>/<topic-name>/<encoding>`
|
||||
|
||||
if not topic.startsWith("/"):
|
||||
return err(NamespacingError.invalidFormat("topic must start with slash"))
|
||||
|
||||
let parts = topic.split('/')
|
||||
|
||||
# Check that we have an expected number of substrings
|
||||
if parts.len != 5:
|
||||
return err(NamespacingError.invalidFormat("invalid topic structure"))
|
||||
|
||||
let namespaced = NamespacedTopic(
|
||||
application: parts[1],
|
||||
version: parts[2],
|
||||
name: parts[3],
|
||||
encoding: parts[4]
|
||||
)
|
||||
|
||||
return ok(namespaced)
|
||||
|
||||
proc `$`*(namespacedTopic: NamespacedTopic): string =
|
||||
## Returns a string representation of a namespaced topic
|
||||
## in the format `/<application>/<version>/<topic-name>/<encoding>`
|
||||
var str = newString(0)
|
||||
|
||||
str.add("/")
|
||||
str.add(namespacedTopic.application)
|
||||
str.add("/")
|
||||
str.add(namespacedTopic.version)
|
||||
str.add("/")
|
||||
str.add(namespacedTopic.name)
|
||||
str.add("/")
|
||||
str.add(namespacedTopic.encoding)
|
||||
|
||||
return str
|
||||
export
|
||||
content_topic,
|
||||
pubsub_topic
|
||||
|
89
waku/v2/protocol/waku_message/topics/content_topic.nim
Normal file
89
waku/v2/protocol/waku_message/topics/content_topic.nim
Normal file
@ -0,0 +1,89 @@
|
||||
## Waku content topics definition and namespacing utils
|
||||
##
|
||||
## See 23/WAKU2-TOPICS RFC: https://rfc.vac.dev/spec/23/
|
||||
|
||||
when (NimMajor, NimMinor) < (1, 4):
|
||||
{.push raises: [Defect].}
|
||||
else:
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
std/strutils,
|
||||
stew/results
|
||||
import
|
||||
./parsing
|
||||
|
||||
export parsing
|
||||
|
||||
|
||||
## Content topic
|
||||
|
||||
type ContentTopic* = string
|
||||
|
||||
const DefaultContentTopic* = ContentTopic("/waku/2/default-content/proto")
|
||||
|
||||
|
||||
## Namespaced content topic
|
||||
|
||||
type
|
||||
NsContentTopic* = object
|
||||
application*: string
|
||||
version*: string
|
||||
name*: string
|
||||
encoding*: string
|
||||
|
||||
proc init*(T: type NsContentTopic, application, version, name, encoding: string): T =
|
||||
NsContentTopic(
|
||||
application: application,
|
||||
version: version,
|
||||
name: name,
|
||||
encoding: encoding
|
||||
)
|
||||
|
||||
|
||||
# Serialization
|
||||
|
||||
proc `$`*(topic: NsContentTopic): string =
|
||||
## Returns a string representation of a namespaced topic
|
||||
## in the format `/<application>/<version>/<topic-name>/<encoding>`
|
||||
"/" & topic.application & "/" & topic.version & "/" & topic.name & "/" & topic.encoding
|
||||
|
||||
|
||||
# Deserialization
|
||||
|
||||
proc parse*(T: type NsContentTopic, topic: ContentTopic|string): ParsingResult[NsContentTopic] =
|
||||
## Splits a namespaced topic string into its constituent parts.
|
||||
## The topic string has to be in the format `/<application>/<version>/<topic-name>/<encoding>`
|
||||
|
||||
if not topic.startsWith("/"):
|
||||
return err(ParsingError.invalidFormat("topic must start with slash"))
|
||||
|
||||
let parts = topic[1..<topic.len].split("/")
|
||||
if parts.len != 4:
|
||||
return err(ParsingError.invalidFormat("invalid topic structure"))
|
||||
|
||||
|
||||
let app = parts[0]
|
||||
if app.len == 0:
|
||||
return err(ParsingError.missingPart("appplication"))
|
||||
|
||||
let ver = parts[1]
|
||||
if ver.len == 0:
|
||||
return err(ParsingError.missingPart("version"))
|
||||
|
||||
let name = parts[2]
|
||||
if name.len == 0:
|
||||
return err(ParsingError.missingPart("topic-name"))
|
||||
|
||||
let enc = parts[3]
|
||||
if enc.len == 0:
|
||||
return err(ParsingError.missingPart("encoding"))
|
||||
|
||||
|
||||
ok(NsContentTopic.init(app, ver, name, enc))
|
||||
|
||||
|
||||
# Content topic compatibility
|
||||
|
||||
converter toContentTopic*(topic: NsContentTopic): ContentTopic =
|
||||
$topic
|
36
waku/v2/protocol/waku_message/topics/parsing.nim
Normal file
36
waku/v2/protocol/waku_message/topics/parsing.nim
Normal file
@ -0,0 +1,36 @@
|
||||
when (NimMajor, NimMinor) < (1, 4):
|
||||
{.push raises: [Defect].}
|
||||
else:
|
||||
{.push raises: [].}
|
||||
|
||||
import stew/results
|
||||
|
||||
|
||||
type
|
||||
ParsingErrorKind* {.pure.} = enum
|
||||
InvalidFormat,
|
||||
MissingPart
|
||||
|
||||
ParsingError* = object
|
||||
case kind*: ParsingErrorKind
|
||||
of InvalidFormat:
|
||||
cause*: string
|
||||
of MissingPart:
|
||||
part*: string
|
||||
|
||||
type ParsingResult*[T] = Result[T, ParsingError]
|
||||
|
||||
|
||||
proc invalidFormat*(T: type ParsingError, cause = "invalid format"): T =
|
||||
ParsingError(kind: ParsingErrorKind.InvalidFormat, cause: cause)
|
||||
|
||||
proc missingPart*(T: type ParsingError, part = "unknown"): T =
|
||||
ParsingError(kind: ParsingErrorKind.MissingPart, part: part)
|
||||
|
||||
|
||||
proc `$`*(err: ParsingError): string =
|
||||
case err.kind:
|
||||
of ParsingErrorKind.InvalidFormat:
|
||||
return "invalid format: " & err.cause
|
||||
of ParsingErrorKind.MissingPart:
|
||||
return "missing part: " & err.part
|
118
waku/v2/protocol/waku_message/topics/pubsub_topic.nim
Normal file
118
waku/v2/protocol/waku_message/topics/pubsub_topic.nim
Normal file
@ -0,0 +1,118 @@
|
||||
## Waku pub-sub topics definition and namespacing utils
|
||||
##
|
||||
## See 23/WAKU2-TOPICS RFC: https://rfc.vac.dev/spec/23/
|
||||
|
||||
when (NimMajor, NimMinor) < (1, 4):
|
||||
{.push raises: [Defect].}
|
||||
else:
|
||||
{.push raises: [].}
|
||||
|
||||
|
||||
import
|
||||
std/strutils,
|
||||
stew/[results, base10]
|
||||
import
|
||||
./parsing
|
||||
|
||||
export parsing
|
||||
|
||||
|
||||
## Pub-sub topic
|
||||
|
||||
type PubsubTopic* = string
|
||||
|
||||
const DefaultPubsubTopic* = PubsubTopic("/waku/2/default-waku/proto")
|
||||
|
||||
|
||||
## Namespaced pub-sub topic
|
||||
|
||||
type
|
||||
NsPubsubTopicKind* {.pure.} = enum
|
||||
StaticSharding,
|
||||
NamedSharding
|
||||
|
||||
type
|
||||
NsPubsubTopic* = object
|
||||
case kind*: NsPubsubTopicKind
|
||||
of NsPubsubTopicKind.StaticSharding:
|
||||
cluster*: uint16
|
||||
shard*: uint16
|
||||
of NsPubsubTopicKind.NamedSharding:
|
||||
name*: string
|
||||
|
||||
proc staticSharding*(T: type NsPubsubTopic, cluster, shard: uint16): T =
|
||||
NsPubsubTopic(
|
||||
kind: NsPubsubTopicKind.StaticSharding,
|
||||
cluster: cluster,
|
||||
shard: shard
|
||||
)
|
||||
|
||||
proc named*(T: type NsPubsubTopic, name: string): T =
|
||||
NsPubsubTopic(
|
||||
kind: NsPubsubTopicKind.NamedSharding,
|
||||
name: name
|
||||
)
|
||||
|
||||
|
||||
# Serialization
|
||||
|
||||
proc `$`*(topic: NsPubsubTopic): string =
|
||||
## Returns a string representation of a namespaced topic
|
||||
## in the format `/waku/2/<raw-topic>
|
||||
case topic.kind:
|
||||
of NsPubsubTopicKind.NamedSharding:
|
||||
"/waku/2/" & topic.name
|
||||
of NsPubsubTopicKind.StaticSharding:
|
||||
"/waku/2/rs/" & $topic.cluster & "/" & $topic.shard
|
||||
|
||||
|
||||
# Deserialization
|
||||
|
||||
const
|
||||
Waku2PubsubTopicPrefix = "/waku/2"
|
||||
StaticShardingPubsubTopicPrefix = Waku2PubsubTopicPrefix & "/rs"
|
||||
|
||||
|
||||
proc parseStaticSharding*(T: type NsPubsubTopic, topic: PubsubTopic|string): ParsingResult[NsPubsubTopic] =
|
||||
if not topic.startsWith(StaticShardingPubsubTopicPrefix):
|
||||
return err(ParsingError.invalidFormat("must start with " & StaticShardingPubsubTopicPrefix))
|
||||
|
||||
let parts = topic[11..<topic.len].split("/")
|
||||
if parts.len != 2:
|
||||
return err(ParsingError.invalidFormat("invalid topic structure"))
|
||||
|
||||
let clusterPart = parts[0]
|
||||
if clusterPart.len == 0:
|
||||
return err(ParsingError.missingPart("shard_cluster_index"))
|
||||
let cluster = ?Base10.decode(uint16, clusterPart).mapErr(proc(err: auto): auto = ParsingError.invalidFormat($err))
|
||||
|
||||
let shardPart = parts[1]
|
||||
if shardPart.len == 0:
|
||||
return err(ParsingError.missingPart("shard_number"))
|
||||
let shard = ?Base10.decode(uint16, shardPart).mapErr(proc(err: auto): auto = ParsingError.invalidFormat($err))
|
||||
|
||||
ok(NsPubsubTopic.staticSharding(cluster, shard))
|
||||
|
||||
proc parseNamedSharding*(T: type NsPubsubTopic, topic: PubsubTopic|string): ParsingResult[NsPubsubTopic] =
|
||||
if not topic.startsWith(Waku2PubsubTopicPrefix):
|
||||
return err(ParsingError.invalidFormat("must start with " & Waku2PubsubTopicPrefix))
|
||||
|
||||
let raw = topic[8..<topic.len]
|
||||
if raw.len == 0:
|
||||
return err(ParsingError.missingPart("topic-name"))
|
||||
|
||||
ok(NsPubsubTopic.named(name=raw))
|
||||
|
||||
proc parse*(T: type NsPubsubTopic, topic: PubsubTopic|string): ParsingResult[NsPubsubTopic] =
|
||||
## Splits a namespaced topic string into its constituent parts.
|
||||
## The topic string has to be in the format `/<application>/<version>/<topic-name>/<encoding>`
|
||||
if topic.startsWith(StaticShardingPubsubTopicPrefix):
|
||||
NsPubsubTopic.parseStaticSharding(topic)
|
||||
else:
|
||||
NsPubsubTopic.parseNamedSharding(topic)
|
||||
|
||||
|
||||
# Pubsub topic compatibility
|
||||
|
||||
converter toPubsubTopic*(topic: NsPubsubTopic): PubsubTopic =
|
||||
$topic
|
Loading…
x
Reference in New Issue
Block a user