diff --git a/apps/wakubridge/message_compat.nim b/apps/wakubridge/message_compat.nim index d952f636a..f6b229761 100644 --- a/apps/wakubridge/message_compat.nim +++ b/apps/wakubridge/message_compat.nim @@ -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//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//rfc26` ## ## 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 diff --git a/tests/v2/test_waku_message_topics.nim b/tests/v2/test_waku_message_topics.nim index 1e46ef922..30f6710d9 100644 --- a/tests/v2/test_waku_message_topics.nim +++ b/tests/v2/test_waku_message_topics.nim @@ -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 diff --git a/waku/v2/protocol/waku_message/topics.nim b/waku/v2/protocol/waku_message/topics.nim index 072c40e2f..1d2720274 100644 --- a/waku/v2/protocol/waku_message/topics.nim +++ b/waku/v2/protocol/waku_message/topics.nim @@ -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 `////` - - 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 `////` - 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 diff --git a/waku/v2/protocol/waku_message/topics/content_topic.nim b/waku/v2/protocol/waku_message/topics/content_topic.nim new file mode 100644 index 000000000..ef2e03464 --- /dev/null +++ b/waku/v2/protocol/waku_message/topics/content_topic.nim @@ -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 `////` + "/" & 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 `////` + + if not topic.startsWith("/"): + return err(ParsingError.invalidFormat("topic must start with slash")) + + let parts = topic[1.. + 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..///` + if topic.startsWith(StaticShardingPubsubTopicPrefix): + NsPubsubTopic.parseStaticSharding(topic) + else: + NsPubsubTopic.parseNamedSharding(topic) + + +# Pubsub topic compatibility + +converter toPubsubTopic*(topic: NsPubsubTopic): PubsubTopic = + $topic