examples/chat: Make chat example compile

This commit is contained in:
Sasha Koshka 2025-10-19 13:18:24 -04:00
parent e5d7ad0702
commit 14a317c2ab
7 changed files with 134 additions and 178 deletions

View File

@ -17,8 +17,8 @@ func main() {
}
address := os.Args[1]
room := os.Args[2]
var nickname hopp.Option[string]; if len(os.Args) >= 4 {
nickname = hopp.O(os.Args[3])
nickname := "Anonymous"; if len(os.Args) >= 4 {
nickname = os.Args[3]
}
trans, err := join(address, room, nickname)
handleErr(1, err)
@ -31,15 +31,11 @@ func main() {
}
}()
for {
message, err := chat.Receive(trans)
message, _, err := chat.Receive(trans)
handleErr(1, err)
switch message := message.(type) {
case *chat.MessageChat:
nickname := "Anonymous"
if value, ok := message.Nickname.Get(); ok {
nickname = value
}
fmt.Fprintf(os.Stdout, "%s: %s\n", nickname, message.Content)
fmt.Fprintf(os.Stdout, "%s: %s\n", message.Nickname, message.Content)
case *chat.MessageJoinNotify:
fmt.Fprintf(os.Stdout, "(i) %s joined the room\n", message.Nickname)
case *chat.MessageLeaveNotify:
@ -48,7 +44,7 @@ func main() {
}
}
func join(address string, room string, nickname hopp.Option[string]) (hopp.Trans, error) {
func join(address string, room string, nickname string) (hopp.Trans, error) {
ctx, done := context.WithTimeout(context.Background(), 16 * time.Second)
defer done()
dialer := hopp.Dialer {
@ -57,41 +53,24 @@ func join(address string, room string, nickname hopp.Option[string]) (hopp.Trans
InsecureSkipVerify: true,
},
}
conn, err := dialer.Dial(ctx, "quic", address)
if err != nil { return nil, err }
err = updateProfile(conn, nickname)
conn, err := dialer.Dial(ctx, "tcp", address)
if err != nil { return nil, err }
transRoom, err := conn.OpenTrans()
if err != nil { return nil, err }
err = chat.Send(transRoom, &chat.MessageJoin {
Room: room,
_, err = chat.Send(transRoom, &chat.MessageJoin {
Room: room,
Nickname: nickname,
})
if err != nil { return nil, err }
return transRoom, nil
}
func send(trans hopp.Trans, content string) error {
return chat.Send(trans, &chat.MessageChat {
_, err := chat.Send(trans, &chat.MessageChat {
Content: content,
})
}
func updateProfile(conn hopp.Conn, nickname hopp.Option[string]) error {
trans, err := conn.OpenTrans()
if err != nil { return err }
defer trans.Close()
err = chat.Send(trans, &chat.MessageUpdateProfile {
Nickname: nickname,
})
if err != nil { return err }
message, err := chat.Receive(trans)
if err != nil { return err }
switch message := message.(type) {
case *chat.MessageError: return message
default: return nil
}
return err
}
func handleErr(code int, err error) {

View File

@ -2,5 +2,5 @@
// source files, run this command from within the root directory of the
// repository:
//
// go run ./cmd/hopp-generate examples/chat/protocol.md examples/chat/protocol
// go run ./cmd/hopp-generate examples/chat/protocol.pdl -o examples/chat/protocol/protocol.go
package chat

View File

@ -3,8 +3,8 @@ package chat
import "fmt"
func (msg *MessageError) Error() string {
if description, ok := msg.Description.Get(); ok {
return fmt.Sprintf("other party sent error: %d %s", msg.Error, description)
if description, ok := msg.Description.Value(); ok {
return fmt.Sprintf("other party sent error: %d %s", msg.Code, description)
} else {
return fmt.Sprintf("other party sent error: %d", msg.Code)
}

View File

@ -20,6 +20,15 @@ type Message interface {
Method() uint16
}
// Send encodes a message and sends it along a transaction.
func Send(trans hopp.Trans, message Message) (n int, err error) {
writer, err := trans.SendWriter(message.Method())
if err != nil { return n, err }
defer writer.Close()
encoder := tape.NewEncoder(writer)
return message.Encode(encoder)
}
// canAssign determines if data from the given source tag can be assigned to
// a Go type represented by destination. It is designed to receive destination
// values from [generate.Generator.generateCanAssign]. The eventual Go type and
@ -225,7 +234,7 @@ func(this *MessageJoin) Decode(decoder *tape.Decoder) (n int, err error) {
// must be sent within a room transaction.
type MessageChat struct {
Content string
Nickname hopp.Option[string]
Nickname string
}
// Method returns the message's method number.
@ -242,36 +251,34 @@ func(this *MessageChat) Encode(encoder *tape.Encoder) (n int, err error) {
nn, err = encoder.WriteUintN(2, tag_8.CN() + 1)
n += nn; if err != nil { return n, err }
{
nn, err = encoder.WriteUint16(0x0001)
n += nn; if err != nil { return n, err }
tag_9 := tape.StringTag(string((*this).Nickname))
nn, err = encoder.WriteUint8(uint8(tag_9))
n += nn; if err != nil { return n, err }
if len((*this).Nickname) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_9.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Nickname)), tag_9.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte((*this).Nickname))
n += nn; if err != nil { return n, err }
nn, err = encoder.WriteUint16(0x0000)
n += nn; if err != nil { return n, err }
tag_9 := tape.StringTag(string((*this).Content))
nn, err = encoder.WriteUint8(uint8(tag_9))
tag_10 := tape.StringTag(string((*this).Content))
nn, err = encoder.WriteUint8(uint8(tag_10))
n += nn; if err != nil { return n, err }
if len((*this).Content) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_9.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Content)), tag_9.CN())
if tag_10.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len((*this).Content)), tag_10.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte((*this).Content))
n += nn; if err != nil { return n, err }
if value, ok := (*this).Nickname.Value(); ok {
nn, err = encoder.WriteUint16(0x0001)
n += nn; if err != nil { return n, err }
tag_10 := tape.StringTag(string(value))
nn, err = encoder.WriteUint8(uint8(tag_10))
n += nn; if err != nil { return n, err }
if len(value) > tape.MaxStructureLength {
return n, tape.ErrTooLong
}
if tag_10.Is(tape.LBA) {
nn, err = encoder.WriteUintN(uint64(len(value)), tag_10.CN())
n += nn; if err != nil { return n, err }
}
nn, err = encoder.Write([]byte(value))
n += nn; if err != nil { return n, err }
}
}
return n, nil
}
@ -573,22 +580,20 @@ func decodeBranch_5c1cf9347bb6d9f41cee64b186392d24_MessageChat(this *MessageChat
tape.Skim(decoder, fieldTag_31)
continue
}
var destination_33 string
var length_34 uint64
var length_33 uint64
if fieldTag_31.Is(tape.LBA) {
length_34, nn, err = decoder.ReadUintN(int(fieldTag_31.CN()))
length_33, nn, err = decoder.ReadUintN(int(fieldTag_31.CN()))
n += nn; if err != nil { return n, err }
} else {
length_34 = uint64(fieldTag_31.CN())
length_33 = uint64(fieldTag_31.CN())
}
if length_34 > uint64(tape.MaxStructureLength) {
if length_33 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_34)
buffer := make([]byte, length_33)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&destination_33) = string(buffer)
this.Nickname = hopp.O(destination_33)
*(&(this.Nickname)) = string(buffer)
default:
tape.Skim(decoder, fieldTag_31)
continue
@ -599,41 +604,41 @@ func decodeBranch_5c1cf9347bb6d9f41cee64b186392d24_MessageChat(this *MessageChat
func decodeBranch_68c536511e6d598462efc482144438e9_MessageJoinNotify(this *MessageJoinNotify, decoder *tape.Decoder, tag tape.Tag) (n int, err error) {
var nn int
var length_35 uint64
if length_35 > uint64(tape.MaxStructureLength) {
var length_34 uint64
if length_34 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
length_35, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
length_34, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
n += nn; if err != nil { return n, err }
for _ = range length_35 {
var fieldKey_36 uint16
fieldKey_36, nn, err = decoder.ReadUint16()
for _ = range length_34 {
var fieldKey_35 uint16
fieldKey_35, nn, err = decoder.ReadUint16()
n += nn; if err != nil { return n, err }
var fieldTag_37 tape.Tag
fieldTag_37, nn, err = decoder.ReadTag()
var fieldTag_36 tape.Tag
fieldTag_36, nn, err = decoder.ReadTag()
n += nn; if err != nil { return n, err }
switch fieldKey_36 {
switch fieldKey_35 {
case 0x0000:
if !(canAssign(tape.LBA, fieldTag_37)) {
tape.Skim(decoder, fieldTag_37)
if !(canAssign(tape.LBA, fieldTag_36)) {
tape.Skim(decoder, fieldTag_36)
continue
}
var length_38 uint64
if fieldTag_37.Is(tape.LBA) {
length_38, nn, err = decoder.ReadUintN(int(fieldTag_37.CN()))
var length_37 uint64
if fieldTag_36.Is(tape.LBA) {
length_37, nn, err = decoder.ReadUintN(int(fieldTag_36.CN()))
n += nn; if err != nil { return n, err }
} else {
length_38 = uint64(fieldTag_37.CN())
length_37 = uint64(fieldTag_36.CN())
}
if length_38 > uint64(tape.MaxStructureLength) {
if length_37 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_38)
buffer := make([]byte, length_37)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&(this.Nickname)) = string(buffer)
default:
tape.Skim(decoder, fieldTag_37)
tape.Skim(decoder, fieldTag_36)
continue
}
}
@ -642,43 +647,84 @@ func decodeBranch_68c536511e6d598462efc482144438e9_MessageJoinNotify(this *Messa
func decodeBranch_68c536511e6d598462efc482144438e9_MessageLeaveNotify(this *MessageLeaveNotify, decoder *tape.Decoder, tag tape.Tag) (n int, err error) {
var nn int
var length_39 uint64
if length_39 > uint64(tape.MaxStructureLength) {
var length_38 uint64
if length_38 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
length_39, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
length_38, nn, err = decoder.ReadUintN(int(tag.CN()) + 1)
n += nn; if err != nil { return n, err }
for _ = range length_39 {
var fieldKey_40 uint16
fieldKey_40, nn, err = decoder.ReadUint16()
for _ = range length_38 {
var fieldKey_39 uint16
fieldKey_39, nn, err = decoder.ReadUint16()
n += nn; if err != nil { return n, err }
var fieldTag_41 tape.Tag
fieldTag_41, nn, err = decoder.ReadTag()
var fieldTag_40 tape.Tag
fieldTag_40, nn, err = decoder.ReadTag()
n += nn; if err != nil { return n, err }
switch fieldKey_40 {
switch fieldKey_39 {
case 0x0000:
if !(canAssign(tape.LBA, fieldTag_41)) {
tape.Skim(decoder, fieldTag_41)
if !(canAssign(tape.LBA, fieldTag_40)) {
tape.Skim(decoder, fieldTag_40)
continue
}
var length_42 uint64
if fieldTag_41.Is(tape.LBA) {
length_42, nn, err = decoder.ReadUintN(int(fieldTag_41.CN()))
var length_41 uint64
if fieldTag_40.Is(tape.LBA) {
length_41, nn, err = decoder.ReadUintN(int(fieldTag_40.CN()))
n += nn; if err != nil { return n, err }
} else {
length_42 = uint64(fieldTag_41.CN())
length_41 = uint64(fieldTag_40.CN())
}
if length_42 > uint64(tape.MaxStructureLength) {
if length_41 > uint64(tape.MaxStructureLength) {
return n, tape.ErrTooLong
}
buffer := make([]byte, length_42)
buffer := make([]byte, length_41)
nn, err = decoder.Read(buffer)
n += nn; if err != nil { return n, err }
*(&(this.Nickname)) = string(buffer)
default:
tape.Skim(decoder, fieldTag_41)
tape.Skim(decoder, fieldTag_40)
continue
}
}
return n, nil
}
// Receive decodes a message from a transaction and returns it as a value.
// Use a type switch to determine what type of message it is.
func Receive(trans hopp.Trans) (message any, n int, err error) {
method, reader, err := trans.ReceiveReader()
decoder := tape.NewDecoder(reader)
if err != nil { return nil, n, err }
switch method {
case 0001:
var message MessageSuccess
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0200:
var message MessageJoin
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0300:
var message MessageChat
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0400:
var message MessageJoinNotify
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0401:
var message MessageLeaveNotify
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
case 0000:
var message MessageError
nn, err := message.Decode(decoder)
n += nn; if err != nil { return nil, n, err }
return message, n, nil
}
return nil, n, hopp.ErrUnknownMethod
}

View File

@ -1,70 +0,0 @@
# Chat Protocol
This document describes a simple chat protocol. To re-generate the source files,
run `go run ./cmd/hopp-generate examples/chat/protocol.md examples/chat/protocol`
## Messages
### 0000 Error
| Tag | Name | Type | Required |
| --: | ----------- | ------ | -------- |
| 0 | Code | U16 | Yes |
| 1 | Description | String | No |
Error is sent by a party when the other party has done something erroneous. The
valid error codes are:
- 0: General, unspecified error
The description field, if specified, determines a human-readable error to be
shown to the user. The sending party must immediately close the transaction
after this message is sent.
### 0001 Success
Success is sent by a party when it has successfully completed a task given to it
by the other party. The sending party must immediately close the transaction
after this message is sent.
### 0100 UpdateProfile
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
UpdateProfile is sent by the client in a new transaction to update the profile
details that will be shown to other connected clients.
### 0200 Join
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Room | String | Yes |
Join is sent by the client when it wishes to join a room. It must begin a new
transaction, and that transaction will persist while the user is in that room.
Messages having to do with the room will be sent along this transaction. To
leave the room, the client must close the transaction.
### 0201 Chat
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
| 1 | Content | String | Yes |
Chat is sent by the client when it wishes to post a message to the room. It is
also relayed by the server to other clients to notify them of the message. It
must be sent within a room transaction.
### 0300 JoinNotify
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
JoinNotify is sent by the server when another client joins the room. It must be
sent within a room transaction.
### 0301 LeaveNotify
| Tag | Name | Type | Required |
| --: | -------- | ------ | -------- |
| 0 | Nickname | String | No |
LeaveNotify is sent by the server when another client leaves the room. It must
be sent within a room transaction.

View File

@ -32,7 +32,7 @@ M0200 Join {
// must be sent within a room transaction.
M0300 Chat {
0000 Content String,
0001 Nickname ?String,
0001 Nickname String,
}
// JoinNotify is sent by the server when another client joins the room. It must be

View File

@ -28,7 +28,7 @@ func main() {
func host(address string, certPath, keyPath string) error {
keyPair, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil { return err }
listener, err := hopp.ListenQUIC("quic", address, &tls.Config {
listener, err := hopp.ListenTLS("tcp", address, &tls.Config {
InsecureSkipVerify: true,
Certificates: []tls.Certificate { keyPair },
})
@ -63,6 +63,7 @@ func (this *client) run() {
log.Println("accepted transaction")
if err != nil {
log.Printf("XXX %v failed: %v", this.conn.RemoteAddr(), err)
continue
}
go this.runTrans(trans)
}
@ -70,7 +71,7 @@ func (this *client) run() {
func (this *client) runTrans(trans hopp.Trans) {
defer trans.Close()
message, err := chat.Receive(trans)
message, _, err := chat.Receive(trans)
if err != nil {
log.Printf(
"XXX %v transaction failed: %v",
@ -97,7 +98,7 @@ func (this *client) transTalk(trans hopp.Trans, initial *chat.MessageJoin) error
if err != nil { return err }
defer this.leaveRoom(trans, room)
for {
message, err := chat.Receive(trans)
message, _, err := chat.Receive(trans)
if err != nil { return err }
switch message := message.(type) {
case *chat.MessageChat:
@ -110,7 +111,7 @@ func (this *client) transTalk(trans hopp.Trans, initial *chat.MessageJoin) error
}
func (this *client) handleMessageChat(trans hopp.Trans, room string, message *chat.MessageChat) error {
log.Println("(). %s #%s: %s", this.nickname.Default("Anonymous"), room, message.Content)
log.Println("(). %s #%s: %s", this.nickname, room, message.Content)
clients, done := clients.RBorrow()
defer done()
for client := range clients {
@ -126,7 +127,7 @@ func (this *client) relayMessage(room string, message *chat.MessageChat) error {
rooms, done := this.rooms.RBorrow()
defer done()
if trans, ok := rooms[room]; ok {
err := chat.Send(trans, message)
_, err := chat.Send(trans, message)
if err != nil {
return fmt.Errorf("could not relay message: %w", err)
}
@ -141,7 +142,7 @@ func (this *client) joinRoom(trans hopp.Trans, room string) error {
return fmt.Errorf("already joined %s", room)
}
rooms[room] = trans
log.Printf("--> user %s joined #%s", this.nickname.Default("Anonymous"), room)
log.Printf("--> user %s joined #%s", this.nickname, room)
return nil
}
@ -152,7 +153,7 @@ func (this *client) leaveRoom(trans hopp.Trans, room string) error {
return fmt.Errorf("not in %s", room)
}
delete(rooms, room)
log.Printf("<-- user %s left #%s", this.nickname.Default("Anonymous"), room)
log.Printf("<-- user %s left #%s", this.nickname, room)
return nil
}