implement tag, tags, pubkey
This commit is contained in:
@@ -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
|
// 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.
|
// 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
|
// Setting binary should be done in accordance with the mimetype being a binary mimetype.
|
||||||
// "application/octet-stream"
|
|
||||||
type C struct {
|
type C struct {
|
||||||
Val []byte
|
Val []byte
|
||||||
Binary bool
|
Binary bool
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/mleku/transit/id"
|
"github.com/mleku/transit/id"
|
||||||
"github.com/mleku/transit/kind"
|
"github.com/mleku/transit/kind"
|
||||||
"github.com/mleku/transit/mime"
|
"github.com/mleku/transit/mime"
|
||||||
|
"github.com/mleku/transit/tag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// E is an event on the transit protocol.
|
// 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"`
|
Kind *kind.K `json:"kind" doc:"short name of the application/intent of the event"`
|
||||||
// Content is the payload of the event.E.
|
// Content is the payload of the event.E.
|
||||||
Content *content.C
|
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.
|
// C is the canonical format for an event.E that is hashed to generate the id.I.
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -5,13 +5,14 @@ go 1.24.3
|
|||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/fatih/color v1.18.0
|
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 (
|
require (
|
||||||
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // 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
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
lukechampine.com/frand v1.5.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -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/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 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
|||||||
10
interfaces/codec.go
Normal file
10
interfaces/codec.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Codec interface {
|
||||||
|
json.Marshaler
|
||||||
|
json.Unmarshaler
|
||||||
|
}
|
||||||
18
lol/log.go
18
lol/log.go
@@ -247,15 +247,15 @@ func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
|
|||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
Err: func(format string, a ...interface{}) error {
|
Err: func(format string, a ...interface{}) error {
|
||||||
if Level.Load() < l {
|
// if Level.Load() < l {
|
||||||
fmt.Fprintf(writer,
|
fmt.Fprintf(writer,
|
||||||
"%s%s %s %s\n",
|
"%s%s %s %s\n",
|
||||||
msgCol(TimeStamper()),
|
msgCol(TimeStamper()),
|
||||||
LevelSpecs[l].Colorizer(LevelSpecs[l].Name, " "),
|
LevelSpecs[l].Colorizer(LevelSpecs[l].Name, " "),
|
||||||
fmt.Sprintf(format, a...),
|
fmt.Sprintf(format, a...),
|
||||||
msgCol(GetLoc(skip)),
|
msgCol(GetLoc(skip)),
|
||||||
)
|
)
|
||||||
}
|
// }
|
||||||
return fmt.Errorf(format, a...)
|
return fmt.Errorf(format, a...)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
50
pubkey/pubkey.go
Normal file
50
pubkey/pubkey.go
Normal 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
38
pubkey/pubkey_test.go
Normal 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
116
tag/tag.go
Normal 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
71
tag/tag_test.go
Normal 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
5
tag/tags.go
Normal 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
31
tag/tags_test.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user