feat(message): differentiate content and pubsub topic namespacing

This commit is contained in:
Lorenzo Delgado 2023-03-07 11:10:36 +01:00 committed by GitHub
parent d5f93e385d
commit 67db35e29d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 372 additions and 122 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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

View 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