implement tag, tags, pubkey

This commit is contained in:
2025-05-21 12:21:19 +01:00
parent 90e68264d1
commit 57a54e855c
12 changed files with 339 additions and 15 deletions

View File

@@ -12,8 +12,7 @@ var BinaryPrefix = []byte("base64:")
// C is a content field for an event.T that can be binary or text, if set to binary, it encodes
// to base64 URL format (with padding). Binary coded values have the prefix "base64:" prefix.
// Setting binary should be done in accordance with the mimetype being
// "application/octet-stream"
// Setting binary should be done in accordance with the mimetype being a binary mimetype.
type C struct {
Val []byte
Binary bool

View File

@@ -5,6 +5,7 @@ import (
"github.com/mleku/transit/id"
"github.com/mleku/transit/kind"
"github.com/mleku/transit/mime"
"github.com/mleku/transit/tag"
)
// E is an event on the transit protocol.
@@ -17,8 +18,8 @@ type E struct {
Kind *kind.K `json:"kind" doc:"short name of the application/intent of the event"`
// Content is the payload of the event.E.
Content *content.C
// Tags are a set of descriptors formatted as a key, value and optional further descriptors.
// Tags are a set of descriptors formatted as a key, value and optional extra fields.
Tags tag.S `json:"tags" doc:"collection of tags with a short key name and value"`
}
// C is the canonical format for an event.E that is hashed to generate the id.I.

5
go.mod
View File

@@ -5,13 +5,14 @@ go 1.24.3
require (
github.com/davecgh/go-spew v1.1.1
github.com/fatih/color v1.18.0
github.com/zeebo/blake3 v0.2.4
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
lukechampine.com/frand v1.5.1
)
require (
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
golang.org/x/sys v0.25.0 // indirect
lukechampine.com/frand v1.5.1 // indirect
)

2
go.sum
View File

@@ -15,6 +15,8 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=

10
interfaces/codec.go Normal file
View File

@@ -0,0 +1,10 @@
package interfaces
import (
"encoding/json"
)
type Codec interface {
json.Marshaler
json.Unmarshaler
}

View File

@@ -247,15 +247,15 @@ func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
return false
},
Err: func(format string, a ...interface{}) error {
if Level.Load() < l {
fmt.Fprintf(writer,
"%s%s %s %s\n",
msgCol(TimeStamper()),
LevelSpecs[l].Colorizer(LevelSpecs[l].Name, " "),
fmt.Sprintf(format, a...),
msgCol(GetLoc(skip)),
)
}
// if Level.Load() < l {
fmt.Fprintf(writer,
"%s%s %s %s\n",
msgCol(TimeStamper()),
LevelSpecs[l].Colorizer(LevelSpecs[l].Name, " "),
fmt.Sprintf(format, a...),
msgCol(GetLoc(skip)),
)
// }
return fmt.Errorf(format, a...)
},
}

50
pubkey/pubkey.go Normal file
View File

@@ -0,0 +1,50 @@
package pubkey
import (
"crypto/ed25519"
"encoding/base64"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/errorf"
)
const Len = ed25519.PublicKeySize
var EncodedLen = base64.RawURLEncoding.EncodedLen(Len)
// P is an ed25519 public key in base64 URL format.
type P struct {
Key ed25519.PublicKey
}
// New creates a new .
func New(pub ed25519.PublicKey) (i *P) {
i = &P{Key: pub}
return
}
func (i *P) MarshalJSON() (b []byte, err error) {
if len(i.Key) != Len {
err = errorf.E("id must be %d bytes long, got %d", Len, len(i.Key))
return
}
b = make([]byte, EncodedLen+2)
b[0] = '"'
b[len(b)-1] = '"'
base64.RawURLEncoding.Encode(b[1:], i.Key)
return
}
func (i *P) UnmarshalJSON(b []byte) (err error) {
if len(b) != EncodedLen+2 {
err = errorf.E("encoded id hash must be %d bytes long, got %d", EncodedLen+2, len(b))
return
}
i.Key = make([]byte, Len)
var n int
if n, err = base64.RawURLEncoding.Decode(i.Key, b[1:EncodedLen+1]); chk.E(err) {
err = errorf.E("error: decoding failed at %d: %s", n, err.Error())
return
}
return
}

38
pubkey/pubkey_test.go Normal file
View File

@@ -0,0 +1,38 @@
package pubkey
import (
"bytes"
"crypto/ed25519"
"encoding/json"
"testing"
"lukechampine.com/frand"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/log"
)
func TestNew(t *testing.T) {
i := generatePubkey()
var err error
var b []byte
if b, err = json.Marshal(&i); chk.E(err) {
t.Fatal(err)
}
log.I.F("encode %0x %s", i.Key, b)
i2 := &P{}
if err = json.Unmarshal(b, i2); chk.E(err) {
t.Fatal(err)
}
log.I.F("decode %0x", i2.Key)
if !bytes.Equal(i.Key, i2.Key) {
t.Fatal("failed to encode/decode")
}
}
func generatePubkey() (p P) {
var pub ed25519.PublicKey
pub, _, _ = ed25519.GenerateKey(frand.Reader)
pp := New(pub)
return *pp
}

116
tag/tag.go Normal file
View File

@@ -0,0 +1,116 @@
package tag
import (
"encoding/json"
"net/url"
"reflect"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/errorf"
"github.com/mleku/transit/id"
"github.com/mleku/transit/pubkey"
)
type T [2]any
// Types is a list of types of tags that are valid. Note that this is all of the types that are
// valid. Additional tag keys may be added in the future but should never be taken away.
var Types = map[string]reflect.Type{
"root": reflect.TypeOf(&id.I{}),
"parent": reflect.TypeOf(&id.I{}),
"event": reflect.TypeOf(&id.I{}),
"user": reflect.TypeOf(&pubkey.P{}),
"hashtag": reflect.TypeOf(""),
"url": reflect.TypeOf(&url.URL{}),
}
// New is used to return an empty type for unmarshalling.
func New(t string) any { return Types[t] }
func (t T) MarshalJSON() (b []byte, err error) {
// first value must be a string
k, ok := t[0].(string)
chk.E(err)
if !ok {
err = errorf.E("key must be a string, got '%v'", k)
return
}
// second value must be one of Types.
var found bool
for _, v := range Types {
if v == reflect.TypeOf(t[1]) {
found = true
break
}
}
// url is a special case because it needs to be rendered using a stringer as it lacks a
// json.MarshalJSON implementation.
if k == "url" {
var u *url.URL
if u, ok = t[1].(*url.URL); !ok {
err = errorf.E("url key does not have a value of type *url.URL: %v",
reflect.TypeOf(t[1]))
return
}
found = true
t[1] = u.String()
}
if k == "hashtag" {
var ht string
if ht, ok = t[1].(string); !ok {
err = errorf.E("hashtag key does not have a value of type []byte: %v",
reflect.TypeOf(t[1]))
return
}
t[1] = string(ht)
}
if !found {
err = errorf.E("tag value is unknown type key: %s value: %v",
t[0], reflect.TypeOf(t[1]))
return
}
// marshal array into tag
tt := [2]any(t)
if b, err = json.Marshal(tt); chk.E(err) {
return
}
return
}
func (t T) UnmarshalJSON(b []byte) (err error) {
var kv []any
if err = json.Unmarshal(b, &kv); chk.E(err) {
return
}
// we only do the above to get the key name
var ok bool
var k string
if k, ok = t[0].(string); !ok {
err = errorf.E("key must be a string, got '%v'", k)
return
}
if _, ok = Types[k]; !ok {
err = errorf.E("unknown key type '%s'", k)
return
}
// the value could be any of the valid types, but all are strings listed in Types.
switch vt := t[1].(type) {
case *url.URL:
_ = vt
// if u, err = url.Parse(vt); chk.E(err) {
// return
// }
//
// case
// ty := New(k)
// if err = json.Unmarshal([]byte(t[1].(string)), &ty); chk.E(err) {
// return
// }
// t[1] = vt
default:
err = errorf.E("invalid type in value field: %v", reflect.TypeOf(t[1]))
return
}
// decode in accordance with the key
return
}

71
tag/tag_test.go Normal file
View File

@@ -0,0 +1,71 @@
package tag
import (
"crypto/ed25519"
"encoding/json"
"net/url"
"testing"
"lukechampine.com/frand"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/id"
"github.com/mleku/transit/log"
"github.com/mleku/transit/pubkey"
)
func TestNew(t *testing.T) {
// root
root := T{"root", generateId()}
var b []byte
var err error
if b, err = json.Marshal(&root); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
// parent
parent := T{"parent", generateId()}
if b, err = json.Marshal(&parent); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
// event
eid := T{"event", generateId()}
if b, err = json.Marshal(&eid); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
// user
pk := T{"user", generatePubkey()}
if b, err = json.Marshal(&pk); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
// hashtag
hashtag := "winning"
ht := T{"hashtag", hashtag}
if b, err = json.Marshal(&ht); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
// url
var u *url.URL
if u, err = url.Parse("http://example.com"); chk.E(err) {
t.Fatal(err)
}
ut := T{"url", u}
if b, err = json.Marshal(&ut); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
}
func generateId() (i *id.I) { return id.New(frand.Bytes(32)) }
func generatePubkey() (p *pubkey.P) {
var pub ed25519.PublicKey
pub, _, _ = ed25519.GenerateKey(frand.Reader)
p = pubkey.New(pub)
return
}

5
tag/tags.go Normal file
View File

@@ -0,0 +1,5 @@
package tag
type S []T
func NewTags(t ...T) (s S) { return S(t) }

31
tag/tags_test.go Normal file
View File

@@ -0,0 +1,31 @@
package tag
import (
"encoding/json"
"net/url"
"testing"
"github.com/mleku/transit/chk"
"github.com/mleku/transit/log"
)
func TestNewTags(t *testing.T) {
root := T{"root", generateId()}
parent := T{"parent", generateId()}
eid := T{"event", generateId()}
pk := T{"user", generatePubkey()}
hashtag := "winning"
ht := T{"hashtag", hashtag}
var u *url.URL
var err error
if u, err = url.Parse("http://example.com"); chk.E(err) {
t.Fatal(err)
}
ut := T{"url", u}
s := NewTags(root, parent, eid, pk, ht, ut)
var b []byte
if b, err = json.Marshal(s); chk.E(err) {
t.Fatal(err)
}
log.I.F("%s", b)
}