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
|
||||
// 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
|
||||
|
||||
@@ -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
5
go.mod
@@ -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
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/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
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
|
||||
},
|
||||
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
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